@conform-ed/qti-react 0.0.18 → 0.0.19

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/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export declare const qtiReactPackageName = "@conform-ed/qti-react";
2
2
  export { v0ContentModel, v0InteractionKinds, isAllowedFlowElement, isInteractionKind, sanitizeAttributes, type ContentModel, type V0InteractionKind, } from "./content-model";
3
3
  export { foldString, mapResponse, matchCorrect, mapResponsePoint, scoreResponse } from "./response-processing";
4
+ export { effectiveItemScore, type EffectiveItemScore } from "./item-score";
4
5
  export { assessmentItemViewFromNormalized, assessmentTestViewFromNormalized, stimulusContentFromNormalized, } from "./normalized-item";
5
6
  export { referenceInteractionKinds, reportItemCapability, type ItemCapabilityOptions } from "./item-capability";
6
7
  export { formatPoint, parseCoords, parsePoint, pointInShape, type Point, type QtiShape } from "./graphic";
package/dist/index.js CHANGED
@@ -387,6 +387,23 @@ function scoreResponse(declaration, response, normalize) {
387
387
  correct
388
388
  };
389
389
  }
390
+ // src/item-score.ts
391
+ function numericOutcome(value) {
392
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
393
+ }
394
+ function effectiveItemScore(scores, outcomes) {
395
+ const scoreOutcome = numericOutcome(outcomes["SCORE"]);
396
+ const maxOutcome = numericOutcome(outcomes["MAXSCORE"]);
397
+ const summedMax = scores.reduce((total, score) => total + score.maxScore, 0);
398
+ if (scoreOutcome !== null) {
399
+ return { raw: scoreOutcome, max: maxOutcome ?? summedMax, fromOutcomes: true };
400
+ }
401
+ return {
402
+ raw: scores.reduce((total, score) => total + score.score, 0),
403
+ max: maxOutcome ?? summedMax,
404
+ fromOutcomes: false
405
+ };
406
+ }
390
407
  // src/normalized-item.ts
