@longsightgroup/qti3-core 0.9.2 → 0.9.4

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.
Files changed (330) hide show
  1. package/README.md +108 -11
  2. package/dist/adaptive-turn-materializer.d.ts +2 -0
  3. package/dist/adaptive-turn-materializer.d.ts.map +1 -1
  4. package/dist/adaptive-turn-materializer.js +25 -19
  5. package/dist/adaptive-turn-materializer.js.map +1 -1
  6. package/dist/adaptive-turn.d.ts +8 -1
  7. package/dist/adaptive-turn.d.ts.map +1 -1
  8. package/dist/adaptive-turn.js +15 -5
  9. package/dist/adaptive-turn.js.map +1 -1
  10. package/dist/assert-never.d.ts +3 -0
  11. package/dist/assert-never.d.ts.map +1 -0
  12. package/dist/assert-never.js +5 -0
  13. package/dist/assert-never.js.map +1 -0
  14. package/dist/attempt-state-constants.d.ts +6 -0
  15. package/dist/attempt-state-constants.d.ts.map +1 -0
  16. package/dist/attempt-state-constants.js +6 -0
  17. package/dist/attempt-state-constants.js.map +1 -0
  18. package/dist/attempt-state.d.ts +5 -0
  19. package/dist/attempt-state.d.ts.map +1 -0
  20. package/dist/attempt-state.js +190 -0
  21. package/dist/attempt-state.js.map +1 -0
  22. package/dist/catalog.js +1 -1
  23. package/dist/catalog.js.map +1 -1
  24. package/dist/content-text.d.ts.map +1 -1
  25. package/dist/content-text.js +3 -0
  26. package/dist/content-text.js.map +1 -1
  27. package/dist/custom-operators.d.ts +13 -0
  28. package/dist/custom-operators.d.ts.map +1 -0
  29. package/dist/custom-operators.js +2 -0
  30. package/dist/custom-operators.js.map +1 -0
  31. package/dist/delivery-preparation.d.ts +20 -0
  32. package/dist/delivery-preparation.d.ts.map +1 -0
  33. package/dist/delivery-preparation.js +71 -0
  34. package/dist/delivery-preparation.js.map +1 -0
  35. package/dist/delivery-redaction.d.ts +5 -0
  36. package/dist/delivery-redaction.d.ts.map +1 -1
  37. package/dist/delivery-redaction.js +27 -6
  38. package/dist/delivery-redaction.js.map +1 -1
  39. package/dist/delivery-security.js +2 -2
  40. package/dist/delivery-security.js.map +1 -1
  41. package/dist/index.d.ts +8 -0
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +8 -0
  44. package/dist/index.js.map +1 -1
  45. package/dist/item-scoring-expectation.d.ts +4 -0
  46. package/dist/item-scoring-expectation.d.ts.map +1 -0
  47. package/dist/item-scoring-expectation.js +6 -0
  48. package/dist/item-scoring-expectation.js.map +1 -0
  49. package/dist/item-submission-materialization.d.ts +25 -0
  50. package/dist/item-submission-materialization.d.ts.map +1 -0
  51. package/dist/item-submission-materialization.js +68 -0
  52. package/dist/item-submission-materialization.js.map +1 -0
  53. package/dist/parser-custom-interactions.js +1 -1
  54. package/dist/parser-custom-interactions.js.map +1 -1
  55. package/dist/parser-declarations.d.ts.map +1 -1
  56. package/dist/parser-declarations.js +4 -4
  57. package/dist/parser-declarations.js.map +1 -1
  58. package/dist/parser-item-metadata.d.ts.map +1 -1
  59. package/dist/parser-item-metadata.js +8 -7
  60. package/dist/parser-item-metadata.js.map +1 -1
  61. package/dist/parser-processing.js +2 -2
  62. package/dist/parser-processing.js.map +1 -1
  63. package/dist/parser-values.d.ts +5 -1
  64. package/dist/parser-values.d.ts.map +1 -1
  65. package/dist/parser-values.js +33 -0
  66. package/dist/parser-values.js.map +1 -1
  67. package/dist/processing-evaluator-boolean.d.ts +8 -0
  68. package/dist/processing-evaluator-boolean.d.ts.map +1 -0
  69. package/dist/processing-evaluator-boolean.js +47 -0
  70. package/dist/processing-evaluator-boolean.js.map +1 -0
  71. package/dist/processing-evaluator-collection.d.ts +12 -0
  72. package/dist/processing-evaluator-collection.d.ts.map +1 -0
  73. package/dist/processing-evaluator-collection.js +35 -0
  74. package/dist/processing-evaluator-collection.js.map +1 -0
  75. package/dist/processing-evaluator-comparison.d.ts +8 -0
  76. package/dist/processing-evaluator-comparison.d.ts.map +1 -0
  77. package/dist/processing-evaluator-comparison.js +88 -0
  78. package/dist/processing-evaluator-comparison.js.map +1 -0
  79. package/dist/processing-evaluator-custom.d.ts +8 -0
  80. package/dist/processing-evaluator-custom.d.ts.map +1 -0
  81. package/dist/processing-evaluator-custom.js +14 -0
  82. package/dist/processing-evaluator-custom.js.map +1 -0
  83. package/dist/processing-evaluator-geometry.d.ts +8 -0
  84. package/dist/processing-evaluator-geometry.d.ts.map +1 -0
  85. package/dist/processing-evaluator-geometry.js +21 -0
  86. package/dist/processing-evaluator-geometry.js.map +1 -0
  87. package/dist/processing-evaluator-numeric.d.ts +8 -0
  88. package/dist/processing-evaluator-numeric.d.ts.map +1 -0
  89. package/dist/processing-evaluator-numeric.js +112 -0
  90. package/dist/processing-evaluator-numeric.js.map +1 -0
  91. package/dist/processing-evaluator-string.d.ts +8 -0
  92. package/dist/processing-evaluator-string.d.ts.map +1 -0
  93. package/dist/processing-evaluator-string.js +33 -0
  94. package/dist/processing-evaluator-string.js.map +1 -0
  95. package/dist/processing-evaluator-variable.d.ts +8 -0
  96. package/dist/processing-evaluator-variable.d.ts.map +1 -0
  97. package/dist/processing-evaluator-variable.js +39 -0
  98. package/dist/processing-evaluator-variable.js.map +1 -0
  99. package/dist/processing-evaluator.d.ts +22 -0
  100. package/dist/processing-evaluator.d.ts.map +1 -0
  101. package/dist/processing-evaluator.js +137 -0
  102. package/dist/processing-evaluator.js.map +1 -0
  103. package/dist/processing-expression-children.d.ts +3 -0
  104. package/dist/processing-expression-children.d.ts.map +1 -0
  105. package/dist/processing-expression-children.js +68 -0
  106. package/dist/processing-expression-children.js.map +1 -0
  107. package/dist/processing-mapping.d.ts +13 -0
  108. package/dist/processing-mapping.d.ts.map +1 -0
  109. package/dist/processing-mapping.js +120 -0
  110. package/dist/processing-mapping.js.map +1 -0
  111. package/dist/processing-operators.d.ts +11 -0
  112. package/dist/processing-operators.d.ts.map +1 -0
  113. package/dist/processing-operators.js +178 -0
  114. package/dist/processing-operators.js.map +1 -0
  115. package/dist/processing-random.d.ts +2 -0
  116. package/dist/processing-random.d.ts.map +1 -0
  117. package/dist/processing-random.js +16 -0
  118. package/dist/processing-random.js.map +1 -0
  119. package/dist/processing-state.d.ts +8 -0
  120. package/dist/processing-state.d.ts.map +1 -0
  121. package/dist/processing-state.js +44 -0
  122. package/dist/processing-state.js.map +1 -0
  123. package/dist/processing-templates.d.ts +3 -0
  124. package/dist/processing-templates.d.ts.map +1 -0
  125. package/dist/processing-templates.js +12 -0
  126. package/dist/processing-templates.js.map +1 -0
  127. package/dist/processing-values.d.ts +15 -0
  128. package/dist/processing-values.d.ts.map +1 -0
  129. package/dist/processing-values.js +150 -0
  130. package/dist/processing-values.js.map +1 -0
  131. package/dist/processing-variables.d.ts +7 -0
  132. package/dist/processing-variables.d.ts.map +1 -0
  133. package/dist/processing-variables.js +40 -0
  134. package/dist/processing-variables.js.map +1 -0
  135. package/dist/qti-package-assets.d.ts +8 -0
  136. package/dist/qti-package-assets.d.ts.map +1 -0
  137. package/dist/qti-package-assets.js +173 -0
  138. package/dist/qti-package-assets.js.map +1 -0
  139. package/dist/qti-package-items.d.ts +14 -0
  140. package/dist/qti-package-items.d.ts.map +1 -0
  141. package/dist/qti-package-items.js +138 -0
  142. package/dist/qti-package-items.js.map +1 -0
  143. package/dist/qti-package-manifest.d.ts +13 -0
  144. package/dist/qti-package-manifest.d.ts.map +1 -0
  145. package/dist/qti-package-manifest.js +109 -0
  146. package/dist/qti-package-manifest.js.map +1 -0
  147. package/dist/qti-package-metadata.d.ts +8 -0
  148. package/dist/qti-package-metadata.d.ts.map +1 -0
  149. package/dist/qti-package-metadata.js +115 -0
  150. package/dist/qti-package-metadata.js.map +1 -0
  151. package/dist/qti-package-paths.d.ts +6 -0
  152. package/dist/qti-package-paths.d.ts.map +1 -0
  153. package/dist/qti-package-paths.js +32 -0
  154. package/dist/qti-package-paths.js.map +1 -0
  155. package/dist/qti-package-types.d.ts +98 -0
  156. package/dist/qti-package-types.d.ts.map +1 -0
  157. package/dist/qti-package-types.js +2 -0
  158. package/dist/qti-package-types.js.map +1 -0
  159. package/dist/qti-package-xml.d.ts +21 -0
  160. package/dist/qti-package-xml.d.ts.map +1 -0
  161. package/dist/qti-package-xml.js +68 -0
  162. package/dist/qti-package-xml.js.map +1 -0
  163. package/dist/qti-package-zip.d.ts +24 -0
  164. package/dist/qti-package-zip.d.ts.map +1 -0
  165. package/dist/qti-package-zip.js +185 -0
  166. package/dist/qti-package-zip.js.map +1 -0
  167. package/dist/qti-package.d.ts +7 -0
  168. package/dist/qti-package.d.ts.map +1 -0
  169. package/dist/qti-package.js +76 -0
  170. package/dist/qti-package.js.map +1 -0
  171. package/dist/response-input.d.ts +9 -0
  172. package/dist/response-input.d.ts.map +1 -0
  173. package/dist/response-input.js +9 -0
  174. package/dist/response-input.js.map +1 -0
  175. package/dist/response-validation-policy.d.ts +31 -0
  176. package/dist/response-validation-policy.d.ts.map +1 -0
  177. package/dist/response-validation-policy.js +158 -0
  178. package/dist/response-validation-policy.js.map +1 -0
  179. package/dist/response-validation.d.ts +23 -0
  180. package/dist/response-validation.d.ts.map +1 -0
  181. package/dist/response-validation.js +157 -0
  182. package/dist/response-validation.js.map +1 -0
  183. package/dist/scoring-disposition-policy.d.ts +8 -0
  184. package/dist/scoring-disposition-policy.d.ts.map +1 -0
  185. package/dist/scoring-disposition-policy.js +24 -0
  186. package/dist/scoring-disposition-policy.js.map +1 -0
  187. package/dist/serializer-processing-expressions.d.ts.map +1 -1
  188. package/dist/serializer-processing-expressions.js +2 -8
  189. package/dist/serializer-processing-expressions.js.map +1 -1
  190. package/dist/serializer-processing-xml.js +2 -2
  191. package/dist/serializer-processing-xml.js.map +1 -1
  192. package/dist/serializer-processing.d.ts.map +1 -1
  193. package/dist/serializer-processing.js +3 -0
  194. package/dist/serializer-processing.js.map +1 -1
  195. package/dist/session.d.ts +5 -14
  196. package/dist/session.d.ts.map +1 -1
  197. package/dist/session.js +76 -1158
  198. package/dist/session.js.map +1 -1
  199. package/dist/shared-vocabulary-coverage-policy.js +1 -1
  200. package/dist/shared-vocabulary-coverage-policy.js.map +1 -1
  201. package/dist/shared-vocabulary-generated-families.js +1 -1
  202. package/dist/shared-vocabulary-generated-families.js.map +1 -1
  203. package/dist/shared-vocabulary-support.js +3 -3
  204. package/dist/shared-vocabulary-support.js.map +1 -1
  205. package/dist/shared-vocabulary.d.ts.map +1 -1
  206. package/dist/shared-vocabulary.js +4 -3
  207. package/dist/shared-vocabulary.js.map +1 -1
  208. package/dist/support-evidence.d.ts +22 -0
  209. package/dist/support-evidence.d.ts.map +1 -0
  210. package/dist/support-evidence.js +93 -0
  211. package/dist/support-evidence.js.map +1 -0
  212. package/dist/support.d.ts.map +1 -1
  213. package/dist/support.js +30 -43
  214. package/dist/support.js.map +1 -1
  215. package/dist/template-presentation-materializer.d.ts +14 -0
  216. package/dist/template-presentation-materializer.d.ts.map +1 -0
  217. package/dist/template-presentation-materializer.js +143 -0
  218. package/dist/template-presentation-materializer.js.map +1 -0
  219. package/dist/trusted-item-session.d.ts +25 -8
  220. package/dist/trusted-item-session.d.ts.map +1 -1
  221. package/dist/trusted-item-session.js +57 -51
  222. package/dist/trusted-item-session.js.map +1 -1
  223. package/dist/trusted-session-materialization.d.ts +33 -0
  224. package/dist/trusted-session-materialization.d.ts.map +1 -0
  225. package/dist/trusted-session-materialization.js +26 -0
  226. package/dist/trusted-session-materialization.js.map +1 -0
  227. package/dist/tts.d.ts.map +1 -1
  228. package/dist/tts.js +16 -2
  229. package/dist/tts.js.map +1 -1
  230. package/dist/validation-geometry.d.ts +8 -0
  231. package/dist/validation-geometry.d.ts.map +1 -0
  232. package/dist/validation-geometry.js +187 -0
  233. package/dist/validation-geometry.js.map +1 -0
  234. package/dist/validation-interactions.d.ts +3 -0
  235. package/dist/validation-interactions.d.ts.map +1 -0
  236. package/dist/validation-interactions.js +644 -0
  237. package/dist/validation-interactions.js.map +1 -0
  238. package/dist/validation-primitives.d.ts +12 -0
  239. package/dist/validation-primitives.d.ts.map +1 -0
  240. package/dist/validation-primitives.js +53 -0
  241. package/dist/validation-primitives.js.map +1 -0
  242. package/dist/validation-processing.d.ts +4 -0
  243. package/dist/validation-processing.d.ts.map +1 -0
  244. package/dist/validation-processing.js +569 -0
  245. package/dist/validation-processing.js.map +1 -0
  246. package/dist/validation-shared-vocabulary-layout.d.ts +10 -0
  247. package/dist/validation-shared-vocabulary-layout.d.ts.map +1 -0
  248. package/dist/validation-shared-vocabulary-layout.js +102 -0
  249. package/dist/validation-shared-vocabulary-layout.js.map +1 -0
  250. package/dist/validation.d.ts.map +1 -1
  251. package/dist/validation.js +27 -1641
  252. package/dist/validation.js.map +1 -1
  253. package/dist/variable-snapshot.d.ts +9 -0
  254. package/dist/variable-snapshot.d.ts.map +1 -0
  255. package/dist/variable-snapshot.js +9 -0
  256. package/dist/variable-snapshot.js.map +1 -0
  257. package/dist/xml.js +1 -1
  258. package/dist/xml.js.map +1 -1
  259. package/package.json +1 -1
  260. package/src/adaptive-turn-materializer.ts +29 -16
  261. package/src/adaptive-turn.ts +29 -6
  262. package/src/assert-never.ts +4 -0
  263. package/src/attempt-state-constants.ts +5 -0
  264. package/src/attempt-state.ts +245 -0
  265. package/src/catalog.ts +1 -1
  266. package/src/content-text.ts +3 -0
  267. package/src/custom-operators.ts +12 -0
  268. package/src/delivery-preparation.ts +121 -0
  269. package/src/delivery-redaction.ts +37 -6
  270. package/src/delivery-security.ts +2 -2
  271. package/src/index.ts +67 -0
  272. package/src/item-scoring-expectation.ts +9 -0
  273. package/src/item-submission-materialization.ts +120 -0
  274. package/src/parser-custom-interactions.ts +1 -1
  275. package/src/parser-declarations.ts +4 -3
  276. package/src/parser-item-metadata.ts +8 -7
  277. package/src/parser-processing.ts +2 -2
  278. package/src/parser-values.ts +36 -0
  279. package/src/processing-evaluator-boolean.ts +48 -0
  280. package/src/processing-evaluator-collection.ts +50 -0
  281. package/src/processing-evaluator-comparison.ts +106 -0
  282. package/src/processing-evaluator-custom.ts +20 -0
  283. package/src/processing-evaluator-geometry.ts +26 -0
  284. package/src/processing-evaluator-numeric.ts +155 -0
  285. package/src/processing-evaluator-string.ts +60 -0
  286. package/src/processing-evaluator-variable.ts +81 -0
  287. package/src/processing-evaluator.ts +173 -0
  288. package/src/processing-expression-children.ts +69 -0
  289. package/src/processing-mapping.ts +153 -0
  290. package/src/processing-operators.ts +184 -0
  291. package/src/processing-random.ts +16 -0
  292. package/src/processing-state.ts +69 -0
  293. package/src/processing-templates.ts +12 -0
  294. package/src/processing-values.ts +162 -0
  295. package/src/processing-variables.ts +83 -0
  296. package/src/qti-package-assets.ts +256 -0
  297. package/src/qti-package-items.ts +240 -0
  298. package/src/qti-package-manifest.ts +210 -0
  299. package/src/qti-package-metadata.ts +143 -0
  300. package/src/qti-package-paths.ts +56 -0
  301. package/src/qti-package-types.ts +117 -0
  302. package/src/qti-package-xml.ts +97 -0
  303. package/src/qti-package-zip.ts +325 -0
  304. package/src/qti-package.ts +176 -0
  305. package/src/response-input.ts +19 -0
  306. package/src/response-validation-policy.ts +218 -0
  307. package/src/response-validation.ts +282 -0
  308. package/src/scoring-disposition-policy.ts +37 -0
  309. package/src/serializer-processing-expressions.ts +2 -12
  310. package/src/serializer-processing-xml.ts +2 -2
  311. package/src/serializer-processing.ts +3 -0
  312. package/src/session.ts +128 -2006
  313. package/src/shared-vocabulary-coverage-policy.ts +1 -1
  314. package/src/shared-vocabulary-generated-families.ts +1 -1
  315. package/src/shared-vocabulary-support.ts +3 -3
  316. package/src/shared-vocabulary.ts +3 -2
  317. package/src/support-evidence.ts +107 -0
  318. package/src/support.ts +59 -50
  319. package/src/template-presentation-materializer.ts +219 -0
  320. package/src/trusted-item-session.ts +86 -78
  321. package/src/trusted-session-materialization.ts +67 -0
  322. package/src/tts.ts +29 -15
  323. package/src/validation-geometry.ts +204 -0
  324. package/src/validation-interactions.ts +831 -0
  325. package/src/validation-primitives.ts +68 -0
  326. package/src/validation-processing.ts +785 -0
  327. package/src/validation-shared-vocabulary-layout.ts +117 -0
  328. package/src/validation.ts +44 -2077
  329. package/src/variable-snapshot.ts +20 -0
  330. package/src/xml.ts +1 -1
