Changelog¶
All notable changes to RevLearning are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[1.0.0] - 2026-10-18¶
The first public release, and the one that freezes the public API.
It was first cut in the repository on 2026-10-02 and then held rather than published. The live conformance run that should have been a formality found that no numeric data-model element could be read back at all on the platform this package is validated against: scores read as null, objective enumeration came back empty, and records could overwrite one another. That is a release blocker. The fix, the tests that fail without it, and a full re-run of the checklist on both standards are in this release. Nothing was ever published under the earlier cut, so there is one 1.0.0 and this is it. The interval is recorded in Internal, because someone judging how this package is maintained should be able to see it.
It was then held a second time, from 2026-10-07 until the date above. That interval is the editor-menu rebrand, a sample scene worth pressing Play on, and two defects found by opening the editor rather than by testing it - a dialog sending people to a menu that does not exist, and three settings labels truncated for every buyer. All of it is below. The same reasoning applies: none of it was ever published, so it belongs in this section rather than in a 1.0.1 nobody would have been able to install 1.0.0 to receive.
Three bodies of work are here: the correctness and hardening pass that followed a full adversarial review, the move to Asset Store distribution, and that defect. Several fixes change behaviour without producing a compile error, so read Changed and Fixed rather than skimming them.
Both standards were run end to end on a live LMS against the packages this release builds, including a readback across a session boundary - the check that caught the defect above, and the one no earlier run performed. The step-by-step record, including the one step that platform makes unreachable, is in Tests~/CONFORMANCE.md.
Changed¶
- RevLearning is distributed on the Unity Asset Store, and the licence reflects it.
LICENSE.mdpreviously granted no rights at all without a separate written agreement with RevGaming, which was correct while the package was licensed bespoke and flatly wrong for anyone buying it from a shop. The package is now governed by the Unity Asset Store EULA, which the licence names as authoritative over itself. - It is an Extension Asset: one licence per seat. Stated in
LICENSE.mdand the README, because it is the question publishers are asked most often and getting it wrong is the buyer's licensing problem rather than their technical one. - RevLearning is delivered as a
.unitypackageintoAssets/RevLearning, not as a package underPackages/. The repository is UPM-shaped because the previous bespoke distribution wasAdd package from git URL; that is now a development layout rather than a shipping one. Every other RevByDesign asset ships this way through its own export tool, and the reason specific to this product is that the sample scenes are the panels the published conformance run was driven through. UnderPackages/they sit behind an Import button in a tab a buyer has to know about; in the Project window somebody opens them on the way to something else.RevLearning_InstallLocation.mdgives the argument, and says plainly which half of RevFramework's version of it does not apply here - there is no half of SCORM to delete, so "you are meant to remove parts of it" is true there and false here. - Installation is Package Manager, My Assets, Import. The README previously described
Add package from git URLwith a repository address supplied under your agreement, which no store buyer has. - Terms outside the standard EULA - site licences, multi-entity, escrow, custom indemnities - are negotiated directly with us and not with Unity. Unity's own guidance says the same, and the licence now points at the right address.
- The copyright notice names both the studio and the entity: "RevByDesign (RevGaming)". The studio rebranded, and the repository was left asserting two different things a few lines apart -
contact@revbydesign.studiodirectly under© 2026 RevGaming. Which of them is right depends on whether RevGaming remains the owning entity or is only a former name, and a copyright notice is a statement about who owns the asset rather than about which brand sells it, so leaving it to be inferred was the one thing not worth doing. Naming both is accurate under either reading. Changed inLICENSE.md, the README, and theauthorfield ofpackage.json. -
The
RevGaming.RevLearningnamespace is deliberately not renamed, and neither are the asmdefs or the menu paths. Every other shipped asset still usesRevGaming, so renaming this one alone would make it the inconsistent one; and after 1.0 a namespace change is a breaking change for every consumer, which is a poor trade for a cosmetic match a buyer never sees. Breaking changes are marked. This is the release that freezes the public API, so several of them exist only because they would cost more to make later. -
Breaking.
LessonStatusis reordered soUnknownis the zero value.Passedheld it before, which madedefault(LessonStatus)the strongest possible claim about a learner nobody had said anything about. Migration: a[SerializeField]of this enum in your own scenes or assets stores the ordinal and will remap on upgrade — check any inspector-setLessonStatusfield. - Breaking.
LessonModeis reordered soNormalis the zero value, matching what both standards treat as the default launch mode.Browseheld it before, so an unassigned field claimed the learner was browsing untracked — the one value that tells a course to stop recording. Same migration note. - Breaking.
Objective.successis nowSuccessStatus?andObjective.completionis nowCompletionStatus?, instead of both beingLessonStatus?. Out-of-axis values used to compile and were then silently dropped on SCORM 2004 while being written on SCORM 1.2. Migration: this breaks at compile time rather than silently — replaceLessonStatus.PassedwithSuccessStatus.Passedonsuccess, andLessonStatus.CompletedwithCompletionStatus.Completedoncompletion. A value the axis has no meaning for no longer has anywhere to go, which is the point. - Breaking.
IXApiTransport,XApiDeliveryResult,XApiDeliveryOutcomeandScormTimeare now internal. Nothing public ever accepted a transport, so the extension point they advertised could not be reached; publishing it later, with a factory overload that takes one, is additive. The supported contract isIXApiRuntime.SendStatementAsync. Migration: this breaks at compile time. If you referenced any of these, you were reading types you could not act on —XApiRuntimeFactoryplusIXApiRuntimecover the supported surface. If you need a custom transport, say so and it can be published deliberately. - Behaviour. An over-limit
suspend_datawrite is now refused and reported on both standards, leaving the last good value intact. It used to fall back to truncation on the helper and SCORM 2004 paths, which overwrote a working save with a prefix of JSON that cannot be deserialized — so on the next launch the learner restarted from nothing. SCORM 1.2's envelope path already refused; the two now agree, and the FAQ has always described this behaviour. - Behaviour. A 401, 403 or 407 from the LRS now halts delivery and keeps the whole backlog rather than discarding it. It used to be treated as per-statement poison, so one flush with an expired token attempted each statement once, dropped it, and erased the persisted queue.
- Behaviour.
XApiRuntimeFactory.Create(null)now returns a fail-closed runtime in player builds instead of the success-faking Editor stub. A build with an unassigned endpoint reported healthy delivery while nothing left the device. - Behaviour. An out-of-range xAPI
rawscore is clamped into[min, max]and reported. An LRS is obliged to reject the whole statement otherwise, soPassed(105)against the default 0–100 range lost the pass entirely. - Behaviour. A WebGL build with compression enabled and Decompression Fallback off now produces no SCORM package, on every build path rather than only from the RevLearning menu. It used to emit one and log "SCORM package created"; that package hangs at the loader on any LMS that does not set
Content-Encoding, which is most of them. The build menu offers to fix the setting, or to package anyway as an explicit choice. - The declared minimum Unity version stays 6000.3, which is what CI holds. An unreleased pass had lowered it to 2021.3; that is withdrawn rather than shipped. 2021.3.45f2 does pass the whole chain by hand — 336/336 tests, a WebGL player, a package at 23/23 — but licence activation fails on 2021.3 in CI, so nothing holds the result on the next commit. Lowering a floor later is additive and non-breaking; shipping a claim no automation defends is not.
- Manifests no longer carry
xsi:schemaLocation. It named ADL and IMS control files by relative path that the package does not ship, and a hint pointing at an absent file is worse than none. Both manifests remain schema-valid. - "Enable Build Injection" is now "Inject Page Helper (unload commit)", and the project settings, post-build log and validator all say what it gates. All three called it diagnostics, while it carries the only commit-on-tab-close there is.
commitOnEachInteractionis labelled SCORM 1.2 only, which is all it ever was; the SCORM 2004 runtime does not read it.- Samples moved to
Samples~and are imported on demand rather than compiled into every project. - Sample scenes no longer reference URP-only components. They were serialized with
UniversalAdditionalCameraDataandUniversalAdditionalLightDataby GUID with no URP dependency declared, so first contact on a Built-in-RP project was a pair of missing-script warnings. CompleteCourseAsyncnow marks the course completed, not passed. UsePassCourseAsyncfor assessed content.SetScorereportscmi.score.scaledon SCORM 2004, and rescales an out-of-0-100 range to a percentage on SCORM 1.2.CommitAsyncandFinishAsynccomplete synchronously.AnsweredAsyncno longer marks the activity complete.Cmi.PassingScore's XML documentation now states that the value is always null for a package built by RevLearning's own packer, naming the two manifest elements that are not emitted. README and the FAQ already said so; the XML doc is what IntelliSense shows, which is where a developer meets the property.- The editor menus are under RevByDesign, not RevGaming.
Tools ▸ RevByDesign ▸ RevLearning ▸ …andProject Settings ▸ RevByDesign ▸ RevLearning. The C# namespaces, the four assembly names and thecom.revgaming.revlearningpackage id are deliberately unchanged - those are a compatibility contract, and a menu path is not. The public-surface freeze covers the Runtime assembly's types and enum ordering; the Editor assembly is not in it and no test asserts on aMenuItemstring. Five strings while nobody has bought the package; a moved menu and a wait for a major version from the first sale onward. The convention, and why the other products take the same change at their next release rather than in a sweep, is recorded as D059 in the RevByDesign decision log.
Added¶
RevLearning_DataAndPrivacy.md, stating what data leaves the machine and who decides. RevLearning's job is transmitting learner state to a system that records it, which makes this a design property rather than an appendix, and in learning and development it is the first question procurement asks. It records that there is no telemetry of any kind, that SCORM has no network layer and reaches only the launching LMS, that xAPI reaches only a configured endpoint, and two things that are not obvious from the feature list: undelivered statements persist to local storage base64-encoded and not encrypted, and the queue is scoped per learner so one learner's backlog cannot be sent under the next learner's identity on a shared machine.RevLearning_InstallLocation.md, stating where the package lands, why, and which two folders you can delete without the runtime noticing.New-StorePackage.ps1, which stages an allow-listedAssets/RevLearningtree for export and refuses to finish if a forbidden path or a tilde folder reached it. The repository root is also a git checkout, a MkDocs project and a Python virtualenv: 153 MB, of which about 150 MB belongs in no customer's project - including the three pre-release documents that name unfixed defects. All of it is gitignored, which Unity's exporter does not read. Three things change on the way out: tildes come off, because Unity ignores a tilde folder anywhere and an invisible sample scene is worse than an absent one;package.jsonis dropped, being inert underAssets/; andTests/is dropped, because underAssets/it would compile into the buyer's project and put our suite in their Test Runner beside their own. The suite still runs in CI on every push.- xAPI language maps can carry a language other than US English.
XApiLanguageEntry,XApiLanguageMap.entries,XApiLanguageMap.For,XApiLanguageMap.With, and three-argumentXApiVerb.Createand four-argumentXApiActivity.Createoverloads taking an RFC 5646 tag. The map held a singleen_USfield and the serializer emitted only that key, so a French activity name reached the LRS taggeden-US. Nothing failed - a one-entry map is legal and the fields are optional - which is exactly why it survived to 1.0: the statements were accepted and only the language metadata was wrong, so any report grouping or filtering by language saw an all-English catalogue.en_USis still the home for US English and still serializes first, so existing content is untouched. These are overloads rather than optional parameters on the frozen two- and three-argument signatures, because adding a parameter alters a public signature instead of adding to the surface. CompletionStatusandSuccessStatusonIScormDataModel, so SCORM 2004's two axes can be reported independently.Cmi.Entry,Cmi.CreditandCmi.PassingScore: whether the launch is a resume, whether it counts, and the score the LMS treats as a pass.XApiActor.FromLmsLearner,FromAccount, andaccountsupport, so xAPI can identify the learner the LMS reported.PendingStatementCount,FlushPendingAsyncandClearPendingStatementsonIXApiRuntime.PassCourseAsyncandFailCourseAsync.DescribeLastError(), which reads the SCORM version from the runtime.- JavaScript test suites for the WebGL bridge and page helper (
Tests~/js), and a CI workflow. LmsObjectives.SetCompletionoverload takingCompletionStatus, which cannot express a value the completion axis has no meaning for.- Declared package
dependenciesfor the three Unity modules the runtime uses (unitywebrequest,jsonserialize,imgui). They were previously assumed present. - Validation panel controls the conformance checklist needs and did not have: an incorrect interaction, split objective score/success/completion writes, exit-mode selection including
TimeOut, an out-of-range score, and a launch context readout. - JavaScript suites now run against both standards (
standards.test.mjs). The SCORM 2004 branch of the bridge previously had no test coverage at all. - A README in every source folder, describing what lives there and the reasoning that cannot be recovered from the code: why a SCORM 1.2
Completedwrite is suppressed once success is recorded whileBrowsedis not, why the xAPI delivery tri-state stays internal while callers seeTask<bool>, why oversized suspend data is refused rather than truncated, and whyLessonStatus.UnknownandLessonMode.Normalsit at zero. - A documentation site generated from those READMEs (
mkdocs.yml,Build-Docs.ps1,requirements.txt). CI builds it with--strict, so an internal link that stops resolving fails a pull request instead of rotting silently, and regenerates the mirror to fail if it has drifted from the READMEs it is generated from. - XML doc summaries on the three public types that had none, so they say something useful in IntelliSense.
IScormDataModel.GetElement/SetElement, a raw accessor for data-model elements the typed members do not wrap —cmi.progress_measure,cmi.comments_from_lms, anything else a course turns out to need. Previously there was no route to an unwrapped element at all, which made a missing one a blocker rather than an inconvenience. It goes through the session guard and reports refusals with the decoded error like every other write, and it applies no translation or validation: the caller spells the element for the standard they are on.- A member-level public surface baseline (
PublicSurfaceTests.MemberBaseline). The existing baseline pinned the set of public types and the enum members with their values, but nothing pinned the members of a class or an interface — so renaming a method, retyping a property, dropping an interface member or adding a setter passed the whole suite, while being exactly the kind of change this release freezes. All 218 public members are now pinned with their return types, parameter types, parameter names and property accessors. - A sample scene you can press Play on.
Samples/Scenes/QuickstartDemois one GameObject carrying two scripts:QuickstartExample, which reports learner state, andQuickstartDemoPanel, which draws what it wrote. Press Play and the Game view shows the live data model read back - connection, lesson and completion status, bookmark, session time, suspend-data size and last error - with no LMS, no WebGL build and nothing to configure. Written by one script and read by another throughRevLearningRuntime.Runtime, so it demonstrates a round trip rather than a script printing its own variables. The panel exists because the scene without it did not work as a demo: the quickstart draws nothing, so Play gave an empty Game view and two "(stub)" lines in a Console you had to know to open, and the reasonable conclusion from an empty screen is that nothing happened. The capability was already there and nothing exposed it: the runtime substitutes an editor stub outside WebGL builds, and the quickstart self-starts. The two validation scenes are a harness for driving a live platform by hand, which the samples README has always said plainly, and a harness is not a substitute for seeing the package work in the first minute.
Fixed¶
- Two bootstrappers in the same frame no longer race each other into a null runtime.
RevLearningRuntime.InitializeAsyncassigned the static runtime field before awaiting the connect, and the checks after the await re-read that field rather than a local. A second caller arriving while the first was suspended replaced the instance; the first then evaluated the second's runtime, found it not yet connected, and nulled the field - and the second resumed to dereference null. AStart()and a UI button both calling Initialize is all it took, and the failure surfaced as aNullReferenceExceptionfrom inside the package or, worse, as a null return while a perfectly healthy LMS session was running. Initialization is now single-flight: a call arriving while one is in progress joins it and receives the same result, the connect is awaited on a local, and the field is published only once the runtime has actually connected.RevLearningRuntime.Runtimenow stays null while initialization is in flight, which is what its documentation always claimed and what callers polling it should have been able to rely on. Only the first caller's cancellation token is honoured, because there is one attempt and cancelling it for one joiner would cancel it for the others. ScormInteraction.latencyis written on SCORM 1.2, read back on SCORM 2004, and survives a resume. The field shipped with a doc comment inviting callers to populate it and three gaps behind it: the 1.2 runtime never wrotecmi.interactions.n.latencyat all, despite the element existing in 1.2 and taking the sameCMITimespanshape assession_time; the 2004 runtime wrote it but hard-codedlatency = nullon readback, so a value did not survive its own session; and the suspend-data mirror did not carry the field, which on 1.2 is the only readback there is because the element is write-only there. One struct through one call therefore recorded a latency on one standard and silently dropped it on the other, depending on which manifest shipped. A refused latency write is now named in the same warning as the rest of the interaction.- A learner's undelivered xAPI statements are addressed to one LRS as well as to one learner. The persisted queue's storage key was scoped by learner alone, and
PlayerPrefsis shared across a storage origin - one browser origin, or one company and product name in a standalone build. A vendor shipping two courses against two different LRSs under one product name therefore had the same learner's backlog restored in the wrong course and POSTed to the wrong endpoint with that course's credentials: missing from the store that wanted it and filed in one that did not. The endpoint now scopes the key alongside the learner, as a hash, so neither the URL nor the learner's address is legible in a browser storage entry. - The Validator gives a verdict. Its all-clear was behind a test for "no check lines at all", which
RunCheckscannot produce because it always appends at least one advisory - so the one summary the window was built to give was unreachable code, and an error sat mid-list among advisories for the reader to spot unaided. The window now leads with a count of what needs action, says "All checks passed" when nothing does, and lists errors before warnings before advisories. The checks themselves are unchanged. - The build tooling's remembered paths belong to one project. "Last build folder", "last build directory" and "last zip" were
EditorPrefskeys with no project qualifier, andEditorPrefsis per user and per machine. With two RevLearning courses open on one machine they shared those values: the Validator inspected whichever project built last and reported on it as though it were this one, and "Open Last Zip" revealed the other course's package. The verdicts looked authoritative while describing a different course. Each key is now suffixed with a hash of the project path. - Every numeric element an LMS reports now reaches your code. Against SCORM Cloud, none of them did. SCORM says
GetValuereturns a characterstring. SCORM Cloud returns a JavaScript number for each numeric element -cmi.score.raw,.min,.max,.scaled,progress_measure, every objective score and every_count- and a string for everything else. Emscripten'slengthBytesUTF8, which the WebGL bridge uses to size a read before fetching it, is documented@param {string}and does not coerce: handed a number it reads.length, findsundefined, skips its loop and returns0. The C# side treats a zero length as an empty element and never makes the second call that would have fetched the value. SoCmi.ScoreRaw,ScoreMin,ScoreMax,ScoreScaledandPassingScoreall read backnullon a registration where the LMS held values this package had written minutes earlier;GetObjectives()returned an empty list while an objective existed; and the interaction count read as zero. Writes were never affected, which is why the data looked correct in the LMS's own reporting throughout, and why this survived a conformance pass that inspected the LMS rather than the readback. The bridge now renders a non-string LMS value as text before measuring it. The boolean and error-code paths in the same file already tolerated non-strings; the read path was the one that did not. - An LMS that does not report
_countno longer has its records overwritten. Whencmi.objectives._countorcmi.interactions._countcould not be read, both runtimes guessed. SCORM 2004 returned a hard-coded1whenever index 0 held an id, which is right for a collection of exactly one and wrong for every other size: with two records stored, a third was judged new, written to index 1, and overwrote the second.SetObjectivehad no fallback at all on either standard, so on that path every objective after the first went to index 0 - three objectives in, the LMS held one. Nothing reported an error, because overwriting a record is a legal thing to ask an LMS to do. Both runtimes now probe ids upward when no count is readable, which is correct for any number of records where guessing was correct only for one, and warn rather than guess if an LMS answers even the thousandth id read with a value. A count the LMS does report is still believed, including when it is larger than what we can see. - The validation panel's Write Score goes through
SetScoreinstead of settingScoreMin/ScoreMax/ScoreRawdirectly, so it writescmi.score.scaledon SCORM 2004 like the shipped path does. A live run measuredscaledcoming back 403, value not initialized after a panel score write, which meant the conformance checklist's score step could not be satisfied by the tool meant to satisfy it. Read Score now reportsscaledtoo, and Write Out-Of-Range Score still writesScoreRawdirectly on purpose — it probes that element's own range behaviour, andSetScorewould rescale the value on 1.2 rather than letting the LMS judge it. - The out-of-range score message no longer claims a refusal on both standards. SCORM 1.2 constrains
score.rawto 0–100 and refuses 150; SCORM 2004 puts no range onscore.rawand accepts it, boundingscore.scaledinstead. The panel now says which to expect for the standard it is running under. - The injected page helper no longer fails silently when it cannot bind one member onto its diagnostics proxy. An unbound member answers with
thisset to the proxy rather than the LMS adapter, which is the fault that madeLastErrorandDescribeLastError()report "no error" for a whole session on one platform; a quiet failure there would have reinstated it undetectably. The handler is kept — removing it would abandon the whole wrap over one awkward member — and now warns with the member name and the consequence. - SCORM 1.2:
MarkCompleted()afterMarkPassed()destroyed the pass. The guard that stops completion retracting a recorded outcome existed only onCompletionStatus, andMarkCompletedwrites throughLessonStatus— so SCORM 2004 and the Editor stub kept the pass while every real 1.2 session replaced it withcompleted, following the finish sequence the FAQ recommends. A learner who passed was recorded as merely complete, and platforms gating certification onlesson_status='passed'withheld it. - Interaction results were invalid on SCORM 2004. An incorrect answer was written as
wrong, which is the SCORM 1.2 spelling; 2004 requiresincorrectand refuses the element as a type mismatch. Every incorrect answer lost its result while correct ones recorded fine — selectively erasing the failure data interaction tracking exists to capture. The result token is now translated per standard, and either spelling is accepted as input. - Interaction types were invalid on SCORM 1.2.
RecordChoicewrotemultiple_choice, a token in neither standard's vocabulary, so strict platforms refused the type element and recorded typeless questions. Both standards now getchoice, which is what the vocabulary actually calls a multiple-choice question. - SCORM 2004 wrote
timeoutfor a timed-out exit. The RTE data model hyphenates it in both standards, so the write was refused and the launch-time resumable-exit default stood — leaving an expired attempt resumable. - SCORM 2004 objective and interaction writes discarded every result with no logging, so a refused element vanished silently while
GetInteractions()reported it anyway. Refusals are now warnings naming the element and the decoded LMS error, on both standards. LmsObjectives.SetCompletionsilently did nothing on SCORM 1.2. It now says so: the standard gives an objective one status element and RevLearning uses it for success.- Both runtimes parsed numbers with the current culture while writing them invariant. Under a comma-decimal culture
"0.8"read back as8, corruptingPassingScore, score readback,cmi.score.scaled,scaled_passing_scoreandprogress_measure. Cmi.SuspendDatareturned RevLearning's internalREVLEARNING:GZIP64:encoding when the runtime had compressed the value to make it fit.- The xAPI delivery contract was inverted in both directions.
SendStatementAsyncreturned false for a statement safely queued offline — prompting callers to retry, which mints a new statement id the LRS cannot deduplicate — and true for one the LRS had permanently refused and the queue had discarded. It now returns true when the statement is accepted for delivery (delivered or durably queued) and false only when it will never arrive, and that contract is documented on the interface and on all nineXApiStatementshelpers. - Re-initializing one xAPI runtime for a second learner copied the first learner's undelivered statements — name and email included — into the second learner's storage and sent them during their session. The kiosk and shared-classroom case the per-learner queue scoping exists to prevent.
- An empty xAPI verb or activity id serialized as
"id":"", which is not an IRI, so the LRS rejected the whole statement. Refused up front instead. context.registrationwas serialized verbatim although the specification types it as a UUID. A malformed one is now warned about and omitted rather than risking the statement.- The injected diagnostics proxy silently emptied
ILmsRuntime.LastErrorandDescribeLastError(). It was built withObject.create(api)and given own properties only for the calls it wraps, so every other method ran withthisbound to the proxy — and an adapter that keeps error state onthis, as SCORM Cloud's does, then had its state shadowed by the proxy's own copy after the first read of the session. A documented public API reported "no error" for a write that had just failed, in the default configuration, and every message that decodes it said "OK" too. Found on a live LMS; no offline test could have seen it, because the test harness kept state in a closure. - Re-answering a question failed on SCORM 2004.
cmi.interactions.n.correct_responsesis a collection, and writing an index that already holds a pattern is refused with 351. The pattern is now written only when the record is created, which is the only time it can change. - The page helper was not injected into a template whose closing body tag was written
</BODY>: the tag was found case-insensitively and then spliced case-sensitively, so the file was rewritten unchanged with the append fallback unreachable and no warning. Since that snippet is the only commit-on-tab-close in the package, a learner closing the tab lost everything since their last explicit save. A duplicated closing tag also installed the handler more than once. - Objectives were recorded as passed and completed by any write, so a score of 20 was stored as a pass.
- Reporting completion on SCORM 2004 overwrote a recorded pass with "unknown".
- The page helper's unload commit never registered, so nothing was flushed when a learner closed the tab.
- Writes after
FinishAsyncreached a terminated API and were discarded. - SCORM 1.2
suspend_datacould exceed 4096 characters once wrapped, and the rejected write was ignored. - Project settings were never written to disk, so batch builds used defaults.
- Rebuilding into the same folder nested the previous package inside the new one.
LmsErrordescribed SCORM 2004 codes using the SCORM 1.2 table.- xAPI statements had no
id, so retries duplicated them; a permanently rejected statement blocked the queue forever; requests had no timeout; and the persisted queue was shared between learners on the same machine. cmi.total_timeover 24 hours parsed as zero.- Re-answering a question created a second SCORM 1.2 interaction record.
- Manifest
schemaLocationpointed at the wrong schema, hrefs were not percent-encoded, and every package shared one identifier. - The compression dialog sent you to a menu that does not exist. When WebGL compression settings would produce a package that cannot launch on an LMS, the dialog offered to build through "RevGaming > RevLearning > Build WebGL + SCORM Package". The menu item is "Build WebGL + SCORM Zip" and always has been, and the path now reads RevByDesign. Being sent to a menu entry that is not there is worst precisely here, where the reader is already stuck.
- Three Project Settings labels were truncated for every buyer. The settings provider never set
EditorGUIUtility.labelWidth, so Unity's ~150px default clipped "Mark Incomplete On Initialize", "Commit On Each Interaction (SCORM 1.2)" and "Inject Page Helper (unload commit)". The truncation landed on the part carrying the meaning: "Commit On Each Interactio" says nothing about the setting applying to 1.2 and not to 2004. Widening the window does not help, because the label column is fixed rather than proportional - which is what made this look like a screenshot problem rather than a defect. - Static state survived into a second Play session under Fast Enter Play mode. Unity 6.6 makes that the default for new projects, and it skips the domain reload that used to clear statics between sessions.
RevLearningRuntimekept the previous session's runtime and its completed initialization task, so pressing Play a second time handed content back a connection the LMS considers closed - an initialize that never happened, against a session that already ended.Reset()existed for tests and tooling and nothing called it automatically; it now runs atSubsystemRegistration, the earliest runtime hook there is, before any scene loads and before content can ask for a runtime. In a player build it runs once on a fresh process and costs two null assignments. The sample panel had the same root cause and needed the opposite fix: leaving Play mode destroys its background texture while the static style outlives it, so it now revalidates the texture rather than discarding a cache that is otherwise still good.
Found by the Asset Store validator, not by the suite. No EditMode test enters Play mode, so nothing here was reachable from 377 passing tests.
Security¶
- The xAPI backlog is now scoped per learner, so one learner's undelivered statements — including their name and email — are no longer restored and sent from another learner's session on a shared machine. This release closes the remaining path: re-initializing a single runtime for a second learner used to carry the first learner's queue across in memory, which the storage scoping alone did not prevent.
Internal¶
- The review's should-fix tier was swept at release rather than assumed closed, and one item was still open. Eleven of the thirteen work packages are done and were verified in the code rather than from memory: invariant-culture parsing, the refuse-don't-truncate suspend policy, the xAPI hardening family including the fail-closed factory and the out-of-range score clamp, the enum ordinals and the internalised dead extension point, the case-insensitive body-tag match, the relabelled injection setting, the commit/finish knobs in both test doubles, the 2004 branch of the JavaScript bridge, the shutdown contract pinned against the real runtimes rather than the stub, and the validator wired into CI with a non-zero exit when it validates nothing. The single-flight bug above is the one that was still open; it is fixed here, with two tests that were run against the unfixed code first and failed - one of them with exactly the
NullReferenceExceptionthe review predicted. - Two items are deliberately not fixed and are documented instead. The 1.2 interaction cache is still not reconciled against the LMS count at Initialize, and the xAPI backlog still has no serialized-byte budget. Both are recorded where a reader meets the behaviour -
Tests~/CONFORMANCE.mdfor the first, the FAQ's persistence section for the second - rather than left to be discovered. The unload helper also still cannot flush the xAPI queue, because an LRS request needs authentication headers no unload-time browser API can carry; the FAQ says so plainly and gives the workaround rather than implying coverage it does not have. - These five fixes were known at 1.0 and shipped unfixed without being named in this file. The pre-release review classified them as post-release improvements, which was a defensible call for every one of them - and the changelog then said nothing, so a buyer reading 1.0.0 had no way to know that 1.2 dropped interaction latency or that two courses on one machine confused the Validator. Deferring work is a decision; leaving it out of the changelog is not. The entries above record the friction and the fix together because the friction was never recorded on its own.
- The public-surface freeze guard earned its place on its first real test. The language-map work initially added an optional parameter to
XApiVerb.Createand a fourth toXApiActivity.Create, which is source compatible and would have compiled everywhere - andPublicSurfaceTestsfailed, correctly, because altering a frozen signature is not the same as adding to the surface. The change became overloads, and the baselines now differ by nine added lines and no removed or altered ones. - One of the new language-map tests was wrong before the code was: it counted
en-USoccurrences across the whole statement, where the activity name legitimately contributes one of its own. It now asserts on the display map rather than the document. - The suite is 377 EditMode tests, up from 336, and the JavaScript suites are 65, up from 60: latency across both standards and through a resume, the language map including tag normalisation and duplicate-key handling, queue isolation per endpoint plus the migration path and its no-overwrite case, the Validator's tally and ordering, collection counting on both standards, and a non-string LMS value through both halves of a bridge read.
- The published test count was stale in five places and said
336. The README,RevLearning_InstallLocation.md, theTests~/CONFORMANCE.mdcoverage table and the store listing all carried the pre-fix figure, so the first customer-facing number a buyer reads understated the suite by thirty-three tests. Corrected to369against CI run 37378442702 and a re-measurement on both editors. Two mentions were left alone deliberately: the[1.0.0]entry below, because history is not rewritten, and the "green at 336/336" line in the conformance record, which dates when a specific proof was gathered rather than stating the count today. - The 6 June commit "Fix SCORM 2004 objective readback in SCORM Cloud" fixed nothing, and its message is wrong. It recorded that "SCORM Cloud can persist objective data at
cmi.objectives.0while returning an empty or zero objective count during runtime reads", and concluded that "the issue was read enumeration, not write/commit behaviour". SCORM Cloud reported the count correctly; our own WebGL bridge discarded it. The commit treated a plausible LMS quirk as the cause, added the hard-coded1above, and shipped aDebug.Logof the objective count that fired on everyGetObjectives()call in release builds. That log is removed. The commit is left standing rather than amended, because history is not rewritten - this entry is the correction. - The JavaScript harness was more forgiving than the runtime it stands in for, and that is what hid this for four months. Its
lengthBytesUTF8double wasBuffer.byteLength(String(s), 'utf8'). ThatString()is precisely the thing Emscripten does not do. All three suites stayed green while the real bridge dropped every numeric read against a real LMS. The double is now a behavioural port of Emscripten's own implementation, including throwing on null as the real one does;stringToUTF8records what it was handed instead of being a no-op, so the fill half of a read is assertable at all; andassertEqualtells1from"1"instead of reporting "expected 1, got 1" in a defect that is entirely about that distinction. - The EditMode suite could have caught the overwrite from the beginning and never did.
FakeLmsBridgeanswers an empty string for anything unseeded, so no test has ever reported a_countand every test already ran down the fallback path. What was missing was a third record: the deepest the suite reached was index 1, the one index at which guessing 1 is accidentally correct. The new tests were run against the unfixed runtimes first, and four of the six failed - reportingcmi.interactions.1.idasq3whereq2belonged, andcmi.objectives.0.idaso3whereo1belonged. - The conformance record's SCORM 2004 step 3 was marked a pass on 2026-10-06 on evidence that did not test the thing. The step asks that a score written through the panel be read back through the panel's Read Score. It was confirmed instead by reading the data model directly in the browser, which bypasses the exact path that was broken - and
Read Scoreon that same live registration reportedraw=, min=, max=, scaled=against an LMS holding all four. The step is recorded as a fail, the open item is reopened, and the run had to be repeated against a corrected build. Substituting a different check for the one the checklist specifies is how a conformance record stops being evidence. - The re-run happened the same day, and the checklist passes end to end on both standards. Steps 1 to 19 bar 17, which that platform's player makes unreachable, against the rebuilt packages. The fix was verified at the artefact level first - the coercion is present twice in the shipped
framework.js, once per read entry point - and then live:Read Scorereportsraw=150, min=0, max=100, scaled=0.85on 2004 andraw=85, min=0, max=100on 1.2, both read back after a session boundary, where the pre-fix build reported nothing. - SCORM 1.2 was affected too, which was an open question when the fix went in. Its adapter returns
_countelements, score elements and even the error code as JavaScript numbers, exactly like the 2004 one. The 1.2 objective path had no count fallback at all, so that standard carried the silent-overwrite case rather than the guessed-count one. Its earlier step 3 pass was genuine but was an in-session readback, and an in-session readback cannot see this defect - the player echoes back the strings the content just wrote. - A published claim in the conformance record was wrong and is corrected there. It said the exit vocabulary's accepted set is identical across the two standards. It is not: the literal
normalis accepted on 2004 and refused 405 on 1.2, and the two standards return different error codes for the spellings they both refuse. The conclusion it was supporting survives, for a better reason -LmsExit.Normalmaps to"", the intersection of the two vocabularies rather than the union - but the stated reason was not evidence. - Two sessions produced no server-side record, and that was the harness rather than the package. Driving the SCO directly in the player's content frame bypasses the player's post-session flow; a session finalises only when it terminates through it. Both standards' step 19 was therefore driven by hand, and the registration then rolled up to
complete / passed / 85.00%. The conformance record says so, because anyone repeating the checklist that way would hit it and could easily read it as a defect. - 1.0.0 was cut on 2026-10-02, tagged, and held for five days. The tag and the documentation site went up before the live conformance run was finished, which is the wrong order, and it is why the readback defect was found after a version number had been assigned rather than before it. Nothing was published, so no customer was affected and the number was reused rather than burned. The lesson is the ordering: the conformance run is a release gate, not a formality to complete once the tag exists.
- Three items below were written as Migration notes and are not migration notes. They described upgrading from the 2026-10-02 cut, which nobody ever ran. They are kept because they are true and because the defects they describe are worth seeing, but a first release has nobody to migrate, and instructions addressed to an audience that does not exist are noise at best.
- A learner's undelivered xAPI statements are addressed to one LRS as well as to one learner. The persisted queue's storage key was scoped by learner alone, and
PlayerPrefsis shared across a storage origin. A vendor shipping two courses against two different LRSs under one product name would have had the same learner's backlog restored in the wrong course and POSTed to the wrong endpoint with that course's credentials. The endpoint now scopes the key alongside the learner, as a hash, so neither the URL nor the learner's address is legible in a browser storage entry. The queue also carries a one-time migration from the old key shape; on a first install it finds nothing and does nothing. - SCORM 2004
GetInteractions()returned a latency of null unconditionally. The field was written to the LMS and then hard-coded to null on readback, so a latency did not survive its own session. An element the LMS never reported still reads back as null rather thanTimeSpan.Zero, because zero is a real latency: it says the learner answered instantly. - Scores and objectives read back as nothing on SCORM Cloud. The defect in the preamble, stated as a consumer would have hit it:
Cmi.ScoreRawand its siblings returned null andGetObjectives()returned an empty list, after a session boundary, against an LMS holding the values this package had written. A course that branched on that silence - recomputing from suspend data, or reading the null as "no previous attempt" - would have taken the wrong branch on every resume. - Unity CI is parked from 2026-10-18, and every run says so. The self-hosted runner holding the activated editor is in storage until around mid-December. The EditMode suite and the runner-liveness watchdog written alongside it are both behind a two-line switch, and a job that does run reports "Unity EditMode did not run (parked)" in the commit's check list, with a warning naming what is and is not covered - the JavaScript suite still gates every push. The shape matters: a job skipped by
if:alone reports the whole run as successful, which was measured on this repository rather than assumed, and a green tick that means less than it appears to is worse than a missing one.Tests~/CONFORMANCE.mdnames what the 6000.3 claim rests on for the parked window, and says what expires that reasoning.
[0.1.0] - 2026-06-21¶
Not tagged. This release predates tagging in this repository, so there is no v0.1.0 ref to link or to compare against — the entries below are the record of it.
First packaged release. The runtime, packaging workflow, and test suite are in place; validated end-to-end on Thrive LXP. Versioned 0.x while LMS coverage continues to expand.
Added¶
ILmsRuntimeabstraction with a unified CMI data model (IScormDataModel) over SCORM 1.2 and SCORM 2004.- Editor / non-WebGL stub runtime so learning flows can be developed and tested without an LMS.
- Status, score, bookmark, completion, suspend-data, objective, and interaction helpers via extension methods on
ILmsRuntime. - SCORM 2004 scaled score support; session exit/resume control (
SuspendAsync,LmsExit) for resumable courses. - xAPI runtime foundation: statement model, verbs, JSON writer, and a resilient queued transport (FIFO, exponential-backoff retry,
PlayerPrefs/IndexedDB persistence) viaXApiRuntimeFactory. LastErrorsurfacing of SCORMGetLastError/LMSGetLastError.- Editor tooling: single-SCO
imsmanifest.xmlbuilder, index injector, post-build packaging, and an LMS validator window. - Runtime configuration asset synchronized from project settings.
- Sample scenes and quickstart examples for SCORM and xAPI.
Security¶
- WebGL bridge no longer logs learner data-model values to the browser console by default; verbose logging is opt-in via
window.__RevLearning_debug.