tessera-learn 0.0.4 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +18 -8
- package/dist/plugin/index.js +3 -2
- package/dist/plugin/index.js.map +1 -1
- package/package.json +1 -1
- package/src/plugin/export.ts +6 -2
- package/src/runtime/adapters/cmi5.ts +371 -94
- package/src/runtime/xapi/publisher.ts +24 -5
package/AGENTS.md
CHANGED
|
@@ -983,14 +983,14 @@ The runtime translates author intent (page visits, quiz scores, completion, pers
|
|
|
983
983
|
|
|
984
984
|
| Runtime event | SCORM 1.2 | SCORM 2004 4th | cmi5 |
|
|
985
985
|
|---------------|-----------|----------------|------|
|
|
986
|
-
| Session start | `LMSInitialize("")`; read `cmi.suspend_data` and `cmi.interactions._count` | `Initialize("")`; read `cmi.suspend_data` and `cmi.interactions._count` | `POST` cmi5 `fetch` URL → token;
|
|
986
|
+
| Session start | `LMSInitialize("")`; read `cmi.suspend_data` and `cmi.interactions._count` | `Initialize("")`; read `cmi.suspend_data` and `cmi.interactions._count` | `POST` cmi5 `fetch` URL → token; `GET` `LMS.LaunchData` State (§10, → session id + Publisher Activity + launchMode + returnURL + masteryScore + moveOn); `GET` `cmi5LearnerPreferences` Agent Profile (§11); build publisher; send **Initialized**; `GET` `tessera-state` for resume |
|
|
987
987
|
| State persisted (page visited, bookmark moved, chunk revealed, `usePersistence` write, etc.) | `LMSSetValue("cmi.suspend_data", json)` (microtask-coalesced) | `SetValue("cmi.suspend_data", json)` (microtask-coalesced) | State API `PUT` `tessera-state` document, chained on the publisher queue |
|
|
988
988
|
| Graded quiz scored | `LMSSetValue("cmi.core.score.raw"\|"min"\|"max", …)` then `LMSSetValue("cmi.core.lesson_status", "passed"\|"failed")` | `SetValue("cmi.score.raw"\|"min"\|"max"\|"scaled", …)` then `SetValue("cmi.success_status", "passed"\|"failed")` | **Passed** or **Failed** statement, with `result.score.scaled` and `result.duration` (one-shot per session) |
|
|
989
|
-
| Course completion changes | Funneled into `cmi.core.lesson_status` (only one field exists) | `SetValue("cmi.completion_status", "completed"\|"incomplete")` | **Completed** statement with `result.completion = true
|
|
989
|
+
| Course completion changes | Funneled into `cmi.core.lesson_status` (only one field exists) | `SetValue("cmi.completion_status", "completed"\|"incomplete")` | **Completed** statement with `result.completion = true` and `result.duration` (one-shot per session). cmi5 §9.5.1 forbids `score` on Completed — the score rides on the subsequent **Passed**/**Failed** instead. |
|
|
990
990
|
| Author marks complete (`completion.mode: "manual"`) | `cmi.core.lesson_status = "completed"` (or `"passed"`/`"failed"` if `requireSuccessStatus` set) | `cmi.completion_status = "completed"`; `cmi.success_status = "unknown"` (or `"passed"`/`"failed"` if `requireSuccessStatus` set) | **Completed** statement; **Passed**/**Failed** if `requireSuccessStatus` set |
|
|
991
991
|
| Question answered (graded or standalone, inside or outside a quiz) | `cmi.interactions.{n}.id` / `student_response` / `result` / `time` / `type` (n continues from prior `_count`) | `cmi.interactions.{n}.id` / `learner_response` / `result` / `timestamp` / `type` (n continues from prior `_count`) | **Answered** statement; object `${activityId}#${questionId}`, definition `cmi.interaction` + `interactionType`, `result.response`, `result.success` |
|
|
992
992
|
| Resume after reload | Read `cmi.suspend_data` on init; manifest is rebuilt from code, not LMS | Read `cmi.suspend_data` on init | State API `GET` `tessera-state`; lifecycle replays from where the prior session left off |
|
|
993
|
-
| Author exit / unload | `LMSSetValue("cmi.core.exit", "suspend"\|"")`, `LMSCommit("")`, `LMSFinish("")` (queue drained synchronously) | `SetValue("cmi.exit", "suspend"\|"normal"\|...)`, `Commit("")`, `Terminate("")` (queue drained synchronously) |
|
|
993
|
+
| Author exit / unload | `LMSSetValue("cmi.core.exit", "suspend"\|"")`, `LMSCommit("")`, `LMSFinish("")` (queue drained synchronously) | `SetValue("cmi.exit", "suspend"\|"normal"\|...)`, `Commit("")`, `Terminate("")` (queue drained synchronously) | **Terminated** (always last on the wire, cmi5 §9.3.6). Explicit-exit path: `adapter.exit()` drains the queue then redirects to `returnURL` (§10.2.6). No Suspended verb — incomplete exit is signalled by Terminated without a preceding Completed; the LMS handles Abandoned and resume on next launch. |
|
|
994
994
|
| Learner identity (xAPI actor synthesis) | `cmi.core.student_id` + `cmi.core.student_name` | `cmi.learner_id` + `cmi.learner_name` | Launch-supplied actor JSON (Identified Agent) |
|
|
995
995
|
| Persistence cap | ~4096 chars per spec; many LMSes allow more, but plan for 4 KB | 64000 chars per spec | LRS-defined (typically unbounded for State API documents) |
|
|
996
996
|
| Score scale exposed to LMS | `score.raw` only (0–100) | `score.raw` (0–100) **and** `score.scaled` (0–1) | `result.score.scaled` (0–1) |
|
|
@@ -1027,15 +1027,25 @@ API discovery: `API_1484_11` via the same parent/opener walk.
|
|
|
1027
1027
|
|
|
1028
1028
|
**Token fetch is single-use** (cmi5 §6.2). On failure, reload from the LMS to retry. The token is used as a `Basic` credential, not Bearer.
|
|
1029
1029
|
|
|
1030
|
-
**Lifecycle order.** **Initialized** → **Answered** (per question on submit) → **Completed** → **Passed** / **Failed** → **
|
|
1030
|
+
**Lifecycle order.** **Initialized** → **Answered** (per question on submit) → **Completed** → **Passed** / **Failed** → **Terminated** (always last, cmi5 §9.3.6). Completed / Passed / Failed are one-shot per session; once dispatched, the corresponding setter no-ops. A reloaded session may re-dispatch them, which is intended: each session sends its lifecycle exactly once. **Satisfied** and **Suspended** are not emitted by the AU — Satisfied is LMS-only (§9.3.9), and Suspended isn't a cmi5 verb (§9.3 enumerates nine; the LMS handles Abandoned / resume on relaunch).
|
|
1031
1031
|
|
|
1032
|
-
**Required result fields.** Completed: `completion: true`, `duration
|
|
1032
|
+
**Required result fields.** Completed: `completion: true`, `duration` (no `score` — §9.5.1 forbids it). Passed: `success: true`, `duration`, `result.score.scaled` when known (§9.3.4 requires `scaled >= masteryScore` when present). Failed: `success: false`, `duration`, `result.score.scaled` when known (§9.3.5 requires `scaled < masteryScore` when present). Terminated: `duration` (§9.5.4.1). On contradiction the verb is preserved and the score is dropped with a console warning.
|
|
1033
1033
|
|
|
1034
|
-
**
|
|
1034
|
+
**Context per Defined Statement.** Categories: `cmi5` Category Activity on every Defined Statement (§9.6.2.1); plus `moveOn` Category on Completed / Passed / Failed (§9.6.2.2). Extensions: `sessionid` (§9.6.3.1) on every statement (Defined and Allowed) — value sourced from `LMS.LaunchData.contextTemplate` when supplied, else minted UUID. `masteryScore` extension on Passed / Failed only (§9.6.3.2). The full `contextTemplate` from `LMS.LaunchData` is merged in (§9.6.2 makes it the AU's base context; §10.2.1 says AU MUST NOT overwrite template values, so the AU's categories are concatenated and deduped against the template's, never replacing them).
|
|
1035
1035
|
|
|
1036
|
-
|
|
1036
|
+
**`LMS.LaunchData` (§10).** Fetched once at init from the State API under `stateId='LMS.LaunchData'`. The AU reads `contextTemplate`, `launchMode`, `returnURL`, `launchParameters`, `masteryScore`, and `moveOn` from it. LaunchData values override anything parsed from the launch URL (§10.2.4 makes LaunchData authoritative). When the document is absent, statements ship without the LMS-supplied Publisher Activity and may be rejected by strict LRSes — a console warning fires.
|
|
1037
1037
|
|
|
1038
|
-
**
|
|
1038
|
+
**Learner Preferences (§11).** `cmi5LearnerPreferences` from the Agent Profile API, fetched *before* Initialized — strict LRSes (SCORM Cloud) track that the GET happened and reject Initialized otherwise. A 404 here is normal (no preferences set); only the GET itself is required. Exposed to author code via `adapter.getLearnerPreferences()`.
|
|
1039
|
+
|
|
1040
|
+
**Launch mode (§10.2.2).** "Normal" launches emit the full lifecycle. "Browse" and "Review" launches emit only Initialized and Terminated — every other Defined Statement is silently suppressed. Exposed via `adapter.getLaunchMode()`.
|
|
1041
|
+
|
|
1042
|
+
**Return URL (§10.2.6).** `adapter.exit()` is the explicit-exit path: calls `terminate()`, awaits the publisher queue so Terminated lands before navigation, then `window.location.assign(returnURL)`. The page-unload `terminate()` path can't redirect (the browser is already navigating). Bare `getReturnURL()` is available for authors who want to inspect the URL without triggering exit.
|
|
1043
|
+
|
|
1044
|
+
**State persistence.** `tessera-state` document via the State API, keyed by `activityId` + `agent` + `registration?` + `stateId='tessera-state'` (distinct from the LMS-owned `LMS.LaunchData` and `cmi5LearnerPreferences` documents). Writes chain onto the publisher's queue so the suspend payload lands before Terminated.
|
|
1045
|
+
|
|
1046
|
+
**Manifest (`cmi5.xml`).** Generated by the plugin: course id + AU id are stable URNs derived from `config.title` (`urn:tessera:{course,au}:<hex>`). `<au>` carries `launchMethod="AnyWindow"` (CourseStructure XSD requires it), `moveOn` (`Completed` for percentage/manual, `CompletedAndPassed` for quiz mode), and `masteryScore` (rounded to 4 decimal places per §10.2.4). The `<url>` is a child element of `<au>`, not an attribute.
|
|
1047
|
+
|
|
1048
|
+
**Not implemented.** No multi-AU courses (one course = one AU in v1). No **Waived** or **Abandoned** verbs (LMS-only). No mid-session actor refresh.
|
|
1039
1049
|
|
|
1040
1050
|
**Local testing.** Upload `dist/*-cmi5.zip` to SCORM Cloud and use the cmi5 dispatch URL it generates, the closest free equivalent to a real LMS launch.
|
|
1041
1051
|
|
package/dist/plugin/index.js
CHANGED
|
@@ -832,16 +832,17 @@ function generateCMI5Xml(config) {
|
|
|
832
832
|
const description = escapeXml(config.description || "");
|
|
833
833
|
const courseId = stableUrn("course", `tessera-course:${config.title || ""}`);
|
|
834
834
|
const auId = stableUrn("au", `tessera-au:${config.title || ""}`);
|
|
835
|
-
const masteryScore = (config.scoring?.passingScore ?? 70) / 100;
|
|
835
|
+
const masteryScore = Number(((config.scoring?.passingScore ?? 70) / 100).toFixed(4));
|
|
836
836
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
837
837
|
<courseStructure xmlns="https://w3id.org/xapi/profiles/cmi5/v1/CourseStructure.xsd">
|
|
838
838
|
<course id="${courseId}">
|
|
839
839
|
<title><langstring lang="en-US">${title}</langstring></title>
|
|
840
840
|
<description><langstring lang="en-US">${description}</langstring></description>
|
|
841
841
|
</course>
|
|
842
|
-
<au id="${auId}"
|
|
842
|
+
<au id="${auId}" launchMethod="AnyWindow" moveOn="${config.completion?.mode === "quiz" ? "CompletedAndPassed" : "Completed"}" masteryScore="${masteryScore}">
|
|
843
843
|
<title><langstring lang="en-US">${title}</langstring></title>
|
|
844
844
|
<description><langstring lang="en-US">${description}</langstring></description>
|
|
845
|
+
<url>index.html</url>
|
|
845
846
|
</au>
|
|
846
847
|
</courseStructure>`;
|
|
847
848
|
}
|