391
408
  function isRecord(value) {
392
409
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -6836,6 +6853,7 @@ export {
6836
6853
  executeTemplateProcessing,
6837
6854
  executeResponseProcessing,
6838
6855
  endAttemptInteraction,
6856
+ effectiveItemScore,
6839
6857
  drawingInteraction,
6840
6858
  defineInteraction,
6841
6859
  createTestSessionStore,
@@ -0,0 +1,17 @@
1
+ import type { ScoreResult } from "./types";
2
+ export interface EffectiveItemScore {
3
+ readonly raw: number;
4
+ readonly max: number;
5
+ /** True when SCORE came from the RP outcomes of record rather than per-variable scoring. */
6
+ readonly fromOutcomes: boolean;
7
+ }
8
+ /**
9
+ * The item score of record (QTI): a numeric SCORE outcome from response processing is
10
+ * authoritative — PCI/RP-scored items (e.g. math-entry) have no per-variable
11
+ * correctResponse basis, so their standard scores read 0. Summed per-variable standard
12
+ * scoring is the fallback for items without RP. MAXSCORE follows the same precedence.
13
+ *
14
+ * Pure and framework-light: client and server (authoritative finalize) share it so the
15
+ * grade of record is derived identically on both sides.
16
+ */
17
+ export declare function effectiveItemScore(scores: readonly ScoreResult[], outcomes: Readonly<Record<string, unknown>>): EffectiveItemScore;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@conform-ed/qti-react",
3
- "version": "0.0.18",
3
+ "version": "0.0.19",
4
4
  "files": [
5
5
  "src",
6
6
  "dist"
@@ -30,11 +30,11 @@
30
30
  "xspattern": "^3.1.0"
31
31
  },
32
32
  "devDependencies": {
33
- "@conform-ed/contracts": "0.0.18",
34
- "@conform-ed/qti-xml": "0.0.18",
33
+ "@conform-ed/contracts": "0.0.19",
34
+ "@conform-ed/qti-xml": "0.0.19",
35
35
  "@types/react": "^19.2.17",
36
36
  "@types/react-dom": "^19",
37
- "happy-dom": "^20.10.2",
37
+ "happy-dom": "^20.10.3",
38
38
  "react": "^19.2.7",
39
39
  "react-dom": "^19"
40
40
  },
package/src/headless.ts CHANGED
@@ -1,11 +1,15 @@
1
1
  /**
2
- * Headless (React-free) surface of @conform-ed/qti-react: the normalize → view adapters
3
- * and the capability gate, importable on a server (e.g. a QTI ingest pipeline) without
4
- * pulling React. Exposed at `@conform-ed/qti-react/headless`; everything here is also
5
- * re-exported from the package root for React consumers.
2
+ * Headless (React-free) surface of @conform-ed/qti-react: the normalize → view adapters,
3
+ * the capability gate, and the **scoring engine** (Response Processing interpreter, the
4
+ * standard per-response scoring templates, the item-score aggregator, and the test-level
5
+ * outcome-processing controller). Importable on a server (e.g. a QTI ingest pipeline or an
6
+ * authoritative grade finalize) without pulling React. Exposed at
7
+ * `@conform-ed/qti-react/headless`; everything here is also re-exported from the package
8
+ * root for React consumers.
6
9
  *
7
- * Keep this entry free of React-coupled imports — only ./normalized-item and
8
- * ./item-capability (value) plus type-only re-exports.
10
+ * Keep this entry free of React-coupled imports — every module re-exported here
11
+ * (./normalized-item, ./item-capability, ./response-processing, ./rp, ./item-score,
12
+ * ./store, ./test, ./response-validity) is verified framework-light.
9
13
  */
10
14
 
11
15
  export {
@@ -14,6 +18,33 @@ export {
14
18
  stimulusContentFromNormalized,
15
19
  } from "./normalized-item";
16
20
  export { referenceInteractionKinds, reportItemCapability, type ItemCapabilityOptions } from "./item-capability";
21
+
22
+ // The scoring engine, headless. Running these server-side re-derives the grade of record
23
+ // from the learner's raw responses + the (server-held) answer keys — see emergent ADR-0019.
24
+ export { foldString, mapResponse, matchCorrect, mapResponsePoint, scoreResponse } from "./response-processing";
25
+ export { effectiveItemScore, type EffectiveItemScore } from "./item-score";
26
+ export {
27
+ applyCorrectResponseOverrides,
28
+ collectRpIssues,
29
+ collectTemplateIssues,
30
+ executeResponseProcessing,
31
+ executeTemplateProcessing,
32
+ mulberry32,
33
+ resolveTemplate,
34
+ } from "./rp";
35
+ export type {
36
+ CustomOperatorImplementation,
37
+ OutcomeDeclarationView,
38
+ OutcomeValue,
39
+ ResponseNormalization,
40
+ ResponseProcessingContext,
41
+ ResponseProcessingResult,
42
+ ResponseProcessingView,
43
+ TemplateDeclarationView,
44
+ } from "./rp";
45
+ export { createAttemptStore, type AttemptSnapshot, type AttemptStore, type AttemptStoreOptions } from "./store";
46
+ export { createTestController, type TestController, type TestSessionState } from "./test";
47
+
17
48
  export type { CapabilityIssue, CapabilityIssueType, CapabilityReport } from "./capability";
18
49
  export type {
19
50
  AssessmentItemView,
@@ -24,3 +55,4 @@ export type {
24
55
  XmlContentNode,
25
56
  } from "./runtime";
26
57
  export type { AssessmentTestView } from "./test";
58
+ export type { Cardinality, ResponseDeclarationView, ResponseValue, ScoreResult } from "./types";
package/src/index.ts CHANGED
@@ -14,6 +14,8 @@ export {
14
14
 
15
15
  export { foldString, mapResponse, matchCorrect, mapResponsePoint, scoreResponse } from "./response-processing";
16
16
 
17
+ export { effectiveItemScore, type EffectiveItemScore } from "./item-score";
18
+
17
19
  export {
18
20
  assessmentItemViewFromNormalized,
19
21
  assessmentTestViewFromNormalized,
@@ -0,0 +1,40 @@
1
+ import type { ScoreResult } from "./types";
2
+
3
+ export interface EffectiveItemScore {
4
+ readonly raw: number;
5
+ readonly max: number;
6
+ /** True when SCORE came from the RP outcomes of record rather than per-variable scoring. */
7
+ readonly fromOutcomes: boolean;
8
+ }
9
+
10
+ function numericOutcome(value: unknown): number | null {
11
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
12
+ }
13
+
14
+ /**
15
+ * The item score of record (QTI): a numeric SCORE outcome from response processing is
16
+ * authoritative — PCI/RP-scored items (e.g. math-entry) have no per-variable
17
+ * correctResponse basis, so their standard scores read 0. Summed per-variable standard
18
+ * scoring is the fallback for items without RP. MAXSCORE follows the same precedence.
19
+ *
20
+ * Pure and framework-light: client and server (authoritative finalize) share it so the
21
+ * grade of record is derived identically on both sides.
22
+ */
23
+ export function effectiveItemScore(
24
+ scores: readonly ScoreResult[],
25
+ outcomes: Readonly<Record<string, unknown>>,
26
+ ): EffectiveItemScore {
27
+ const scoreOutcome = numericOutcome(outcomes["SCORE"]);
28
+ const maxOutcome = numericOutcome(outcomes["MAXSCORE"]);
29
+ const summedMax = scores.reduce((total, score) => total + score.maxScore, 0);
30
+
31
+ if (scoreOutcome !== null) {
32
+ return { raw: scoreOutcome, max: maxOutcome ?? summedMax, fromOutcomes: true };
33
+ }
34
+
35
+ return {
36
+ raw: scores.reduce((total, score) => total + score.score, 0),
37
+ max: maxOutcome ?? summedMax,
38
+ fromOutcomes: false,
39
+ };
40
+ }