Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
47a69e3
Breaking: Version 2 improvements (fixes #29)
oliverfoster Jul 24, 2025
b6cdba5
Markdown caps
oliverfoster Jul 24, 2025
f35cfaf
Merdown edits
oliverfoster Jul 24, 2025
bb2e6fb
jsdoc updates
oliverfoster Jul 24, 2025
fc471eb
jsdoc fixes
oliverfoster Jul 28, 2025
a0213c4
jsdoc fixes
oliverfoster Jul 28, 2025
7b59a5b
jsdoc updates
oliverfoster Jul 28, 2025
9c6bb79
Formalise canReset
oliverfoster Jul 30, 2025
4a06e65
jsdoc typo
oliverfoster Jul 30, 2025
59be83f
Extend adaptmodelset to include more questionmodel interfacing
oliverfoster Aug 12, 2025
6b89283
Fixed findDescendantModels and getAllDescendantModels for detached mo…
oliverfoster Aug 19, 2025
a1fb354
Resolving conflicts & Implementing logging improvements.
joe-replin Jan 2, 2026
ebdd9e4
Merge branch 'master' of https://github.com/adaptlearning/adapt-contr…
joe-replin Jan 2, 2026
8e5cd11
Schema nesting issue.
joe-replin Jan 2, 2026
8650fb5
Reverting FW version
joe-replin Jan 2, 2026
b93a076
Fix wording in IntersectionSet description
oliverfoster Mar 17, 2026
8001592
Update to remove the duplicated event trigger for 'scoring:update'. M…
danielghost Mar 18, 2026
c530c6d
Incorprated the log logic from https://github.com/adaptlearning/adapt…
danielghost Mar 19, 2026
53b5cb6
Fixed `@typedef` paths and typos.
danielghost Apr 2, 2026
67e9b5a
Reverted `AdaptModelSet` to extend `ScoringSet`. Amended filter metho…
danielghost Apr 2, 2026
b54b50a
Adjusted lifecycle to distinguish between a first attempt and a resta…
danielghost Apr 2, 2026
6b579dc
Updated objective functionality to incorporate https://github.com/ada…
danielghost Apr 2, 2026
383b0e8
Moved the log for the scoring updates functionality into a `Journal` …
danielghost Apr 13, 2026
c2b06d5
Fixed issue with `TotalSets` completing each time it was restored bec…
danielghost Apr 15, 2026
0656f5b
Renamed `Journal` to better reflect its purpose. Added a journal for …
danielghost Apr 15, 2026
ed442f3
Amended the journal for `TotalSets` to use its sets as the modifiers …
danielghost Apr 15, 2026
e24a0db
Exported some missing set utility functions.
danielghost Apr 16, 2026
7ee59ad
Exported `Passmark` so this can be used by other plugins (could alrea…
danielghost Apr 16, 2026
dcd16d4
Journal refactor (#31)
oliverfoster Apr 17, 2026
a52ae23
Added guard checks to prevent false positives when checking `isComple…
danielghost Apr 17, 2026
5a461a1
Fixed backwards compatibility issue introduced with c2b06d5404a1d9e52…
danielghost Apr 17, 2026
2ffa801
Amended passed/failed logic so this is only relevant for sets with a …
danielghost Apr 21, 2026
d8fcc39
Prevent direct calls to `reset` when `canReset:false`. Fixed jsdoc is…
danielghost Apr 21, 2026
0ab116e
Updated the handebars helpers.
danielghost Apr 22, 2026
71a5dab
Added `averageScaledScore` to allow a distinction between point-weigh…
danielghost Apr 24, 2026
26cd9d3
Amended the lifecycle so it correctly resets sets based on the inters…
danielghost May 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 141 additions & 0 deletions INTERSECTION_QUERY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# Intersection query syntax

## Preamble

### What is an intersection
* Equal intersection is when the first and second model are equal
* Descendant intersection is when the first model is a descendant of the second
* Ancestor intersection is when the first model is a ancestor of the second
* Intersections identify models which have overlapping interests in the hierarchy

### Query API
```js
const queryString = "#a-300 #performance"
Adapt.scoring.getSubsetsByQuery(queryString)
Adapt.scoring.getSubsetByQuery(queryString)
const pathString = "a-300.performance"
Adapt.scoring.getSubsetByPath(pathString)
```

### IntersectionSet
All sets representing any collection of models are likely to extend the most basic interface `IntersectionSet`.

It is possible to use these literal attributes for queries on `IntersectionSet`:
* `#setId` or `[id=setId]` or `[#setId]` the set with id `setId`, ids are unique
* `setType` or `[type=setType]` all sets with type `setType`
* `(isEnabled)` or `(isEnabled=true)` and `(isEnabled=false)` filtered by `isEnabled`
* `(isOptional)` or `(isOptional=true)` and `(isOptional=false)` filtered by `isOptional`
* `(isAvailable)` or `(isAvailable=true)` and `(isAvailable=false)` filtered by `isAvailable`
* `(isModelAvailableInHierarchy)` or `(isModelAvailableInHierarchy=true)` and `(isModelAvailableInHierarchy=false)` filtered by `isModelAvailableInHierarchy`, this returns true if the set is `_isAvailable` and not detached from the hierarchy
* `(isPopulated)` or `(isPopulated=true)` and `(isPopulated=false)` filtered by `isPopulated`, this returns true if the set has `models`.
* `(isNotPopulated)` is an alias for `(isPopulated=false)`

These intersection attributes are available for querying all set instances:
* `[modelId=a-300]` will select all sets intersecting model `a-300`

### AdaptModelSet
The `AdaptModelSet` instances allow selections and intersections over the Adapt models.

For each `AdaptModel`, in the `core/js/data` API, there is a corresponding `AdaptModelSet`. Each set inherits the same `id`, `model` and `modelId` from its `AdaptModel` and has `models` of `[model]`. These sets inherit all of the query attributes from `IntersectionSet`.

These literal attributes are available for query on `AdaptModelSet` instances:
* `#a-300` or `[id=a-300]` or `[#a-300]` the set with id `a-300`, ids are unique
* `adapt` or `[type=adapt]` all sets with type `adapt`
* `[modelTypeGroup=course|contentobject|menu|page|group|article|block|component|question]` all sets with the specified typegroup
* `[modelType=course|menu|page|article|block|component]` all sets with the specified type
* `(isComplete)` or `(isComplete=true)` and `(isComplete=false)` filtered by `isComplete`
* `(isIncomplete)` is an alias for `(isComplete=false)`
* `(isPassed)` is an alias for `(isComplete)`
* `(isFailed)` is always `false`

### ScoringSet
For all sets representing a scoring collection of models, such as questions, they likely extend `ScoringSet`.

Here you can use these additional literal attributes for queries:
* `(isComplete)` or `(isComplete=true)` and `(isComplete=false)` filtered by `isComplete`, is determined by the specific type of scoring set
* `(isIncomplete)` is an alias for `(isComplete=false)`
* `(isPassed)` is determined by the specific type of scoring set
* `(isFailed)` is and alias `(isComplete,isPassed=false)`

### TotalSets
This is a set of sets, it can sum the scores of the registered or intersecting sets.

It extends `ScoringSet` with the caveat that it sums properties from registered or intersecting scoring sets and completion sets rather than registered or intersecting models.

It represents the overall completion, score, correctness, pass and fail of the course.

It has these literal attributes:
* `#total` or `[id=total]` or `[#total]` the set with id `total`, ids are unique
* `total` or `[type=total]` all sets with type `total`

### Selection query syntax
All selection query can have three optional parts, as `first[second](third)`.

The first part defaults to an empty string, which means all sets. It can select either by set type or by set id using the shorthand `setType` or `#setId`. For native sets, `adapt` would return all native `AdaptModelSet` sets and `#a-300` would return only the `AdaptModelSet` representing the article `a-300`, as set ids are unique.

The second part is an optional list of attributes about which to multiply the first part. Such that `adapt[modelId=a-300,modelType=block]` will return all `AdaptModelSets` which intersect `modelId=a-300` and all `AdaptModelSets` which have `modelType=block`.

The third part is an optional list of attributes which filter the selected sets on the specified attributes. `adapt[modelId=a-300,modelType=block](isComplete)` will return all `AdaptModelSets` which intersect `modelId=a-300` and all `AdaptModelSets` which have `modelType=block`, where their `isComplete` properties are `true`.

tldr: `selectionQuery = typeOrId[multipliedByAttributes](filteredByAttributes)`

### Selection query examples
Selection queries follow the `selectionQuery` syntax of `typeOrId[multipliedByAttributes](filteredByAttributes)`.

These queries only select from available sets, they do not cause intersections or cloned sets to be created:
* `"assessment"` sets of `AssessmentSet`, `type=assessment`
* `"#assessment-300"` the `AssessmentSet` with `id=assessment-300`, ids are unique
* `"#a-300"` the `AdaptModelSet` with `id=a-300`, ids are unique
* `"assessment[id=assessment-300]"` the `AssessmentSet` with `id=assessment-01`, ids are unique
* `"assessment[id=assessment-300,id=assessment-400]"` sets of `AssessmentSet` with `id=assessment-300` and `id=assessment-400`, ids are unique
* `"[#assessment-300,#assessment-400]"` does the same as above
* `"assessment[modelId=a-05]"` the `AssessmentSet` of `type=assessment` intersecting model `a-05`
* `"adapt[modelType=article]"` sets of `AdaptModelSet` representing articles
* `"adapt[modelComponent=mcq]"` sets of `AdaptModelSet` representing mcqs
* `"adapt[modelComponent=mcq,modelComponent=gmcq]"` sets of `AdaptModelSet` representing mcqs and gmcqs
* `"adapt[modelTypeGroup=question]"` sets of `AdaptModelSet` representing questions
* `"adapt[modelComponent=mcq,modelComponent=gmcq](isComplete)"` sets of `AdaptModelSet` representing all completed mcqs and gmcqs

### Selection query multiplications
The query interface parts `first` and `second` are multiplied.

The multiplication happens in columns, each entry in each column is multiplied into a list of all possible combinations. Such that `[[1], [2,4]] = [[1,2],[1,4]]`. The resultant combinations are used to perform the selection query accordingly.

tldr: When multiplying the selection parts `article` and `[#a-200,#a-300]`, by using `article[#a-200,#a-300]`, we ask the computer to select both `article[#a-200]` and `article[#a-300]`.

### Intersection query examples
The `intersectionQuery` syntax is `selectionQuery selectionQuery selectionQuery...` or `typeOrId[multipliedByAttributes](filteredByAttributes) typeOrId[multipliedByAttributes](filteredByAttributes) typeOrId[multipliedByAttributes](filteredByAttributes)...`, where the space signifies a multiplication.

An intersection always produces a new set, created from the class of the final column:
* `"assessment[id=assessment-01] assessment[id=assessment-05]"` creates an intersection of set `id=assessment-01` and `id=assessment-05`, returning a `type=assessment`, `AssessmentSet` instance
* `"assessment[id=assessment-01,id=assessment-05] assessment[id=assessment-10,assessment-15]"` creates four intersection sets, of type `assessment` or `AssessmentSet`, intersecting `#assessment-01` with `#assessment-10`, `#assessment-01` with `#assessment-15`, `#assessment-05` with `#assessment-10` and `assessment-05` with `assessment-15`
* `"[id=assessment-01,id=assessment-05] [id=assessment-10,id=assessment-15]"` would do the same as above, ids are unique
* `"[#assessment-01,#assessment-05] [#assessment-10,#assessment-15]"` would do the same as above, ids are unique
* `"[modelType=article] assessment"` returns intersection sets of all `modelType=articles` sets with all `type=assessment` sets, return `AssessmentSet` intersection set instances
* `"[modelType=article] assessment(isPopulated)"` returns all article assessments intersections that have models after their intersections
* `"[modelType=article](isComplete) assessment(isPopulated)"` returns all article assessments intersections that have models from completed articles

### Intersection query multiplications
With `intersectionQuery`, all of the separate `selectionQuery` sections are multiplied together before a resultant set is produced from the final class.

The multiplication happens in columns, each selection entry in each column is multiplied into a list of all possible combinations. Such that `[[1], [2,4]] = [[1,2],[1,4]]`. The resultant combinations are used to perform the intersection accordingly.

When multiplying the intersection parts `[modelType=article]` and `assessment`, by using `[modelType=article] assessment`, we ask the computer to select all articles and all assessments and then multiply them together and give the resultant intersections, which would be of type assessment.

Anti-pattern walkthrough: In the above example, `[modelType=article] assessment`, if we had 20 articles and 2 assessments, we'd have 40 resultant intersected assessment sets. If each assessment belonged to only one article then we would have 38 useless intersections of articles intersecting assessment where there is no relation. As intersections reduce the number of models in the resultant set, and they reduce the models intersecting with the set models used to product it, we could solve this problem by using `[modelType=article] assessment(isPopulated)`. Using an `(isPopulated)` filter would return only all of the article assessment intersections which have models left after intersection. In this can it would probably be easier just to fetch the assessments directly using `assessment[modelType=article]`, rather than using an intersection, as the assessment lives on an article.

## Primary use-case
We have a performance metric that covers questions in the whole course and we have an assessment in one article that intersects some of the same questions. We want to know the sum of the performance score for just the assessment questions.

To do this, assuming the `AssessmentSet` sits on article `a-300` and has the article's blocks as its models and assuming a `PerformanceSet`, with id `performance` and a `score` property, sits on the course object and has all of the questions in the course as its models, the following example will satisfy our use-case:
```js
const intersectedSets = Adapt.scoring.getSubsetsByQuery("#a-300 #performance")
const firstIntersectedSet = intersectedSets[0]
const assessmentPerformanceScore = firstIntersectedSet.score
// or, as we expect only one intersected set
const firstIntersectedSet = Adapt.scoring.getSubsetByQuery("#a-300 #performance")
const assessmentPerformanceScore = firstIntersectedSet.score
```
Machine translation: Select two columns of sets, column 1 should have sets where `id = 'a-300'` and column 2 should have sets where `id = 'performance'`. Multiply both columns, such that there is a list of every combination. Clone each item in column 2 of the list and reduce its `.models` by those models intersecting its combination from column 1. Return the array of intersected cloned sets. Select the first. Return the value of its `.score` property.

English translation: Return a performance metric score for just questions intersecting article `a-300`.
34 changes: 34 additions & 0 deletions LIFECYCLE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Lifecycle
Each Set which inherits from [`LifecycleSet`](js/LifecycleSet.js) has a lifecycle and order. The primary purpose of the lifecycle is to aid in the startup and reset of sets, with the order determining the Set's position in the lifecycle phase execution.

## Phases
There are 8 external lifecycle phases, 6 set callback functions and 2 internal triggers for each set. The set callback functions are called by the external phase controller ['Lifecycle'](js/Lifecycle.js) and the phase renderer ['LifecycleRenderer'](js/LifecycleRenderer.js). Each cycle is grouped at 30 frames per second, on a browser animation frame, and before each cycle is performed, the relevant sets are grouped, sorted and processed in phase and position order.

### External phases
| Name | Description |
| --- | --- |
| init | All sets are sent here after being instantiated and registered |
| restore | All sets are sent here after the init phase |
| start | All sets are sent here after the restore phase |
| reset | All sets are sent here if `Adapt.scoring.reset()` is called |
| restart | Sets are sent here if any set on the model, or the model on which they sit has been reset using `.reset()` |
| leave | Sets are sent here if the user leaves the content object in which its `modelId` sits |
| visit | Sets are sent here if the user visits the content object in which its `modelId` sits |
| update | Sets are sent here if any intersecting model changes across its `_isAvailable`, `_isInteractionComplete`, `_isActive` or `_isVisited` attributes or if any intersecting set calls `.update()` |

## Set callback functions
| Name | Description |
| --- | --- |
| onInit | Called after it is instantiated and registered, in the init phase |
| onRestore | Called after onInit, in the restore phase, return true/false to signify restore |
| onStart | Called after onRestore, if `wasRestored = false` |
| onRestart | Called on the set after reset |
| onLeave | Called when leaving their content object |
| onVisit | Called when visiting their content object |
| onUpdate | Called when any intersecting model changes across its `_isAvailable`, `_isInteractionComplete`, `_isActive` or `_isVisited` attributes or if any intersecting set calls `.update()` |

## Set trigger functions
| Name | Description |
| --- | --- |
| update | Triggers the update phase for all sets intersecting the modelId |
| reset | Triggers the restart phase for all sets at the modelId |
30 changes: 21 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ An extension to expose an API for a collection of scoring sets used throughout t

A scoring set consists of a collection of models from which scores (`minScore`, `maxScore`, `score`, `scaledScore`, `correctness`, `scaledCorrectness`), completion and passing statuses can be derived. Each set is only responsible for values derived from it's models, and has no perception of those from other scoring sets. A set may be a collection of content, such as an [assessment](https://github.com/adaptlearning/adapt-contrib-scoringAssessment); a collection of scores assigned directly to content; a collection of other scoring sets.

Each plugin will register a new set type with the Scoring API, and identify its associations with other models, along with any extended functionality specific to that plugin. This will allow sets to be evaluated for any intersections within the course structure by comparing overlapping hierachies. Scoring sets allow multiple scores to be categorised as required, providing the ability to evaluate user performance across different areas.
Each plugin will register a new set type with the Scoring API, and identify its associations with other models, along with any extended functionality specific to that plugin. This will allow sets to be evaluated for any intersections within the course structure by comparing overlapping hierarchies. Scoring sets allow multiple scores to be categorised as required, providing the ability to evaluate user performance across different areas.

A `TotalSets` scoring set is used to represent all sets contributing to the scoring and completion of the course scoring objective.

### Attributes

Expand All @@ -16,9 +18,9 @@ Scoring sets should be modular in the JSON configuration, with each set added as

**title** (string): A title for the set. Not required, but exposed should it be used for reporting purposes.

**_isScoreIncluded** (boolean): Determines whether the set should be included in the overall score.
**_isScoreIncluded** (boolean): Determines whether the set should be included in `TotalSets`, contributing to the overall score.

**_isCompletionRequired** (boolean): Determines whether the set should be included in the completion checks.
**_isCompletionRequired** (boolean): Determines whether the set should be included in `TotalSets` and evaluated when checking completion.

### Events

Expand All @@ -28,6 +30,8 @@ The following events are triggered for each scoring set:
**Adapt#scoring:set:register**<br>
**Adapt#scoring:[set.type]:restored**<br>
**Adapt#scoring:set:restored**<br>
**Adapt#scoring:[set.type]:update**<br>
**Adapt#scoring:set:update**<br>
**Adapt#scoring:[set.type]:complete**<br>
**Adapt#scoring:set:complete**<br>
**Adapt#scoring:[set.type]:passed**<br>
Expand Down Expand Up @@ -68,13 +72,21 @@ The attributes listed below are used in *course.json* to configure the overall s

## Events

The following events are triggered:
The following [lifecycle](LIFECYCLE.md) events are triggered:

**Adapt#scoring:lifecycle:restored**<br>
**Adapt#scoring:lifecycle:start**<br>
**Adapt#scoring:lifecycle:update**<br>
**Adapt#scoring:lifecycle:reset**

For overall scoring and completion, the events triggered by `TotalSets` should be utilised:

**Adapt#scoring:update**<br>
**Adapt#scoring:reset**<br>
**Adapt#scoring:restored**<br>
**Adapt#scoring:complete**<br>
**Adapt#scoring:pass**
**Adapt#scoring:total:register**<br>
**Adapt#scoring:total:restored**<br>
**Adapt#scoring:total:update**<br>
**Adapt#scoring:total:complete**<br>
**Adapt#scoring:total:passed**<br>
**Adapt#scoring:total:reset**

For backward compatibility the following events are triggered if `"_isBackwardCompatible": true`:

Expand Down
Loading