Roadmap
Generated from docs/ROADMAP-TODO.md
The full specification is docs/ROADMAP-PROMPT.md. Items marked “Coming” across this site link back to the phases below. ✓ marks a done item, ☐ an open one.
AI-native headless LMS on Wellms (Escola LMS). Differentiator: courses that stay in sync with their sources (“Living Course”) and adapt to each learner, with every element cited.
Full spec for Claude Code: docs/ROADMAP-PROMPT.md. Working rules: CLAUDE.md. Run sessions with
“Read docs/ROADMAP-PROMPT.md and start Phase N”. Every phase: explore → plan → approval →
small commits → tests → summary.
Decisions made
Section titled “Decisions made”-
✓ (2026-10-11) Feature catalogue (
docs/plans/feature-catalogue.md): Scalar as the docs API browser, a newFeatureIndexcatalogue component for the landing, H5P content types taken from h5p.org, landings translated (PL, ZH) and docs English only,ItemListJSON-LD on, OpenAPI responses completed for the featured packages -
✓ (2026-10-09) Phase 3 plan (
docs/plans/phase-3.md) approved with all 17 decisions of its section 18 as recommended (issue #27 closed) -
✓ (2026-10-09) Phase 1 decisions 1–52, Phase 2 decisions 1–28 and the Phase 3 plan with its 17 decisions confirmed by the product owner
-
✓ (2026-10-09) Brand identity “Orbital Folio” chosen by the product owner (indigo #0F2B46, orange #FF7A2E); logo drawn as SVG, applied to platform and product surfaces, not to tenants; orange is an accent only (ADR 0038, Proposed; #25). A trademark check on the name and mark is still recommended before launch
-
✓ (2026-10-09) ADRs 0013–0034 accepted
-
☐ (2026-10-09) Post-Phase 2 bug batch: ADRs 0063–0070 proposed, awaiting acceptance (tenant AI settings, studio applied state, tutor demo login, APP_KEY, quiz time limit key, scheduler lock, CI scope, admin on Node 24)
-
✓ (2026-10-09) Phase 1 defaults confirmed: students get
scorm_track-update; SVG served as attachment with CSP (no sanitiser); LTI Instructor → tutor, never admin, no e-mail account linking; LiaScript player fetched at image build time; production content origin on a separate registrable domain -
✓ (2026-10-09) Production content origin is a same-site subdomain (
{slug}.content.ulams.app), not a separate domain (supersedes the earlier default); mitigations shipped, ADR 0014 amended -
✓ (2026-10-09) GHCR images are public; the upstream EscolaLMS security reports stay as public issues
-
✓ (2026-10-09) Replace the illustrative incident log on the On-Call landing with real course content
-
✓ (2026-10-09) Phase 1 and Phase 2 plans approved; ADRs 0008 (reference frontend: Astro SSR, plain TS SDK, agent UI catalogue), 0009 (LLM layer), 0010 (Course Blueprint), 0011 (AG-UI over SSE) and 0012 (LTI 1.3) accepted
-
✓ Base: Wellms (Laravel,
escolalms/*packages), headless -
✓ Killer feature: Living Course (source sync with diff + citations, progress preserved)
-
✓ Second pillar: personalisation via new
learner-insightspackage -
✓ Do not use
escolalms/recommender -
✓ Generative UI: A2UI v0.9 + own component catalogue, transported over AG-UI; declarative by default, model-written code only in the sandboxed
simulationcomponent -
✓ Commerce: Sylius 2.x as a separate headless service behind one frontend and one admin; LMS owns entitlements, Sylius owns catalogue/cart/checkout/taxes/invoices;
CommerceProviderinterface; Wellmspayments/cart/vouchersretired after migration -
✓ Business model: open core (free self-hosted core; paid cloud, enterprise, support)
-
✓ Niche: developer education / customer education for dev tools
-
✓ (new) One monorepo
admin/+api/+front/(+docs/), allescolalms/*packages vendored as source, Turborepo + Yarn workspaces (ADR 0001, 0005) -
✓ (new) Rename EscolaLMS / Wellms to ulams in code, config and infrastructure (ADR 0002)
-
✓ (new) H5P only in the separate GPL service
api/h5p(Lumi), embedded via iframe; no GPL code in the API or frontend bundles (ADR 0003) -
✓ (new) styled-components replaced by CSS custom properties (
--ulams-*) (ADR 0004) -
✓ (new) Remove
recommenderfrom the API composition, not just stop using it (ADR 0006) -
✓ (new) Repository: public
github.com/ulams-dev/ulams, no AI attribution in history -
✓ (2026-10-09) Build the agent-first
ulamsCLI core now (login and tokens,ulams api, main nouns,ulams mcp); course-as-code after Phase 3. Plandocs/plans/cli.md(draft, waiting for approval), ADRs 0072–0079 Proposed; open owner questions #74–#79 -
☐ (2026-10-09) Interactive topic type and three new demo academies (gravity, poland, ulam): plan
docs/plans/interactive-demos.md(draft, waiting for approval), ADRs 0086–0089 Proposed; owner questions #146 (approve), #147 (gravity repo), #148 (content licences), #149 (poland scope), #150 (on by default), #151 (Ulam fact review) -
✓ (2026-10-09) Product owner, on the interactive demos (#147, #148, #149, #150):
qunabu/Gravityand the poland repository are his own code and are used under MIT inside ulams (no GPL separation, no checksum download; ADR 0088 amended); three outside commits of gravity are left out (bc9d770, 9db0edc, 4adaa1b), and so are its music track and Moon photograph; poland is rebuilt without the saved article copy, its map or the mp4 (map regenerated from Natural Earth via world-atlas, reply framing dropped, primary sources only, EN and PL); MIT for code and CC BY 4.0 for course text; the Interactive topic type is on for every tenant with network off
Open decisions
Section titled “Open decisions”- ☐ Final name (favourite ULAMS; alternatives Wellam, Monte, Spiral, Automata, UlamOS)
- ✓ GitHub organisation:
ulamsis taken; registerulams-dev(fallbacks:ulams-hq,ulamslabs,ulams-ai) (note:ulams-dev/ulamscreated and pushed 2026-10-08) - ☐ Check domains (ulams.ai, ulams.dev) and trademarks
- ☐ Check legal aspects of using the Ulam name
- ✓ (new) Copyright of the original EscolaLMS/Wellms code and
scorm-player: owned by the product owner; admin and scorm-player licensed MIT
- ✓ GitHub organisation:
- ✓ Move MCP server (7.5) right after Phase 2? Cheap to build, strong demo (yes, 2026-10-09: the local
ulams mcpships with the CLI core;docs/plans/cli.md, #73) - ☐ Move certificates (6.1) earlier if compliance is the priority segment
- ☐ Multitenancy for the POC: one deployment, tenant per subdomain with own theme?
- ☐ Prototype the Sylius order → entitlement flow early (highest-risk commerce piece)
- ✓ Add the spec file to the repo as
docs/ROADMAP-PROMPT.md - ✓ (new) Approve the Phase 1 and Phase 2 plans (
docs/plans/phase-1.md,docs/plans/phase-2.md) and ADRs 0009–0011 (LLM layer, Course Blueprint, AG-UI over SSE) (approved 2026-10-09; ADR 0008 and 0012 too)
Product principles (tie-breakers)
Section titled “Product principles (tie-breakers)”UX over feature count · human approves every AI change (diff) · grounded and cited · standards over lock-in · cost-aware (log tokens/cost from day one) · developer-first · agent-ready · easy self-hosting · open core.
Market context: buyers rank UX 70%, price 63%, integrations 59%, AI 30%; most-wanted AI feature is personalisation (65%); trust is the new competitive axis; compliance and extended enterprise are top buyer needs. Competitor Coursebox already does doc → course; their weakness is generic, stale content.
Phase 0: Foundation and audit
Section titled “Phase 0: Foundation and audit”Plan (new): docs/plans/leftovers-0-2.md (draft, waiting for approval; ADRs 0040–0056 Proposed) covers every
open Phase 0, 1 and 2 item as work packages L0-01…L2-24; owner questions #41–#44, #46–#63.
0.1 Explore and report (no code)
Section titled “0.1 Explore and report (no code)”- ✓ Map repo, packages, versions; course → lesson → topic model and topic types (see docs/reports/phase-0-audit.md)
- ✓ Report on
headless-h5p,scorm,cmi5,lrs,tracker,reports,payments,cart,vouchers,translations,settings,templates,notifications(see docs/reports/phase-0-audit.md) - ✓ Can
recommenderbe safely disabled or removed? What depends on it? (removed from API, admin and front; nothing else depended on it; ADR 0006; webcam-capture leftover tracked in 0.1c) - ☐ Multitenancy via
gecche/laravel-multidomain: current setup, dynamic subdomains possible? (partial: dynamic subdomains work via the tenancy package (ulams:tenant:create); fixed shared Redis keys, unknown-host fallback and boot-time worker lists; remaining: tenant video queue, per-tenant storage credentials, production DNS/TLS) - ✓ Inventory of learner activity data (tracker, xAPI/cmi5, SCORM CMI, H5P, quizzes, progress, logins): storage, granularity, retention, gaps (see docs/reports/phase-0-audit.md)
- ✓ How content updates preserve learner progress today (see docs/reports/phase-0-audit.md)
- ✓ Tests, CI, code style, queues (Horizon), storage, existing AI code (explored; no AI code exists; the baseline failures (core 6, auth 3) no longer reproduce and the quarantine list
api/phpunit.quarantine.xmlis empty) - ✓ Licence audit of all
escolalms/*and key dependencies for open core (LICENSING.md and docs/reports/phase-0-audit.md; remediation items below) - ✓ Runtime dependency inventory (input for Phase 8) (see docs/reports/phase-0-audit.md)
- ✓ Commerce audit: what Wellms commerce does, dependent flows, Sylius 2.x API coverage, Stripe / Przelewy24 gateways, Sylius MCP admin tool, B2B options (see docs/reports/phase-0-audit.md)
0.1b Monorepo foundation (new)
Section titled “0.1b Monorepo foundation (new)”- ✓ (new) Monorepo with the full history of the three repositories under
api/,admin/,front/ - ✓ (new) Vendor the 50 PHP packages into
api/packages; noescolalms/*incomposer.json - ✓ (new) Vendor the JS libraries into
front/src/libandadmin/src/lib; Yarn workspaces + Turborepo - ✓ (new) Rename to ulams, with data migration for existing databases
- ☐ (new) H5P as the isolated Lumi service
api/h5p(partial: service, Laravel index package, Caddy routing and admin/front iframe embedding done; multi-tenant resolver in progress) - ✓ (new) Remove the PHP H5P server completely and replace it with the Node.js service: no
h5p/h5p-core,h5p/h5p-editororheadless-h5pleft incomposer.json/composer.lockor the code; Laravel keeps only the read-onlyapi/packages/h5pindex and HTTP client (ADR 0003) - ✓ (new) Remove
recommenderand its admin/front screens - ✓ (new) Replace styled-components with CSS custom properties everywhere (front, its component library and the admin markdown editor; blocked by lint; verified with the visual regression harness)
- ✓ (new) Demo content seeder for the three experience courses (
front/docs/design/experiences.md) - ✓ (new) Root README, AGENTS.md and per-package READMEs for the monorepo
- ☐ (new) Documentation site (Astro Starlight,
front/docs-site): guides per audience, reference pages generated from the code, every ADR and the roadmap rendered fromdocs/, coverage check over packages, admin routes, learner routes and topic types, GitHub Pages deploy (partial: on branchdocs/starlight-site, not merged; Pages source and private vulnerability reporting to be enabled) - ☐ (new) Remaining legacy references (partial:
escolalms/phpreplaced by a base built in-repo, ReportBro removed and replaced by pdfme): replace theescolalms/phpandescolalms/reportbro-serverimages, decide on upstream provenance links, reword ADR prose, retarget Docker Hub publishing workflows, replace theulams.appplaceholder domain, recreate SQL views in pre-rename databases - ✓ (new) Fix
php artisan route:list(Mattermost client connects in its constructor) - ✓ (new) CI (partial: root
ci.ymlwith path filters, PHP shards, licence guards and Dependabot committed; not yet run on GitHub — the branch is unpushed; publishing workflows intentionally dropped): move workflows to the root.github/with path filters; drop MySQL services; run Jest in admin/front; Dockerfiles build from the repo root (done: workflows are onmainand run on GitHub; admin Jest (3 suites) and the front tests run in thejsjob; yarn installs are frozen; the quarantine is empty) - ✓ (new) Remove the non-existent
packages/tracker/srcpath from Swagger (done); consider Git LFS for large test fixtures; revisit exact pins (faker-markdown-generator,tzsk/sms) (no LFS: CI rejects new files over 2 MB and the 24 MB and 6.8 MB SCORM mocks are generated minimal packages;faker-markdown-generatormoved torequire-dev;tzsk/smsstays^10.0; owner confirmation of no LFS pending #48) - ✓ (new) Lean workers for local development and the demo profile (
ULAMS_WORKERS_MODE=lean: one loop over the domains withulams:tenant:work-once, one builder/long-job process, one scheduler loop, no Horizon, php-fpmondemand; ADR 0083 amendment;per-tenantstays the production default)
0.1c Security and audit follow-ups (new)
Section titled “0.1c Security and audit follow-ups (new)”-
✓ (new) Replace the GPL PHP libraries
trax2/framework(lrs) andlaraveldaily/laravel-invoiceswith first-party code -
✓ (new) Payment callbacks must verify the payment with the provider (Stripe signature/status, P24 verification); RevenueCat off by default and server-verified
-
✓ (new) Remove the consultation webcam capture and its unauthenticated upload endpoints (recommender leftover)
-
✓ (new) Authenticate the Jitsi recording webhook and restrict the downloaded URL (SSRF)
-
✓ (new) Verify JWT signatures in the LRS guard
-
✓ (new) Fix the ungrouped
orWhereinCourseAccessService::getUserCourseIdsand similar queries -
✓ (new) Remove the tracker Logs screen in admin and other tracker leftovers
-
✓ (new) Fix the tenant video processing queue (jobs dispatched to a queue no tenant worker consumes)
-
☐ (new)
Relation::enforceMorphMapfor topic types so class renames never orphan data -
✓ (new) ADR for the tenancy package (docs/decisions/0007)
-
☐ (new) Upgrade PostgreSQL 12 (EOL) to 16/17 with a tested dump/restore path
-
☐ (new) Drop Soketi until realtime is needed (broadcast driver is
log); Laravel Reverb after 0.2 -
✓ (new) cmi5 for learners: give students the cmi5 launch permission and serve AU files from object storage (they sit on the local disk that Caddy does not serve) — found by the demo seeders (students have
cmi5_read;CMI5_DISKfollowsSCORM_DISK;cmi5:move-to-bucketcopies old packages; AUs play from the content origin infront/web; ADR 0046) -
☐ (new) Containers cannot reach
storage.localhost(it resolves to the container itself); use the internal MinIO endpoint for server-side fetches (e.g. Image topic creation) -
✓ (new) Platform bucket publicly readable by default (
MINIO_DEFAULT_BUCKETS=ulams:download) -
✓ (new) Demo course seeders for the three experiences (
make demo-seed,demo-seed-tenants) -
✓ (new) Security follow-ups (medium) (done and merged:
auth:apiandtags_liston admin tag routes,POST api/images/imglimits and throttle, client payment parameters allow-listed with server price/currency/trial values winning,payProductpurchasability, vouchers search grouping,GroupTreedepth limit and cycle safety,_ignitionabsent from demo and production images (ADR 0071);POST api/cmi5/fetchno longer echoes a token: it exchanges a one-time launch token for an LRS-only session token (ADR 0046)) -
☐ (new) Stripe: handle the 3-D Secure redirect in the front and document the webhook setup (
PAYMENTS_STRIPE_WEBHOOK_SECRET,/api/payments-gateways/webhook/stripe); RevenueCat receipt verifier (partial: 3-D Secure redirect in the legacy front and webhook docs done; the RevenueCat verifier is obsolete by default, pending owner decision #46) -
✓ (new) Jitsi: confirm the JaaS webhook signature format against the JaaS docs; configure
JITSI_RECORDING_HOSTS -
✓ (new) Drop the unused
analyze_enabledcolumns (consultations, webinars) and clean up stored meeting frames in tenant buckets -
✓ (new) Remove the Stripe test key committed in
api/docker/envs/*.example(keys emptied in the six env files; rolling the key at Stripe is an owner action, #49) -
☐ (new) Responsible disclosure: the payment-callback, LRS-token, webcam-upload and course-access issues exist in the upstream EscolaLMS packages; notify upstream users (partial: notice drafted in
docs/security/upstream-notice.md; sending it is an owner action, #50) -
☐ (new) mjml: the
mjmlcompose service is not on theulamsnetwork andMJML_API_URLis not set (templates fall back silently); wire it or drop the service -
☐ (new) Publish images to GHCR (
ghcr.io/ulams-dev/*, decided 2026-10-08); publish the base image asulams/php:8.3with source offers for its GPL programs (seeapi/docker/php/NOTICE) -
☐ (new) Delete
front/src/style/(two unused styled-components helpers; excluded from tsconfig, eslint and the guard until removed) -
✓ (new) Cart on tenants crashes without a Stripe publishable key (
stripe.tsxcallsstripeKey.includeson null); show a configuration message instead -
☐ (new) Yarn install on Node 23 needs
--ignore-engines(vitest engines); CI pins Node 22 -
☐ (new) Dependency holds (Dependabot ignore rules in
.github/dependabot.yml):sharp0.35 fails to load in the docs-site build on the CI runner (MissingSharp; Astro depends onsharp ^0.34);@ant-design/pro-components2.8.5 to 2.8.10 break admin typecheck (@ant-design/pro-form2.31.5+ declaration files importsrc/...). Revisit when Astro supports sharp 0.35 and when a pro-form release fixes its typings -
☐ (new) ESLint 10 in
admin(umi lint, fabric config) andfront(vite,@typescript-eslint7, legacy.eslintrc); the other five packages are on eslint 10 already. The Dependabot major ignore foreslintand@eslint/jscomes off when both move to flat config -
☐ (new) Prettier 3 in
admin(2.8.8) andfront(2.4.1): deferred, Dependabot major ignore forprettier. Front has no prettier config or CI check (only the lint-staged step, which already skipssrc/liband the other front/* workspaces), so the upgrade would reformat all offront/src(about 600 files; trailing-comma default changes) for no behavioural gain; admin’s CI step resolvesprettier/bin-prettier.js, which prettier 3 does not ship. Do it as one dedicated formatting commit (with.git-blame-ignore-revs) together with the old front’s retirement -
☐ (new) Stripe in the old
frontcart:@stripe/react-stripe-js7 needs@stripe/stripe-js10 (front has 1.54), a 6-major jump through Elements and PaymentElement code that no CI test exercises. Deferred, Dependabot major ignore for both packages. Revisit with the Sylius checkout (the LMS cart is not the long-term payment path) or when the cart gets an end-to-end test against Stripe test mode -
☐ (new) Replace MinIO with SeaweedFS (or RustFS) and give each tenant its own S3 identity (ADR 0041, plan L0-03)
-
✓ (new)
ulams:upgrade: one idempotent per-tenant upgrade command (plan L0-19) (ADR 0081; the cmi5 and frame steps run once their commands land) -
✓ (new) Fix
Cmi5Policy::deletechecking the read permission (newcmi5_deletepermission, admins only; plan L0-09) -
☐ (new) Five packages with
@OA\annotations are missing from the Swagger scan paths (plan L0-11)
0.2 Framework upgrade
Section titled “0.2 Framework upgrade”- ✓ Upgrade plan from Laravel 9 (EOL) to supported Laravel/PHP: order, breaking changes, forks/patches needed, risks (docs/plans/phase-0.md: 9 → 10 → 11 → 12 → 13 on PHP 8.4)
- ✓ Implement after approval with test suite green at every step
(steps 1–4 done and merged to main in PR #1 — Laravel 13.35 on PHP 8.4 (Passport 13 with data migration for the
platform and every tenant, Testbench 11, PHPUnit 12; query cache dropped,
treestoneit/shopping-cartvendored asapi/packages/shopping-cart, Mattermost Laravel wrapper replaced), no new test failures; see docs/plans/phase-0.md B.11–B.14) - ✓ (new) Decide on Passport 13’s device-code routes (
oauth/device*, exposed by default, unused): keep or disable (disabled,e6c21b9e) - ☐ (new) Move the
@OA\docblock annotations (223 files) to PHP attributes and drop the abandoneddoctrine/annotations - ✓ (new) Smaller admin and front images: nginx-unprivileged instead of Apache+PHP, with runtime settings injected without PHP (approved 2026-10-09; after Phase 1)
Phase 1: Content formats and integrations
Section titled “Phase 1: Content formats and integrations”Plan (new): docs/plans/phase-1.md (approved 2026-10-09; decisions to confirm in its section 14): M1.1
upload hardening and content origin → M1.2–M1.4 LTI 1.3 → M1.5 LiaScript → M1.6–M1.7 Adapt → M1.8 H5P
items → M1.9 conformance. Work branch: phase-1/content-formats. Open items: docs/plans/leftovers-0-2.md section 4.
1.1 LiaScript
Section titled “1.1 LiaScript”- ✓ Versioned Markdown + assets as course source (
packages/liascript) - ✓ CRUD API (create from Markdown, upload
.md/zip, update, delete, fetch source) (plus versions list and restore) - ✓ Rendering decision: self-hosted LiaScript vs export to SCORM/xAPI; no dependency on
liascript.github.io (the LiaScript SCORM 1.2 build, fetched at image build time with a pinned version
and SHA-256, runs on the tenant content origin with our SCORM API page; completion at the last section
or on completed/passed;
docs/plans/phase-1.md5.5) - ✓ (new) LiaScript topic type (learners), admin editor with preview and version diff, export/import strategy
(topic type, Astro
LiaScriptLesson, admin editor with versions, diff, restore and a live preview of unsaved text; course export carries the current text and assets, import creates a new document; ADR 0016) - ✓ (new) Run
sh packages/liascript/bin/fetch-player.shin the dev api container once (the image build does it; the bind mount hides it)
1.2 Adapt Learning
Section titled “1.2 Adapt Learning”- ✓ Path A: import built SCORM zip (
adapt-contrib-spoor) (detected on upload,scorm.source_format = adapt, admin tag; generated spoor-style fixture) - ✓ Path B (feature flag): JSON source, schema-validated, isolated build worker (partial:
packages/adaptbehindADAPT_SOURCE_ENABLEDwith versioned sources, structural validation, queued build and import through Path A; GPL workerapi/adapt-builder(adapt_framework v5.56.2, compose profileadapt, real build round trip in the nightly conformance workflow); ADR 0013 (Proposed); an admin screen pending)
1.3 LTI 1.3 (high priority)
Section titled “1.3 LTI 1.3 (high priority)”- ✓ LTI Platform: launch external tools, AGS grade passback, deep linking (API, admin screens and topic form with “pick content from the tool”, players in both fronts, ADR 0012; launching a Moodle 5.0 course and receiving Moodle’s grade verified in the nightly conformance workflow; the saLTIre job needs an operator run)
- ✓ LTI Tool: expose our courses to Moodle, Canvas etc. (launch, user/role mapping, course access, deep-linking course picker, grade passback, admin platform screens and landing pages in both fronts; Moodle 5.0 launch and grade passback verified in the nightly conformance workflow; inside an LMS iframe the front’s session cookie can be blocked as third-party, so platforms should open ulams in a new window)
- ✓ Key rotation, nonce/state validation, per-tenant registrations (
ulams:lti:rotate-keysmonthly, provisioning steplti_keys, single-use hints/state/nonce/jti inlti_nonces, registrations in the tenant database, isolation tests) - ✓ (new) Admin UI for LTI: tools and platforms screens, external-tool topic form with “pick content from tool” (Integrations → LTI)
- ✓ (new) LTI: Client-Side OIDC (platform storage via
postMessage) on the tool side, NRPS on the platform side, per-toolframe-srcin the CSP (client-side OIDC:lti_storage_target,POST /api/lti/tool/launch/verify, browser test with a fake platform in the nightly conformance; NRPS:GET /api/lti/platform/nrps/{course}per tool switch;frame-src: the front readsGET /api/lti/frame-origins) - ✓ (new) Run
ulams:lti:rotate-keys --initfor existing tenants (new tenants get it at provisioning) (done byulams:upgrade, steplti_keys, ADR 0081)
1.4 Shared
Section titled “1.4 Shared”- ✓ Upload hardening (zip-slip, MIME, size limits, virus-scan hook) (
packages/uploads: SCORM, cmi5, course import, file manager; clamd hook tested with a fake clamd, compose profileavnot run in CI) - ✓ Isolated origin / strict CSP for third-party JS (SCORM, Adapt, LiaScript and cmi5 play from the
per-tenant content origin with a strict CSP, files served by
/api/contentfrom local or bucket disks (ADR 0046); the front/admin CSP is enforced in development and switches withCSP_ENFORCE(ADR 0044)) - ✓ (new) Zip-slip: SCORM (
ScormService::unzipScormArchive) and cmi5 (Cmi5UploadService) extract archives withZipArchive::extractToand no entry-path checks; replace with a safe extractor (M1.1) - ✓ (new) The SCORM player loads
scorm-againfrom the jsDelivr CDN; vendor it (air-gapped installs) - ✓ (new) SCORM/cmi5 content of all tenants is served from the shared
storage.localhostorigin; move packages and players to a per-tenant content origin (M1.1) (SCORM and cmi5 done,<slug>.content.localhost,api/docs/content-origin.md; runulams:tenant:sync-envso existing tenants getCONTENT_ORIGIN) - ✓ (new) Course import read files outside the extracted archive through paths in
content.json(e.g.../../../.envas a category icon, published to the bucket); paths now resolved inside it - ✓ (new) SVG/HTML uploads served from the bucket: stored with
Content-Disposition: attachmentand an extension-basedContent-Type; storage origin sendsscript-src 'none'for SVG (follow-up of 0.2) - ✓ (new) Students have no
scorm_track-updatepermission, so the legacy/api/scorm/trackrejects them and the front’s legacy SCORM player never tracked (seeded for students, confirmed 2026-10-09; re-runPermissionsSeederon existing tenants). SCORM completion now completes the SCORM topics using the SCO - ☐ (new) Production: serve content origins from a separate registrable domain (not same-site with the
app), and add registered LTI tool origins to the front/admin
frame-src(documented inapi/docs/content-origin.md; deployment pending). Note 2026-10-09: the owner chose the same-site*.content.ulams.appinstead; the separate domain stays supported (ADR 0014, amended) (partial: deployment docs with DNS and TLS steps done (operators/content-origin) and the registered LTI tool origins are in the front’sframe-src(ADR 0044); the real domain is owner decision #24) - ✓ (new) Same-site content subdomain hardening:
__Host-cookies, exact-Origin checks on front and API, sandboxed player frames, COOP/CORP headers, both modes documented - ✓ (new) Enforce the front/admin CSP after a week of clean reports; add a report collector (collector
POST /api/csp-report, admin listGET /api/admin/csp-reports,report-uri/report-toon every policy, the front builds its CSP per request,CSP_ENFORCEandULAMS_CSP_HEADERswitch enforcement; ADR 0044; production turns it on after a clean week, seeoperators/security-headers) - ✓ (new) H5P service multitenancy via its
TenantResolver(per-tenant key, database, bucket) (env-file resolver; per-tenantH5P_INTERNAL_TOKEN; library administration limited to the platform; production mounts limited to an exported least-privilege config (ulams:h5p:export-config,compose.h5p.prod.yml); idle-tenant eviction (TENANT_IDLE_EVICT_MS); ADR 0015) - ✓ (new) H5P: refresh the player model when the 5-minute Passport token rotates; redact
_tokenin all proxies’ access logs (Caddy and the H5P service redact_token; embed pages swap refreshed tokens in order, unit-tested; the old React front now refreshes the token before it expires) - ✓ Policies, OpenAPI annotations, fixtures and tests (LiaScript, Adapt A+B, LTI round-trip) (permissions
lti_manage,liascript_manage,adapt_manage, OpenAPI for every new endpoint, fixtures and tests against in-test fakes;.github/workflows/nightly-conformance.yml(opt-in) with the Adapt worker build, Moodle 5.0 in both LTI directions (passed locally) and an operator-driven saLTIre job; ADR 0019) - ✓ (new)
TopicFinishedfired before the learner’s progress was saved, so listeners running at once (sync queue) sent the previous LTI score; now dispatched after saving (found by the Moodle run, ADR 0018) - ☐ (new) Turn on the nightly conformance runs (
NIGHTLY_CONFORMANCE=true) and run the saLTIre job once with an operator - ✓ (new) Adapt Path B admin screen (sources, versions, build status)
- ✓ (new) Astro front: H5P plays without a token, so learner state is not restored (decide: a short-lived
H5P token from the BFF, or state through the BFF) (decided: state through the BFF, ADR 0045; the
/h5pproxy adds the session token server-side for the player’s own calls, the frame still getstoken: nulland the model’s URLs carry no_token) - ✓ (new) Production: set
H5P_SERVICE_CONFIG_DIR, runulams:h5p:export-configand start the H5P service withcompose.h5p.prod.yml - ✓ (new) H5P xAPI progress endpoint rejects statement objects (
ProgressService::h5p()typedstring) (plan L1-06; the statement is stored as JSON, its verb as the event) - ☐ (new)
yarn installon Node 24 fails in admin’s postinstall (max setup: umi’s esmi feature loadshttp-deceiver, which needs the removedhttp_parserbinding); CI and.nvmrcuse Node 22
1.5 Interactive packages (new)
Section titled “1.5 Interactive packages (new)”Plan: docs/plans/interactive-demos.md (M1–M2); ADRs 0086, 0087.
- ✓ (new) Interactive topic type: versioned zip packages with a
ulams-interactive.jsonmanifest, played in an opaque sandbox on the content origin with a CSP per version, steps and step ranges per topic, text alternatives, background mode (ADR 0086; on by default pending #150) (PRs #162, #174; network origins need a confirmation on upload, #172) - ✓ (new)
ulams-ixv1 bridge protocol and the MIT@ulams/interactive-bridgelibrary (ADR 0087) (PR #162) - ✓ (new)
InteractiveLessoncatalogue component with background (full-bleed) mode, reduced-motion posters, WebGL and timeout fallbacks, keyboard flow (PR #173; “Mark as complete” stays as a fallback, #171) - ✓ (new) Interactive package library and topic editor in the admin;
ulams topics create-interactiveand its MCP tool; docs pages for creators, the bridge and the content origin (PR #173)
Phase 2: AI Course Builder
Section titled “Phase 2: AI Course Builder”Note (new): a first Course Builder plan was drafted on 2026-10-08 (LLM layer in api/packages/ai,
tenancy package, Course Blueprint, LiaScript and Adapt topic types, admin module). It predates this
roadmap; Phase 2 is re-planned from this spec after Phases 0–1.
Plan (new): docs/plans/phase-2.md (approved 2026-10-09; follows Phase 1). First milestone
M2.1 “chat course building”: upload → interview → outline diff → approved generation with citations →
approved apply through domain services → element chat edits. Designs:
front/docs/design/stitch/course-builder/. Open items and M2.2–M2.5: docs/plans/leftovers-0-2.md sections 2 and 5.
- ✓ (new) Course Builder author area in the reference web app (
front/web,/studio); the admin only links to it (M2.1, branchphase-2/course-builder; ADR 0022) - ✓ (new) AG-UI event log and SSE stream from Laravel, carrying A2UI surfaces (ADR 0011; A2UI as
a2ui-surfaceactivity snapshots, ADR 0023; cache-key wake instead of pub/sub, ADR 0029) - ✓ (new) Builder components in
@ulams/uiand the course landing document in the catalogue format - ✓ (new) Studio: edit the Course Brief from the brief panel (
Editon a row opens the interview’s own control, saves throughPUT …/brief; price, theme and site never mark stages stale) - ☐ (new) Detect admin edits made after an apply before re-applying (ADR 0010 drift check)
- ✓ (new) Vendor the A2UI v0.9 JSON Schemas in
@ulams/uifor dev-mode validation (plan 13.2; L2-02; the studio validatesa2ui-surfaceenvelopes in dev, tests cover every surface kind) - ☐ (new) Operations for the builder: a separate PHP-FPM pool and Caddy route for
…/sessions/{id}/events, a dailycourse-builder:prune-events, a Horizon queue for builder jobs - ☐ (new) Run the opt-in cross-tenant check
TenantIsolationTest::testCourseBuilderSessionsDoNotCrossTenants(written; needsTENANCY_INTEGRATION=1and two probe tenants) - ☐ (new) Regenerate the OpenAPI spec and SDK path types for the builder endpoints (the SDK uses hand-written types; the API carries the annotations)
- ✓ (new) Normalise
yarn.lockwith a realyarn install(a freshyarn installleaves it unchanged;--frozen-lockfilein CI is the check) (entries for@ag-ui/core1.0.2 anddiff9.0.0 were added by hand while the disk was full) - ✓ (new) Delete the RichText/GIFT content row when a topic is deleted (topic repository leaves it; the applier deletes topics through the repository)
2.1 LLM layer
Section titled “2.1 LLM layer”- ✓ Provider abstraction, model per task via config (Sonnet default, Haiku for light steps)
(
api/packages/ai; Anthropic, fake and disabled drivers; other providers in 8.2) - ✓ Structured outputs validated by JSON Schema, retry then graceful failure
- ✓ Prompt caching for sources (live eval: lesson and quiz calls after the first read ~4.8k cached tokens)
- ✓ Per-call logging: model, tokens, cost, latency, tenant, course; running cost per course
(
ai_calls,ai:usage, cost streamed to the studio) - ✓ Hard limits (source size, tokens per course, concurrency) (plus per-session cost, daily sessions, tenant monthly spend, eval spend)
- ✓ Versioned prompt files with README (
api/packages/course-builder/resources/prompts)
2.2 Ingestion
Section titled “2.2 Ingestion”- ✓ PDF, Markdown, DOCX → Source Document with stable fragment IDs (first-party DOCX converter, ADR 0026)
- ✓ Untrusted content handling + prompt-injection tests (feature tests and a live eval fixture)
- ✓ Design (don’t build) image/video ingestion (design note in
docs/plans/phase-2.md6.4)
2.3 Interview
Section titled “2.3 Interview”- ✓ Adaptive chips/buttons with defaults and “decide for me”
- ✓ Audience, duration, tone, theme preset + accent, free/paid (via
CommerceProvider; interim: existingpayments), assessments, language (partial: audience, level, duration and lesson length, tone, assessments, language, theme preset + accent and a free/paid question: Course Brief v2; the product is created throughCommerceProvider, ADR 0049) - ✓ Editable Course Brief (schema-validated brief v2 with decided-by per field, editable in the studio panel and through the API with stale marking)
2.4 Generation pipeline (queued, resumable, streamed)
Section titled “2.4 Generation pipeline (queued, resumable, streamed)”- ✓ Learning objectives proposed and approved by the author first (with inline edits)
- ✓ Outline mapped to source fragments and objectives
- ✓ Lessons in parallel from the component registry (rich text, LiaScript, H5P) (rich text in a concurrency window; LiaScript lessons with cited self-checks, lessons with an H5P activity from three allow-listed libraries and lessons with an interactive from the library, chosen per lesson in the outline; ADR 0050, L2-11, L2-12)
- ✓ Assessments with explanations, each traceable to a fragment (per-lesson quizzes and a final test, GIFT rendered by our code, support check against the cited text)
- ✓ Metadata (title, description, SEO, pricing) (a suggested price for a paid course, confirmed by the author; ADR 0049)
- ✓ Tenant provisioning: subdomain, theme, publish, commerce channel/product if paid (theme, the product (inactive at apply, active at publish), the publish check and the generated landing on the current site; a new site for platform operators through the platform tenant API and a session transfer, ADR 0048; the Sylius channel is Phase 6.4)
- ✓ Course Blueprint: versioned JSON, stable IDs, citations; entities created via domain services; persisted per stage; progress streamed (SSE/websockets) (ADR 0010, 0025)
- ✓ (new) Long jobs on dedicated queue connections:
<driver>-builder(retry_after 2400) for Course Builder, Living Course and Adapt builds,<driver>-long-jobfor video and course clone; workers and Horizon with matching timeouts; config testQueueRetryAfterConfigTest(ADR 0083 amendment)
2.5 Element-level chat editing
Section titled “2.5 Element-level chat editing”- ✓ Select element → chat → structured patch → diff → apply (course, module, lesson, block, question)
- ☐ Blueprint versions: undo/redo/restore; global edits via queued pipeline (partial: undo, redo and restore with re-apply done; global edits are M2.3)
2.6 Author UX
Section titled “2.6 Author UX”- ✓ Upload → interview → live progress → tree + preview → element chat (e2e on the fake driver)
- ✓ Sources panel; retry a single failed step; themed learner frontend (retry of a single step and
source passages behind every citation; the learner front is the existing one with the tenant
theme; the workspace sources panel lists every section with the elements citing it, the
uncovered sections and the sections of the selected element,
GET …/sessions/{s}/citations; L2-10)
2.7 Generative UI
Section titled “2.7 Generative UI”Architecture
- ✓ Verify current A2UI / AG-UI versions and choose renderer (CopilotKit vs own) (A2UI v0.9,
@ag-ui/core1.0.2, own renderer; ADR 0011, 0023) - ☐ UI component catalogue: name, props JSON Schema, model description, accessible
implementation, text fallback (partial: the 17 builder components and the approved learner
layout set (Timeline, FlipCards, CodeBlock, PracticeActivity, Callout, Steps, ComparisonTable,
H5PFrame, LiaScriptLesson; L2-20); playground at
/catalogue/in the docs site, L2-19) - ✓
render_uivalidated server-side; invalid/unknown → text fallback (structured output choice validated against the@ulams/uimanifest) - ✓ Progressive streaming with skeletons; interactions sent back as structured events
Builder components (MVP)
- ✓ Interview controls · theme picker with live preview · drag-and-drop outline editor (the editor moves, renames, adds and removes modules and lessons with drag and drop and with buttons, each change an author version; L2-14)
- ✓ Lesson preview card · variant comparison · quiz question card (variant comparison: 2–3 options side by side, choose one; L2-13)
- ☐ Diff view · generation progress with retry and cost · publish summary with warnings (partial: diff view, progress, the apply summary and the publish summary with blocking items and warnings done; critique results in the summary are M2.4)
Learner layouts (feature flag)
- ☐ AI-composed declarative lesson layouts from approved components, stored in blueprint (partial: the approved components and their manifest are done (L2-20), and the Layout topic type stores and renders them (M6); generation is L2-21)
- ✓ (new) Layout topic type, rendering only (ADR 0052;
docs/plans/interactive-demos.mdM6): catalogue documents as LMS topics so flip cards, timelines and practice activities can be course items; generation stays in L2-21 (API packagetopic-type-layoutwith server-side validation against the manifest copy, admin JSON editor with validation and preview link, lesson player rendering with a Prose fallback, completion by view or the first PracticeActivity attempt,ulams topics create-layoutand the MCP tool, docs)
Pedagogical guardrails
- ☐ Mandatory scaffolding: intro → toolbox → graded challenges → tiered hints →
explanatory feedback → worked solution after attempt (partial: the
PracticeActivitycomponent enforces the slots and hides the solution until an attempt (L2-20); generation is L2-21) - ☐ Four pillars check: objective alignment, agency, scaffolding, formative feedback
Generate-then-refine loop
- ☐ Critics: pedagogy, grounding, mechanics, visual/UX, accessibility; retry budget then flag to author
- ☐ Playwright agent solvability check incl. adversarial actions
- ☐ Critique results and iterations shown in publish summary
Simulations (opt-in)
- ☐
simulationcomponent: sandboxed iframe, isolated origin, strict CSP, no network, typed postMessage - ☐ Must pass solvability loop; author approval required; off by default in self-hosted
Adaptive interface (feature flag)
- ☐ Per-learner density, chunking, navigation, visible hints from Learner Insights
- ☐ Remediation components: Feynman reflection, elaborative questions, worked examples
- ☐ Surveys with SUS, UEQ, NASA-TLX + behavioural metrics
Impact measurement
- ☐ Built-in A/B experiments per course; delayed retention (3–7 days) as primary metric
- ☐ Results visible to authors; opt-in per tenant, consent where required
Quality
- ✓ Component playground with model-facing descriptions (in the docs site instead of Storybook, ADR 0054,
default pending #57;
/catalogue/, L2-19) - ✓ Schema, fallback, interaction round-trip and accessibility tests per component (builder catalogue: vitest + axe in jsdom; axe on every studio screen in the e2e)
- ☐ Evals: right component choice, no raw markup outside
simulation, simulation pass rate (partial:course-builder:evalchecks interview component choice, DiffView for chat edits and no raw markup; simulations are M2.5)
Phase 3: Living Course (killer feature)
Section titled “Phase 3: Living Course (killer feature)”Plan: docs/plans/phase-3.md (approved 2026-10-09; ADRs 0030–0034 Accepted). Milestones
M3.1 revisions and fragment diff (re-upload) → M3.2 impact and staleness → M3.3 AI update proposals →
M3.4 progress rules → M3.5 audit and notifications → M3.6 Git, webhooks, polling → M3.7 URL connector →
M3.8 evals and E2E. Designs: front/docs/design/stitch/living-course/.
- ☐ Source connectors: re-upload → Git (path + branch) → Drive / Notion as plugins (partial: upload, Git
(GitHub, GitLab, Gitea/Forgejo) and URL connectors done, plugin contract and example connector in
docs/living-course/connector-plugins.md; Drive and Notion not built, see the(new)item) - ✓ Change detection (webhook, poll, manual) with fragment-level diff
- ✓ Impact analysis via citations, incl. quiz answers that may now be wrong
- ✓ Update proposals: patches with reasons, reviewed as one diff (accept all / per element / reject)
- ✓ Progress rules: minor edit keeps completion; changed quiz answer → re-attempt; never silently change past scores
- ✓ Staleness signals per course and element
- ✓ Audit trail (who accepted what, when, which source revision)
- ✓ Tests: source v1/v2 fixtures; progress survives accepted update (API:
ProgressSurvivesUpdateTest, eval fixtures with recorded live answers; studio e2e on the fake driver) - ✓ (new) URL connector (web pages on one host, CSS selector, HTML → Markdown)
- ✓ (new) Shared SSRF-safe HTTP client in
core(extracted fromlti; IPv6, CGNAT, redirects re-checked) - ✓ (new) GIFT: snapshot the max score per attempt and archive questions instead of deleting them
- ☐ (new) Suggest a new lesson for newly added, uncovered source sections (partial: uncovered sections are
listed in the proposal as
uncovereditems; no generated lesson proposal yet) - ☐ (new) Generic Git (
gitCLI) connector for hosts without a supported API - ☐ (new) Google Drive and Notion connector plugins (designed in
docs/plans/phase-3.md6.6)
Phase 4: Personalisation
Section titled “Phase 4: Personalisation”Plan (new): docs/plans/phase-4.md (draft, waiting for approval; ADRs 0057–0062 Proposed). Milestones M4.1 signal
stream → M4.2 risk scoring → M4.3 privacy and transparency → M4.4 nudges and recovery → M4.5 remediations →
M4.6 author analytics → M4.7 AI tutor → M4.8 adaptive interface, experiments, evals. Designs:
front/docs/design/stitch/personalisation/. Owner questions #59–#63.
4.1 learner-insights package
Section titled “4.1 learner-insights package”- ☐ Append-only learner signal stream mapped to blueprint element IDs; queues; backfill
- ☐ Rule-based risk scoring with human-readable reasons; per-tenant thresholds
- ☐
RiskScorerinterface for future ML - ☐ Statuses and events:
LearnerStruggling,LearnerAtRisk - ☐ Personal remediations (learner-scoped, grounded, cached per struggle pattern)
- ☐ Nudges via
notifications, rate-limited - ☐ Recovery rate measurement
- ☐ Author analytics; high-struggle elements → update proposals
- ☐ (new) Per-course analytics dashboard for authors and admins: progress, completion, quiz performance, at-risk learners and the sections that confuse people, per course (Phase 4, M4.6 author analytics; shown as “Coming” in the white-label section of the platform landing)
- ☐ Privacy: per-tenant toggle, retention, explanations, minimal data to LLM
- ☐ Rule unit tests, synthetic learner journeys, tenant isolation
- ☐ (new) Streaming tutor answers (LLM client streaming over the SSE event log)
- ☐ (new) Partition
learner_signalsby month when a tenant exceeds 50M rows - ☐ (new) Remediations for courses not built with the Course Builder
- ☐ (new)
TopicProgressUpdatedevent incoursesfor non-completion progress (ids only)
4.2 AI tutor
Section titled “4.2 AI tutor”- ☐ Answers only from course sources with citations; says when out of scope
- ☐ No quiz answers during active attempts
- ☐ Rate/cost limits; anonymised analytics
4.3 Trust and transparency
Section titled “4.3 Trust and transparency”- ☐ Reasons for every recommendation · AI on/off and provider per tenant
- ☐ AI content labelling · documented data flows, no training on customer content
Phase 5: UX and reference frontend
Section titled “Phase 5: UX and reference frontend”- ☐ Audit Wellms frontends; evolve or build new reference app (justify; SSR/SEO) (partial: new Astro
SSR app
front/web, ADR 0008 Proposed; landing, course, lesson player, quiz, finish for the three demo tenants; plan and numbers indocs/plans/phase-5-reference-frontend.md; uncommitted) - ☐ Themeable from builder presets; per-tenant theme (partial:
--ulams-*theme files per demo preset infront/ui/src/styles/themes, chosen fromtheme.theme; accent fromtheme.accentapplied server-side with AA contrast; builder presets not connected yet) - ☐ (new) Brand import: built-in brand importer (no agent needed) that generates a tenant theme preset from a Figma
file, a Stitch DESIGN.md or a brand guide (tokens, fonts, logo), proposed as a diff the admin approves (today an
agent with the ulams CLI and the design tool’s MCP does it:
ulams theme set,ulams settings set) - ☐ PWA offline mode with tested sync/conflict rules
- ☐ Single frontend for LMS and Sylius commerce (catalogue, checkout, account)
- ☐ Web components (my courses, continue, catalogue, quiz, tutor, certificate badge) (partial:
<ulams-quiz>,<ulams-h5p>,<ulams-video>,<ulams-progress>infront/ui/src/elements) - ☐ TypeScript SDK from OpenAPI; widget docs with live examples (partial:
@ulams/sdkinfront/sdk, fetch-only; request paths typed from the generated spec, response types hand-written because the spec has no response schemas; no widget docs yet) - ☐ Learner UX: “continue”, “what’s next”, progress and time estimates, short lessons
- ☐ Semantic search with cited AI answers
- ☐ WCAG 2.2 AA + European Accessibility Act; axe in CI (partial: reference frontend has landmarks, skip link, focus-visible, reduced motion, 360 px layout and a theme contrast test; axe scan of every page type in the Playwright e2e passes, not wired into CI yet)
- ☐ Admin UX: templates, guided empty states, sample course, bulk operations, saved filters
- ☐ AI transparency everywhere (diffs, citations, reasons one click away)
- ☐ Metrics: time to first course, time to first enrolment, admin task times
- ☐ (new) Reference frontend for the demos: UI catalogue
@ulams/ui(JSON-schema registry, renderer with fallbacks, landing pages as A2UI-shaped documents), BFF with httpOnly session, demo auto-login, SWR cache of public API data (partial: implemented and tested, uncommitted; needs the Caddy switch of*.app.localhostto :4321 and the API fixes listed in the plan) - ☐ (new) Platform product landing on the platform host (
app.localhost) with the live demos; account area; webinars/events/consultations pages (partial: implemented and tested, uncommitted; seedocs/plans/phase-5-reference-frontend.md, batch 2) - ✓ (new) Three new free demo academies,
gravity(Gravity Lab, 3D simulation),poland(Poland, Measured / Polska w liczbach, map in the background, EN and PL) andulam(The Scottish Book: Stanisław Ulam and the Lwów School, five MIT interactives), each with a preset, landing, certificate, demo users and the hourly reset (ADR 0089;docs/plans/interactive-demos.mdM3–M10) (done: M9a Ulam research as docs,docs/plans/interactive-demos-ulam-facts.mdanddocs/plans/interactive-demos-ulam-outline.md; the owner raised no objection in #151; M9b done (ADR 0095): the Ulam course (welcome, eight modules, sixteen lessons, 38 module quiz questions and a 14-question final test, five interactives, four licensed photographs), seeded by the demo seeders,demo-content/ulam/{facts,sources}.jsonandCREDITS.mdwritten from the sheet, the problem cards and the journey map filled with sourced content; M7 done: the three tenants, presets, landings, certificates, demo users, hourly reset and placeholder free courses (ADR 0093: public showcase endpoint behind the landing heroes); M8 done (ADR 0094): the gravity course (nine modules, 56 questions, final test) and the two poland courses (EN and PL, nine chapters each), fact-checked (demo-content/*/FACTCHECK.md), heroes playing the real scene and map; the landing hero of the Ulam course plays the spiral; open owner questions: #205 FACTCHECK reading, #209 the café photo, #210 Problem 77(a)) - ✓ (new) Six demos on the platform landing with one-click learner and admin login;
make demo-seed-tenantsseeds all six,make demo-reset-allresets them; README and docs site updated (M10; the phone layout of the lesson player no longer overflows with a long course title or a long source URL) - ✓ (new) The platform landing in Polish and Chinese (
/pl/,/zh/; ADR 0096): Astro i18n routes, one parallel document per language kept in step by a test, translated data files and component strings, hreflang and canonical URLs, a header language switcher that keeps the section, system CJK fonts; copy awaits a native review (owner-action issue) - ✓ (new) The demo landing heroes are clean, self-running showcase visuals instead of the lesson player (ADR 0093
amendment):
init.showcasein the bridge, a manifestshowcase(loop steps and still), a<ulams-showcase>hero that paints a still, starts after first paint, pauses off screen, stays a still under reduced motion and keeps a small “Try it” link; gravity (scene alone), poland (map layers, no panel) and ulam (the spiral winding out) - ✓ (new)
demo-content/: content packages (ADR 0088, amended 2026-10-09) with a boundary lint (workspace, lint, harness, the gravity (M3), poland (M4) and five Ulam (M5) packages, all with sourced content after M9b)- ✓ (new) gravity package: the owner’s simulator, MIT, 44 steps EN and PL, posters, bridge adapter, Playwright and axe on a throwaway server (M3)
- ✓ (new) poland package: map and charts in EN and PL, 40 steps, regenerated Natural Earth map, primary
sources only, shared map engine, posters, Playwright and axe (M4); figures not yet re-checked at their
publishers are listed in
demo-content/poland/README.md(M8b) - ✓ (new) five Ulam interactives, MIT:
spiral,monte-carlo,automaton,scottish-bookandlwow-map(M5; the last two now carry the sourced content of the fact sheet, M9b: nine problem cards, the journey notes and the inset;automatonimplements the Schrandt-Ulam rule) - ✓ (new) the Ulam course: “Stanisław Ulam and the Lwów School of Mathematics”, eight modules, a final test, a certificate and a sources lesson; every quiz question traced to the fact sheet (ADR 0095, M9b)
Phase 6: Market-essential modules
Section titled “Phase 6: Market-essential modules”6.1 Certificates and compliance
Section titled “6.1 Certificates and compliance”- ☐ Extend existing Wellms certificates (don’t duplicate)
- ☐ (new) Remove ReportBro completely and replace it with pdfme (recommended: MIT, actively
maintained, WYSIWYG designer for variable-based PDF templates, JSON templates, QR/barcode schemas).
Why: the ReportBro designer (
reportbro-designer, AGPL-3.0) is bundled into admin, the server image runsreportbro-lib(AGPL-3.0), and the defaultREPORTBRO_URLsends certificate data to reportbro.com. Removal checklist:- ☐ Admin: replace
components/PdfEditorandcomponents/TemplateFieldswith the@pdfme/uidesigner; dropreportbro-designerfromadmin/package.json(partial: designer with variables panel, API preview and save done, typecheck/build pass; not yet exercised in a browser; admin Jest is broken at baseline, so the new helper tests inPdfEditor/template.test.tsdo not run) - ☐ API
templates-pdf: replaceReportBroService/contract, thereportbro/report/runroutes andFabricPdfControllerwith a pdfme renderer client; keep the existing variables andCourseFinishedflow; store templates as pdfme JSON (partial: done and tested —PdfRendererContract,POST /api/admin/pdfs/preview, fonts proxy, certificates rendered once and stored; uncommitted) - ☐ Renderer: small MIT Node worker
api/pdf(pdfme generator; the API image has no Node), reached over HTTP likeapi/h5p; QR schema for certificate verification URLs (6.1) (partial: service, tests and Docker image done; the QR points to{APP_URL}/certificates/verify/{id}, the verification page does not exist yet) - ☐ Remove the
reportbroservice fromapi/docker-compose.yml,REPORTBRO_URLfrom config and.env.example, and its mentions in docs andLICENSING.md(partial: done; historical mentions remain indocs/reports/phase-0-audit.md,docs/plans/phase-0.mdand ADR 0002) - ☐ Migrate existing templates (one-off converter or re-create); pdfme templates for the demo
certificates; tests for template CRUD, rendering and the CourseFinished certificate (partial:
converter migration +
templates-pdf:migrate-reportbrodone; themed JSON for coffee/oncall/nightsky intemplates-pdf/resources/pdfme, not yet assigned by the demo seeding) - ☐ Until then: set
REPORTBRO_URLto the local server so no data leaves the installation (obsolete once the pdfme change is merged: ReportBro andREPORTBRO_URLare gone)
- ☐ Admin: replace
- ☐ Verification URL/QR, expiry, recertification, reminders
- ☐ Mandatory training with due dates and manager escalation
- ☐ Compliance reports and audit export (linked to Phase 3 audit trail)
6.2 Automated enrolment and paths
Section titled “6.2 Automated enrolment and paths”- ☐ Rule engine (attribute → path, due in N days), on change and on schedule
- ☐ Learning paths with prerequisites; lifecycle notifications
6.3 Identity and HR
Section titled “6.3 Identity and HR”- ☐ SAML 2.0 and OIDC per tenant · SCIM 2.0 · HRIS import (CSV/API first)
6.4 Commerce (Sylius) and extended enterprise
Section titled “6.4 Commerce (Sylius) and extended enterprise”- ☐
CommerceProviderinterface (sync product, create checkout, handle order events) (partial: interface and the Wellms cart adapter shipped inapi/packages/commerce, ADR 0049; the Sylius adapter is pending) - ☐ Sylius adapter as default implementation
- ☐ Entitlements model in LMS (access, validity, source order or seat package)
- ☐ Catalogue sync: course/bundle/subscription/seat package → digital Sylius product
- ☐ Order → entitlement via signed, idempotent webhooks + reconciliation job; never grant access from frontend redirect
- ☐ Single login: LMS as identity source; Sylius customer linked on first checkout
- ☐ Tenant ↔ Sylius channel (catalogue, prices, currency, locale, tax zone), created at provisioning
- ☐ Unified admin: prices, coupons, orders, refunds in LMS admin; native Sylius admin for advanced settings
- ☐ B2B seat packages (Sylius product + LMS seat pool), invoices from Sylius
- ☐ Migration of orders, vouchers and access from Wellms packages with zero lost access
- ☐ EU VAT (OSS) for digital services: confirm Sylius handling, document options
- ☐ Per-tenant branding, catalogue, sales pages
- ☐ Partner/customer portals with scoped admins and reports
6.5 Analytics and ROI
Section titled “6.5 Analytics and ROI”- ☐ Dashboards (completion, results, struggle/recovery, certificates, overdue)
- ☐ BI export/API, scheduled reports, optional business KPI link
6.6 Skills and competencies
Section titled “6.6 Skills and competencies”- ☐ Competency framework; AI-suggested tags confirmed by author
- ☐ Learner skill profiles feeding Learner Insights and path rules
Phase 7: Developer experience and agents
Section titled “Phase 7: Developer experience and agents”7.1 Course-as-code
Section titled “7.1 Course-as-code”- ☐ Markdown + YAML/JSON format with JSON Schema
- ☐ CLI
ulams: init, validate, preview, push, pull, diff, publish - ☐ Two-way Git sync with diff-based conflicts
- ☐ GitHub Action + Docker image; preview deployment per PR
- ☐ Git merge triggers Living Course update proposal
- ☐ (new) CLI plan
docs/plans/cli.md: agent-firstulamsCLI and MCP server (draft, waiting for approval; ADRs 0072–0079) - ✓ (new) M1 CLI core:
front/cliworkspace, command registry, output contract and exit codes, profiles,login(token/password/demo),whoami,ulams api,schema,describe - ✓ (new) M2 noun commands generated from OpenAPI + overrides, topic uploads of every type, pagination,
--dry-run,--wait,apply -f, coverage matrix enforced in CI - ✓ (new) M4 course builder commands with AG-UI events as NDJSON,
--waiton run status, builder and Living Course commands as MCP tools (ADR 0084; e2e on a fake-driver tenant) - ✓ (new) CLI device login against the real server,
ulams tokens,logout --revoke,ulams login --scopes(e2e with a Playwright approval) - ✓ (new)
make dev-reloadrebuilds the class map and per-domain caches; device login endpoints get their own rate limit buckets - ✓ (new) OpenAPI covers the course builder and Living Course; SDK types and CLI spec regenerated (504 of 547 operations covered, 0 missing)
- ✓ (new) “My tokens” on the web account page (create, list, revoke; axe)
- ☐ (new) M5 course-as-code: Blueprint v2, Markdown + directives format, sync base and conflict diffs (after Phase 3; citations for author blocks pending #78)
- ☐ (new) M6 CLI release: npm
ulams(pending #77), bun-compiled binaries (signing pending #76), Docker image - ☐ (new) Endpoints to import a blueprint as a builder version and export a course as a blueprint (for M5)
7.2 Code exercises
Section titled “7.2 Code exercises”- ☐ WebContainers/Sandpack (JS/TS), Pyodide (Python), optional server sandbox
- ☐ Autograding with author tests → attempts and learner signals
- ☐ AI hints without revealing solutions
7.3 API, SDK, webhooks
Section titled “7.3 API, SDK, webhooks”- ☐ Complete published OpenAPI; TS SDK first, PHP second
- ☐ Stripe-style webhooks (signed, retries, replay, delivery log, test sends, versioned events)
- ☐ Scoped API keys with rate limits and usage stats
- ✓ (new) S1 scoped personal access tokens (
area:read|write, presets, fail-closed route map), agent audit log,Idempotency-Key,X-Request-Id,GET /api/meta; admin “API tokens” page (ADR 0074) - ✓ (new) S2 device login: own RFC 8628 flow +
/cli/authorizepage in the web app (ADR 0075; pending #74) - ✓ (new) S3 platform tenant API with queued provisioning, off by default,
ulams tenants …(ADR 0078, 0085; #79 default “off by default”; ADR acceptance pending) - ✓ (new) S4 course builder run-status endpoint
GET /api/admin/course-builder/runs/{run} - ☐ (new) S5 OpenAPI response schemas for the top 60 operations the CLI uses, after L0-11; stable operationIds
- ☐ (new) API browser in the docs site (Scalar over
api/scripts/openapi.php, built in docs CI without a database); OpenAPI forimagesfixed and response bodies added for video states, files and course import (partial: implemented in the feature-catalogue PR, pending merge; questionnaire and question endpoints now have response bodies too) - ☐ (new) Feature index on the platform landing (
FeatureIndex, 14 collapsed groups, 101 features including MCP, teacher and sales analytics, 54 H5P content types, EN/PL/ZH,ItemListJSON-LD) with a drift test againstapi/packages(partial: implemented in the feature-catalogue PR, pending merge) - ☐
npx create-ulams/docker compose upwith seed data - ☐ Docs site with runnable examples; free cloud sandbox tenant (partial: Starlight site in
front/docs-siteon branchdocs/starlight-site; runnable examples and the sandbox tenant pending)
7.4 Plugin system
Section titled “7.4 Plugin system”- ☐ Extension points (content, UI catalogue, connectors, risk rules, webhooks, admin pages)
- ☐ Manifest, versioning, compatibility checks; marketplace-ready design (not built)
7.5 MCP server
Section titled “7.5 MCP server”- ☐ Admin/author tools (courses, builder, enrolment, reports, update proposals, certificates)
- ☐ Learner tools (my courses, next lesson, quiz, tutor) with progress recorded
- ☐ Rich UI: A2UI over MCP → MCP Apps for simulations → Markdown fallback
- ☐ Delegate commerce actions to Sylius MCP tool
- ☐ Agent safety: scoped tokens, dry-run, idempotency keys, human approval, rate limits, agent audit log
- ☐ Tool description evals with typical agent tasks
- ☐ (new) First version on Cloudflare Workers (TypeScript, Agents SDK, OAuth) against the current REST
API: hand-written course/topic/quiz tools + tools generated from the OpenAPI spec
(note: the CLI plan proposes the local
ulams mcpfirst and this as the later hosted variant from the same registry; pending #75) - ✓ (new) M3
ulams mcp(stdio + Streamable HTTP) generated from the CLI registry: toolsets, annotations, confirmation for destructive tools, resources, MCP client tests, agent eval (ADR 0076)
7.6 Machine-readable content
Section titled “7.6 Machine-readable content”- ☐
llms.txt, Markdown version of every page, public schemas,AGENTS.md - ☐ Knowledge export (cited chunks for company RAG), auto re-export on change
Phase 8: Self-hosting
Section titled “Phase 8: Self-hosting”-
☐ (new) R&D: Cloudflare deployment (TypeScript + Hono gateway Worker, Laravel as Cloudflare Container, R2, Hyperdrive, Queues, Durable Objects) with a one-command
wrangler deployfor a dev environment; strangler migration of the API to Hono only if the spike succeeds -
☐ (new) Shared-hosting variant: the API on MyDevil.net with cron workers and Cloudflare in front (
deploy/mydevil, docsoperators/install-mydevil, ADR 0091; partial: runbook, scripts and the code changes (ulams:tenant:work-once, manual tenant database, R2 policy switch) done, not run on a real account: needs SSH andbin/check-host.sh) -
☐ One app image + PostgreSQL + optional Redis (DB fallback)
-
☐ Commerce as optional profile: + one Sylius image, shared PostgreSQL server (separate DB)
-
☐
docker compose upand Helm chart with sane defaults -
☐ Setup wizard (admin, domain, mail, storage, AI provider or none, commerce link)
-
✓ (new) Production reference: one VPS behind Cloudflare (tunnel, flat tenant hosts, R2, cache rules, backups to R2, install, upgrade, backup, restore and tenant scripts) in
deploy/vps-cloudflare/, guide in Operators, ADR 0092. Prepared and validated locally with the published images and MinIO; nothing is deployed -
☐ (new) First production deployment on the VPS and Cloudflare (owner publishes later, issue #24): staging install, restore drill, R2 custom domains, tunnel, then the launch switches (landing
actual, CSP enforce) -
☐ Any AI provider (OpenAI-compatible, Anthropic, Ollama/vLLM); full function without AI
-
☐ Air-gapped: no telemetry by default, no external CDNs, offline licence, data residency
-
☐ Safe migrations, stable/LTS channels, backup/restore tested in CI
-
☐ Health checks, Prometheus metrics, structured logs, zero-downtime guidance
-
☐ SBOM, signed images, CVE scanning, hardening guide, ISO 27001 / GDPR audit docs
Quality bar (every phase)
Section titled “Quality bar (every phase)”- ☐ Mocked LLM in tests + eval command on golden fixtures (schema, citation coverage, quiz answers supported, duration, cost)
- ☐ Tenant isolation tests for every endpoint
- ☐ WCAG 2.2 AA checks for learner-facing UI
- ☐ A/B experiment + delayed-retention metric for every feature claiming learning benefit
- ☐ E2E: upload → interview → live course → chat edit → source change → accepted update → progress intact
- ☐ Commerce E2E: buy → webhook → access granted; refund → access revoked; missed webhook repaired by reconciliation
- ☐ Existing tests and linters green; H5P/SCORM unchanged; README per package
Out of scope (for now)
Section titled “Out of scope (for now)”Image/video generation, AI avatars, custom themes beyond presets, real-time multi-author collaboration, fine-tuning, billing for the builder itself.
Key references
Section titled “Key references”- Google Research, learning interactives (2026): research.google/blog/the-future-of-practice-enabling-teachers-to-create-learning-interactives-with-generative-ui/ · arXiv 2609.20738
- Learn Your Way: arXiv 2509.13348, 2509.18664
- A2UI and MCP Apps: developers.googleblog.com/en/a2ui-and-mcp-apps/
- Sylius: github.com/Sylius/Sylius