@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,144 @@
1
+ import { createHash } from "node:crypto";
2
+ import { Effect, Schema } from "effect";
3
+ import { Sha256HashSchema } from "#contracts/ids";
4
+ import { ActiveAppLocaleListSchema, AppLocaleSchema, ENGLISH_APP_LOCALE_CODE, GERMAN_APP_LOCALE_CODE, INDONESIAN_APP_LOCALE_CODE, } from "#contracts/locale";
5
+ import { canonicalizeQuranAttribution } from "#contracts/quran/attribution";
6
+ import { QuranNameProvenanceScopeSchema, QuranTranslationProvenanceScopeSchema, quranNameProvenanceScope, quranNameSourceForScope, quranTafsirSourceId, quranTranslationProvenanceScope, quranTranslationSourceForScope, } from "#contracts/quran/identity";
7
+ import { QuranProvenanceStatusSchema } from "#contracts/quran/snapshot/spec";
8
+ import { hasCompleteQuranSourceCopy, QuranSourceAttributionSchema, } from "#contracts/quran/source";
9
+ /** Quran source fields that require independent provenance decisions. */
10
+ const QuranStaticProvenanceScopeSchema = Schema.Literals([
11
+ "arabic-text",
12
+ "metadata",
13
+ ]);
14
+ const QuranExternalTafsirProvenanceScopeSchema = Schema.Literals([
15
+ "en-tafsir-access",
16
+ "de-tafsir-access",
17
+ ]);
18
+ const QuranNonTranslationProvenanceScopeSchema = Schema.Union([
19
+ QuranStaticProvenanceScopeSchema,
20
+ QuranExternalTafsirProvenanceScopeSchema,
21
+ Schema.Literal("id-tafsir"),
22
+ ]);
23
+ /** Complete source-field vocabulary supported by current Quran publication. */
24
+ export const QuranProvenanceScopeSchema = Schema.Union([
25
+ QuranNonTranslationProvenanceScopeSchema,
26
+ QuranNameProvenanceScopeSchema,
27
+ QuranTranslationProvenanceScopeSchema,
28
+ ]);
29
+ /** Derives the exact provenance scope order for one active locale set. */
30
+ export function quranProvenanceScopes(activeAppLocales) {
31
+ const scopes = ["arabic-text"];
32
+ for (const appLocale of activeAppLocales) {
33
+ scopes.push(quranNameProvenanceScope(appLocale));
34
+ scopes.push(quranTranslationProvenanceScope(appLocale));
35
+ if (appLocale === AppLocaleSchema.make("en")) {
36
+ scopes.push("en-tafsir-access");
37
+ }
38
+ else if (appLocale === AppLocaleSchema.make("id")) {
39
+ scopes.push("id-tafsir");
40
+ }
41
+ else {
42
+ scopes.push("de-tafsir-access");
43
+ }
44
+ }
45
+ scopes.push("metadata");
46
+ return scopes;
47
+ }
48
+ const QURAN_STATIC_PROVENANCE_SOURCE = {
49
+ "arabic-text": "tanzil-text",
50
+ "de-tafsir-access": quranTafsirSourceId(GERMAN_APP_LOCALE_CODE),
51
+ "en-tafsir-access": quranTafsirSourceId(ENGLISH_APP_LOCALE_CODE),
52
+ "id-tafsir": quranTafsirSourceId(INDONESIAN_APP_LOCALE_CODE),
53
+ metadata: "tanzil-metadata",
54
+ };
55
+ /** Resolves the only official source allowed to prove one provenance scope. */
56
+ export function quranSourceForProvenanceScope(scope) {
57
+ if (Schema.is(QuranTranslationProvenanceScopeSchema)(scope)) {
58
+ return quranTranslationSourceForScope(scope);
59
+ }
60
+ if (Schema.is(QuranNameProvenanceScopeSchema)(scope)) {
61
+ return quranNameSourceForScope(scope);
62
+ }
63
+ return QURAN_STATIC_PROVENANCE_SOURCE[scope];
64
+ }
65
+ /** One reviewed official artifact and its field-level permission decision. */
66
+ export const QuranProvenanceRecordSchema = Schema.Struct({
67
+ attribution: QuranSourceAttributionSchema,
68
+ evidence: Schema.Trimmed.check(Schema.isNonEmpty()),
69
+ scope: QuranProvenanceScopeSchema,
70
+ status: QuranProvenanceStatusSchema,
71
+ }).pipe(Schema.check(Schema.makeFilter(({ attribution, scope }) => attribution.id === quranSourceForProvenanceScope(scope), { message: "Expected each Quran scope to bind its official source." })));
72
+ /** Checks exact active-locale coverage and canonical scope order. */
73
+ function hasCanonicalSourceCoverage(input) {
74
+ const expected = quranProvenanceScopes(input.activeAppLocales);
75
+ return (input.records.length === expected.length &&
76
+ input.records.every((record, index) => record.scope === expected[index] &&
77
+ record.attribution.id === quranSourceForProvenanceScope(record.scope) &&
78
+ hasCompleteQuranSourceCopy(record.attribution, input.activeAppLocales)));
79
+ }
80
+ /** Checks that the declared gate status matches every reviewed record. */
81
+ function hasCoherentProvenanceStatus(input) {
82
+ const expected = input.records.some((record) => record.status === "blocked")
83
+ ? "blocked"
84
+ : "approved";
85
+ return input.status === expected;
86
+ }
87
+ /** Complete ordered evidence set that gates Quran production publication. */
88
+ export const QuranProvenanceManifestSchema = Schema.Struct({
89
+ activeAppLocales: ActiveAppLocaleListSchema,
90
+ digest: Sha256HashSchema,
91
+ records: Schema.NonEmptyArray(QuranProvenanceRecordSchema),
92
+ status: QuranProvenanceStatusSchema,
93
+ }).pipe(Schema.check(Schema.makeFilter(hasCanonicalSourceCoverage, {
94
+ message: "Expected exact active-locale Quran provenance scope coverage.",
95
+ })), Schema.check(Schema.makeFilter(hasCoherentProvenanceStatus, {
96
+ message: "Expected Quran provenance status to match its complete evidence.",
97
+ })));
98
+ /** Quran provenance omitted or misordered an active-locale scope. */
99
+ export class QuranProvenanceCoverageError extends Schema.TaggedError()("QuranProvenanceCoverageError", {
100
+ actualScopes: Schema.Array(QuranProvenanceScopeSchema),
101
+ }) {
102
+ }
103
+ /** Node could not compute the deterministic provenance digest. */
104
+ export class QuranProvenanceHashError extends Schema.TaggedError()("QuranProvenanceHashError", {}) {
105
+ }
106
+ const PROVENANCE_DOMAIN = "nakafa.aksara.quran-provenance";
107
+ /** Produces stable JSON for one reviewed Quran provenance record. */
108
+ export function canonicalizeQuranProvenance(record) {
109
+ return JSON.stringify({
110
+ attribution: canonicalizeQuranAttribution(record.attribution),
111
+ evidence: record.evidence,
112
+ scope: record.scope,
113
+ status: record.status,
114
+ });
115
+ }
116
+ /** Digests exact ordered provenance records under their active locale set. */
117
+ export const hashQuranProvenance = Effect.fn("AksaraContracts.hashQuranProvenance")((input) => Effect.try({
118
+ catch: () => new QuranProvenanceHashError(),
119
+ try: () => {
120
+ const hash = createHash("sha256")
121
+ .update(`${PROVENANCE_DOMAIN}\n`)
122
+ .update(JSON.stringify(input.activeAppLocales))
123
+ .update("\n");
124
+ for (const record of input.records) {
125
+ hash.update(canonicalizeQuranProvenance(record));
126
+ hash.update("\n");
127
+ }
128
+ return Sha256HashSchema.make(`sha256:${hash.digest("hex")}`);
129
+ },
130
+ }));
131
+ /** Builds a snapshot provenance gate from exact reviewed source records. */
132
+ export const makeQuranProvenanceManifest = Effect.fn("AksaraContracts.makeQuranProvenanceManifest")(function* (input) {
133
+ const decoded = yield* Schema.decodeUnknownEffect(Schema.Struct({
134
+ activeAppLocales: ActiveAppLocaleListSchema,
135
+ records: Schema.NonEmptyArray(QuranProvenanceRecordSchema),
136
+ }).pipe(Schema.check(Schema.makeFilter(hasCanonicalSourceCoverage))))(input).pipe(Effect.mapError(() => new QuranProvenanceCoverageError({
137
+ actualScopes: input.records.map(({ scope }) => scope),
138
+ })));
139
+ const digest = yield* hashQuranProvenance(decoded);
140
+ const status = decoded.records.some((record) => record.status === "blocked")
141
+ ? "blocked"
142
+ : "approved";
143
+ return QuranProvenanceManifestSchema.make({ ...decoded, digest, status });
144
+ });
@@ -0,0 +1,33 @@
1
+ import { Effect, Schema, Stream } from "effect";
2
+ import type { ActiveAppLocaleList } from "#contracts/locale";
3
+ import type { QuranSnapshotRow } from "#contracts/quran/snapshot/row";
4
+ import { QuranRowHashError } from "#contracts/quran/snapshot/row/hash";
5
+ declare const QuranRowIntegrityError_base: Schema.Class<QuranRowIntegrityError, Schema.TaggedStruct<"QuranRowIntegrityError", {
6
+ readonly actual: Schema.brand<Schema.String, "@NakafaAI/AksaraSha256Hash">;
7
+ readonly expected: Schema.brand<Schema.String, "@NakafaAI/AksaraSha256Hash">;
8
+ }>, import("effect/Cause").YieldableError>;
9
+ /** A current row hash does not authenticate its structured payload. */
10
+ export declare class QuranRowIntegrityError extends QuranRowIntegrityError_base {
11
+ }
12
+ declare const QuranRowOrderError_base: Schema.Class<QuranRowOrderError, Schema.TaggedStruct<"QuranRowOrderError", {
13
+ readonly actual: Schema.String;
14
+ readonly expected: Schema.String;
15
+ }>, import("effect/Cause").YieldableError>;
16
+ /** A current row does not match the next deterministic snapshot identity. */
17
+ export declare class QuranRowOrderError extends QuranRowOrderError_base {
18
+ }
19
+ /** Digests authenticated current rows under one active locale set. */
20
+ export declare const digestQuranRows: <E, R>(input: {
21
+ readonly activeAppLocales: ActiveAppLocaleList;
22
+ readonly rows: Stream.Stream<Pick<QuranSnapshotRow, "payload" | "rowHash">, E, R>;
23
+ }) => Effect.Effect<{
24
+ attributionCount: number;
25
+ chunkCount: number;
26
+ projectionCount: number;
27
+ projectionDigest: string & import("effect/Brand").Brand<"@NakafaAI/AksaraSha256Hash">;
28
+ runtimeCount: number;
29
+ runtimeDigest: string & import("effect/Brand").Brand<"@NakafaAI/AksaraSha256Hash">;
30
+ searchCount: number;
31
+ searchDigest: string & import("effect/Brand").Brand<"@NakafaAI/AksaraSha256Hash">;
32
+ }, E | QuranRowHashError | QuranRowIntegrityError | QuranRowOrderError, R>;
33
+ export {};
@@ -0,0 +1,232 @@
1
+ import { createHash } from "node:crypto";
2
+ import { Effect, Schema, Stream } from "effect";
3
+ import { Sha256HashSchema } from "#contracts/ids";
4
+ import { AppLocaleSchema } from "#contracts/locale";
5
+ import { hashQuranRow, QuranRowHashError, } from "#contracts/quran/snapshot/row/hash";
6
+ import { quranSourceIds } from "#contracts/quran/source";
7
+ import { QURAN_ATTRIBUTION_COUNT, QURAN_CHUNK_SIZE, QURAN_SURAH_COUNT, QURAN_VERSE_COUNT, } from "#contracts/quran/spec";
8
+ const RUNTIME_DOMAIN = "nakafa.aksara.quran-runtime";
9
+ const SEARCH_DOMAIN = "nakafa.aksara.quran-search";
10
+ const PROJECTION_DOMAIN = "nakafa.aksara.quran-projection";
11
+ /** A current row hash does not authenticate its structured payload. */
12
+ export class QuranRowIntegrityError extends Schema.TaggedError()("QuranRowIntegrityError", {
13
+ actual: Sha256HashSchema,
14
+ expected: Sha256HashSchema,
15
+ }) {
16
+ }
17
+ /** A current row does not match the next deterministic snapshot identity. */
18
+ export class QuranRowOrderError extends Schema.TaggedError()("QuranRowOrderError", {
19
+ actual: Schema.String,
20
+ expected: Schema.String,
21
+ }) {
22
+ }
23
+ /** Resolves one complete stable current row identity. */
24
+ function rowIdentity(payload) {
25
+ if (payload.kind === "quran-attribution") {
26
+ return `quran-attribution:${payload.activeAppLocales.join(",")}:${payload.sources.map(({ id }) => id).join(":")}`;
27
+ }
28
+ if (payload.kind === "quran-surah") {
29
+ return `quran-surah:${payload.number}`;
30
+ }
31
+ if (payload.kind === "quran-chunk") {
32
+ return [
33
+ "quran-chunk",
34
+ payload.surahNumber,
35
+ payload.firstVerse,
36
+ payload.lastVerse,
37
+ payload.firstQuranNumber,
38
+ ].join(":");
39
+ }
40
+ return [
41
+ "quran-search",
42
+ payload.surahNumber,
43
+ payload.appLocale,
44
+ payload.route,
45
+ payload.graph.alignmentId,
46
+ payload.graph.assetId,
47
+ payload.graph.conceptId,
48
+ payload.graph.learningObjectId,
49
+ payload.graph.lensId,
50
+ ].join(":");
51
+ }
52
+ /** Keeps current locale closure and digest state private to one replay. */
53
+ class QuranDigestState {
54
+ #activeAppLocales;
55
+ #runtime = createHash("sha256").update(`${RUNTIME_DOMAIN}\n`);
56
+ #search = createHash("sha256").update(`${SEARCH_DOMAIN}\n`);
57
+ #projection = createHash("sha256").update(`${PROJECTION_DOMAIN}\n`);
58
+ #nextQuranNumber = 1;
59
+ #nextSearchLocale;
60
+ #nextSearchSurah = 1;
61
+ #nextSurah = 1;
62
+ #nextSurahVerse = 0;
63
+ #surahVerseCount = 0;
64
+ attributionCount = 0;
65
+ chunkCount = 0;
66
+ projectionCount = 0;
67
+ runtimeCount = 0;
68
+ searchCount = 0;
69
+ /** Initializes one isolated replay under the signed active locale set. */
70
+ constructor(activeAppLocales) {
71
+ this.#activeAppLocales = activeAppLocales;
72
+ this.#nextSearchLocale = activeAppLocales[0];
73
+ }
74
+ /** Returns the only row identity valid at the current stream position. */
75
+ expectedIdentity() {
76
+ if (this.attributionCount === 0) {
77
+ return `quran-attribution:${this.#activeAppLocales.join(",")}:${quranSourceIds(this.#activeAppLocales).join(":")}`;
78
+ }
79
+ if (this.#nextSurah <= QURAN_SURAH_COUNT) {
80
+ if (this.#nextSurahVerse === 0) {
81
+ return `quran-surah:${this.#nextSurah}`;
82
+ }
83
+ const lastVerse = Math.min(this.#nextSurahVerse + QURAN_CHUNK_SIZE - 1, this.#surahVerseCount);
84
+ return [
85
+ "quran-chunk",
86
+ this.#nextSurah,
87
+ this.#nextSurahVerse,
88
+ lastVerse,
89
+ this.#nextQuranNumber,
90
+ ].join(":");
91
+ }
92
+ if (this.#nextSearchSurah <= QURAN_SURAH_COUNT) {
93
+ return [
94
+ "quran-search",
95
+ this.#nextSearchSurah,
96
+ this.#nextSearchLocale,
97
+ `quran/${this.#nextSearchSurah}`,
98
+ `alignment:quran:quran-surah:${this.#nextSearchSurah}`,
99
+ `asset:${this.#nextSearchLocale}:quran:quran-surah:${this.#nextSearchSurah}`,
100
+ `concept:quran:surah:${this.#nextSearchSurah}`,
101
+ `lo:quran-surah:${this.#nextSearchSurah}`,
102
+ "lens:quran",
103
+ ].join(":");
104
+ }
105
+ return "end";
106
+ }
107
+ /** Checks translation and Tafsir entries against active locale policy. */
108
+ validateVerseLocales(payload) {
109
+ if (payload.kind !== "quran-chunk") {
110
+ return true;
111
+ }
112
+ const expectedTafsir = this.#activeAppLocales.includes(AppLocaleSchema.make("id"))
113
+ ? ["id"]
114
+ : [];
115
+ return payload.verses.every((verse) => JSON.stringify(verse.translations.map((translation) => translation.appLocale)) === JSON.stringify(this.#activeAppLocales) &&
116
+ JSON.stringify(verse.tafsir.map((tafsir) => tafsir.appLocale)) ===
117
+ JSON.stringify(expectedTafsir));
118
+ }
119
+ /** Advances deterministic identity state after one accepted row. */
120
+ advance(payload) {
121
+ if (payload.kind === "quran-attribution") {
122
+ return;
123
+ }
124
+ if (payload.kind === "quran-surah") {
125
+ this.#surahVerseCount = payload.numberOfVerses;
126
+ this.#nextSurahVerse = 1;
127
+ return;
128
+ }
129
+ if (payload.kind === "quran-chunk") {
130
+ this.#nextQuranNumber = payload.firstQuranNumber + payload.verses.length;
131
+ if (payload.lastVerse === this.#surahVerseCount) {
132
+ this.#nextSurah += 1;
133
+ this.#nextSurahVerse = 0;
134
+ return;
135
+ }
136
+ this.#nextSurahVerse = payload.lastVerse + 1;
137
+ return;
138
+ }
139
+ const localeIndex = this.#activeAppLocales.indexOf(this.#nextSearchLocale);
140
+ const nextLocale = this.#activeAppLocales[localeIndex + 1];
141
+ if (nextLocale !== undefined) {
142
+ this.#nextSearchLocale = nextLocale;
143
+ return;
144
+ }
145
+ this.#nextSearchLocale = this.#activeAppLocales[0];
146
+ this.#nextSearchSurah += 1;
147
+ }
148
+ /** Adds one authenticated and correctly ordered row to all digests. */
149
+ update(row) {
150
+ const expected = this.expectedIdentity();
151
+ const actual = rowIdentity(row.payload);
152
+ if (actual !== expected || !this.validateVerseLocales(row.payload)) {
153
+ return Effect.fail(new QuranRowOrderError({ actual, expected }));
154
+ }
155
+ this.advance(row.payload);
156
+ const canonical = `${row.payload.kind}\n${row.rowHash}\n`;
157
+ return Effect.try({
158
+ catch: () => new QuranRowHashError({ scope: "row" }),
159
+ try: () => {
160
+ this.#projection.update(canonical);
161
+ this.projectionCount += 1;
162
+ if (row.payload.kind === "quran-search") {
163
+ this.#search.update(canonical);
164
+ this.searchCount += 1;
165
+ return;
166
+ }
167
+ this.#runtime.update(canonical);
168
+ this.runtimeCount += 1;
169
+ if (row.payload.kind === "quran-attribution") {
170
+ this.attributionCount += 1;
171
+ }
172
+ if (row.payload.kind === "quran-chunk") {
173
+ this.chunkCount += 1;
174
+ }
175
+ },
176
+ });
177
+ }
178
+ /** Rejects an incomplete stream before consuming digest state. */
179
+ validateComplete() {
180
+ const expectedSearchCount = QURAN_SURAH_COUNT * this.#activeAppLocales.length;
181
+ if (this.expectedIdentity() === "end" &&
182
+ this.#nextQuranNumber === QURAN_VERSE_COUNT + 1 &&
183
+ this.attributionCount === QURAN_ATTRIBUTION_COUNT &&
184
+ this.runtimeCount ===
185
+ QURAN_ATTRIBUTION_COUNT + QURAN_SURAH_COUNT + this.chunkCount &&
186
+ this.searchCount === expectedSearchCount &&
187
+ this.projectionCount === this.runtimeCount + this.searchCount) {
188
+ return Effect.void;
189
+ }
190
+ return Effect.fail(new QuranRowOrderError({
191
+ actual: this.expectedIdentity(),
192
+ expected: "end",
193
+ }));
194
+ }
195
+ /** Finalizes all ordered current Quran digest domains. */
196
+ digest() {
197
+ return {
198
+ attributionCount: this.attributionCount,
199
+ chunkCount: this.chunkCount,
200
+ projectionCount: this.projectionCount,
201
+ projectionDigest: Sha256HashSchema.make(`sha256:${this.#projection.digest("hex")}`),
202
+ runtimeCount: this.runtimeCount,
203
+ runtimeDigest: Sha256HashSchema.make(`sha256:${this.#runtime.digest("hex")}`),
204
+ searchCount: this.searchCount,
205
+ searchDigest: Sha256HashSchema.make(`sha256:${this.#search.digest("hex")}`),
206
+ };
207
+ }
208
+ }
209
+ /** Authenticates one current row before advancing ordered state. */
210
+ const updateQuranDigest = Effect.fn("AksaraContracts.updateQuranDigest")(function* (state, row) {
211
+ const expected = yield* hashQuranRow(row.payload);
212
+ if (expected !== row.rowHash) {
213
+ return yield* new QuranRowIntegrityError({
214
+ actual: row.rowHash,
215
+ expected,
216
+ });
217
+ }
218
+ yield* state.update(row);
219
+ });
220
+ /** Digests authenticated current rows under one active locale set. */
221
+ export const digestQuranRows = Effect.fn("AksaraContracts.digestQuranRows")(function* (input) {
222
+ const state = yield* Effect.try({
223
+ catch: () => new QuranRowHashError({ scope: "row" }),
224
+ try: () => new QuranDigestState(input.activeAppLocales),
225
+ });
226
+ yield* input.rows.pipe(Stream.runForEach((row) => updateQuranDigest(state, row)));
227
+ yield* state.validateComplete();
228
+ return yield* Effect.try({
229
+ catch: () => new QuranRowHashError({ scope: "row" }),
230
+ try: () => state.digest(),
231
+ });
232
+ });
@@ -0,0 +1,51 @@
1
+ import { Effect, Schema } from "effect";
2
+ import { type QuranSnapshot, type QuranSnapshotFacts } from "#contracts/quran/snapshot/spec";
3
+ declare const QuranSnapshotHashError_base: Schema.Class<QuranSnapshotHashError, Schema.TaggedStruct<"QuranSnapshotHashError", {}>, import("effect/Cause").YieldableError>;
4
+ /** Node could not compute a deterministic Quran snapshot identity. */
5
+ export declare class QuranSnapshotHashError extends QuranSnapshotHashError_base {
6
+ }
7
+ /** Serializes Quran snapshot facts in stable signed field order. */
8
+ export declare function canonicalizeQuranSnapshot(input: QuranSnapshotFacts): string;
9
+ /** Creates the content-addressed identity of one complete Quran snapshot. */
10
+ export declare const makeQuranSnapshot: (input: {
11
+ readonly activeAppLocales: readonly [("de" | "en" | "id") & import("effect/Brand").Brand<"@NakafaAI/AksaraAppLocale">, ...(("de" | "en" | "id") & import("effect/Brand").Brand<"@NakafaAI/AksaraAppLocale">)[]];
12
+ readonly attributionCount: number;
13
+ readonly chunkCount: number;
14
+ readonly projectionCount: number;
15
+ readonly projectionDigest: string & import("effect/Brand").Brand<"@NakafaAI/AksaraSha256Hash">;
16
+ readonly provenanceDigest: string & import("effect/Brand").Brand<"@NakafaAI/AksaraSha256Hash">;
17
+ readonly provenanceStatus: "approved" | "blocked";
18
+ readonly runtimeCount: number;
19
+ readonly runtimeDigest: string & import("effect/Brand").Brand<"@NakafaAI/AksaraSha256Hash">;
20
+ readonly searchCount: number;
21
+ readonly searchDigest: string & import("effect/Brand").Brand<"@NakafaAI/AksaraSha256Hash">;
22
+ readonly sourceBytes: number;
23
+ readonly sourceDigest: string & import("effect/Brand").Brand<"@NakafaAI/AksaraSha256Hash">;
24
+ readonly sourceFileCount: number;
25
+ readonly surahCount: number;
26
+ readonly tafsirLocales: readonly "id"[];
27
+ readonly verseCount: number;
28
+ }) => Effect.Effect<{
29
+ readonly activeAppLocales: readonly [("de" | "en" | "id") & import("effect/Brand").Brand<"@NakafaAI/AksaraAppLocale">, ...(("de" | "en" | "id") & import("effect/Brand").Brand<"@NakafaAI/AksaraAppLocale">)[]];
30
+ readonly attributionCount: number;
31
+ readonly chunkCount: number;
32
+ readonly projectionCount: number;
33
+ readonly projectionDigest: string & import("effect/Brand").Brand<"@NakafaAI/AksaraSha256Hash">;
34
+ readonly provenanceDigest: string & import("effect/Brand").Brand<"@NakafaAI/AksaraSha256Hash">;
35
+ readonly provenanceStatus: "approved" | "blocked";
36
+ readonly runtimeCount: number;
37
+ readonly runtimeDigest: string & import("effect/Brand").Brand<"@NakafaAI/AksaraSha256Hash">;
38
+ readonly searchCount: number;
39
+ readonly searchDigest: string & import("effect/Brand").Brand<"@NakafaAI/AksaraSha256Hash">;
40
+ readonly sourceBytes: number;
41
+ readonly sourceDigest: string & import("effect/Brand").Brand<"@NakafaAI/AksaraSha256Hash">;
42
+ readonly sourceFileCount: number;
43
+ readonly surahCount: number;
44
+ readonly tafsirLocales: readonly "id"[];
45
+ readonly verseCount: number;
46
+ readonly format: "localized-quran-snapshot";
47
+ readonly snapshotId: string & import("effect/Brand").Brand<"@NakafaAI/AksaraSha256Hash">;
48
+ }, QuranSnapshotHashError, never>;
49
+ /** Recomputes the content identity of one stored current snapshot. */
50
+ export declare function verifyQuranSnapshotHash(snapshot: QuranSnapshot): Effect.Effect<string & import("effect/Brand").Brand<"@NakafaAI/AksaraSha256Hash">, QuranSnapshotHashError, never>;
51
+ export {};
@@ -0,0 +1,50 @@
1
+ import { createHash } from "node:crypto";
2
+ import { Effect, Schema } from "effect";
3
+ import { Sha256HashSchema } from "#contracts/ids";
4
+ import { QURAN_SNAPSHOT_FORMAT, QuranSnapshotSchema, } from "#contracts/quran/snapshot/spec";
5
+ const SNAPSHOT_DOMAIN = "nakafa.aksara.localized-quran-snapshot";
6
+ /** Node could not compute a deterministic Quran snapshot identity. */
7
+ export class QuranSnapshotHashError extends Schema.TaggedError()("QuranSnapshotHashError", {}) {
8
+ }
9
+ /** Serializes Quran snapshot facts in stable signed field order. */
10
+ export function canonicalizeQuranSnapshot(input) {
11
+ return JSON.stringify({
12
+ activeAppLocales: input.activeAppLocales,
13
+ attributionCount: input.attributionCount,
14
+ chunkCount: input.chunkCount,
15
+ format: QURAN_SNAPSHOT_FORMAT,
16
+ projectionCount: input.projectionCount,
17
+ projectionDigest: input.projectionDigest,
18
+ provenanceDigest: input.provenanceDigest,
19
+ provenanceStatus: input.provenanceStatus,
20
+ runtimeCount: input.runtimeCount,
21
+ runtimeDigest: input.runtimeDigest,
22
+ searchCount: input.searchCount,
23
+ searchDigest: input.searchDigest,
24
+ sourceBytes: input.sourceBytes,
25
+ sourceDigest: input.sourceDigest,
26
+ sourceFileCount: input.sourceFileCount,
27
+ surahCount: input.surahCount,
28
+ tafsirLocales: input.tafsirLocales,
29
+ verseCount: input.verseCount,
30
+ });
31
+ }
32
+ /** Creates the content-addressed identity of one complete Quran snapshot. */
33
+ export const makeQuranSnapshot = Effect.fn("AksaraContracts.makeQuranSnapshot")((input) => Effect.try({
34
+ catch: () => new QuranSnapshotHashError(),
35
+ try: () => {
36
+ const snapshotId = Sha256HashSchema.make(`sha256:${createHash("sha256")
37
+ .update(`${SNAPSHOT_DOMAIN}\n${canonicalizeQuranSnapshot(input)}`)
38
+ .digest("hex")}`);
39
+ return QuranSnapshotSchema.make({
40
+ ...input,
41
+ format: QURAN_SNAPSHOT_FORMAT,
42
+ snapshotId,
43
+ });
44
+ },
45
+ }));
46
+ /** Recomputes the content identity of one stored current snapshot. */
47
+ export function verifyQuranSnapshotHash(snapshot) {
48
+ const { format: _format, snapshotId: _snapshotId, ...facts } = snapshot;
49
+ return makeQuranSnapshot(facts).pipe(Effect.map((value) => value.snapshotId));
50
+ }