@@ -0,0 +1,256 @@
1
+ import { pushPackageDiagnostic } from "./qti-package-paths.js";
2
+ import type {
3
+ QtiAssessmentTestPackageModel,
4
+ QtiManifestResource,
5
+ QtiPackageAsset,
6
+ QtiPackageAssetSource,
7
+ QtiPackageItem,
8
+ } from "./qti-package-types.js";
9
+ import {
10
+ isPackageRelativeHref,
11
+ packageDescendants,
12
+ resolvePackageHref,
13
+ type PackageXmlFile,
14
+ } from "./qti-package-xml.js";
15
+ import type { QtiPackageEntry } from "./qti-package-zip.js";
16
+ import type { QtiDiagnostic } from "./types.js";
17
+
18
+ interface PendingAssetReference {
19
+ readonly href: string;
20
+ readonly source: QtiPackageAssetSource;
21
+ readonly referencedBy: string;
22
+ }
23
+
24
+ type PackageAssetRefSpec =
25
+ | { readonly kind: "attribute"; readonly localName: string; readonly attribute: string }
26
+ | { readonly kind: "text"; readonly localName: string };
27
+
28
+ const PACKAGE_ASSET_REF_SPECS: readonly PackageAssetRefSpec[] = [
29
+ { kind: "attribute", localName: "qti-stylesheet", attribute: "href" },
30
+ { kind: "attribute", localName: "qti-assessment-stimulus-ref", attribute: "href" },
31
+ { kind: "attribute", localName: "img", attribute: "src" },
32
+ { kind: "attribute", localName: "object", attribute: "data" },
33
+ { kind: "attribute", localName: "audio", attribute: "src" },
34
+ { kind: "attribute", localName: "video", attribute: "src" },
35
+ { kind: "attribute", localName: "source", attribute: "src" },
36
+ { kind: "attribute", localName: "track", attribute: "src" },
37
+ { kind: "text", localName: "qti-file-href" },
38
+ { kind: "text", localName: "qti-resource-icon" },
39
+ ];
40
+
41
+ export function discoverContentAssetHrefs(
42
+ xmlFile: PackageXmlFile,
43
+ diagnostics: QtiDiagnostic[],
44
+ ): string[] {
45
+ if (!xmlFile.root) return [];
46
+
47
+ const refs: string[] = [];
48
+ for (const spec of PACKAGE_ASSET_REF_SPECS) {
49
+ if (spec.kind === "attribute") {
50
+ collectPackageRelativeAttributeRefs(
51
+ refs,
52
+ xmlFile,
53
+ spec.localName,
54
+ spec.attribute,
55
+ diagnostics,
56
+ );
57
+ } else {
58
+ collectPackageRelativeTextRefs(refs, xmlFile, spec.localName, diagnostics);
59
+ }
60
+ }
61
+ return [...new Set(refs)];
62
+ }
63
+
64
+ export function collectPackageAssets(
65
+ resourcesByIdentifier: ReadonlyMap<string, QtiManifestResource>,
66
+ assessmentTest: QtiAssessmentTestPackageModel | undefined,
67
+ items: readonly QtiPackageItem[],
68
+ entriesByPath: ReadonlyMap<string, QtiPackageEntry>,
69
+ diagnostics: QtiDiagnostic[],
70
+ ): QtiPackageAsset[] {
71
+ const pending: PendingAssetReference[] = [];
72
+ const selectedResourceIds = new Set<string>();
73
+ if (assessmentTest?.manifestResourceIdentifier !== undefined) {
74
+ selectedResourceIds.add(assessmentTest.manifestResourceIdentifier);
75
+ }
76
+ for (const item of items) {
77
+ if (item.manifestResourceIdentifier !== undefined) {
78
+ selectedResourceIds.add(item.manifestResourceIdentifier);
79
+ }
80
+ }
81
+
82
+ for (const resourceId of selectedResourceIds) {
83
+ for (const resource of resourceClosure(resourceId, resourcesByIdentifier)) {
84
+ for (const file of resource.files) {
85
+ if (isPrimaryPackageXml(file.href, assessmentTest, items)) continue;
86
+ pending.push({
87
+ href: file.href,
88
+ source: "manifest-resource",
89
+ referencedBy: resource.identifier,
90
+ });
91
+ }
92
+ }
93
+ }
94
+
95
+ if (assessmentTest) {
96
+ for (const itemRef of assessmentTest.itemRefs) {
97
+ if (!entriesByPath.has(itemRef.href)) {
98
+ pushPackageDiagnostic(
99
+ diagnostics,
100
+ "package.reference.missing",
101
+ "error",
102
+ `Assessment test item reference ${itemRef.href} was not found in the package.`,
103
+ itemRef.href,
104
+ );
105
+ }
106
+ }
107
+ for (const href of assessmentTest.assetHrefs) {
108
+ pending.push({ href, source: "assessment-test-content", referencedBy: assessmentTest.href });
109
+ if (!entriesByPath.has(href)) {
110
+ pushPackageDiagnostic(
111
+ diagnostics,
112
+ "package.asset.missing",
113
+ "error",
114
+ `Package asset ${href} referenced from ${assessmentTest.href} was not found.`,
115
+ href,
116
+ );
117
+ }
118
+ }
119
+ }
120
+
121
+ for (const item of items) {
122
+ for (const href of item.assetHrefs) {
123
+ pending.push({ href, source: "item-content", referencedBy: item.href });
124
+ if (!entriesByPath.has(href)) {
125
+ pushPackageDiagnostic(
126
+ diagnostics,
127
+ "package.asset.missing",
128
+ "error",
129
+ `Package asset ${href} referenced from ${item.href} was not found.`,
130
+ href,
131
+ );
132
+ }
133
+ }
134
+ }
135
+
136
+ return materializeAssets(pending, entriesByPath);
137
+ }
138
+
139
+ function resourceClosure(
140
+ rootIdentifier: string,
141
+ resourcesByIdentifier: ReadonlyMap<string, QtiManifestResource>,
142
+ ): QtiManifestResource[] {
143
+ const closed: QtiManifestResource[] = [];
144
+ const visited = new Set<string>();
145
+ const pending = [rootIdentifier];
146
+
147
+ for (let index = 0; index < pending.length; index += 1) {
148
+ const identifier = pending[index];
149
+ if (!identifier || visited.has(identifier)) continue;
150
+ visited.add(identifier);
151
+ const resource = resourcesByIdentifier.get(identifier);
152
+ if (!resource) continue;
153
+ closed.push(resource);
154
+ pending.push(...resource.dependencies);
155
+ }
156
+
157
+ return closed;
158
+ }
159
+
160
+ function isPrimaryPackageXml(
161
+ href: string,
162
+ assessmentTest: QtiAssessmentTestPackageModel | undefined,
163
+ items: readonly { readonly href: string }[],
164
+ ): boolean {
165
+ if (assessmentTest?.href === href) return true;
166
+ return items.some((item) => item.href === href);
167
+ }
168
+
169
+ function collectPackageRelativeAttributeRefs(
170
+ refs: string[],
171
+ xmlFile: PackageXmlFile,
172
+ localName: string,
173
+ attribute: string,
174
+ diagnostics: QtiDiagnostic[],
175
+ ): void {
176
+ for (const node of packageDescendants(xmlFile.root, localName)) {
177
+ const href = node.attributes[attribute];
178
+ if (!isPackageRelativeHref(href)) continue;
179
+ const resolved = resolvePackageHref(xmlFile.path, href.trim(), diagnostics);
180
+ if (resolved) refs.push(resolved);
181
+ }
182
+ }
183
+
184
+ function collectPackageRelativeTextRefs(
185
+ refs: string[],
186
+ xmlFile: PackageXmlFile,
187
+ localName: string,
188
+ diagnostics: QtiDiagnostic[],
189
+ ): void {
190
+ for (const node of packageDescendants(xmlFile.root, localName)) {
191
+ const href = node.text.trim();
192
+ if (!isPackageRelativeHref(href)) continue;
193
+ const resolved = resolvePackageHref(xmlFile.path, href, diagnostics);
194
+ if (resolved) refs.push(resolved);
195
+ }
196
+ }
197
+
198
+ function materializeAssets(
199
+ pending: readonly PendingAssetReference[],
200
+ entriesByPath: ReadonlyMap<string, QtiPackageEntry>,
201
+ ): QtiPackageAsset[] {
202
+ const assetsByHref = new Map<string, PendingAssetReference[]>();
203
+ for (const reference of pending) {
204
+ if (!entriesByPath.has(reference.href)) continue;
205
+ const references = assetsByHref.get(reference.href) ?? [];
206
+ references.push(reference);
207
+ assetsByHref.set(reference.href, references);
208
+ }
209
+
210
+ return [...assetsByHref.entries()]
211
+ .map(([href, references]): QtiPackageAsset => {
212
+ const firstReference = references[0];
213
+ return {
214
+ href,
215
+ mediaType: detectPackageMediaType(href),
216
+ source: firstReference?.source ?? "item-content",
217
+ referencedBy: [...new Set(references.map((reference) => reference.referencedBy))],
218
+ };
219
+ })
220
+ .toSorted((left, right) => left.href.localeCompare(right.href));
221
+ }
222
+
223
+ export function detectPackageMediaType(href: string): string | undefined {
224
+ const extension = href.slice(href.lastIndexOf(".") + 1).toLowerCase();
225
+ switch (extension) {
226
+ case "css":
227
+ return "text/css";
228
+ case "gif":
229
+ return "image/gif";
230
+ case "jpg":
231
+ case "jpeg":
232
+ return "image/jpeg";
233
+ case "mp3":
234
+ return "audio/mpeg";
235
+ case "mp4":
236
+ return "video/mp4";
237
+ case "ogg":
238
+ return "audio/ogg";
239
+ case "png":
240
+ return "image/png";
241
+ case "svg":
242
+ return "image/svg+xml";
243
+ case "vtt":
244
+ return "text/vtt";
245
+ case "wav":
246
+ return "audio/wav";
247
+ case "webm":
248
+ return "video/webm";
249
+ case "webp":
250
+ return "image/webp";
251
+ case "xml":
252
+ return "application/xml";
253
+ default:
254
+ return undefined;
255
+ }
256
+ }
@@ -0,0 +1,240 @@
1
+ import type { QtiPackageXmlNode } from "./package-xml.js";
2
+ import { parseQtiXml } from "./parser.js";
3
+ import { pushPackageDiagnostic } from "./qti-package-paths.js";
4
+ import { discoverContentAssetHrefs } from "./qti-package-assets.js";
5
+ import { QTI_PACKAGE_MANIFEST_PATH } from "./qti-package-manifest.js";
6
+ import { parseStandardAlignments, parseTimingMetadata } from "./qti-package-metadata.js";
7
+ import type {
8
+ QtiAssessmentTestItemRef,
9
+ QtiAssessmentTestPackageModel,
10
+ QtiManifestResource,
11
+ QtiPackageItem,
12
+ QtiPackageItemSource,
13
+ QtiPackageShape,
14
+ } from "./qti-package-types.js";
15
+ import {
16
+ packageDescendants,
17
+ parsePackageXml,
18
+ pushXmlDiagnostics,
19
+ resolvePackageHref,
20
+ scopeDiagnosticToPackagePath,
21
+ type PackageXmlFile,
22
+ } from "./qti-package-xml.js";
23
+ import type { QtiPackageEntry } from "./qti-package-zip.js";
24
+ import type { QtiDiagnostic } from "./types.js";
25
+
26
+ export interface PackageItemReference {
27
+ readonly href: string;
28
+ readonly source: QtiPackageItemSource;
29
+ readonly manifestResourceIdentifier?: string | undefined;
30
+ readonly assessmentItemRefIdentifier?: string | undefined;
31
+ }
32
+
33
+ export function parseAssessmentTestPackageModel(
34
+ packageShape: QtiPackageShape,
35
+ assessmentTestResources: readonly QtiManifestResource[],
36
+ entriesByPath: ReadonlyMap<string, QtiPackageEntry>,
37
+ xmlFilesByPath: ReadonlyMap<string, PackageXmlFile>,
38
+ diagnostics: QtiDiagnostic[],
39
+ ): QtiAssessmentTestPackageModel | undefined {
40
+ if (packageShape !== "assessment-test-resource") return undefined;
41
+
42
+ const firstResource = assessmentTestResources[0];
43
+ if (!firstResource) return undefined;
44
+
45
+ if (assessmentTestResources.length > 1) {
46
+ pushPackageDiagnostic(
47
+ diagnostics,
48
+ "package.assessmentTest.resource.ambiguous",
49
+ "error",
50
+ "Manifest contains multiple QTI assessment-test resources.",
51
+ QTI_PACKAGE_MANIFEST_PATH,
52
+ );
53
+ }
54
+
55
+ const href = firstResource.href;
56
+ if (!href) return undefined;
57
+ if (!entriesByPath.has(href)) {
58
+ pushPackageDiagnostic(
59
+ diagnostics,
60
+ "package.assessmentTest.missing",
61
+ "error",
62
+ `Assessment test resource ${href} was not found in the package.`,
63
+ href,
64
+ );
65
+ return undefined;
66
+ }
67
+
68
+ const entry = entriesByPath.get(href);
69
+ if (!entry) return undefined;
70
+ const xmlFile = xmlFilesByPath.get(href) ?? parsePackageXml(entry);
71
+ const testDiagnostics: QtiDiagnostic[] = [];
72
+ pushXmlDiagnostics(xmlFile, testDiagnostics);
73
+ const root = xmlFile.root?.localName === "qti-assessment-test" ? xmlFile.root : undefined;
74
+ if (xmlFile.root && !root) {
75
+ pushPackageDiagnostic(
76
+ testDiagnostics,
77
+ "package.assessmentTest.root",
78
+ "error",
79
+ `Expected qti-assessment-test root, found ${xmlFile.root.localName}.`,
80
+ href,
81
+ );
82
+ }
83
+
84
+ const itemRefs = root ? parseAssessmentTestItemRefs(root, href, testDiagnostics) : [];
85
+ const assetHrefs = root ? discoverContentAssetHrefs(xmlFile, testDiagnostics) : [];
86
+ const scopedDiagnostics = testDiagnostics.map((diagnostic) =>
87
+ scopeDiagnosticToPackagePath(href, diagnostic),
88
+ );
89
+ diagnostics.push(...scopedDiagnostics);
90
+
91
+ return {
92
+ href,
93
+ identifier: root?.attributes.identifier ?? "",
94
+ title: root?.attributes.title,
95
+ manifestResourceIdentifier: firstResource.identifier,
96
+ itemRefs,
97
+ timing: root ? parseTimingMetadata(root, href) : undefined,
98
+ standards: root ? parseStandardAlignments(root, href) : [],
99
+ assetHrefs,
100
+ diagnostics: scopedDiagnostics,
101
+ attributes: root ? { ...root.attributes } : {},
102
+ xml: xmlFile.xml,
103
+ };
104
+ }
105
+
106
+ function parseAssessmentTestItemRefs(
107
+ root: QtiPackageXmlNode,
108
+ sourcePath: string,
109
+ diagnostics: QtiDiagnostic[],
110
+ ): QtiAssessmentTestItemRef[] {
111
+ const itemRefs: QtiAssessmentTestItemRef[] = [];
112
+ for (const refNode of packageDescendants(root, "qti-assessment-item-ref")) {
113
+ const rawHref = refNode.attributes.href;
114
+ if (!rawHref) {
115
+ pushPackageDiagnostic(
116
+ diagnostics,
117
+ "package.assessmentTest.itemRef.href.missing",
118
+ "error",
119
+ "qti-assessment-item-ref is missing href.",
120
+ sourcePath,
121
+ );
122
+ continue;
123
+ }
124
+
125
+ const href = resolvePackageHref(sourcePath, rawHref, diagnostics);
126
+ if (!href) continue;
127
+ itemRefs.push({
128
+ identifier: refNode.attributes.identifier,
129
+ href,
130
+ attributes: { ...refNode.attributes },
131
+ });
132
+ }
133
+ return itemRefs;
134
+ }
135
+
136
+ export function itemReferencesForPackageShape(
137
+ packageShape: QtiPackageShape,
138
+ itemResources: readonly QtiManifestResource[],
139
+ assessmentTest: QtiAssessmentTestPackageModel | undefined,
140
+ diagnostics: QtiDiagnostic[],
141
+ ): PackageItemReference[] {
142
+ if (packageShape === "assessment-test-resource") {
143
+ return (assessmentTest?.itemRefs ?? []).map((itemRef) => ({
144
+ href: itemRef.href,
145
+ source: "assessment-test",
146
+ assessmentItemRefIdentifier: itemRef.identifier,
147
+ }));
148
+ }
149
+
150
+ if (packageShape !== "manifest-item-resources") return [];
151
+
152
+ const references: PackageItemReference[] = [];
153
+ const seen = new Set<string>();
154
+ for (const resource of itemResources) {
155
+ const href = resource.href;
156
+ if (!href) continue;
157
+ if (seen.has(href)) {
158
+ pushPackageDiagnostic(
159
+ diagnostics,
160
+ "package.manifest.itemResource.href.duplicate",
161
+ "error",
162
+ `Multiple manifest item resources point to ${href}.`,
163
+ href,
164
+ );
165
+ continue;
166
+ }
167
+ seen.add(href);
168
+ references.push({
169
+ href,
170
+ source: "manifest",
171
+ manifestResourceIdentifier: resource.identifier,
172
+ });
173
+ }
174
+ return references;
175
+ }
176
+
177
+ export function parsePackageItems(
178
+ references: readonly PackageItemReference[],
179
+ entriesByPath: ReadonlyMap<string, QtiPackageEntry>,
180
+ xmlFilesByPath: ReadonlyMap<string, PackageXmlFile>,
181
+ diagnostics: QtiDiagnostic[],
182
+ ): QtiPackageItem[] {
183
+ const items: QtiPackageItem[] = [];
184
+ const seen = new Set<string>();
185
+
186
+ for (const reference of references) {
187
+ if (seen.has(reference.href)) {
188
+ pushPackageDiagnostic(
189
+ diagnostics,
190
+ "package.item.reference.duplicate",
191
+ "warning",
192
+ `Package item ${reference.href} is referenced more than once.`,
193
+ reference.href,
194
+ );
195
+ continue;
196
+ }
197
+ seen.add(reference.href);
198
+
199
+ const entry = entriesByPath.get(reference.href);
200
+ if (!entry) {
201
+ pushPackageDiagnostic(
202
+ diagnostics,
203
+ "package.item.missing",
204
+ "error",
205
+ `Package item reference ${reference.href} was not found.`,
206
+ reference.href,
207
+ );
208
+ continue;
209
+ }
210
+
211
+ const xmlFile = xmlFilesByPath.get(reference.href) ?? parsePackageXml(entry);
212
+ // Full QTI parse for document model; lightweight tree above supplies metadata and asset refs.
213
+ const parsed = parseQtiXml(xmlFile.xml);
214
+ const itemDiagnostics = parsed.diagnostics.map((diagnostic) =>
215
+ scopeDiagnosticToPackagePath(reference.href, diagnostic),
216
+ );
217
+ diagnostics.push(...itemDiagnostics);
218
+
219
+ const assetHrefs = xmlFile.root ? discoverContentAssetHrefs(xmlFile, diagnostics) : [];
220
+ const root = xmlFile.root?.localName === "qti-assessment-item" ? xmlFile.root : undefined;
221
+ const item = parsed.document?.item;
222
+
223
+ items.push({
224
+ href: reference.href,
225
+ source: reference.source,
226
+ manifestResourceIdentifier: reference.manifestResourceIdentifier,
227
+ assessmentItemRefIdentifier: reference.assessmentItemRefIdentifier,
228
+ identifier: item?.identifier,
229
+ title: item?.title,
230
+ document: parsed.document,
231
+ timing: root ? parseTimingMetadata(root, reference.href) : undefined,
232
+ standards: root ? parseStandardAlignments(root, reference.href) : [],
233
+ assetHrefs,
234
+ diagnostics: itemDiagnostics,
235
+ xml: xmlFile.xml,
236
+ });
237
+ }
238
+
239
+ return items;
240
+ }
@@ -0,0 +1,210 @@
1
+ import type { QtiPackageXmlNode } from "./package-xml.js";
2
+ import { normalizePackagePath, pushPackageDiagnostic } from "./qti-package-paths.js";
3
+ import type { QtiManifestFile, QtiManifestResource, QtiPackageShape } from "./qti-package-types.js";
4
+ import { childPackageElements, packageDescendants, stripHrefSuffix } from "./qti-package-xml.js";
5
+ import type { QtiPackageEntry } from "./qti-package-zip.js";
6
+ import type { QtiDiagnostic } from "./types.js";
7
+
8
+ export const QTI_PACKAGE_MANIFEST_PATH = "imsmanifest.xml";
9
+
10
+ export function isQtiItemResource(type: string): boolean {
11
+ return type.toLowerCase().startsWith("imsqti_item_xmlv3p0");
12
+ }
13
+
14
+ export function isQtiAssessmentTestResource(type: string): boolean {
15
+ return type.toLowerCase().startsWith("imsqti_test_xmlv3p0");
16
+ }
17
+
18
+ export function parseManifestResources(
19
+ manifestRoot: QtiPackageXmlNode,
20
+ entriesByPath: ReadonlyMap<string, QtiPackageEntry>,
21
+ diagnostics: QtiDiagnostic[],
22
+ ): QtiManifestResource[] {
23
+ const resourceNodes = packageDescendants(manifestRoot, "resources").flatMap((resources) =>
24
+ childPackageElements(resources, "resource"),
25
+ );
26
+ const resources: QtiManifestResource[] = [];
27
+
28
+ for (const node of resourceNodes) {
29
+ const identifier = node.attributes.identifier ?? "";
30
+ if (identifier.length === 0) {
31
+ pushPackageDiagnostic(
32
+ diagnostics,
33
+ "package.manifest.resource.identifier.missing",
34
+ "error",
35
+ "Manifest resource is missing an identifier.",
36
+ QTI_PACKAGE_MANIFEST_PATH,
37
+ );
38
+ }
39
+
40
+ const files = parseManifestFiles(node, entriesByPath, diagnostics);
41
+ const href =
42
+ node.attributes.href === undefined
43
+ ? firstXmlManifestFileHref(files)
44
+ : normalizePackagePath(
45
+ stripHrefSuffix(node.attributes.href),
46
+ `manifest resource ${identifier || "(missing identifier)"} href`,
47
+ diagnostics,
48
+ );
49
+ const dependencies = parseManifestDependencies(node, diagnostics);
50
+
51
+ resources.push({
52
+ identifier,
53
+ type: node.attributes.type ?? "",
54
+ href,
55
+ files,
56
+ dependencies,
57
+ attributes: { ...node.attributes },
58
+ });
59
+ }
60
+
61
+ return resources;
62
+ }
63
+
64
+ function parseManifestFiles(
65
+ resourceNode: QtiPackageXmlNode,
66
+ entriesByPath: ReadonlyMap<string, QtiPackageEntry>,
67
+ diagnostics: QtiDiagnostic[],
68
+ ): QtiManifestFile[] {
69
+ const files: QtiManifestFile[] = [];
70
+ for (const fileNode of packageDescendants(resourceNode, "file")) {
71
+ const rawHref = fileNode.attributes.href;
72
+ if (!rawHref) {
73
+ pushPackageDiagnostic(
74
+ diagnostics,
75
+ "package.manifest.file.href.missing",
76
+ "error",
77
+ "Manifest file entry is missing href.",
78
+ QTI_PACKAGE_MANIFEST_PATH,
79
+ );
80
+ continue;
81
+ }
82
+
83
+ const href = normalizePackagePath(stripHrefSuffix(rawHref), "manifest file href", diagnostics);
84
+ if (!href) continue;
85
+ files.push({ href, attributes: { ...fileNode.attributes } });
86
+ if (!entriesByPath.has(href)) {
87
+ pushPackageDiagnostic(
88
+ diagnostics,
89
+ "package.manifest.file.missing",
90
+ "error",
91
+ `Manifest file reference ${href} was not found in the package.`,
92
+ href,
93
+ );
94
+ }
95
+ }
96
+ return files;
97
+ }
98
+
99
+ function parseManifestDependencies(
100
+ resourceNode: QtiPackageXmlNode,
101
+ diagnostics: QtiDiagnostic[],
102
+ ): string[] {
103
+ const dependencies: string[] = [];
104
+ for (const dependencyNode of packageDescendants(resourceNode, "dependency")) {
105
+ const identifierref = dependencyNode.attributes.identifierref;
106
+ if (!identifierref) {
107
+ pushPackageDiagnostic(
108
+ diagnostics,
109
+ "package.manifest.dependency.identifierref.missing",
110
+ "error",
111
+ "Manifest dependency is missing identifierref.",
112
+ QTI_PACKAGE_MANIFEST_PATH,
113
+ );
114
+ continue;
115
+ }
116
+ dependencies.push(identifierref);
117
+ }
118
+ return [...new Set(dependencies)];
119
+ }
120
+
121
+ function firstXmlManifestFileHref(files: readonly QtiManifestFile[]): string | undefined {
122
+ return files.find((file) => file.href.toLowerCase().endsWith(".xml"))?.href;
123
+ }
124
+
125
+ export function indexManifestResources(
126
+ resources: readonly QtiManifestResource[],
127
+ diagnostics: QtiDiagnostic[],
128
+ ): Map<string, QtiManifestResource> {
129
+ const byIdentifier = new Map<string, QtiManifestResource>();
130
+ for (const resource of resources) {
131
+ const identifier = resource.identifier;
132
+ if (identifier.length === 0) continue;
133
+ if (byIdentifier.has(identifier)) {
134
+ pushPackageDiagnostic(
135
+ diagnostics,
136
+ "package.manifest.resource.identifier.duplicate",
137
+ "error",
138
+ `Manifest contains duplicate resource identifier ${identifier}.`,
139
+ QTI_PACKAGE_MANIFEST_PATH,
140
+ );
141
+ continue;
142
+ }
143
+ byIdentifier.set(identifier, resource);
144
+ }
145
+ return byIdentifier;
146
+ }
147
+
148
+ export function diagnoseManifestDependencyReferences(
149
+ resources: readonly QtiManifestResource[],
150
+ resourcesByIdentifier: ReadonlyMap<string, QtiManifestResource>,
151
+ diagnostics: QtiDiagnostic[],
152
+ ): void {
153
+ for (const resource of resources) {
154
+ for (const dependency of resource.dependencies) {
155
+ if (resourcesByIdentifier.has(dependency)) continue;
156
+ pushPackageDiagnostic(
157
+ diagnostics,
158
+ "package.manifest.dependency.missing",
159
+ "error",
160
+ `Manifest resource ${resource.identifier} depends on missing resource ${dependency}.`,
161
+ QTI_PACKAGE_MANIFEST_PATH,
162
+ );
163
+ }
164
+ }
165
+ }
166
+
167
+ export function diagnosePrimaryResourceHrefs(
168
+ resources: readonly QtiManifestResource[],
169
+ diagnostics: QtiDiagnostic[],
170
+ ): void {
171
+ for (const resource of resources) {
172
+ if (resource.href) continue;
173
+ pushPackageDiagnostic(
174
+ diagnostics,
175
+ "package.manifest.resource.href.missing",
176
+ "error",
177
+ `Manifest resource ${resource.identifier || "(missing identifier)"} is missing a primary XML href.`,
178
+ QTI_PACKAGE_MANIFEST_PATH,
179
+ );
180
+ }
181
+ }
182
+
183
+ export function detectPackageShape(
184
+ itemResources: readonly QtiManifestResource[],
185
+ assessmentTestResources: readonly QtiManifestResource[],
186
+ diagnostics: QtiDiagnostic[],
187
+ ): QtiPackageShape {
188
+ if (assessmentTestResources.length > 0 && itemResources.length > 0) {
189
+ pushPackageDiagnostic(
190
+ diagnostics,
191
+ "package.shape.ambiguous",
192
+ "error",
193
+ "Manifest contains both QTI item resources and QTI assessment-test resources.",
194
+ QTI_PACKAGE_MANIFEST_PATH,
195
+ );
196
+ return "unknown";
197
+ }
198
+
199
+ if (assessmentTestResources.length > 0) return "assessment-test-resource";
200
+ if (itemResources.length > 0) return "manifest-item-resources";
201
+
202
+ pushPackageDiagnostic(
203
+ diagnostics,
204
+ "package.shape.unsupported",
205
+ "error",
206
+ "Manifest does not contain QTI item or assessment-test resources.",
207
+ QTI_PACKAGE_MANIFEST_PATH,
208
+ );
209
+ return "unknown";
210
+ }