@lssm/lib.contracts-runtime-core 3.0.1

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 (227) hide show
  1. package/README.md +110 -0
  2. package/dist/approval.d.ts +13 -0
  3. package/dist/approval.js +2 -0
  4. package/dist/approval.test.d.ts +1 -0
  5. package/dist/browser/approval.js +1 -0
  6. package/dist/browser/collaboration.js +0 -0
  7. package/dist/browser/contracts-runtime-core.feature.js +1 -0
  8. package/dist/browser/engine/backoff.js +1 -0
  9. package/dist/browser/engine/cache-store.js +1 -0
  10. package/dist/browser/engine/conflict-registry.js +1 -0
  11. package/dist/browser/engine/conflict-resolution.js +1 -0
  12. package/dist/browser/engine/data-engine-host.js +1 -0
  13. package/dist/browser/engine/data-engine-test-helpers.js +1 -0
  14. package/dist/browser/engine/data-engine.js +1 -0
  15. package/dist/browser/engine/dedup.js +1 -0
  16. package/dist/browser/engine/events.js +1 -0
  17. package/dist/browser/engine/index.js +1 -0
  18. package/dist/browser/engine/maintenance.js +1 -0
  19. package/dist/browser/engine/mutation-engine.js +1 -0
  20. package/dist/browser/engine/mutation-queue.js +1 -0
  21. package/dist/browser/engine/mutation-types.js +0 -0
  22. package/dist/browser/engine/no-cache-store.js +1 -0
  23. package/dist/browser/engine/online-only-data-engine.js +1 -0
  24. package/dist/browser/engine/online-only-mutation-engine.js +1 -0
  25. package/dist/browser/engine/query-engine.js +1 -0
  26. package/dist/browser/engine/state.js +1 -0
  27. package/dist/browser/engine/types.js +0 -0
  28. package/dist/browser/forms-platform-csv-columns.js +1 -0
  29. package/dist/browser/forms-platform-csv.js +2 -0
  30. package/dist/browser/forms-platform-management.js +1 -0
  31. package/dist/browser/forms-platform-navigation.js +1 -0
  32. package/dist/browser/forms-platform-submission.js +1 -0
  33. package/dist/browser/forms-platform-types.js +1 -0
  34. package/dist/browser/forms-platform-utils.js +1 -0
  35. package/dist/browser/forms-platform.js +2 -0
  36. package/dist/browser/index.js +3 -0
  37. package/dist/browser/local-offline.js +1 -0
  38. package/dist/browser/observability.js +1 -0
  39. package/dist/browser/reachability.js +1 -0
  40. package/dist/browser/redaction.js +1 -0
  41. package/dist/browser/storage/indexeddb.web.js +1 -0
  42. package/dist/browser/storage/keyed-store.js +1 -0
  43. package/dist/browser/storage/memory.js +1 -0
  44. package/dist/browser/storage/port.js +1 -0
  45. package/dist/browser/transport.js +2 -0
  46. package/dist/collaboration.d.ts +57 -0
  47. package/dist/collaboration.js +1 -0
  48. package/dist/contracts-runtime-core.feature.d.ts +1 -0
  49. package/dist/contracts-runtime-core.feature.js +2 -0
  50. package/dist/engine/backoff.d.ts +16 -0
  51. package/dist/engine/backoff.js +2 -0
  52. package/dist/engine/backoff.test.d.ts +1 -0
  53. package/dist/engine/cache-store.d.ts +19 -0
  54. package/dist/engine/cache-store.js +2 -0
  55. package/dist/engine/conflict-registry.d.ts +10 -0
  56. package/dist/engine/conflict-registry.js +2 -0
  57. package/dist/engine/conflict-registry.test.d.ts +1 -0
  58. package/dist/engine/conflict-resolution.d.ts +24 -0
  59. package/dist/engine/conflict-resolution.js +2 -0
  60. package/dist/engine/conflict-resolution.test.d.ts +1 -0
  61. package/dist/engine/data-engine-host.d.ts +30 -0
  62. package/dist/engine/data-engine-host.js +2 -0
  63. package/dist/engine/data-engine-local-offline.test.d.ts +1 -0
  64. package/dist/engine/data-engine-test-helpers.d.ts +30 -0
  65. package/dist/engine/data-engine-test-helpers.js +2 -0
  66. package/dist/engine/data-engine.d.ts +46 -0
  67. package/dist/engine/data-engine.js +2 -0
  68. package/dist/engine/data-engine.test.d.ts +1 -0
  69. package/dist/engine/dedup.d.ts +9 -0
  70. package/dist/engine/dedup.js +2 -0
  71. package/dist/engine/events.d.ts +76 -0
  72. package/dist/engine/events.economic-evidence.test.d.ts +1 -0
  73. package/dist/engine/events.js +2 -0
  74. package/dist/engine/index.d.ts +17 -0
  75. package/dist/engine/index.js +2 -0
  76. package/dist/engine/maintenance.d.ts +18 -0
  77. package/dist/engine/maintenance.js +2 -0
  78. package/dist/engine/maintenance.test.d.ts +1 -0
  79. package/dist/engine/mutation-engine.d.ts +2 -0
  80. package/dist/engine/mutation-engine.js +2 -0
  81. package/dist/engine/mutation-queue.d.ts +16 -0
  82. package/dist/engine/mutation-queue.js +2 -0
  83. package/dist/engine/mutation-queue.test.d.ts +1 -0
  84. package/dist/engine/mutation-types.d.ts +118 -0
  85. package/dist/engine/mutation-types.js +1 -0
  86. package/dist/engine/no-cache-store.d.ts +3 -0
  87. package/dist/engine/no-cache-store.js +2 -0
  88. package/dist/engine/online-only-data-engine.d.ts +9 -0
  89. package/dist/engine/online-only-data-engine.js +2 -0
  90. package/dist/engine/online-only-mutation-engine.d.ts +3 -0
  91. package/dist/engine/online-only-mutation-engine.js +2 -0
  92. package/dist/engine/query-engine.d.ts +7 -0
  93. package/dist/engine/query-engine.js +2 -0
  94. package/dist/engine/query-engine.test.d.ts +1 -0
  95. package/dist/engine/state.d.ts +37 -0
  96. package/dist/engine/state.js +2 -0
  97. package/dist/engine/types.d.ts +70 -0
  98. package/dist/engine/types.js +1 -0
  99. package/dist/forms-platform-choice.test.d.ts +1 -0
  100. package/dist/forms-platform-csv-columns.d.ts +7 -0
  101. package/dist/forms-platform-csv-columns.js +2 -0
  102. package/dist/forms-platform-csv.d.ts +12 -0
  103. package/dist/forms-platform-csv.js +3 -0
  104. package/dist/forms-platform-management.d.ts +12 -0
  105. package/dist/forms-platform-management.js +2 -0
  106. package/dist/forms-platform-navigation.d.ts +2 -0
  107. package/dist/forms-platform-navigation.js +2 -0
  108. package/dist/forms-platform-submission.d.ts +7 -0
  109. package/dist/forms-platform-submission.js +2 -0
  110. package/dist/forms-platform-types.d.ts +51 -0
  111. package/dist/forms-platform-types.js +2 -0
  112. package/dist/forms-platform-utils.d.ts +6 -0
  113. package/dist/forms-platform-utils.js +2 -0
  114. package/dist/forms-platform-utils.test.d.ts +1 -0
  115. package/dist/forms-platform.d.ts +19 -0
  116. package/dist/forms-platform.js +3 -0
  117. package/dist/forms-platform.test.d.ts +1 -0
  118. package/dist/index.d.ts +25 -0
  119. package/dist/index.js +4 -0
  120. package/dist/local-offline.d.ts +17 -0
  121. package/dist/local-offline.js +2 -0
  122. package/dist/local-offline.test.d.ts +1 -0
  123. package/dist/native/approval.js +130 -0
  124. package/dist/native/collaboration.js +0 -0
  125. package/dist/native/contracts-runtime-core.feature.js +17 -0
  126. package/dist/native/engine/backoff.js +16 -0
  127. package/dist/native/engine/cache-store.js +49 -0
  128. package/dist/native/engine/conflict-registry.js +51 -0
  129. package/dist/native/engine/conflict-resolution.js +81 -0
  130. package/dist/native/engine/data-engine-host.js +1119 -0
  131. package/dist/native/engine/data-engine-test-helpers.js +117 -0
  132. package/dist/native/engine/data-engine.js +1106 -0
  133. package/dist/native/engine/dedup.js +22 -0
  134. package/dist/native/engine/events.js +19 -0
  135. package/dist/native/engine/index.js +1189 -0
  136. package/dist/native/engine/maintenance.js +36 -0
  137. package/dist/native/engine/mutation-engine.js +360 -0
  138. package/dist/native/engine/mutation-queue.js +26 -0
  139. package/dist/native/engine/mutation-types.js +0 -0
  140. package/dist/native/engine/no-cache-store.js +15 -0
  141. package/dist/native/engine/online-only-data-engine.js +422 -0
  142. package/dist/native/engine/online-only-mutation-engine.js +70 -0
  143. package/dist/native/engine/query-engine.js +360 -0
  144. package/dist/native/engine/state.js +116 -0
  145. package/dist/native/engine/types.js +0 -0
  146. package/dist/native/forms-platform-csv-columns.js +31 -0
  147. package/dist/native/forms-platform-csv.js +150 -0
  148. package/dist/native/forms-platform-management.js +179 -0
  149. package/dist/native/forms-platform-navigation.js +27 -0
  150. package/dist/native/forms-platform-submission.js +207 -0
  151. package/dist/native/forms-platform-types.js +12 -0
  152. package/dist/native/forms-platform-utils.js +69 -0
  153. package/dist/native/forms-platform.js +429 -0
  154. package/dist/native/index.js +1879 -0
  155. package/dist/native/local-offline.js +81 -0
  156. package/dist/native/observability.js +182 -0
  157. package/dist/native/reachability.js +28 -0
  158. package/dist/native/redaction.js +44 -0
  159. package/dist/native/storage/async-storage.native.js +91 -0
  160. package/dist/native/storage/keyed-store.js +74 -0
  161. package/dist/native/storage/memory.js +26 -0
  162. package/dist/native/storage/port.js +16 -0
  163. package/dist/native/transport.js +27 -0
  164. package/dist/node/approval.js +1 -0
  165. package/dist/node/collaboration.js +0 -0
  166. package/dist/node/contracts-runtime-core.feature.js +1 -0
  167. package/dist/node/engine/backoff.js +1 -0
  168. package/dist/node/engine/cache-store.js +1 -0
  169. package/dist/node/engine/conflict-registry.js +1 -0
  170. package/dist/node/engine/conflict-resolution.js +1 -0
  171. package/dist/node/engine/data-engine-host.js +1 -0
  172. package/dist/node/engine/data-engine-test-helpers.js +1 -0
  173. package/dist/node/engine/data-engine.js +1 -0
  174. package/dist/node/engine/dedup.js +1 -0
  175. package/dist/node/engine/events.js +1 -0
  176. package/dist/node/engine/index.js +1 -0
  177. package/dist/node/engine/maintenance.js +1 -0
  178. package/dist/node/engine/mutation-engine.js +1 -0
  179. package/dist/node/engine/mutation-queue.js +1 -0
  180. package/dist/node/engine/mutation-types.js +0 -0
  181. package/dist/node/engine/no-cache-store.js +1 -0
  182. package/dist/node/engine/online-only-data-engine.js +1 -0
  183. package/dist/node/engine/online-only-mutation-engine.js +1 -0
  184. package/dist/node/engine/query-engine.js +1 -0
  185. package/dist/node/engine/state.js +1 -0
  186. package/dist/node/engine/types.js +0 -0
  187. package/dist/node/forms-platform-csv-columns.js +1 -0
  188. package/dist/node/forms-platform-csv.js +2 -0
  189. package/dist/node/forms-platform-management.js +1 -0
  190. package/dist/node/forms-platform-navigation.js +1 -0
  191. package/dist/node/forms-platform-submission.js +1 -0
  192. package/dist/node/forms-platform-types.js +1 -0
  193. package/dist/node/forms-platform-utils.js +1 -0
  194. package/dist/node/forms-platform.js +2 -0
  195. package/dist/node/index.js +3 -0
  196. package/dist/node/local-offline.js +1 -0
  197. package/dist/node/observability.js +1 -0
  198. package/dist/node/reachability.js +1 -0
  199. package/dist/node/redaction.js +1 -0
  200. package/dist/node/storage/keyed-store.js +1 -0
  201. package/dist/node/storage/memory.js +1 -0
  202. package/dist/node/storage/port.js +1 -0
  203. package/dist/node/transport.js +2 -0
  204. package/dist/observability.d.ts +34 -0
  205. package/dist/observability.js +2 -0
  206. package/dist/observability.test.d.ts +1 -0
  207. package/dist/reachability.d.ts +63 -0
  208. package/dist/reachability.js +2 -0
  209. package/dist/reachability.test.d.ts +1 -0
  210. package/dist/redaction.d.ts +34 -0
  211. package/dist/redaction.js +2 -0
  212. package/dist/redaction.test.d.ts +1 -0
  213. package/dist/storage/async-storage.native.d.ts +23 -0
  214. package/dist/storage/async-storage.native.test.d.ts +1 -0
  215. package/dist/storage/indexeddb.web.d.ts +13 -0
  216. package/dist/storage/indexeddb.web.js +2 -0
  217. package/dist/storage/keyed-store.d.ts +25 -0
  218. package/dist/storage/keyed-store.js +2 -0
  219. package/dist/storage/keyed-store.test.d.ts +1 -0
  220. package/dist/storage/memory.d.ts +11 -0
  221. package/dist/storage/memory.js +2 -0
  222. package/dist/storage/memory.test.d.ts +1 -0
  223. package/dist/storage/port.d.ts +34 -0
  224. package/dist/storage/port.js +2 -0
  225. package/dist/transport.d.ts +53 -0
  226. package/dist/transport.js +3 -0
  227. package/package.json +219 -0
