@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,26 @@
1
+ import { Effect, Schema, Stream } from "effect";
2
+ import type { ActiveAppLocaleList } from "#contracts/locale";
3
+ import { type ProgramSnapshotRow } from "#contracts/program/snapshot/row";
4
+ import { ProgramRowHashError } from "#contracts/program/snapshot/row-hash";
5
+ import type { ProgramCounts } from "#contracts/program/snapshot/spec";
6
+ declare const ProgramDigestError_base: Schema.Class<ProgramDigestError, Schema.TaggedStruct<"ProgramDigestError", {
7
+ readonly code: Schema.Literals<readonly ["count", "integrity", "key", "order", "parent", "program", "route", "root", "slug"]>;
8
+ readonly identity: Schema.String;
9
+ }>, import("effect/Cause").YieldableError>;
10
+ /** An aggregate program stream is incomplete, duplicated, or tampered. */
11
+ export declare class ProgramDigestError extends ProgramDigestError_base {
12
+ }
13
+ /** Digests a complete current program snapshot under its active locale set. */
14
+ export declare const digestProgramRows: <E, R>(input: {
15
+ readonly activeAppLocales: ActiveAppLocaleList;
16
+ readonly expected?: ProgramCounts;
17
+ readonly rows: Stream.Stream<ProgramSnapshotRow, E, R>;
18
+ }) => Effect.Effect<{
19
+ curriculumRowCount: number;
20
+ programRowCount: number;
21
+ rowCount: number;
22
+ sitemapCount: number;
23
+ slugCount: number;
24
+ rowDigest: string & import("effect/Brand").Brand<"@NakafaAI/AksaraSha256Hash">;
25
+ }, E | ProgramDigestError | ProgramRowHashError, R>;
26
+ export {};
@@ -0,0 +1,220 @@
1
+ import { createHash } from "node:crypto";
2
+ import { Effect, Schema, Stream } from "effect";
3
+ import { Sha256HashSchema } from "#contracts/ids";
4
+ import { curriculumNamespace, } from "#contracts/program/curriculum";
5
+ import { canonicalizeProgramSnapshotRow, } from "#contracts/program/snapshot/row";
6
+ import { ProgramRowHashError, verifyProgramSnapshotRowHash, } from "#contracts/program/snapshot/row-hash";
7
+ import { compareCodeUnits } from "#contracts/text/order";
8
+ const DIGEST_DOMAIN = "nakafa.aksara.program-rows";
9
+ /** An aggregate program stream is incomplete, duplicated, or tampered. */
10
+ export class ProgramDigestError extends Schema.TaggedError()("ProgramDigestError", {
11
+ code: Schema.Literals([
12
+ "count",
13
+ "integrity",
14
+ "key",
15
+ "order",
16
+ "parent",
17
+ "program",
18
+ "route",
19
+ "root",
20
+ "slug",
21
+ ]),
22
+ identity: Schema.String,
23
+ }) {
24
+ }
25
+ /** Serializes source-derived counts for exact replay comparison. */
26
+ function countIdentity(counts) {
27
+ return [
28
+ counts.curriculumRowCount,
29
+ counts.programRowCount,
30
+ counts.rowCount,
31
+ counts.sitemapCount,
32
+ counts.slugCount,
33
+ ].join(":");
34
+ }
35
+ /** Resolves one required localized program identity. */
36
+ function translationFor(program, appLocale) {
37
+ return program.translations.find((translation) => translation.appLocale === appLocale);
38
+ }
39
+ /** Checks that one root is the exact localized route owned by its program. */
40
+ function isExactProgramRoot(row, program) {
41
+ const translation = translationFor(program, row.appLocale);
42
+ return (translation !== undefined &&
43
+ row.iconKey === program.iconKey &&
44
+ row.order === program.displayOrder &&
45
+ row.publicPath ===
46
+ `${curriculumNamespace(row.appLocale)}/${translation.publicSlug}` &&
47
+ row.title === translation.title);
48
+ }
49
+ /** Keeps current locale closure, identity, and digest state in one replay. */
50
+ class ProgramDigestState {
51
+ #activeAppLocales;
52
+ #hash = createHash("sha256").update(`${DIGEST_DOMAIN}\n`);
53
+ #nodes = new Set();
54
+ #paths = new Set();
55
+ #programs = new Map();
56
+ #routes = new Set();
57
+ #roots = new Set();
58
+ #slugs = new Set();
59
+ #lastCurriculumKey;
60
+ #lastProgramOrder = 0;
61
+ curriculumRowCount = 0;
62
+ programRowCount = 0;
63
+ sitemapCount = 0;
64
+ /** Initializes one isolated replay under the signed active locale set. */
65
+ constructor(activeAppLocales) {
66
+ this.#activeAppLocales = activeAppLocales;
67
+ }
68
+ /** Returns source-independent count evidence collected from accepted rows. */
69
+ counts() {
70
+ return {
71
+ curriculumRowCount: this.curriculumRowCount,
72
+ programRowCount: this.programRowCount,
73
+ rowCount: this.curriculumRowCount + this.programRowCount,
74
+ sitemapCount: this.sitemapCount,
75
+ slugCount: this.#slugs.size,
76
+ };
77
+ }
78
+ /** Adds one verified current row after checking ownership and order. */
79
+ add(record) {
80
+ return record.kind === "program"
81
+ ? this.#addProgram(record)
82
+ : this.#addCurriculum(record);
83
+ }
84
+ /** Finalizes complete catalog, root, locale, and count evidence. */
85
+ validateComplete(expected) {
86
+ const expectedRoots = new Set();
87
+ for (const program of this.#programs.values()) {
88
+ if (program.navigation.model !== "curriculum-tree") {
89
+ continue;
90
+ }
91
+ for (const locale of this.#activeAppLocales) {
92
+ expectedRoots.add(`${program.key}\0${locale}`);
93
+ }
94
+ }
95
+ const counts = this.counts();
96
+ const expectedCounts = expected ?? counts;
97
+ if (this.programRowCount > 0 &&
98
+ this.#slugs.size ===
99
+ this.programRowCount * this.#activeAppLocales.length &&
100
+ expectedRoots.size === this.#roots.size &&
101
+ [...expectedRoots].every((root) => this.#roots.has(root)) &&
102
+ countIdentity(counts) === countIdentity(expectedCounts)) {
103
+ return Effect.void;
104
+ }
105
+ return Effect.fail(new ProgramDigestError({
106
+ code: "count",
107
+ identity: JSON.stringify({
108
+ actual: counts,
109
+ expected,
110
+ rootCount: this.#roots.size,
111
+ }),
112
+ }));
113
+ }
114
+ /** Consumes the complete canonical stream digest. */
115
+ digest() {
116
+ return Effect.try({
117
+ catch: () => new ProgramRowHashError({ scope: "digest" }),
118
+ try: () => Sha256HashSchema.make(`sha256:${this.#hash.digest("hex")}`),
119
+ });
120
+ }
121
+ /** Adds one catalog row before any current curriculum records. */
122
+ #addProgram(record) {
123
+ const { row } = record;
124
+ const translationLocales = row.translations.map((translation) => translation.appLocale);
125
+ if (this.curriculumRowCount > 0 ||
126
+ row.displayOrder <= this.#lastProgramOrder) {
127
+ return Effect.fail(new ProgramDigestError({ code: "order", identity: row.key }));
128
+ }
129
+ if (this.#programs.has(row.key) ||
130
+ JSON.stringify(translationLocales) !==
131
+ JSON.stringify(this.#activeAppLocales)) {
132
+ return Effect.fail(new ProgramDigestError({ code: "key", identity: row.key }));
133
+ }
134
+ for (const translation of row.translations) {
135
+ const slug = `${translation.appLocale}\0${translation.publicSlug}`;
136
+ if (this.#slugs.has(slug)) {
137
+ return Effect.fail(new ProgramDigestError({ code: "slug", identity: slug }));
138
+ }
139
+ this.#slugs.add(slug);
140
+ }
141
+ this.#programs.set(row.key, row);
142
+ this.#lastProgramOrder = row.displayOrder;
143
+ this.programRowCount += 1;
144
+ return this.#updateDigest(record);
145
+ }
146
+ /** Adds one localized route after validating identity and ancestry. */
147
+ #addCurriculum(record) {
148
+ const { row } = record;
149
+ const program = this.#programs.get(row.programKey);
150
+ if (!this.#activeAppLocales.includes(row.appLocale) ||
151
+ program?.navigation.model !== "curriculum-tree") {
152
+ return Effect.fail(new ProgramDigestError({ code: "program", identity: row.programKey }));
153
+ }
154
+ const orderKey = `${row.programKey}\0${row.appLocale}\0${row.publicPath}`;
155
+ if (this.#lastCurriculumKey !== undefined &&
156
+ compareCodeUnits(this.#lastCurriculumKey, orderKey) >= 0) {
157
+ return Effect.fail(new ProgramDigestError({ code: "order", identity: orderKey }));
158
+ }
159
+ const pathIdentity = `${row.programKey}\0${row.appLocale}\0${row.publicPath}`;
160
+ const routeIdentity = `${row.appLocale}\0${row.publicPath}`;
161
+ const nodeIdentity = `${row.programKey}\0${row.appLocale}\0${row.nodeKey}`;
162
+ if (this.#nodes.has(nodeIdentity) ||
163
+ this.#paths.has(pathIdentity) ||
164
+ this.#routes.has(routeIdentity)) {
165
+ return Effect.fail(new ProgramDigestError({ code: "route", identity: routeIdentity }));
166
+ }
167
+ if (row.parentPath === undefined) {
168
+ const rootIdentity = `${row.programKey}\0${row.appLocale}`;
169
+ if (!isExactProgramRoot(row, program)) {
170
+ return Effect.fail(new ProgramDigestError({ code: "root", identity: rootIdentity }));
171
+ }
172
+ this.#roots.add(rootIdentity);
173
+ }
174
+ else if (!this.#paths.has(`${row.programKey}\0${row.appLocale}\0${row.parentPath}`)) {
175
+ return Effect.fail(new ProgramDigestError({ code: "parent", identity: nodeIdentity }));
176
+ }
177
+ this.#lastCurriculumKey = orderKey;
178
+ this.#nodes.add(nodeIdentity);
179
+ this.#paths.add(pathIdentity);
180
+ this.#routes.add(routeIdentity);
181
+ this.curriculumRowCount += 1;
182
+ this.sitemapCount += row.sitemap ? 1 : 0;
183
+ return this.#updateDigest(record);
184
+ }
185
+ /** Adds canonical current record bytes and their authenticated row hash. */
186
+ #updateDigest(record) {
187
+ return Effect.try({
188
+ catch: () => new ProgramRowHashError({ scope: "digest" }),
189
+ try: () => {
190
+ this.#hash
191
+ .update(canonicalizeProgramSnapshotRow(record))
192
+ .update("\0")
193
+ .update(record.rowHash)
194
+ .update("\n");
195
+ },
196
+ });
197
+ }
198
+ }
199
+ /** Verifies one current row hash before advancing aggregate state. */
200
+ const updateProgramDigest = Effect.fn("AksaraContracts.updateProgramDigest")(function* (state, record) {
201
+ const expected = yield* verifyProgramSnapshotRowHash(record);
202
+ if (expected !== record.rowHash) {
203
+ return yield* new ProgramDigestError({
204
+ code: "integrity",
205
+ identity: record.kind === "program" ? record.row.key : record.row.publicPath,
206
+ });
207
+ }
208
+ yield* state.add(record);
209
+ });
210
+ /** Digests a complete current program snapshot under its active locale set. */
211
+ export const digestProgramRows = Effect.fn("AksaraContracts.digestProgramRows")(function* (input) {
212
+ const state = yield* Effect.try({
213
+ catch: () => new ProgramRowHashError({ scope: "digest" }),
214
+ try: () => new ProgramDigestState(input.activeAppLocales),
215
+ });
216
+ yield* input.rows.pipe(Stream.runForEach((record) => updateProgramDigest(state, record)));
217
+ yield* state.validateComplete(input.expected);
218
+ const rowDigest = yield* state.digest();
219
+ return { ...state.counts(), rowDigest };
220
+ });
@@ -0,0 +1,31 @@
1
+ import { Effect, Schema } from "effect";
2
+ import { type ProgramSnapshot, type ProgramSnapshotFacts } from "#contracts/program/snapshot/spec";
3
+ declare const ProgramSnapshotHashError_base: Schema.Class<ProgramSnapshotHashError, Schema.TaggedStruct<"ProgramSnapshotHashError", {}>, import("effect/Cause").YieldableError>;
4
+ /** Node could not compute a deterministic program snapshot identity. */
5
+ export declare class ProgramSnapshotHashError extends ProgramSnapshotHashError_base {
6
+ }
7
+ /** Serializes current program facts in stable signed field order. */
8
+ export declare function canonicalizeProgramSnapshot(input: ProgramSnapshotFacts): string;
9
+ /** Creates the content-addressed identity of one complete program snapshot. */
10
+ export declare const makeProgramSnapshot: (input: {
11
+ readonly curriculumRowCount: number;
12
+ readonly programRowCount: number;
13
+ readonly rowCount: number;
14
+ readonly sitemapCount: number;
15
+ readonly slugCount: number;
16
+ readonly activeAppLocales: readonly [("de" | "en" | "id") & import("effect/Brand").Brand<"@NakafaAI/AksaraAppLocale">, ...(("de" | "en" | "id") & import("effect/Brand").Brand<"@NakafaAI/AksaraAppLocale">)[]];
17
+ readonly rowDigest: string & import("effect/Brand").Brand<"@NakafaAI/AksaraSha256Hash">;
18
+ }) => Effect.Effect<{
19
+ readonly curriculumRowCount: number;
20
+ readonly programRowCount: number;
21
+ readonly rowCount: number;
22
+ readonly sitemapCount: number;
23
+ readonly slugCount: number;
24
+ readonly activeAppLocales: readonly [("de" | "en" | "id") & import("effect/Brand").Brand<"@NakafaAI/AksaraAppLocale">, ...(("de" | "en" | "id") & import("effect/Brand").Brand<"@NakafaAI/AksaraAppLocale">)[]];
25
+ readonly rowDigest: string & import("effect/Brand").Brand<"@NakafaAI/AksaraSha256Hash">;
26
+ readonly format: "localized-program-snapshot";
27
+ readonly snapshotId: string & import("effect/Brand").Brand<"@NakafaAI/AksaraSha256Hash">;
28
+ }, ProgramSnapshotHashError, never>;
29
+ /** Recomputes the content identity of one stored current snapshot. */
30
+ export declare function verifyProgramSnapshotHash(snapshot: ProgramSnapshot): Effect.Effect<string & import("effect/Brand").Brand<"@NakafaAI/AksaraSha256Hash">, ProgramSnapshotHashError, never>;
31
+ export {};
@@ -0,0 +1,40 @@
1
+ import { createHash } from "node:crypto";
2
+ import { Effect, Schema } from "effect";
3
+ import { Sha256HashSchema } from "#contracts/ids";
4
+ import { PROGRAM_SNAPSHOT_FORMAT, ProgramSnapshotSchema, } from "#contracts/program/snapshot/spec";
5
+ const SNAPSHOT_DOMAIN = "nakafa.aksara.localized-program-snapshot";
6
+ /** Node could not compute a deterministic program snapshot identity. */
7
+ export class ProgramSnapshotHashError extends Schema.TaggedError()("ProgramSnapshotHashError", {}) {
8
+ }
9
+ /** Serializes current program facts in stable signed field order. */
10
+ export function canonicalizeProgramSnapshot(input) {
11
+ return JSON.stringify({
12
+ activeAppLocales: input.activeAppLocales,
13
+ curriculumRowCount: input.curriculumRowCount,
14
+ format: PROGRAM_SNAPSHOT_FORMAT,
15
+ programRowCount: input.programRowCount,
16
+ rowCount: input.rowCount,
17
+ rowDigest: input.rowDigest,
18
+ sitemapCount: input.sitemapCount,
19
+ slugCount: input.slugCount,
20
+ });
21
+ }
22
+ /** Creates the content-addressed identity of one complete program snapshot. */
23
+ export const makeProgramSnapshot = Effect.fn("AksaraContracts.makeProgramSnapshot")((input) => Effect.try({
24
+ catch: () => new ProgramSnapshotHashError(),
25
+ try: () => {
26
+ const snapshotId = Sha256HashSchema.make(`sha256:${createHash("sha256")
27
+ .update(`${SNAPSHOT_DOMAIN}\n${canonicalizeProgramSnapshot(input)}`)
28
+ .digest("hex")}`);
29
+ return ProgramSnapshotSchema.make({
30
+ ...input,
31
+ format: PROGRAM_SNAPSHOT_FORMAT,
32
+ snapshotId,
33
+ });
34
+ },
35
+ }));
36
+ /** Recomputes the content identity of one stored current snapshot. */
37
+ export function verifyProgramSnapshotHash(snapshot) {
38
+ const { format: _format, snapshotId: _snapshotId, ...facts } = snapshot;
39
+ return makeProgramSnapshot(facts).pipe(Effect.map((value) => value.snapshotId));
40
+ }
@@ -0,0 +1,155 @@
1
+ import { Effect, Schema } from "effect";
2
+ import { type ProgramSnapshotRow } from "#contracts/program/snapshot/row";
3
+ declare const ProgramRowHashError_base: Schema.Class<ProgramRowHashError, Schema.TaggedStruct<"ProgramRowHashError", {
4
+ readonly scope: Schema.Literals<readonly ["digest", "row"]>;
5
+ }>, import("effect/Cause").YieldableError>;
6
+ /** Node could not compute one deterministic program row identity. */
7
+ export declare class ProgramRowHashError extends ProgramRowHashError_base {
8
+ }
9
+ /** Creates one authenticated current program record. */
10
+ export declare const makeProgramSnapshotRow: (row: {
11
+ readonly defaultCoverageStatus: "archived" | "available" | "hidden" | "partial" | "planned";
12
+ readonly displayOrder: number;
13
+ readonly iconKey: "advanced" | "assessment" | "certificate" | "course" | "diploma" | "early-years" | "global-education" | "grade-1" | "grade-10" | "grade-11" | "grade-12" | "grade-2" | "grade-3" | "grade-4" | "grade-5" | "grade-6" | "grade-7" | "grade-8" | "grade-9" | "high-school" | "mathematics" | "middle-school" | "primary-school" | "school" | "science" | "standards" | "state";
14
+ readonly key: string & import("effect/Brand").Brand<"@NakafaAI/AksaraLearningProgramKey">;
15
+ readonly kind: "admission-exam" | "assessment" | "custom-program" | "institution-program" | "school-curriculum";
16
+ readonly navigation: {
17
+ readonly levels: readonly ["class" | "course" | "domain" | "lesson" | "phase" | "section" | "set" | "stage" | "subject" | "topic" | "track" | "unit", ...("class" | "course" | "domain" | "lesson" | "phase" | "section" | "set" | "stage" | "subject" | "topic" | "track" | "unit")[]];
18
+ readonly model: "course-unit-lesson" | "curriculum-tree" | "exam-domain-set" | "track-topic";
19
+ };
20
+ readonly provider: {
21
+ readonly homeCountry?: string | undefined;
22
+ readonly kind: "institution" | "learner" | "nakafa" | "official";
23
+ readonly name: string;
24
+ };
25
+ readonly recommendedCountry?: string | undefined;
26
+ readonly sources: readonly [{
27
+ readonly label: string;
28
+ readonly retrievedAt: string;
29
+ readonly reviewAfter?: string | undefined;
30
+ readonly type: "institution-document" | "nakafa-editorial" | "official-blueprint" | "official-policy" | "official-portal";
31
+ readonly url: string;
32
+ }, ...{
33
+ readonly label: string;
34
+ readonly retrievedAt: string;
35
+ readonly reviewAfter?: string | undefined;
36
+ readonly type: "institution-document" | "nakafa-editorial" | "official-blueprint" | "official-policy" | "official-portal";
37
+ readonly url: string;
38
+ }[]];
39
+ readonly translations: readonly [{
40
+ readonly appLocale: ("de" | "en" | "id") & import("effect/Brand").Brand<"@NakafaAI/AksaraAppLocale">;
41
+ readonly publicSlug: string;
42
+ readonly title: string;
43
+ }, ...{
44
+ readonly appLocale: ("de" | "en" | "id") & import("effect/Brand").Brand<"@NakafaAI/AksaraAppLocale">;
45
+ readonly publicSlug: string;
46
+ readonly title: string;
47
+ }[]];
48
+ readonly version: {
49
+ readonly endsAt?: string | undefined;
50
+ readonly label: string;
51
+ readonly startsAt?: string | undefined;
52
+ };
53
+ }) => Effect.Effect<{
54
+ readonly kind: "program";
55
+ readonly row: {
56
+ readonly defaultCoverageStatus: "archived" | "available" | "hidden" | "partial" | "planned";
57
+ readonly displayOrder: number;
58
+ readonly iconKey: "advanced" | "assessment" | "certificate" | "course" | "diploma" | "early-years" | "global-education" | "grade-1" | "grade-10" | "grade-11" | "grade-12" | "grade-2" | "grade-3" | "grade-4" | "grade-5" | "grade-6" | "grade-7" | "grade-8" | "grade-9" | "high-school" | "mathematics" | "middle-school" | "primary-school" | "school" | "science" | "standards" | "state";
59
+ readonly key: string & import("effect/Brand").Brand<"@NakafaAI/AksaraLearningProgramKey">;
60
+ readonly kind: "admission-exam" | "assessment" | "custom-program" | "institution-program" | "school-curriculum";
61
+ readonly navigation: {
62
+ readonly levels: readonly ["class" | "course" | "domain" | "lesson" | "phase" | "section" | "set" | "stage" | "subject" | "topic" | "track" | "unit", ...("class" | "course" | "domain" | "lesson" | "phase" | "section" | "set" | "stage" | "subject" | "topic" | "track" | "unit")[]];
63
+ readonly model: "course-unit-lesson" | "curriculum-tree" | "exam-domain-set" | "track-topic";
64
+ };
65
+ readonly provider: {
66
+ readonly homeCountry?: string | undefined;
67
+ readonly kind: "institution" | "learner" | "nakafa" | "official";
68
+ readonly name: string;
69
+ };
70
+ readonly recommendedCountry?: string | undefined;
71
+ readonly sources: readonly [{
72
+ readonly label: string;
73
+ readonly retrievedAt: string;
74
+ readonly reviewAfter?: string | undefined;
75
+ readonly type: "institution-document" | "nakafa-editorial" | "official-blueprint" | "official-policy" | "official-portal";
76
+ readonly url: string;
77
+ }, ...{
78
+ readonly label: string;
79
+ readonly retrievedAt: string;
80
+ readonly reviewAfter?: string | undefined;
81
+ readonly type: "institution-document" | "nakafa-editorial" | "official-blueprint" | "official-policy" | "official-portal";
82
+ readonly url: string;
83
+ }[]];
84
+ readonly translations: readonly [{
85
+ readonly appLocale: ("de" | "en" | "id") & import("effect/Brand").Brand<"@NakafaAI/AksaraAppLocale">;
86
+ readonly publicSlug: string;
87
+ readonly title: string;
88
+ }, ...{
89
+ readonly appLocale: ("de" | "en" | "id") & import("effect/Brand").Brand<"@NakafaAI/AksaraAppLocale">;
90
+ readonly publicSlug: string;
91
+ readonly title: string;
92
+ }[]];
93
+ readonly version: {
94
+ readonly endsAt?: string | undefined;
95
+ readonly label: string;
96
+ readonly startsAt?: string | undefined;
97
+ };
98
+ };
99
+ readonly rowHash: string & import("effect/Brand").Brand<"@NakafaAI/AksaraSha256Hash">;
100
+ }, ProgramRowHashError, never>;
101
+ /** Creates one authenticated current curriculum record. */
102
+ export declare const makeCurriculumSnapshotRow: (row: {
103
+ readonly appLocale: ("de" | "en" | "id") & import("effect/Brand").Brand<"@NakafaAI/AksaraAppLocale">;
104
+ readonly canonicalPath?: (string & import("effect/Brand").Brand<"@NakafaAI/AksaraPublicPath">) | undefined;
105
+ readonly displayGroupIconKey?: "advanced" | "assessment" | "certificate" | "course" | "diploma" | "early-years" | "global-education" | "grade-1" | "grade-10" | "grade-11" | "grade-12" | "grade-2" | "grade-3" | "grade-4" | "grade-5" | "grade-6" | "grade-7" | "grade-8" | "grade-9" | "high-school" | "mathematics" | "middle-school" | "primary-school" | "school" | "science" | "standards" | "state" | undefined;
106
+ readonly displayGroupTitle?: string | undefined;
107
+ readonly iconKey: "advanced" | "assessment" | "certificate" | "course" | "diploma" | "early-years" | "global-education" | "grade-1" | "grade-10" | "grade-11" | "grade-12" | "grade-2" | "grade-3" | "grade-4" | "grade-5" | "grade-6" | "grade-7" | "grade-8" | "grade-9" | "high-school" | "mathematics" | "middle-school" | "primary-school" | "school" | "science" | "standards" | "state";
108
+ readonly kind: "curriculum-context";
109
+ readonly level: "class" | "course" | "domain" | "lesson" | "phase" | "section" | "set" | "stage" | "subject" | "topic" | "track" | "unit";
110
+ readonly materialCardDescription?: string | undefined;
111
+ readonly materialCardTitle?: string | undefined;
112
+ readonly materialContextNodeKey?: (string & import("effect/Brand").Brand<"@NakafaAI/AksaraCurriculumNodeKey">) | undefined;
113
+ readonly materialContextParentPath?: (string & import("effect/Brand").Brand<"@NakafaAI/AksaraPublicPath">) | undefined;
114
+ readonly materialContextPublicPath?: (string & import("effect/Brand").Brand<"@NakafaAI/AksaraPublicPath">) | undefined;
115
+ readonly materialDomain?: (string & import("effect/Brand").Brand<"@NakafaAI/AksaraMaterialDomain">) | undefined;
116
+ readonly materialKey?: (string & import("effect/Brand").Brand<"@NakafaAI/AksaraMaterialKey">) | undefined;
117
+ readonly nodeKey: string;
118
+ readonly order: number;
119
+ readonly parentPath?: (string & import("effect/Brand").Brand<"@NakafaAI/AksaraPublicPath">) | undefined;
120
+ readonly programKey: string & import("effect/Brand").Brand<"@NakafaAI/AksaraLearningProgramKey">;
121
+ readonly publicPath: string & import("effect/Brand").Brand<"@NakafaAI/AksaraPublicPath">;
122
+ readonly sitemap: boolean;
123
+ readonly sourcePath: string & import("effect/Brand").Brand<"@NakafaAI/AksaraCorpusSourcePath">;
124
+ readonly title: string;
125
+ }) => Effect.Effect<{
126
+ readonly kind: "curriculum";
127
+ readonly row: {
128
+ readonly appLocale: ("de" | "en" | "id") & import("effect/Brand").Brand<"@NakafaAI/AksaraAppLocale">;
129
+ readonly canonicalPath?: (string & import("effect/Brand").Brand<"@NakafaAI/AksaraPublicPath">) | undefined;
130
+ readonly displayGroupIconKey?: "advanced" | "assessment" | "certificate" | "course" | "diploma" | "early-years" | "global-education" | "grade-1" | "grade-10" | "grade-11" | "grade-12" | "grade-2" | "grade-3" | "grade-4" | "grade-5" | "grade-6" | "grade-7" | "grade-8" | "grade-9" | "high-school" | "mathematics" | "middle-school" | "primary-school" | "school" | "science" | "standards" | "state" | undefined;
131
+ readonly displayGroupTitle?: string | undefined;
132
+ readonly iconKey: "advanced" | "assessment" | "certificate" | "course" | "diploma" | "early-years" | "global-education" | "grade-1" | "grade-10" | "grade-11" | "grade-12" | "grade-2" | "grade-3" | "grade-4" | "grade-5" | "grade-6" | "grade-7" | "grade-8" | "grade-9" | "high-school" | "mathematics" | "middle-school" | "primary-school" | "school" | "science" | "standards" | "state";
133
+ readonly kind: "curriculum-context";
134
+ readonly level: "class" | "course" | "domain" | "lesson" | "phase" | "section" | "set" | "stage" | "subject" | "topic" | "track" | "unit";
135
+ readonly materialCardDescription?: string | undefined;
136
+ readonly materialCardTitle?: string | undefined;
137
+ readonly materialContextNodeKey?: (string & import("effect/Brand").Brand<"@NakafaAI/AksaraCurriculumNodeKey">) | undefined;
138
+ readonly materialContextParentPath?: (string & import("effect/Brand").Brand<"@NakafaAI/AksaraPublicPath">) | undefined;
139
+ readonly materialContextPublicPath?: (string & import("effect/Brand").Brand<"@NakafaAI/AksaraPublicPath">) | undefined;
140
+ readonly materialDomain?: (string & import("effect/Brand").Brand<"@NakafaAI/AksaraMaterialDomain">) | undefined;
141
+ readonly materialKey?: (string & import("effect/Brand").Brand<"@NakafaAI/AksaraMaterialKey">) | undefined;
142
+ readonly nodeKey: string;
143
+ readonly order: number;
144
+ readonly parentPath?: (string & import("effect/Brand").Brand<"@NakafaAI/AksaraPublicPath">) | undefined;
145
+ readonly programKey: string & import("effect/Brand").Brand<"@NakafaAI/AksaraLearningProgramKey">;
146
+ readonly publicPath: string & import("effect/Brand").Brand<"@NakafaAI/AksaraPublicPath">;
147
+ readonly sitemap: boolean;
148
+ readonly sourcePath: string & import("effect/Brand").Brand<"@NakafaAI/AksaraCorpusSourcePath">;
149
+ readonly title: string;
150
+ };
151
+ readonly rowHash: string & import("effect/Brand").Brand<"@NakafaAI/AksaraSha256Hash">;
152
+ }, ProgramRowHashError, never>;
153
+ /** Recomputes one current row identity for streamed integrity checks. */
154
+ export declare function verifyProgramSnapshotRowHash(record: ProgramSnapshotRow): Effect.Effect<string & import("effect/Brand").Brand<"@NakafaAI/AksaraSha256Hash">, ProgramRowHashError, never>;
155
+ export {};
@@ -0,0 +1,34 @@
1
+ import { createHash } from "node:crypto";
2
+ import { Effect, Schema } from "effect";
3
+ import { Sha256HashSchema } from "#contracts/ids";
4
+ import { CurriculumRouteRecordSchema, canonicalizeProgramSnapshotRow, LearningProgramRecordSchema, } from "#contracts/program/snapshot/row";
5
+ const ROW_DOMAIN = "nakafa.aksara.program-row";
6
+ /** Node could not compute one deterministic program row identity. */
7
+ export class ProgramRowHashError extends Schema.TaggedError()("ProgramRowHashError", { scope: Schema.Literals(["digest", "row"]) }) {
8
+ }
9
+ /** Hashes one current program or curriculum row. */
10
+ function hashProgramRow(record) {
11
+ return Effect.try({
12
+ catch: () => new ProgramRowHashError({ scope: "row" }),
13
+ try: () => Sha256HashSchema.make(`sha256:${createHash("sha256")
14
+ .update(`${ROW_DOMAIN}\n${canonicalizeProgramSnapshotRow(record)}`)
15
+ .digest("hex")}`),
16
+ });
17
+ }
18
+ /** Creates one authenticated current program record. */
19
+ export const makeProgramSnapshotRow = Effect.fn("AksaraContracts.makeProgramSnapshotRow")(function* (row) {
20
+ const input = { kind: "program", row };
21
+ const rowHash = yield* hashProgramRow(input);
22
+ return LearningProgramRecordSchema.make({ ...input, rowHash });
23
+ });
24
+ /** Creates one authenticated current curriculum record. */
25
+ export const makeCurriculumSnapshotRow = Effect.fn("AksaraContracts.makeCurriculumSnapshotRow")(function* (row) {
26
+ const input = { kind: "curriculum", row };
27
+ const rowHash = yield* hashProgramRow(input);
28
+ return CurriculumRouteRecordSchema.make({ ...input, rowHash });
29
+ });
30
+ /** Recomputes one current row identity for streamed integrity checks. */
31
+ export function verifyProgramSnapshotRowHash(record) {
32
+ const { rowHash: _rowHash, ...input } = record;
33
+ return hashProgramRow(input);
34
+ }