Skip to content

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.


  • ✓ (2026-10-11) Feature catalogue (docs/plans/feature-catalogue.md): Scalar as the docs API browser, a new FeatureIndex catalogue component for the landing, H5P content types taken from h5p.org, landings translated (PL, ZH) and docs English only, ItemList JSON-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-insights package

  • ✓ 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 simulation component

  • ✓ 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; CommerceProvider interface; Wellms payments/cart/vouchers retired 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/), all escolalms/* 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 recommender from 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 ulams CLI core now (login and tokens, ulams api, main nouns, ulams mcp); course-as-code after Phase 3. Plan docs/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/Gravity and 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

  • ☐ Final name (favourite ULAMS; alternatives Wellam, Monte, Spiral, Automata, UlamOS)
    • ✓ GitHub organisation: ulams is taken; register ulams-dev (fallbacks: ulams-hq, ulamslabs, ulams-ai) (note: ulams-dev/ulams created 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
  • ✓ Move MCP server (7.5) right after Phase 2? Cheap to build, strong demo (yes, 2026-10-09: the local ulams mcp ships 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)

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.


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.

  • ✓ 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 recommender be 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.xml is 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)
  • ✓ (new) Monorepo with the full history of the three repositories under api/, admin/, front/
  • ✓ (new) Vendor the 50 PHP packages into api/packages; no escolalms/* in composer.json
  • ✓ (new) Vendor the JS libraries into front/src/lib and admin/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-editor or headless-h5p left in composer.json/composer.lock or the code; Laravel keeps only the read-only api/packages/h5p index and HTTP client (ADR 0003)
  • ✓ (new) Remove recommender and 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 from docs/, coverage check over packages, admin routes, learner routes and topic types, GitHub Pages deploy (partial: on branch docs/starlight-site, not merged; Pages source and private vulnerability reporting to be enabled)
  • ☐ (new) Remaining legacy references (partial: escolalms/php replaced by a base built in-repo, ReportBro removed and replaced by pdfme): replace the escolalms/php and escolalms/reportbro-server images, decide on upstream provenance links, reword ADR prose, retarget Docker Hub publishing workflows, replace the ulams.app placeholder 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.yml with 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 on main and run on GitHub; admin Jest (3 suites) and the front tests run in the js job; yarn installs are frozen; the quarantine is empty)
  • ✓ (new) Remove the non-existent packages/tracker/src path 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-generator moved to require-dev; tzsk/sms stays ^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 with ulams:tenant:work-once, one builder/long-job process, one scheduler loop, no Horizon, php-fpm ondemand; ADR 0083 amendment; per-tenant stays the production default)
  • ✓ (new) Replace the GPL PHP libraries trax2/framework (lrs) and laraveldaily/laravel-invoices with 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 orWhere in CourseAccessService::getUserCourseIds and 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::enforceMorphMap for 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_DISK follows SCORM_DISK; cmi5:move-to-bucket copies old packages; AUs play from the content origin in front/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:api and tags_list on admin tag routes, POST api/images/img limits and throttle, client payment parameters allow-listed with server price/currency/trial values winning, payProduct purchasability, vouchers search grouping, GroupTree depth limit and cycle safety, _ignition absent from demo and production images (ADR 0071); POST api/cmi5/fetch no 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_enabled columns (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 mjml compose service is not on the ulams network and MJML_API_URL is 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 as ulams/php:8.3 with source offers for its GPL programs (see api/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.tsx calls stripeKey.includes on 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): sharp 0.35 fails to load in the docs-site build on the CI runner (MissingSharp; Astro depends on sharp ^0.34); @ant-design/pro-components 2.8.5 to 2.8.10 break admin typecheck (@ant-design/pro-form 2.31.5+ declaration files import src/...). 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) and front (vite, @typescript-eslint 7, legacy .eslintrc); the other five packages are on eslint 10 already. The Dependabot major ignore for eslint and @eslint/js comes off when both move to flat config

  • ☐ (new) Prettier 3 in admin (2.8.8) and front (2.4.1): deferred, Dependabot major ignore for prettier. Front has no prettier config or CI check (only the lint-staged step, which already skips src/lib and the other front/* workspaces), so the upgrade would reformat all of front/src (about 600 files; trailing-comma default changes) for no behavioural gain; admin’s CI step resolves prettier/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 front cart: @stripe/react-stripe-js 7 needs @stripe/stripe-js 10 (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::delete checking the read permission (new cmi5_delete permission, admins only; plan L0-09)

  • ☐ (new) Five packages with @OA\ annotations are missing from the Swagger scan paths (plan L0-11)

  • ✓ 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-cart vendored as api/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 abandoned doctrine/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)

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.

  • ✓ 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.md 5.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.sh in the dev api container once (the image build does it; the bind mount hides it)
  • ✓ 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/adapt behind ADAPT_SOURCE_ENABLED with versioned sources, structural validation, queued build and import through Path A; GPL worker api/adapt-builder (adapt_framework v5.56.2, compose profile adapt, real build round trip in the nightly conformance workflow); ADR 0013 (Proposed); an admin screen pending)
  • ✓ 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-keys monthly, provisioning step lti_keys, single-use hints/state/nonce/jti in lti_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-tool frame-src in 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 reads GET /api/lti/frame-origins)
  • ✓ (new) Run ulams:lti:rotate-keys --init for existing tenants (new tenants get it at provisioning) (done by ulams:upgrade, step lti_keys, ADR 0081)
  • ✓ 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 profile av not 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/content from local or bucket disks (ADR 0046); the front/admin CSP is enforced in development and switches with CSP_ENFORCE (ADR 0044))
  • ✓ (new) Zip-slip: SCORM (ScormService::unzipScormArchive) and cmi5 (Cmi5UploadService) extract archives with ZipArchive::extractTo and no entry-path checks; replace with a safe extractor (M1.1)
  • ✓ (new) The SCORM player loads scorm-again from the jsDelivr CDN; vendor it (air-gapped installs)
  • ✓ (new) SCORM/cmi5 content of all tenants is served from the shared storage.localhost origin; move packages and players to a per-tenant content origin (M1.1) (SCORM and cmi5 done, <slug>.content.localhost, api/docs/content-origin.md; run ulams:tenant:sync-env so existing tenants get CONTENT_ORIGIN)
  • ✓ (new) Course import read files outside the extracted archive through paths in content.json (e.g. ../../../.env as a category icon, published to the bucket); paths now resolved inside it
  • ✓ (new) SVG/HTML uploads served from the bucket: stored with Content-Disposition: attachment and an extension-based Content-Type; storage origin sends script-src 'none' for SVG (follow-up of 0.2)
  • ✓ (new) Students have no scorm_track-update permission, so the legacy /api/scorm/track rejects them and the front’s legacy SCORM player never tracked (seeded for students, confirmed 2026-10-09; re-run PermissionsSeeder on 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 in api/docs/content-origin.md; deployment pending). Note 2026-10-09: the owner chose the same-site *.content.ulams.app instead; 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’s frame-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 list GET /api/admin/csp-reports, report-uri/report-to on every policy, the front builds its CSP per request, CSP_ENFORCE and ULAMS_CSP_HEADER switch enforcement; ADR 0044; production turns it on after a clean week, see operators/security-headers)
  • ✓ (new) H5P service multitenancy via its TenantResolver (per-tenant key, database, bucket) (env-file resolver; per-tenant H5P_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 _token in 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) TopicFinished fired 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 /h5p proxy adds the session token server-side for the player’s own calls, the frame still gets token: null and the model’s URLs carry no _token)
  • ✓ (new) Production: set H5P_SERVICE_CONFIG_DIR, run ulams:h5p:export-config and start the H5P service with compose.h5p.prod.yml
  • ✓ (new) H5P xAPI progress endpoint rejects statement objects (ProgressService::h5p() typed string) (plan L1-06; the statement is stored as JSON, its verb as the event)
  • ☐ (new) yarn install on Node 24 fails in admin’s postinstall (max setup: umi’s esmi feature loads http-deceiver, which needs the removed http_parser binding); CI and .nvmrc use Node 22

Plan: docs/plans/interactive-demos.md (M1–M2); ADRs 0086, 0087.

  • ✓ (new) Interactive topic type: versioned zip packages with a ulams-interactive.json manifest, 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-ix v1 bridge protocol and the MIT @ulams/interactive-bridge library (ADR 0087) (PR #162)
  • ✓ (new) InteractiveLesson catalogue 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-interactive and its MCP tool; docs pages for creators, the bridge and the content origin (PR #173)

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, branch phase-2/course-builder; ADR 0022)
  • ✓ (new) AG-UI event log and SSE stream from Laravel, carrying A2UI surfaces (ADR 0011; A2UI as a2ui-surface activity snapshots, ADR 0023; cache-key wake instead of pub/sub, ADR 0029)
  • ✓ (new) Builder components in @ulams/ui and the course landing document in the catalogue format
  • ✓ (new) Studio: edit the Course Brief from the brief panel (Edit on a row opens the interview’s own control, saves through PUT …/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/ui for dev-mode validation (plan 13.2; L2-02; the studio validates a2ui-surface envelopes 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 daily course-builder:prune-events, a Horizon queue for builder jobs
  • ☐ (new) Run the opt-in cross-tenant check TenantIsolationTest::testCourseBuilderSessionsDoNotCrossTenants (written; needs TENANCY_INTEGRATION=1 and 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.lock with a real yarn install (a fresh yarn install leaves it unchanged; --frozen-lockfile in CI is the check) (entries for @ag-ui/core 1.0.2 and diff 9.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)
  • ✓ 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)
  • ✓ 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.md 6.4)
  • ✓ Adaptive chips/buttons with defaults and “decide for me”
  • ✓ Audience, duration, tone, theme preset + accent, free/paid (via CommerceProvider; interim: existing payments), 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 through CommerceProvider, 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-job for video and course clone; workers and Horizon with matching timeouts; config test QueueRetryAfterConfigTest (ADR 0083 amendment)
  • ✓ 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)
  • ✓ 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)

Architecture

  • ✓ Verify current A2UI / AG-UI versions and choose renderer (CopilotKit vs own) (A2UI v0.9, @ag-ui/core 1.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_ui validated server-side; invalid/unknown → text fallback (structured output choice validated against the @ulams/ui manifest)
  • ✓ 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.md M6): catalogue documents as LMS topics so flip cards, timelines and practice activities can be course items; generation stays in L2-21 (API package topic-type-layout with 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-layout and the MCP tool, docs)

Pedagogical guardrails

  • ☐ Mandatory scaffolding: intro → toolbox → graded challenges → tiered hints → explanatory feedback → worked solution after attempt (partial: the PracticeActivity component 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)

  • ☐ simulation component: 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:eval checks interview component choice, DiffView for chat edits and no raw markup; simulations are M2.5)

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 from lti; 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 uncovered items; no generated lesson proposal yet)
  • ☐ (new) Generic Git (git CLI) connector for hosts without a supported API
  • ☐ (new) Google Drive and Notion connector plugins (designed in docs/plans/phase-3.md 6.6)

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.

  • ☐ Append-only learner signal stream mapped to blueprint element IDs; queues; backfill
  • ☐ Rule-based risk scoring with human-readable reasons; per-tenant thresholds
  • ☐ RiskScorer interface 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_signals by month when a tenant exceeds 50M rows
  • ☐ (new) Remediations for courses not built with the Course Builder
  • ☐ (new) TopicProgressUpdated event in courses for non-completion progress (ids only)
  • ☐ Answers only from course sources with citations; says when out of scope
  • ☐ No quiz answers during active attempts
  • ☐ Rate/cost limits; anonymised analytics
  • ☐ Reasons for every recommendation · AI on/off and provider per tenant
  • ☐ AI content labelling · documented data flows, no training on customer content

  • ☐ 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 in docs/plans/phase-5-reference-frontend.md; uncommitted)
  • ☐ Themeable from builder presets; per-tenant theme (partial: --ulams-* theme files per demo preset in front/ui/src/styles/themes, chosen from theme.theme; accent from theme.accent applied 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> in front/ui/src/elements)
  • ☐ TypeScript SDK from OpenAPI; widget docs with live examples (partial: @ulams/sdk in front/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.localhost to :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; see docs/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) and ulam (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.md M3–M10) (done: M9a Ulam research as docs, docs/plans/interactive-demos-ulam-facts.md and docs/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}.json and CREDITS.md written 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-tenants seeds all six, make demo-reset-all resets 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.showcase in the bridge, a manifest showcase (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-book and lwow-map (M5; the last two now carry the sourced content of the fact sheet, M9b: nine problem cards, the journey notes and the inset; automaton implements 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)

  • ☐ 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 runs reportbro-lib (AGPL-3.0), and the default REPORTBRO_URL sends certificate data to reportbro.com. Removal checklist:
    • ☐ Admin: replace components/PdfEditor and components/TemplateFields with the @pdfme/ui designer; drop reportbro-designer from admin/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 in PdfEditor/template.test.ts do not run)
    • ☐ API templates-pdf: replace ReportBroService/contract, the reportbro/report/run routes and FabricPdfController with a pdfme renderer client; keep the existing variables and CourseFinished flow; 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 like api/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 reportbro service from api/docker-compose.yml, REPORTBRO_URL from config and .env.example, and its mentions in docs and LICENSING.md (partial: done; historical mentions remain in docs/reports/phase-0-audit.md, docs/plans/phase-0.md and 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-reportbro done; themed JSON for coffee/oncall/nightsky in templates-pdf/resources/pdfme, not yet assigned by the demo seeding)
    • ☐ Until then: set REPORTBRO_URL to the local server so no data leaves the installation (obsolete once the pdfme change is merged: ReportBro and REPORTBRO_URL are gone)
  • ☐ 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)
  • ☐ Rule engine (attribute → path, due in N days), on change and on schedule
  • ☐ Learning paths with prerequisites; lifecycle notifications
  • ☐ 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”
  • ☐ CommerceProvider interface (sync product, create checkout, handle order events) (partial: interface and the Wellms cart adapter shipped in api/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
  • ☐ Dashboards (completion, results, struggle/recovery, certificates, overdue)
  • ☐ BI export/API, scheduled reports, optional business KPI link
  • ☐ Competency framework; AI-suggested tags confirmed by author
  • ☐ Learner skill profiles feeding Learner Insights and path rules

  • ☐ 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-first ulams CLI and MCP server (draft, waiting for approval; ADRs 0072–0079)
  • ✓ (new) M1 CLI core: front/cli workspace, 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, --wait on 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-reload rebuilds 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)
  • ☐ WebContainers/Sandpack (JS/TS), Pyodide (Python), optional server sandbox
  • ☐ Autograding with author tests → attempts and learner signals
  • ☐ AI hints without revealing solutions
  • ☐ 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/authorize page 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 for images fixed 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, ItemList JSON-LD) with a drift test against api/packages (partial: implemented in the feature-catalogue PR, pending merge)
  • ☐ npx create-ulams / docker compose up with seed data
  • ☐ Docs site with runnable examples; free cloud sandbox tenant (partial: Starlight site in front/docs-site on branch docs/starlight-site; runnable examples and the sandbox tenant pending)
  • ☐ Extension points (content, UI catalogue, connectors, risk rules, webhooks, admin pages)
  • ☐ Manifest, versioning, compatibility checks; marketplace-ready design (not built)
  • ☐ 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 mcp first 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)
  • ☐ llms.txt, Markdown version of every page, public schemas, AGENTS.md
  • ☐ Knowledge export (cited chunks for company RAG), auto re-export on change

  • ☐ (new) R&D: Cloudflare deployment (TypeScript + Hono gateway Worker, Laravel as Cloudflare Container, R2, Hyperdrive, Queues, Durable Objects) with a one-command wrangler deploy for 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, docs operators/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 and bin/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 up and 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


  • ☐ 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

Image/video generation, AI avatars, custom themes beyond presets, real-time multi-author collaboration, fine-tuning, billing for the builder itself.

  • 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