@@ -0,0 +1,429 @@
1
+ // src/forms-platform-types.ts
2
+ class FormsPlatformError extends Error {
3
+ code;
4
+ constructor(code, message) {
5
+ super(message);
6
+ this.code = code;
7
+ this.name = "FormsPlatformError";
8
+ }
9
+ }
10
+
11
+ // src/forms-platform-utils.ts
12
+ import {
13
+ compileFormDocument,
14
+ validateFormDocument
15
+ } from "@lssm/lib.contracts-spec/forms";
16
+ function requireFormDefinition(definition, scope) {
17
+ const matches = definition?.owner.product === scope.product && definition.owner.tenantRef === scope.tenantRef && definition.owner.workspaceRef === scope.workspaceRef && definition.owner.ownerRef === scope.ownerRef;
18
+ if (!definition || !matches) {
19
+ throw new FormsPlatformError("FORM_NOT_FOUND", "Form definition was not found in the caller scope");
20
+ }
21
+ return definition;
22
+ }
23
+ function validateRuntimeFormDocument(document) {
24
+ const issues = validateFormDocument(document);
25
+ if (issues.length) {
26
+ throw new FormsPlatformError("INVALID_FORM_DOCUMENT", issues.map((issue) => issue.message).join("; "));
27
+ }
28
+ compileFormDocument(document);
29
+ }
30
+ function formAssuranceMeetsRequirement(assurance, requirement) {
31
+ if (requirement === "none")
32
+ return true;
33
+ if (requirement === "either")
34
+ return assurance !== "anonymous";
35
+ if (requirement === "both")
36
+ return assurance === "both";
37
+ return assurance === requirement || assurance === "both";
38
+ }
39
+ function resolveFormResponseStatus(document, assurance) {
40
+ if (formAssuranceMeetsRequirement(assurance, document.verification.requirement)) {
41
+ return "completed";
42
+ }
43
+ return document.verification.unverifiedDisposition === "pending-hidden" ? "pending-verification" : "completed-unverified";
44
+ }
45
+ function resolveFormLocale(document, candidates) {
46
+ const supported = new Set(document.content.enabledLocales.map((locale) => locale.toLowerCase()));
47
+ for (const candidate of candidates) {
48
+ if (!candidate)
49
+ continue;
50
+ const normalized = candidate.trim().toLowerCase();
51
+ const exact = document.content.enabledLocales.find((locale) => locale.toLowerCase() === normalized);
52
+ if (supported.has(normalized) && exact)
53
+ return exact;
54
+ const language = normalized.split("-")[0];
55
+ const match = document.content.enabledLocales.find((locale) => locale.toLowerCase().split("-")[0] === language);
56
+ if (match)
57
+ return match;
58
+ }
59
+ return document.content.defaultLocale;
60
+ }
61
+
62
+ // src/forms-platform-management.ts
63
+ function createFormsManagementService(dependencies) {
64
+ const emit = async (key, payload) => dependencies.emit?.(key, payload);
65
+ return {
66
+ async create(scope, document) {
67
+ validateRuntimeFormDocument(document);
68
+ const now = dependencies.now();
69
+ const definition = {
70
+ id: dependencies.id("definition"),
71
+ owner: scope,
72
+ status: "draft",
73
+ draft: document,
74
+ draftVersion: 1,
75
+ createdAt: now,
76
+ updatedAt: now
77
+ };
78
+ await dependencies.store.insertDefinition(definition);
79
+ await emit("forms.definition.created", {
80
+ definitionId: definition.id,
81
+ occurredAt: now
82
+ });
83
+ return definition;
84
+ },
85
+ async saveDraft(scope, definitionId, expectedVersion, document) {
86
+ validateRuntimeFormDocument(document);
87
+ const current = requireFormDefinition(await dependencies.store.getDefinition(scope, definitionId), scope);
88
+ if (current.status === "archived") {
89
+ throw new FormsPlatformError("FORM_ARCHIVED", "Archived forms cannot be edited");
90
+ }
91
+ const next = {
92
+ ...current,
93
+ draft: document,
94
+ draftVersion: expectedVersion + 1,
95
+ updatedAt: dependencies.now()
96
+ };
97
+ if (!await dependencies.store.updateDefinition(next, expectedVersion)) {
98
+ throw new FormsPlatformError("DRAFT_CONFLICT", "The form draft changed; reload before saving");
99
+ }
100
+ await emit("forms.draft.saved", {
101
+ definitionId,
102
+ occurredAt: next.updatedAt
103
+ });
104
+ return next;
105
+ },
106
+ async publish(scope, definitionId, expectedVersion) {
107
+ return dependencies.store.transaction(async (store) => {
108
+ const current = requireFormDefinition(await store.getDefinition(scope, definitionId), scope);
109
+ if (current.draftVersion !== expectedVersion) {
110
+ throw new FormsPlatformError("DRAFT_CONFLICT", "The form draft changed before publish");
111
+ }
112
+ validateRuntimeFormDocument(current.draft);
113
+ const previous = current.publishedRevisionId ? await store.getRevision(current.publishedRevisionId) : undefined;
114
+ const publishedAt = dependencies.now();
115
+ const revision = {
116
+ id: dependencies.id("revision"),
117
+ definitionId,
118
+ revision: (previous?.revision ?? 0) + 1,
119
+ document: structuredClone(current.draft),
120
+ digest: await dependencies.digest(JSON.stringify(current.draft)),
121
+ publishedAt
122
+ };
123
+ const definition = {
124
+ ...current,
125
+ status: "published",
126
+ publishedRevisionId: revision.id,
127
+ publicToken: current.publicToken ?? dependencies.publicToken(),
128
+ updatedAt: publishedAt
129
+ };
130
+ await store.insertRevision(revision);
131
+ if (!await store.updateDefinition(definition, expectedVersion)) {
132
+ throw new FormsPlatformError("DRAFT_CONFLICT", "The form changed during publish");
133
+ }
134
+ await emit("forms.definition.published", {
135
+ definitionId,
136
+ revisionId: revision.id,
137
+ occurredAt: publishedAt
138
+ });
139
+ return { definition, revision };
140
+ });
141
+ },
142
+ async close(scope, definitionId) {
143
+ const current = requireFormDefinition(await dependencies.store.getDefinition(scope, definitionId), scope);
144
+ if (current.status !== "published") {
145
+ throw new FormsPlatformError("FORM_NOT_PUBLISHED", "Only a published form can be closed");
146
+ }
147
+ const next = {
148
+ ...current,
149
+ status: "closed",
150
+ updatedAt: dependencies.now()
151
+ };
152
+ await dependencies.store.updateDefinition(next);
153
+ await emit("forms.definition.closed", {
154
+ definitionId,
155
+ occurredAt: next.updatedAt
156
+ });
157
+ return next;
158
+ },
159
+ async archive(scope, definitionId) {
160
+ const current = requireFormDefinition(await dependencies.store.getDefinition(scope, definitionId), scope);
161
+ if (current.status === "archived")
162
+ return current;
163
+ const next = {
164
+ ...current,
165
+ status: "archived",
166
+ updatedAt: dependencies.now()
167
+ };
168
+ await dependencies.store.updateDefinition(next);
169
+ await emit("forms.definition.archived", {
170
+ definitionId,
171
+ occurredAt: next.updatedAt
172
+ });
173
+ return next;
174
+ }
175
+ };
176
+ }
177
+
178
+ // src/forms-platform-submission.ts
179
+ import {
180
+ buildZodWithRelations,
181
+ compileFormDocument as compileFormDocument2,
182
+ FORM_AI_FULL_DATA_CONSENT_NOTICE_VERSION
183
+ } from "@lssm/lib.contracts-spec/forms";
184
+ function validateCanonicalFormAnswers(document, answers) {
185
+ const questionsById = new Map(document.questions.map((question) => [question.id, question]));
186
+ const portableFieldNames = new Set(Object.keys(document.formSpec?.modelFields ?? {}));
187
+ const unknownIds = Object.keys(answers).filter((id) => !questionsById.has(id) && !portableFieldNames.has(id));
188
+ if (unknownIds.length) {
189
+ throw new FormsPlatformError("INVALID_RESPONSE", `Unknown question IDs: ${unknownIds.join(", ")}`);
190
+ }
191
+ for (const question of document.questions) {
192
+ if (!["single-choice", "multi-select", "dropdown"].includes(question.kind))
193
+ continue;
194
+ const value = answers[question.id];
195
+ if (value == null)
196
+ continue;
197
+ const allowed = new Set(question.options?.map((option) => option.value));
198
+ const valid = question.kind === "multi-select" ? Array.isArray(value) && value.every((item) => typeof item === "string" && allowed.has(item)) : typeof value === "string" && allowed.has(value);
199
+ if (!valid)
200
+ throw new FormsPlatformError("INVALID_RESPONSE", `Answer for ${question.id} is not a declared option`);
201
+ }
202
+ const runtimeAnswers = Object.fromEntries(Object.entries(answers).map(([id, value]) => [
203
+ questionsById.get(id)?.name ?? id,
204
+ value
205
+ ]));
206
+ const result = buildZodWithRelations(compileFormDocument2(document)).safeParse(runtimeAnswers);
207
+ if (!result.success) {
208
+ throw new FormsPlatformError("INVALID_RESPONSE", `Response does not match the published form: ${String(result.error)}`);
209
+ }
210
+ }
211
+ function publicDocument(document) {
212
+ const {
213
+ kind,
214
+ version,
215
+ id,
216
+ titleKey,
217
+ descriptionKey,
218
+ content,
219
+ questions,
220
+ pages,
221
+ verification,
222
+ formSpec,
223
+ ai,
224
+ beforeDestination,
225
+ afterDestination
226
+ } = document;
227
+ return structuredClone({
228
+ kind,
229
+ version,
230
+ id,
231
+ titleKey,
232
+ descriptionKey,
233
+ content,
234
+ questions,
235
+ pages,
236
+ verification,
237
+ formSpec,
238
+ ai: {
239
+ mode: ai.mode,
240
+ dataMode: ai.dataMode,
241
+ ...ai.dataMode === "full-with-consent" ? {
242
+ consentNoticeVersion: FORM_AI_FULL_DATA_CONSENT_NOTICE_VERSION
243
+ } : {}
244
+ },
245
+ beforeDestination,
246
+ afterDestination
247
+ });
248
+ }
249
+ async function resolveAssurance(dependencies, evidenceRef, definitionId, publicToken) {
250
+ if (!evidenceRef)
251
+ return "anonymous";
252
+ if (!dependencies.resolveAssurance) {
253
+ throw new FormsPlatformError("VERIFICATION_UNAVAILABLE", "Verification evidence cannot be resolved");
254
+ }
255
+ return dependencies.resolveAssurance(evidenceRef, definitionId, publicToken);
256
+ }
257
+ function createFormsSubmissionService(dependencies) {
258
+ return {
259
+ async getPublic(publicToken, localeCandidates) {
260
+ const definition = await dependencies.store.getDefinitionByPublicToken(publicToken);
261
+ if (!definition || definition.status !== "published" || !definition.publishedRevisionId) {
262
+ throw new FormsPlatformError("FORM_NOT_FOUND", "Published form was not found");
263
+ }
264
+ const revision = await dependencies.store.getRevision(definition.publishedRevisionId, { publicToken });
265
+ if (!revision) {
266
+ throw new FormsPlatformError("REVISION_NOT_FOUND", "Published form revision was not found");
267
+ }
268
+ return {
269
+ definitionId: definition.id,
270
+ revisionId: revision.id,
271
+ revision: revision.revision,
272
+ document: publicDocument(revision.document),
273
+ locale: resolveFormLocale(revision.document, localeCandidates)
274
+ };
275
+ },
276
+ async submit(input) {
277
+ const definition = await dependencies.store.getDefinitionByPublicToken(input.publicToken);
278
+ if (!definition || definition.status !== "published") {
279
+ throw new FormsPlatformError("FORM_NOT_FOUND", "Form is not accepting responses");
280
+ }
281
+ const revision = await dependencies.store.getRevision(input.revisionId, {
282
+ publicToken: input.publicToken
283
+ });
284
+ if (!revision || revision.definitionId !== definition.id) {
285
+ throw new FormsPlatformError("REVISION_MISMATCH", "Response revision does not match the public session");
286
+ }
287
+ validateCanonicalFormAnswers(revision.document, input.answers);
288
+ const assurance = await resolveAssurance(dependencies, input.verificationEvidenceRef, definition.id, input.publicToken);
289
+ if (revision.document.verification.timing === "before" && !formAssuranceMeetsRequirement(assurance, revision.document.verification.requirement)) {
290
+ throw new FormsPlatformError("VERIFICATION_REQUIRED", "Verification is required before submission");
291
+ }
292
+ const now = dependencies.now();
293
+ const response = {
294
+ id: dependencies.id("response"),
295
+ definitionId: definition.id,
296
+ revisionId: revision.id,
297
+ locale: resolveFormLocale(revision.document, [input.locale]),
298
+ status: resolveFormResponseStatus(revision.document, assurance),
299
+ authorAssurance: assurance,
300
+ answers: structuredClone(input.answers),
301
+ createdAt: now,
302
+ updatedAt: now,
303
+ submittedAt: now
304
+ };
305
+ const stored = await dependencies.store.insertResponseIdempotently(response, input.publicToken, input.idempotencyKey);
306
+ if (!stored.inserted)
307
+ return stored.response;
308
+ await dependencies.emit?.("forms.response.submitted", {
309
+ definitionId: response.definitionId,
310
+ revisionId: response.revisionId,
311
+ responseId: response.id,
312
+ occurredAt: now
313
+ });
314
+ return stored.response;
315
+ }
316
+ };
317
+ }
318
+
319
+ // src/forms-platform-csv-columns.ts
320
+ import { flattenPortableFormFields } from "@lssm/lib.contracts-spec/forms";
321
+ function formCsvColumns(document) {
322
+ const questionNames = new Set(document.questions.map(({ name }) => name));
323
+ const questions = document.questions.map(questionColumn);
324
+ const portableSensitive = new Set(document.formSpec?.policy?.pii ?? []);
325
+ const allFields = flattenPortableFormFields(document.formSpec?.fields ?? []);
326
+ const portable = Object.entries(document.formSpec?.modelFields ?? {}).flatMap(([name, modelField]) => {
327
+ if (questionNames.has(name))
328
+ return [];
329
+ const field = allFields.find((candidate) => candidate.name === name);
330
+ return [
331
+ {
332
+ id: name,
333
+ labelKey: field?.labelI18n ?? name,
334
+ sensitive: portableSensitive.has(name) || [...portableSensitive].some((path) => path.startsWith(`${name}.`)) || field?.kind === "email" || field?.kind === "phone" || modelField.type === "email" || modelField.type === "phone"
335
+ }
336
+ ];
337
+ });
338
+ return [...questions, ...portable];
339
+ }
340
+ function questionColumn(question) {
341
+ return {
342
+ id: question.id,
343
+ labelKey: question.labelKey,
344
+ sensitive: question.sensitive === true || question.kind === "email" || question.kind === "phone"
345
+ };
346
+ }
347
+
348
+ // src/forms-platform-csv.ts
349
+ var RESPONSE_EXTENSIONS_COLUMN = "response_extensions:AI follow-ups";
350
+ function csvCell(value) {
351
+ const raw = Array.isArray(value) ? value.every((item) => typeof item !== "object") ? value.join(";") : JSON.stringify(value) : value == null ? "" : typeof value === "object" ? JSON.stringify(value) : String(value);
352
+ const protectedValue = /^[\s]*[=+\-@]/.test(raw) ? `'${raw}` : raw;
353
+ return `"${protectedValue.replaceAll('"', '""')}"`;
354
+ }
355
+ function csvRow(values) {
356
+ return `${values.map(csvCell).join(",")}\r
357
+ `;
358
+ }
359
+ function responseRow(revision, response, options) {
360
+ if (response.revisionId !== revision.id) {
361
+ throw new Error("CSV responses must belong to the exported revision");
362
+ }
363
+ const columns = formCsvColumns(revision.document);
364
+ const baseIds = new Set(columns.map(({ id }) => id));
365
+ const extensions = Object.fromEntries(Object.entries(response.answers).filter(([id]) => !baseIds.has(id)));
366
+ return [
367
+ response.id,
368
+ response.status,
369
+ response.locale,
370
+ response.submittedAt ?? "",
371
+ ...columns.map((column) => {
372
+ const mayIncludeSensitive = options.includeSensitive && options.sensitiveGrant?.authorized;
373
+ if (!mayIncludeSensitive && column.sensitive) {
374
+ return "[REDACTED]";
375
+ }
376
+ return response.answers[column.id];
377
+ }),
378
+ options.includeSensitive && options.sensitiveGrant?.authorized ? extensions : "[REDACTED]"
379
+ ];
380
+ }
381
+ function csvHeaders(revision, locale) {
382
+ const resolved = resolveFormLocale(revision.document, [locale]);
383
+ const catalog = revision.document.content.catalogs[resolved] ?? {};
384
+ return [
385
+ "response_id",
386
+ "status",
387
+ "locale",
388
+ "submitted_at",
389
+ ...formCsvColumns(revision.document).map((column) => `${column.id}:${catalog[column.labelKey] ?? column.labelKey}`),
390
+ RESPONSE_EXTENSIONS_COLUMN
391
+ ];
392
+ }
393
+ function serializeFormResponsesCsv(revision, responses, options = {}) {
394
+ return `\uFEFF${csvRow(csvHeaders(revision, options.locale))}${responses.map((response) => csvRow(responseRow(revision, response, options))).join("")}`;
395
+ }
396
+ async function* streamFormResponsesCsv(revision, responses, options = {}) {
397
+ yield `\uFEFF${csvRow(csvHeaders(revision, options.locale))}`;
398
+ for await (const response of responses) {
399
+ yield csvRow(responseRow(revision, response, options));
400
+ }
401
+ }
402
+ // src/forms-platform-navigation.ts
403
+ import {
404
+ evalPredicate
405
+ } from "@lssm/lib.contracts-spec/forms";
406
+ function resolveNextFormPage(document, currentPageId, answers) {
407
+ const currentIndex = document.pages.findIndex((page) => page.id === currentPageId);
408
+ if (currentIndex < 0) {
409
+ throw new FormsPlatformError("PAGE_NOT_FOUND", "Current form page does not exist");
410
+ }
411
+ const rule = document.pages[currentIndex]?.nextPageRules?.find((candidate) => evalPredicate(answers, candidate.when));
412
+ return rule?.nextPageId ?? document.pages[currentIndex]?.defaultNextPageId ?? document.pages[currentIndex + 1]?.id;
413
+ }
414
+
415
+ // src/forms-platform.ts
416
+ function createFormsPlatformService(dependencies) {
417
+ return {
418
+ ...createFormsManagementService(dependencies),
419
+ ...createFormsSubmissionService(dependencies)
420
+ };
421
+ }
422
+ export {
423
+ FormsPlatformError,
424
+ createFormsPlatformService,
425
+ resolveFormLocale,
426
+ resolveNextFormPage,
427
+ serializeFormResponsesCsv,
428
+ streamFormResponsesCsv
429
+ };