@nakafa/aksara-contracts 0.27.0

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 (269) hide show
  1. package/LICENSE +185 -0
  2. package/README.md +73 -0
  3. package/dist/artifact/integrity.d.ts +29 -0
  4. package/dist/artifact/integrity.js +54 -0
  5. package/dist/artifact/limits.d.ts +26 -0
  6. package/dist/artifact/limits.js +46 -0
  7. package/dist/artifact/source.d.ts +21 -0
  8. package/dist/artifact/source.js +24 -0
  9. package/dist/artifact/spec.d.ts +148 -0
  10. package/dist/artifact/spec.js +92 -0
  11. package/dist/artifact/verify.d.ts +26 -0
  12. package/dist/artifact/verify.js +18 -0
  13. package/dist/cache/content.d.ts +52 -0
  14. package/dist/cache/content.js +80 -0
  15. package/dist/content.d.ts +168 -0
  16. package/dist/content.js +107 -0
  17. package/dist/country.d.ts +4 -0
  18. package/dist/country.js +8 -0
  19. package/dist/date.d.ts +24 -0
  20. package/dist/date.js +44 -0
  21. package/dist/decode.d.ts +4 -0
  22. package/dist/decode.js +8 -0
  23. package/dist/delivery.d.ts +4 -0
  24. package/dist/delivery.js +7 -0
  25. package/dist/errors.d.ts +10 -0
  26. package/dist/errors.js +8 -0
  27. package/dist/graph/family.d.ts +45 -0
  28. package/dist/graph/family.js +85 -0
  29. package/dist/graph/identity.d.ts +19 -0
  30. package/dist/graph/identity.js +39 -0
  31. package/dist/graph/spec.d.ts +20 -0
  32. package/dist/graph/spec.js +26 -0
  33. package/dist/hash/text.d.ts +10 -0
  34. package/dist/hash/text.js +20 -0
  35. package/dist/ids.d.ts +27 -0
  36. package/dist/ids.js +63 -0
  37. package/dist/limits.d.ts +10 -0
  38. package/dist/limits.js +10 -0
  39. package/dist/locale.d.ts +37 -0
  40. package/dist/locale.js +53 -0
  41. package/dist/material/domain.d.ts +4 -0
  42. package/dist/material/domain.js +8 -0
  43. package/dist/preview/artifact.d.ts +198 -0
  44. package/dist/preview/artifact.js +20 -0
  45. package/dist/preview/auth.d.ts +71 -0
  46. package/dist/preview/auth.js +49 -0
  47. package/dist/preview/document.d.ts +692 -0
  48. package/dist/preview/document.js +90 -0
  49. package/dist/preview/spec.d.ts +2517 -0
  50. package/dist/preview/spec.js +142 -0
  51. package/dist/preview/target.d.ts +172 -0
  52. package/dist/preview/target.js +70 -0
  53. package/dist/program/curriculum.d.ts +69 -0
  54. package/dist/program/curriculum.js +181 -0
  55. package/dist/program/snapshot/digest.d.ts +26 -0
  56. package/dist/program/snapshot/digest.js +220 -0
  57. package/dist/program/snapshot/hash.d.ts +31 -0
  58. package/dist/program/snapshot/hash.js +40 -0
  59. package/dist/program/snapshot/row-hash.d.ts +155 -0
  60. package/dist/program/snapshot/row-hash.js +34 -0
  61. package/dist/program/snapshot/row.d.ts +139 -0
  62. package/dist/program/snapshot/row.js +28 -0
  63. package/dist/program/snapshot/spec.d.ts +36 -0
  64. package/dist/program/snapshot/spec.js +39 -0
  65. package/dist/program/spec.d.ts +85 -0
  66. package/dist/program/spec.js +197 -0
  67. package/dist/projection/article.d.ts +145 -0
  68. package/dist/projection/article.js +150 -0
  69. package/dist/projection/digest.d.ts +261 -0
  70. package/dist/projection/digest.js +59 -0
  71. package/dist/projection/hash.d.ts +3 -0
  72. package/dist/projection/hash.js +10 -0
  73. package/dist/projection/material.d.ts +109 -0
  74. package/dist/projection/material.js +160 -0
  75. package/dist/projection/page.d.ts +112 -0
  76. package/dist/projection/page.js +106 -0
  77. package/dist/projection/question.d.ts +520 -0
  78. package/dist/projection/question.js +109 -0
  79. package/dist/projection/spec.d.ts +503 -0
  80. package/dist/projection/spec.js +56 -0
  81. package/dist/projection/verify.d.ts +281 -0
  82. package/dist/projection/verify.js +86 -0
  83. package/dist/question/identity.d.ts +175 -0
  84. package/dist/question/identity.js +205 -0
  85. package/dist/question/item.d.ts +453 -0
  86. package/dist/question/item.js +146 -0
  87. package/dist/question/response.d.ts +175 -0
  88. package/dist/question/response.js +150 -0
  89. package/dist/quran/attribution.d.ts +48 -0
  90. package/dist/quran/attribution.js +44 -0
  91. package/dist/quran/identity.d.ts +105 -0
  92. package/dist/quran/identity.js +100 -0
  93. package/dist/quran/notes.d.ts +56 -0
  94. package/dist/quran/notes.js +142 -0
  95. package/dist/quran/provenance.d.ts +257 -0
  96. package/dist/quran/provenance.js +144 -0
  97. package/dist/quran/snapshot/digest.d.ts +33 -0
  98. package/dist/quran/snapshot/digest.js +232 -0
  99. package/dist/quran/snapshot/hash.d.ts +51 -0
  100. package/dist/quran/snapshot/hash.js +50 -0
  101. package/dist/quran/snapshot/row/hash.d.ts +748 -0
  102. package/dist/quran/snapshot/row/hash.js +115 -0
  103. package/dist/quran/snapshot/row.d.ts +354 -0
  104. package/dist/quran/snapshot/row.js +91 -0
  105. package/dist/quran/snapshot/spec.d.ts +50 -0
  106. package/dist/quran/snapshot/spec.js +66 -0
  107. package/dist/quran/source.d.ts +206 -0
  108. package/dist/quran/source.js +153 -0
  109. package/dist/quran/spec.d.ts +47 -0
  110. package/dist/quran/spec.js +41 -0
  111. package/dist/quran/text.d.ts +7 -0
  112. package/dist/quran/text.js +11 -0
  113. package/dist/release/canonical.d.ts +23 -0
  114. package/dist/release/canonical.js +29 -0
  115. package/dist/release/current/evidence.d.ts +424 -0
  116. package/dist/release/current/evidence.js +41 -0
  117. package/dist/release/current/runtime.d.ts +4 -0
  118. package/dist/release/current/runtime.js +15 -0
  119. package/dist/release/current/state.d.ts +567 -0
  120. package/dist/release/current/state.js +91 -0
  121. package/dist/release/digest.d.ts +54 -0
  122. package/dist/release/digest.js +71 -0
  123. package/dist/release/hash.d.ts +72 -0
  124. package/dist/release/hash.js +14 -0
  125. package/dist/release/head.d.ts +206 -0
  126. package/dist/release/head.js +131 -0
  127. package/dist/release/items.d.ts +98 -0
  128. package/dist/release/items.js +114 -0
  129. package/dist/release/lifecycle.d.ts +297 -0
  130. package/dist/release/lifecycle.js +97 -0
  131. package/dist/release/origin.d.ts +30 -0
  132. package/dist/release/origin.js +24 -0
  133. package/dist/release/policy.d.ts +45 -0
  134. package/dist/release/policy.js +124 -0
  135. package/dist/release/result/digest.d.ts +127 -0
  136. package/dist/release/result/digest.js +140 -0
  137. package/dist/release/result/spec.d.ts +4 -0
  138. package/dist/release/result/spec.js +5 -0
  139. package/dist/release/rollback/digest.d.ts +126 -0
  140. package/dist/release/rollback/digest.js +92 -0
  141. package/dist/release/rollback/spec.d.ts +1729 -0
  142. package/dist/release/rollback/spec.js +156 -0
  143. package/dist/release/route/digest.d.ts +45 -0
  144. package/dist/release/route/digest.js +59 -0
  145. package/dist/release/route/page.d.ts +56 -0
  146. package/dist/release/route/page.js +46 -0
  147. package/dist/release/route/spec.d.ts +49 -0
  148. package/dist/release/route/spec.js +40 -0
  149. package/dist/release/route/verify.d.ts +66 -0
  150. package/dist/release/route/verify.js +67 -0
  151. package/dist/release/signing.d.ts +6 -0
  152. package/dist/release/signing.js +38 -0
  153. package/dist/release/snapshot/data.d.ts +475 -0
  154. package/dist/release/snapshot/data.js +74 -0
  155. package/dist/release/snapshot/evidence-requirement.d.ts +22 -0
  156. package/dist/release/snapshot/evidence-requirement.js +21 -0
  157. package/dist/release/snapshot/evidence.d.ts +58 -0
  158. package/dist/release/snapshot/evidence.js +14 -0
  159. package/dist/release/snapshot/program-evidence.d.ts +17 -0
  160. package/dist/release/snapshot/program-evidence.js +39 -0
  161. package/dist/release/snapshot/quran-evidence.d.ts +27 -0
  162. package/dist/release/snapshot/quran-evidence.js +43 -0
  163. package/dist/release/snapshot/scope.d.ts +60 -0
  164. package/dist/release/snapshot/scope.js +102 -0
  165. package/dist/release/snapshot/spec.d.ts +193 -0
  166. package/dist/release/snapshot/spec.js +139 -0
  167. package/dist/release/snapshot/tryout-evidence.d.ts +22 -0
  168. package/dist/release/snapshot/tryout-evidence.js +86 -0
  169. package/dist/release/snapshot/verify.d.ts +710 -0
  170. package/dist/release/snapshot/verify.js +147 -0
  171. package/dist/release/spec.d.ts +491 -0
  172. package/dist/release/spec.js +200 -0
  173. package/dist/release/verify.d.ts +98 -0
  174. package/dist/release/verify.js +70 -0
  175. package/dist/renderer/compatibility.d.ts +35 -0
  176. package/dist/renderer/compatibility.js +48 -0
  177. package/dist/renderer/component.d.ts +99 -0
  178. package/dist/renderer/component.js +102 -0
  179. package/dist/renderer/contract.d.ts +144 -0
  180. package/dist/renderer/contract.js +122 -0
  181. package/dist/renderer/domain.d.ts +6 -0
  182. package/dist/renderer/domain.js +18 -0
  183. package/dist/renderer/manifest.d.ts +86 -0
  184. package/dist/renderer/manifest.js +49 -0
  185. package/dist/renderer/selection.d.ts +39 -0
  186. package/dist/renderer/selection.js +87 -0
  187. package/dist/runtime/error.d.ts +8 -0
  188. package/dist/runtime/error.js +20 -0
  189. package/dist/runtime/protected/limits.d.ts +10 -0
  190. package/dist/runtime/protected/limits.js +15 -0
  191. package/dist/runtime/protected/spec.d.ts +328 -0
  192. package/dist/runtime/protected/spec.js +79 -0
  193. package/dist/runtime/protected/verify.d.ts +97 -0
  194. package/dist/runtime/protected/verify.js +72 -0
  195. package/dist/runtime/result.d.ts +12 -0
  196. package/dist/runtime/result.js +17 -0
  197. package/dist/runtime/spec.d.ts +705 -0
  198. package/dist/runtime/spec.js +61 -0
  199. package/dist/runtime/verify.d.ts +479 -0
  200. package/dist/runtime/verify.js +123 -0
  201. package/dist/signature/spec.d.ts +53 -0
  202. package/dist/signature/spec.js +29 -0
  203. package/dist/signature/trusted.d.ts +19 -0
  204. package/dist/signature/trusted.js +32 -0
  205. package/dist/signature/verify.d.ts +10 -0
  206. package/dist/signature/verify.js +82 -0
  207. package/dist/text/order.d.ts +2 -0
  208. package/dist/text/order.js +10 -0
  209. package/dist/text/syntax.d.ts +6 -0
  210. package/dist/text/syntax.js +15 -0
  211. package/dist/transport/batch.d.ts +971 -0
  212. package/dist/transport/batch.js +101 -0
  213. package/dist/transport/failure.d.ts +168 -0
  214. package/dist/transport/failure.js +194 -0
  215. package/dist/transport/group.d.ts +2946 -0
  216. package/dist/transport/group.js +47 -0
  217. package/dist/transport/limits.d.ts +30 -0
  218. package/dist/transport/limits.js +30 -0
  219. package/dist/transport/request.d.ts +9333 -0
  220. package/dist/transport/request.js +121 -0
  221. package/dist/transport/response.d.ts +6247 -0
  222. package/dist/transport/response.js +181 -0
  223. package/dist/transport/runtime.d.ts +77 -0
  224. package/dist/transport/runtime.js +33 -0
  225. package/dist/transport/snapshot.d.ts +978 -0
  226. package/dist/transport/snapshot.js +70 -0
  227. package/dist/tryout/canonical.d.ts +2 -0
  228. package/dist/tryout/canonical.js +11 -0
  229. package/dist/tryout/catalog-hash.d.ts +134 -0
  230. package/dist/tryout/catalog-hash.js +203 -0
  231. package/dist/tryout/catalog.d.ts +376 -0
  232. package/dist/tryout/catalog.js +147 -0
  233. package/dist/tryout/content-hash.d.ts +89 -0
  234. package/dist/tryout/content-hash.js +72 -0
  235. package/dist/tryout/identity.d.ts +12 -0
  236. package/dist/tryout/identity.js +46 -0
  237. package/dist/tryout/key.d.ts +4 -0
  238. package/dist/tryout/key.js +8 -0
  239. package/dist/tryout/language.d.ts +24 -0
  240. package/dist/tryout/language.js +32 -0
  241. package/dist/tryout/locale-closure.d.ts +20 -0
  242. package/dist/tryout/locale-closure.js +164 -0
  243. package/dist/tryout/placement-closure.d.ts +5 -0
  244. package/dist/tryout/placement-closure.js +30 -0
  245. package/dist/tryout/placement-hash.d.ts +11 -0
  246. package/dist/tryout/placement-hash.js +55 -0
  247. package/dist/tryout/placement.d.ts +280 -0
  248. package/dist/tryout/placement.js +82 -0
  249. package/dist/tryout/row-hash.d.ts +28 -0
  250. package/dist/tryout/row-hash.js +50 -0
  251. package/dist/tryout/runtime/canonical.d.ts +8 -0
  252. package/dist/tryout/runtime/canonical.js +33 -0
  253. package/dist/tryout/runtime/hash.d.ts +32 -0
  254. package/dist/tryout/runtime/hash.js +11 -0
  255. package/dist/tryout/runtime/source.d.ts +42 -0
  256. package/dist/tryout/runtime/source.js +47 -0
  257. package/dist/tryout/runtime/spec.d.ts +58 -0
  258. package/dist/tryout/runtime/spec.js +21 -0
  259. package/dist/tryout/runtime/verify.d.ts +61 -0
  260. package/dist/tryout/runtime/verify.js +69 -0
  261. package/dist/tryout/snapshot/canonical.d.ts +7 -0
  262. package/dist/tryout/snapshot/canonical.js +17 -0
  263. package/dist/tryout/snapshot/hash.d.ts +8 -0
  264. package/dist/tryout/snapshot/hash.js +19 -0
  265. package/dist/tryout/snapshot/spec.d.ts +46 -0
  266. package/dist/tryout/snapshot/spec.js +30 -0
  267. package/dist/tryout/spec.d.ts +13 -0
  268. package/dist/tryout/spec.js +15 -0
  269. package/package.json +506 -0
@@ -0,0 +1,72 @@
1
+ import { createHash } from "node:crypto";
2
+ import { Schema } from "effect";
3
+ import { withPublicationDates } from "#contracts/date";
4
+ import { AppLocaleSchema, ArtifactLocaleSchema, DeliveryLanguageSchema, } from "#contracts/locale";
5
+ import { QuestionKeySchema } from "#contracts/question/identity";
6
+ import { canonicalQuestionBlueprint, QuestionBlueprintSchema, } from "#contracts/question/item";
7
+ import { canonicalQuestionResponse, QuestionResponseSchema, } from "#contracts/question/response";
8
+ import { TryoutKeySchema } from "#contracts/tryout/key";
9
+ import { AssessmentLanguagePolicySchema, canonicalAssessmentLanguagePolicy, } from "#contracts/tryout/language";
10
+ import { TryoutContentHashSchema, TryoutSourceRevisionSchema, } from "#contracts/tryout/spec";
11
+ const MULTIPLE_NEWLINES = /\n{3,}/gu;
12
+ /** Exact authored facts that define one app and delivery-language question. */
13
+ export const TryoutContentInputSchema = withPublicationDates({
14
+ answerArtifactLocale: ArtifactLocaleSchema,
15
+ answerBody: Schema.String,
16
+ appLocale: AppLocaleSchema,
17
+ blueprint: Schema.optionalKey(QuestionBlueprintSchema),
18
+ deliveryLanguage: DeliveryLanguageSchema,
19
+ languagePolicy: AssessmentLanguagePolicySchema,
20
+ questionArtifactLocale: ArtifactLocaleSchema,
21
+ questionBody: Schema.String,
22
+ response: QuestionResponseSchema,
23
+ sourcePath: QuestionKeySchema,
24
+ sourceRevision: TryoutSourceRevisionSchema,
25
+ stimulusKey: Schema.optionalKey(TryoutKeySchema),
26
+ }).pipe(Schema.check(Schema.makeFilter(({ answerArtifactLocale, appLocale, deliveryLanguage, questionArtifactLocale, }) => String(answerArtifactLocale) === String(appLocale) &&
27
+ String(questionArtifactLocale) === String(deliveryLanguage), {
28
+ message: "Expected answer locale to match app locale and question locale to match delivery language.",
29
+ })));
30
+ /** Normalizes authored body spacing without changing MDX markup. */
31
+ function normalizeBody(body) {
32
+ return body.replace(MULTIPLE_NEWLINES, "\n\n").trim();
33
+ }
34
+ /** Converts one validated date-only value to its UTC midnight epoch. */
35
+ function dateEpoch(date) {
36
+ const year = Number.parseInt(date.slice(0, 4), 10);
37
+ const month = Number.parseInt(date.slice(5, 7), 10);
38
+ const day = Number.parseInt(date.slice(8, 10), 10);
39
+ return Date.UTC(year, month - 1, day);
40
+ }
41
+ /** Serializes every current question-pair identity field in stable order. */
42
+ export function canonicalizeTryoutContent(input) {
43
+ return JSON.stringify({
44
+ answerArtifactLocale: input.answerArtifactLocale,
45
+ answerBody: normalizeBody(input.answerBody),
46
+ appLocale: input.appLocale,
47
+ ...(input.blueprint === undefined
48
+ ? {}
49
+ : { blueprint: canonicalQuestionBlueprint(input.blueprint) }),
50
+ ...(input.dateModified === undefined
51
+ ? {}
52
+ : { dateModified: dateEpoch(input.dateModified) }),
53
+ datePublished: dateEpoch(input.datePublished),
54
+ deliveryLanguage: input.deliveryLanguage,
55
+ languagePolicy: canonicalAssessmentLanguagePolicy(input.languagePolicy),
56
+ questionArtifactLocale: input.questionArtifactLocale,
57
+ questionBody: normalizeBody(input.questionBody),
58
+ response: canonicalQuestionResponse(input.response),
59
+ sourcePath: input.sourcePath,
60
+ sourceRevision: input.sourceRevision,
61
+ ...(input.stimulusKey === undefined
62
+ ? {}
63
+ : { stimulusKey: input.stimulusKey }),
64
+ });
65
+ }
66
+ /** Hashes one complete current question pair through its canonical identity. */
67
+ export function hashTryoutContent(input) {
68
+ const digest = createHash("sha256")
69
+ .update(canonicalizeTryoutContent(input), "utf8")
70
+ .digest("hex");
71
+ return TryoutContentHashSchema.make(digest);
72
+ }
@@ -0,0 +1,12 @@
1
+ import type { TryoutCatalogNodeIdentity, TryoutCatalogRow } from "#contracts/tryout/catalog";
2
+ import type { TryoutPlacementSource } from "#contracts/tryout/placement";
3
+ /** Builds one deterministic current node identity before its row is loaded. */
4
+ export declare function tryoutCatalogNodeIdentity(input: TryoutCatalogNodeIdentity): string;
5
+ /** Builds one deterministic current hierarchy identity from its signed row. */
6
+ export declare function tryoutCatalogIdentity(row: TryoutCatalogRow): string;
7
+ /** Builds the deterministic placement identity across application locales. */
8
+ export declare function tryoutPlacementIdentity(row: TryoutPlacementSource): string;
9
+ /** Builds one locale-neutral placement identity for closure checks. */
10
+ export declare function tryoutPlacementLogicalIdentity(row: TryoutPlacementSource): string;
11
+ /** Compares placements in the order used by question-head binding. */
12
+ export declare function compareTryoutPlacements(left: TryoutPlacementSource, right: TryoutPlacementSource): -1 | 0 | 1;
@@ -0,0 +1,46 @@
1
+ import { compareCodeUnits } from "#contracts/text/order";
2
+ /** Builds one deterministic current node identity before its row is loaded. */
3
+ export function tryoutCatalogNodeIdentity(input) {
4
+ return [
5
+ input.appLocale,
6
+ input.kind,
7
+ input.countryKey,
8
+ "examKey" in input ? input.examKey : "",
9
+ "trackKey" in input ? input.trackKey : "",
10
+ "setKey" in input ? input.setKey : "",
11
+ "sectionKey" in input ? input.sectionKey : "",
12
+ ].join("\0");
13
+ }
14
+ /** Builds one deterministic current hierarchy identity from its signed row. */
15
+ export function tryoutCatalogIdentity(row) {
16
+ return tryoutCatalogNodeIdentity(row);
17
+ }
18
+ /** Builds the deterministic placement identity across application locales. */
19
+ export function tryoutPlacementIdentity(row) {
20
+ return [
21
+ row.countryKey,
22
+ row.examKey,
23
+ row.trackKey,
24
+ row.setKey,
25
+ row.sectionKey,
26
+ row.questionOrder,
27
+ row.questionContentKey,
28
+ row.appLocale,
29
+ ].join("\0");
30
+ }
31
+ /** Builds one locale-neutral placement identity for closure checks. */
32
+ export function tryoutPlacementLogicalIdentity(row) {
33
+ return [
34
+ row.countryKey,
35
+ row.examKey,
36
+ row.trackKey,
37
+ row.setKey,
38
+ row.sectionKey,
39
+ row.questionOrder,
40
+ row.questionContentKey,
41
+ ].join("\0");
42
+ }
43
+ /** Compares placements in the order used by question-head binding. */
44
+ export function compareTryoutPlacements(left, right) {
45
+ return compareCodeUnits(tryoutPlacementIdentity(left), tryoutPlacementIdentity(right));
46
+ }
@@ -0,0 +1,4 @@
1
+ import { Schema } from "effect";
2
+ /** Stable lowercase identity for one try-out hierarchy node. */
3
+ export declare const TryoutKeySchema: Schema.String;
4
+ export type TryoutKey = typeof TryoutKeySchema.Type;
@@ -0,0 +1,8 @@
1
+ import { Schema } from "effect";
2
+ import { isLowerKebab } from "#contracts/text/syntax";
3
+ /** Stable lowercase identity for one try-out hierarchy node. */
4
+ export const TryoutKeySchema = Schema.String.pipe(Schema.check(Schema.makeFilter(isLowerKebab, {
5
+ description: "Lowercase kebab-case try-out source key.",
6
+ identifier: "TryoutKey",
7
+ message: "Invalid try-out key.",
8
+ })), Schema.check(Schema.isMaxLength(128)));
@@ -0,0 +1,24 @@
1
+ import { Schema } from "effect";
2
+ import { type AppLocale, type ArtifactLocale, type DeliveryLanguage } from "#contracts/locale";
3
+ /** Source-owned policy for the language presented by one assessment section. */
4
+ export declare const AssessmentLanguagePolicySchema: Schema.Union<readonly [Schema.Struct<{
5
+ readonly kind: Schema.Literal<"app-locale">;
6
+ }>, Schema.Struct<{
7
+ readonly kind: Schema.Literal<"fixed">;
8
+ readonly language: Schema.brand<Schema.Literals<readonly ["en", "id", "de"]>, "@NakafaAI/AksaraDeliveryLanguage">;
9
+ }>]>;
10
+ export type AssessmentLanguagePolicy = typeof AssessmentLanguagePolicySchema.Type;
11
+ /** Resolves one section's delivered language without inspecting its name. */
12
+ export declare function deliveryLanguageForPolicy(policy: AssessmentLanguagePolicy, appLocale: AppLocale): DeliveryLanguage;
13
+ /** Resolves the exact immutable question artifact locale for one app locale. */
14
+ export declare function questionArtifactLocaleForPolicy(policy: AssessmentLanguagePolicy, appLocale: AppLocale): ArtifactLocale;
15
+ /** Lists unique prompt and response locales required by one section policy. */
16
+ export declare function questionArtifactLocalesForPolicy(policy: AssessmentLanguagePolicy): readonly (("de" | "en" | "id") & import("effect/Brand").Brand<"@NakafaAI/AksaraArtifactLocale">)[];
17
+ /** Returns policy facts in stable field order for signed canonicalizers. */
18
+ export declare function canonicalAssessmentLanguagePolicy(policy: AssessmentLanguagePolicy): {
19
+ kind: "fixed";
20
+ language: ("de" | "en" | "id") & import("effect/Brand").Brand<"@NakafaAI/AksaraDeliveryLanguage">;
21
+ } | {
22
+ language?: never;
23
+ kind: "app-locale";
24
+ };
@@ -0,0 +1,32 @@
1
+ import { Schema } from "effect";
2
+ import { ACTIVE_APP_LOCALES, ArtifactLocaleSchema, DeliveryLanguageSchema, } from "#contracts/locale";
3
+ /** Source-owned policy for the language presented by one assessment section. */
4
+ export const AssessmentLanguagePolicySchema = Schema.Union([
5
+ Schema.Struct({ kind: Schema.Literal("app-locale") }),
6
+ Schema.Struct({
7
+ kind: Schema.Literal("fixed"),
8
+ language: DeliveryLanguageSchema,
9
+ }),
10
+ ]);
11
+ /** Resolves one section's delivered language without inspecting its name. */
12
+ export function deliveryLanguageForPolicy(policy, appLocale) {
13
+ return policy.kind === "fixed"
14
+ ? policy.language
15
+ : DeliveryLanguageSchema.make(appLocale);
16
+ }
17
+ /** Resolves the exact immutable question artifact locale for one app locale. */
18
+ export function questionArtifactLocaleForPolicy(policy, appLocale) {
19
+ return ArtifactLocaleSchema.make(deliveryLanguageForPolicy(policy, appLocale));
20
+ }
21
+ /** Lists unique prompt and response locales required by one section policy. */
22
+ export function questionArtifactLocalesForPolicy(policy) {
23
+ return Object.freeze([
24
+ ...new Set(ACTIVE_APP_LOCALES.map((appLocale) => questionArtifactLocaleForPolicy(policy, appLocale))),
25
+ ]);
26
+ }
27
+ /** Returns policy facts in stable field order for signed canonicalizers. */
28
+ export function canonicalAssessmentLanguagePolicy(policy) {
29
+ return policy.kind === "fixed"
30
+ ? { kind: policy.kind, language: policy.language }
31
+ : { kind: policy.kind };
32
+ }
@@ -0,0 +1,20 @@
1
+ import { Effect, Schema, Stream } from "effect";
2
+ import type { ActiveAppLocaleList } from "#contracts/locale";
3
+ import type { TryoutCatalogRecord } from "#contracts/tryout/catalog";
4
+ import type { TryoutPlacementRecord } from "#contracts/tryout/placement";
5
+ declare const TryoutClosureError_base: Schema.Class<TryoutClosureError, Schema.TaggedStruct<"TryoutClosureError", {
6
+ readonly actual: Schema.String;
7
+ readonly code: Schema.Literals<readonly ["assessed-language", "duplicate-locale", "fact-mismatch", "inactive-locale", "missing-locale", "missing-section", "question-count"]>;
8
+ readonly expected: Schema.String;
9
+ readonly identity: Schema.String;
10
+ }>, import("effect/Cause").YieldableError>;
11
+ /** A try-out snapshot is incomplete or inconsistent across app locales. */
12
+ export declare class TryoutClosureError extends TryoutClosureError_base {
13
+ }
14
+ /** Verifies catalog facts, language policy, and inventory across app locales. */
15
+ export declare const verifyTryoutLocaleClosure: <CatalogError, CatalogContext, PlacementError, PlacementContext>(input: {
16
+ readonly activeAppLocales: ActiveAppLocaleList;
17
+ readonly catalog: Stream.Stream<TryoutCatalogRecord, CatalogError, CatalogContext>;
18
+ readonly placements: Stream.Stream<TryoutPlacementRecord, PlacementError, PlacementContext>;
19
+ }) => Effect.Effect<void, CatalogError | PlacementError | TryoutClosureError, CatalogContext | PlacementContext>;
20
+ export {};
@@ -0,0 +1,164 @@
1
+ import { Effect, Schema, Stream } from "effect";
2
+ import { canonicalizeTryoutCatalogFacts, tryoutCatalogLogicalIdentity, tryoutSectionLogicalIdentity, } from "#contracts/tryout/catalog-hash";
3
+ import { tryoutPlacementLogicalIdentity } from "#contracts/tryout/identity";
4
+ import { canonicalizeAssessedLanguagePlacementFacts, canonicalizeLocaleNeutralPlacementFacts, } from "#contracts/tryout/placement-closure";
5
+ /** A try-out snapshot is incomplete or inconsistent across app locales. */
6
+ export class TryoutClosureError extends Schema.TaggedError()("TryoutClosureError", {
7
+ actual: Schema.String,
8
+ code: Schema.Literals([
9
+ "assessed-language",
10
+ "duplicate-locale",
11
+ "fact-mismatch",
12
+ "inactive-locale",
13
+ "missing-locale",
14
+ "missing-section",
15
+ "question-count",
16
+ ]),
17
+ expected: Schema.String,
18
+ identity: Schema.String,
19
+ }) {
20
+ }
21
+ /** Serializes locale sets through the active list's signed canonical order. */
22
+ function localeSetIdentity(locales, activeAppLocales) {
23
+ return JSON.stringify(activeAppLocales.filter((locale) => locales.has(locale)));
24
+ }
25
+ /** Adds one active locale to a logical identity without duplicates. */
26
+ function addLocale(localesByIdentity, activeAppLocales, identity, appLocale) {
27
+ if (!activeAppLocales.includes(appLocale)) {
28
+ return Effect.fail(new TryoutClosureError({
29
+ actual: appLocale,
30
+ code: "inactive-locale",
31
+ expected: JSON.stringify(activeAppLocales),
32
+ identity,
33
+ }));
34
+ }
35
+ const locales = localesByIdentity.get(identity) ?? new Set();
36
+ if (locales.has(appLocale)) {
37
+ return Effect.fail(new TryoutClosureError({
38
+ actual: appLocale,
39
+ code: "duplicate-locale",
40
+ expected: "one row per active app locale",
41
+ identity,
42
+ }));
43
+ }
44
+ locales.add(appLocale);
45
+ localesByIdentity.set(identity, locales);
46
+ return Effect.void;
47
+ }
48
+ /** Confirms every logical row closes over the exact active locale list. */
49
+ function validateLocales(localesByIdentity, activeAppLocales) {
50
+ if (localesByIdentity.size === 0) {
51
+ return Effect.fail(new TryoutClosureError({
52
+ actual: "[]",
53
+ code: "missing-locale",
54
+ expected: JSON.stringify(activeAppLocales),
55
+ identity: "empty",
56
+ }));
57
+ }
58
+ return Effect.forEach(localesByIdentity, ([identity, locales]) => {
59
+ const actual = localeSetIdentity(locales, activeAppLocales);
60
+ const expected = JSON.stringify(activeAppLocales);
61
+ if (actual === expected) {
62
+ return Effect.void;
63
+ }
64
+ return Effect.fail(new TryoutClosureError({
65
+ actual,
66
+ code: "missing-locale",
67
+ expected,
68
+ identity,
69
+ }));
70
+ }, { discard: true });
71
+ }
72
+ /** Adds one catalog row and compares its locale-neutral facts. */
73
+ function addCatalogRow(state, activeAppLocales, row) {
74
+ const identity = tryoutCatalogLogicalIdentity(row);
75
+ const facts = canonicalizeTryoutCatalogFacts(row);
76
+ const expectedFacts = state.factsByIdentity.get(identity);
77
+ if (expectedFacts !== undefined && expectedFacts !== facts) {
78
+ return Effect.fail(new TryoutClosureError({
79
+ actual: facts,
80
+ code: "fact-mismatch",
81
+ expected: expectedFacts,
82
+ identity,
83
+ }));
84
+ }
85
+ state.factsByIdentity.set(identity, facts);
86
+ if (row.kind === "section") {
87
+ state.sections.set(identity, row.questionCount);
88
+ }
89
+ return addLocale(state.localesByIdentity, activeAppLocales, identity, row.appLocale).pipe(Effect.as(state));
90
+ }
91
+ /** Adds one placement after binding it to a real catalog section. */
92
+ function addPlacement(state, catalog, activeAppLocales, row) {
93
+ const identity = tryoutPlacementLogicalIdentity(row);
94
+ const sectionIdentity = tryoutSectionLogicalIdentity(row);
95
+ if (!catalog.sections.has(sectionIdentity)) {
96
+ return Effect.fail(new TryoutClosureError({
97
+ actual: "missing",
98
+ code: "missing-section",
99
+ expected: "catalog section",
100
+ identity: sectionIdentity,
101
+ }));
102
+ }
103
+ const facts = canonicalizeLocaleNeutralPlacementFacts(row);
104
+ const expectedFacts = state.factsByIdentity.get(identity);
105
+ if (expectedFacts !== undefined && expectedFacts !== facts) {
106
+ return Effect.fail(new TryoutClosureError({
107
+ actual: facts,
108
+ code: "fact-mismatch",
109
+ expected: expectedFacts,
110
+ identity,
111
+ }));
112
+ }
113
+ state.factsByIdentity.set(identity, facts);
114
+ if (row.languagePolicy.kind === "fixed") {
115
+ const assessedFacts = canonicalizeAssessedLanguagePlacementFacts(row);
116
+ const expectedAssessedFacts = state.assessedFacts.get(identity);
117
+ if (expectedAssessedFacts !== undefined &&
118
+ expectedAssessedFacts !== assessedFacts) {
119
+ return Effect.fail(new TryoutClosureError({
120
+ actual: assessedFacts,
121
+ code: "assessed-language",
122
+ expected: expectedAssessedFacts,
123
+ identity,
124
+ }));
125
+ }
126
+ state.assessedFacts.set(identity, assessedFacts);
127
+ }
128
+ const sectionLocaleIdentity = `${sectionIdentity}\0${row.appLocale}`;
129
+ state.countsBySectionLocale.set(sectionLocaleIdentity, (state.countsBySectionLocale.get(sectionLocaleIdentity) ?? 0) + 1);
130
+ return addLocale(state.localesByIdentity, activeAppLocales, identity, row.appLocale).pipe(Effect.as(state));
131
+ }
132
+ /** Confirms each localized section owns its declared question inventory. */
133
+ function validateQuestionCounts(catalog, placements, activeAppLocales) {
134
+ return Effect.forEach(catalog.sections, ([identity, questionCount]) => Effect.forEach(activeAppLocales, (appLocale) => {
135
+ const actual = placements.countsBySectionLocale.get(`${identity}\0${appLocale}`) ??
136
+ 0;
137
+ if (actual === questionCount) {
138
+ return Effect.void;
139
+ }
140
+ return Effect.fail(new TryoutClosureError({
141
+ actual: String(actual),
142
+ code: "question-count",
143
+ expected: String(questionCount),
144
+ identity: `${identity}\0${appLocale}`,
145
+ }));
146
+ }, { discard: true }), { discard: true });
147
+ }
148
+ /** Verifies catalog facts, language policy, and inventory across app locales. */
149
+ export const verifyTryoutLocaleClosure = Effect.fn("AksaraContracts.verifyTryoutLocaleClosure")(function* (input) {
150
+ const catalog = yield* input.catalog.pipe(Stream.runFoldEffect(() => ({
151
+ factsByIdentity: new Map(),
152
+ localesByIdentity: new Map(),
153
+ sections: new Map(),
154
+ }), (state, record) => addCatalogRow(state, input.activeAppLocales, record.row)));
155
+ yield* validateLocales(catalog.localesByIdentity, input.activeAppLocales);
156
+ const placements = yield* input.placements.pipe(Stream.runFoldEffect(() => ({
157
+ assessedFacts: new Map(),
158
+ countsBySectionLocale: new Map(),
159
+ factsByIdentity: new Map(),
160
+ localesByIdentity: new Map(),
161
+ }), (state, record) => addPlacement(state, catalog, input.activeAppLocales, record.row)));
162
+ yield* validateLocales(placements.localesByIdentity, input.activeAppLocales);
163
+ yield* validateQuestionCounts(catalog, placements, input.activeAppLocales);
164
+ });
@@ -0,0 +1,5 @@
1
+ import type { TryoutPlacement } from "#contracts/tryout/placement";
2
+ /** Serializes assessed prompt facts reused across application locales. */
3
+ export declare function canonicalizeAssessedLanguagePlacementFacts(row: TryoutPlacement): string;
4
+ /** Serializes placement facts that cannot vary with application locale. */
5
+ export declare function canonicalizeLocaleNeutralPlacementFacts(row: TryoutPlacement): string;
@@ -0,0 +1,30 @@
1
+ import { canonicalQuestionBlueprint } from "#contracts/question/item";
2
+ import { canonicalQuestionResponse, canonicalQuestionResponseStructure, } from "#contracts/question/response";
3
+ import { canonicalAssessmentLanguagePolicy } from "#contracts/tryout/language";
4
+ /** Serializes assessed prompt facts reused across application locales. */
5
+ export function canonicalizeAssessedLanguagePlacementFacts(row) {
6
+ return JSON.stringify({
7
+ deliveryLanguage: row.deliveryLanguage,
8
+ questionArtifactHash: row.questionArtifactHash,
9
+ questionArtifactLocale: row.questionArtifactLocale,
10
+ questionContentKey: row.questionContentKey,
11
+ response: canonicalQuestionResponse(row.response),
12
+ });
13
+ }
14
+ /** Serializes placement facts that cannot vary with application locale. */
15
+ export function canonicalizeLocaleNeutralPlacementFacts(row) {
16
+ return JSON.stringify({
17
+ answerContentKey: row.answerContentKey,
18
+ ...(row.blueprint === undefined
19
+ ? {}
20
+ : { blueprint: canonicalQuestionBlueprint(row.blueprint) }),
21
+ languagePolicy: canonicalAssessmentLanguagePolicy(row.languagePolicy),
22
+ questionContentKey: row.questionContentKey,
23
+ questionSourcePath: row.questionSourcePath,
24
+ rendererDomain: row.rendererDomain,
25
+ response: canonicalQuestionResponseStructure(row.response),
26
+ scope: row.scope,
27
+ sourceRevision: row.sourceRevision,
28
+ ...(row.stimulusKey === undefined ? {} : { stimulusKey: row.stimulusKey }),
29
+ });
30
+ }
@@ -0,0 +1,11 @@
1
+ import type { Stream } from "effect";
2
+ import { type TryoutPlacement, type TryoutPlacementRecord } from "#contracts/tryout/placement";
3
+ /** Serializes one artifact-bound placement with stable field order. */
4
+ export declare function canonicalizeTryoutPlacement(row: TryoutPlacement): string;
5
+ /** Creates one immutable authenticated placement record. */
6
+ export declare function makeTryoutPlacementRecord(row: TryoutPlacement): TryoutPlacementRecord;
7
+ /** Digests canonically ordered placements in constant space. */
8
+ export declare function digestTryoutPlacements<E, R>(records: Stream.Stream<TryoutPlacementRecord, E, R>): import("effect/Effect").Effect<{
9
+ count: number;
10
+ digest: string & import("effect/Brand").Brand<"@NakafaAI/AksaraSha256Hash">;
11
+ }, E | import("#contracts/tryout/row-hash").TryoutDigestError, R>;
@@ -0,0 +1,55 @@
1
+ import { canonicalQuestionBlueprint } from "#contracts/question/item";
2
+ import { canonicalQuestionResponse } from "#contracts/question/response";
3
+ import { hashTryoutCanonical } from "#contracts/tryout/canonical";
4
+ import { tryoutPlacementIdentity } from "#contracts/tryout/identity";
5
+ import { canonicalAssessmentLanguagePolicy } from "#contracts/tryout/language";
6
+ import { TryoutPlacementRecordSchema, } from "#contracts/tryout/placement";
7
+ import { digestTryoutRecords } from "#contracts/tryout/row-hash";
8
+ const PLACEMENT_DOMAIN = "nakafa.aksara.tryout-placements";
9
+ /** Serializes one artifact-bound placement with stable field order. */
10
+ export function canonicalizeTryoutPlacement(row) {
11
+ return JSON.stringify({
12
+ answerArtifactHash: row.answerArtifactHash,
13
+ answerArtifactLocale: row.answerArtifactLocale,
14
+ answerContentKey: row.answerContentKey,
15
+ appLocale: row.appLocale,
16
+ ...(row.blueprint === undefined
17
+ ? {}
18
+ : { blueprint: canonicalQuestionBlueprint(row.blueprint) }),
19
+ contentHash: row.contentHash,
20
+ countryKey: row.countryKey,
21
+ deliveryLanguage: row.deliveryLanguage,
22
+ examKey: row.examKey,
23
+ languagePolicy: canonicalAssessmentLanguagePolicy(row.languagePolicy),
24
+ questionArtifactHash: row.questionArtifactHash,
25
+ questionArtifactLocale: row.questionArtifactLocale,
26
+ questionContentKey: row.questionContentKey,
27
+ questionOrder: row.questionOrder,
28
+ questionSourcePath: row.questionSourcePath,
29
+ rendererDomain: row.rendererDomain,
30
+ response: canonicalQuestionResponse(row.response),
31
+ scope: row.scope,
32
+ sectionKey: row.sectionKey,
33
+ setKey: row.setKey,
34
+ sourceRevision: row.sourceRevision,
35
+ ...(row.stimulusKey === undefined ? {} : { stimulusKey: row.stimulusKey }),
36
+ trackKey: row.trackKey,
37
+ });
38
+ }
39
+ /** Creates one immutable authenticated placement record. */
40
+ export function makeTryoutPlacementRecord(row) {
41
+ return TryoutPlacementRecordSchema.make({
42
+ row,
43
+ rowHash: hashTryoutCanonical(PLACEMENT_DOMAIN, canonicalizeTryoutPlacement(row)),
44
+ });
45
+ }
46
+ /** Digests canonically ordered placements in constant space. */
47
+ export function digestTryoutPlacements(records) {
48
+ return digestTryoutRecords({
49
+ canonicalize: canonicalizeTryoutPlacement,
50
+ domain: PLACEMENT_DOMAIN,
51
+ identity: tryoutPlacementIdentity,
52
+ records,
53
+ rowHash: (row) => makeTryoutPlacementRecord(row).rowHash,
54
+ });
55
+ }