@ai-matrx/records 0.87.31 → 0.92.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 (107) hide show
  1. package/CHANGELOG.md +98 -0
  2. package/README.md +115 -0
  3. package/dist/{client-E0VQ0NPO.d.ts → client-DE9Mq6YX.d.cts} +8 -5
  4. package/dist/{client-C3ZZNfGH.d.cts → client-vJAb7g9r.d.ts} +8 -5
  5. package/dist/core/index.cjs +25 -7
  6. package/dist/core/index.cjs.map +1 -1
  7. package/dist/core/index.d.cts +23 -12
  8. package/dist/core/index.d.ts +23 -12
  9. package/dist/core/index.js +25 -7
  10. package/dist/core/index.js.map +1 -1
  11. package/dist/data/index.cjs +19 -7
  12. package/dist/data/index.cjs.map +1 -1
  13. package/dist/data/index.d.cts +12 -142
  14. package/dist/data/index.d.ts +12 -142
  15. package/dist/data/index.js +19 -7
  16. package/dist/data/index.js.map +1 -1
  17. package/dist/{declareTable-BK3zufr7.d.cts → declareTable-Btg5bql1.d.cts} +4 -4
  18. package/dist/{declareTable-B12XybRp.d.ts → declareTable-JKt2q34d.d.ts} +4 -4
  19. package/dist/entity/index.cjs +984 -0
  20. package/dist/entity/index.cjs.map +1 -0
  21. package/dist/entity/index.d.cts +36 -0
  22. package/dist/entity/index.d.ts +36 -0
  23. package/dist/entity/index.js +964 -0
  24. package/dist/entity/index.js.map +1 -0
  25. package/dist/entity/react.cjs +1024 -0
  26. package/dist/entity/react.cjs.map +1 -0
  27. package/dist/entity/react.d.cts +511 -0
  28. package/dist/entity/react.d.ts +511 -0
  29. package/dist/entity/react.js +1015 -0
  30. package/dist/entity/react.js.map +1 -0
  31. package/dist/{field-BanTVGK2.d.cts → field-BJBnTyJ0.d.cts} +1 -1
  32. package/dist/{field-BPABOJ2Y.d.ts → field-Ti5iAxFr.d.ts} +1 -1
  33. package/dist/forms/index.cjs +984 -0
  34. package/dist/forms/index.cjs.map +1 -0
  35. package/dist/forms/index.d.cts +333 -0
  36. package/dist/forms/index.d.ts +333 -0
  37. package/dist/forms/index.js +961 -0
  38. package/dist/forms/index.js.map +1 -0
  39. package/dist/index.cjs +10 -1
  40. package/dist/index.cjs.map +1 -1
  41. package/dist/index.d.cts +11 -8
  42. package/dist/index.d.ts +11 -8
  43. package/dist/index.js +10 -1
  44. package/dist/index.js.map +1 -1
  45. package/dist/memory/index.d.cts +7 -6
  46. package/dist/memory/index.d.ts +7 -6
  47. package/dist/port-CPVBfDxS.d.cts +301 -0
  48. package/dist/port-D2i2o7Lg.d.ts +301 -0
  49. package/dist/portal-Db4CYyHe.d.ts +331 -0
  50. package/dist/portal-DfeY2Boy.d.cts +331 -0
  51. package/dist/{ports-TMWX-t9l.d.ts → ports-BWtxAnXh.d.ts} +2 -2
  52. package/dist/{ports-D8-iuY1P.d.cts → ports-DSU2RsR6.d.cts} +2 -2
  53. package/dist/react/index.cjs +55 -523
  54. package/dist/react/index.cjs.map +1 -1
  55. package/dist/react/index.d.cts +14 -1
  56. package/dist/react/index.d.ts +14 -1
  57. package/dist/react/index.js +52 -527
  58. package/dist/react/index.js.map +1 -1
  59. package/dist/realtime/index.d.cts +2 -2
  60. package/dist/realtime/index.d.ts +2 -2
  61. package/dist/{record-DpB2QgAZ.d.cts → record-BQi92d4-.d.cts} +7 -1
  62. package/dist/{record-DpB2QgAZ.d.ts → record-BQi92d4-.d.ts} +7 -1
  63. package/dist/relation-DMwkmzgN.d.cts +230 -0
  64. package/dist/relation-DlcqV_W5.d.ts +230 -0
  65. package/dist/rule-B8kR0l01.d.ts +250 -0
  66. package/dist/rule-BMViCLmW.d.cts +250 -0
  67. package/dist/search/index.cjs +283 -0
  68. package/dist/search/index.cjs.map +1 -0
  69. package/dist/search/index.d.cts +44 -0
  70. package/dist/search/index.d.ts +44 -0
  71. package/dist/search/index.js +260 -0
  72. package/dist/search/index.js.map +1 -0
  73. package/dist/setup/index.cjs +6854 -0
  74. package/dist/setup/index.cjs.map +1 -0
  75. package/dist/setup/index.d.cts +87 -0
  76. package/dist/setup/index.d.ts +87 -0
  77. package/dist/setup/index.js +6829 -0
  78. package/dist/setup/index.js.map +1 -0
  79. package/dist/{subscription-D7KQBHXN.d.ts → subscription-5CLN3Jug.d.ts} +3 -249
  80. package/dist/{subscription-BD9DnJxX.d.cts → subscription-D-VEzvyy.d.cts} +3 -249
  81. package/dist/{table-kind-BZu8mtND.d.cts → table-kind-CVp43g6h.d.ts} +3 -558
  82. package/dist/{table-kind-xKxTHtrV.d.ts → table-kind-Cm6PXkWx.d.cts} +3 -558
  83. package/dist/templates/index.d.cts +10 -9
  84. package/dist/templates/index.d.ts +10 -9
  85. package/dist/typed-table/index.cjs +19 -7
  86. package/dist/typed-table/index.cjs.map +1 -1
  87. package/dist/typed-table/index.d.cts +10 -7
  88. package/dist/typed-table/index.d.ts +10 -7
  89. package/dist/typed-table/index.js +19 -7
  90. package/dist/typed-table/index.js.map +1 -1
  91. package/dist/{types-CLP5H4Wk.d.cts → types-9Q5SHRDJ.d.cts} +2 -2
  92. package/dist/{types-DmPkWQic.d.ts → types-CfYy-vdV.d.ts} +2 -2
  93. package/dist/{types-C2uspRtf.d.cts → types-Cs2MyE3u.d.cts} +2 -2
  94. package/dist/{types-Dv61jStB.d.ts → types-DF-dxB_l.d.ts} +2 -2
  95. package/dist/types-DTEGaxso.d.cts +139 -0
  96. package/dist/types-Dji5SpFS.d.ts +139 -0
  97. package/dist/use-cases/index.d.cts +5 -5
  98. package/dist/use-cases/index.d.ts +5 -5
  99. package/dist/{validate-CWVToe1T.d.ts → validate-BydKd-Le.d.ts} +1 -1
  100. package/dist/{validate-DEB_mn4Y.d.cts → validate-CqJakTKk.d.cts} +1 -1
  101. package/dist/versions/index.cjs +375 -0
  102. package/dist/versions/index.cjs.map +1 -0
  103. package/dist/versions/index.d.cts +160 -0
  104. package/dist/versions/index.d.ts +160 -0
  105. package/dist/versions/index.js +353 -0
  106. package/dist/versions/index.js.map +1 -0
  107. package/package.json +62 -2
@@ -0,0 +1,961 @@
1
+ // src/portal.ts
2
+ var PORTAL_ACCENTS = ["slate", "green", "amber", "red", "blue", "violet", "teal"];
3
+ function isPortalAccent(word) {
4
+ return typeof word === "string" && PORTAL_ACCENTS.includes(word);
5
+ }
6
+
7
+ // src/forms/forms.ts
8
+ var FORM_STATE_WORDS = {
9
+ draft: "Not published \u2014 the link does not answer yet.",
10
+ open: "Open \u2014 anyone with the link can answer it.",
11
+ closed: "Closed \u2014 it keeps its answers and takes no more.",
12
+ full: "Full \u2014 it has all the answers it was set up to take."
13
+ };
14
+ var FORM_FLOWS = ["one-at-a-time", "single-page"];
15
+ function formPresentation(spec) {
16
+ return {
17
+ intro: spec.intro ?? null,
18
+ flow: spec.flow ?? "one-at-a-time",
19
+ thank_you: thankYouDocument(spec.thankYou ?? null),
20
+ submit_label: spec.submitLabel ?? null,
21
+ look: formLookDocument(spec.look ?? null),
22
+ welcome: welcomeDocument(spec.welcome ?? null),
23
+ endings: (spec.endings ?? []).map(endingDocument),
24
+ hidden_fields: spec.hiddenFields ?? [],
25
+ theme: themeDocument(spec.theme ?? null),
26
+ ...typeof spec.showOwnerHeader === "boolean" ? { show_owner_header: spec.showOwnerHeader } : {},
27
+ ...typeof spec.autoAdvance === "boolean" ? { auto_advance: spec.autoAdvance } : {},
28
+ ...spec.notify && (spec.notify.webhookUrl || spec.notify.webhookAutomationId || spec.notify.emailRespondent) ? {
29
+ notify: strip({
30
+ webhook_url: text(spec.notify.webhookUrl),
31
+ webhook_automation_id: text(spec.notify.webhookAutomationId),
32
+ email_respondent: spec.notify.emailRespondent ? true : null,
33
+ respondent_field: spec.notify.emailRespondent ? text(spec.notify.respondentField) : null
34
+ })
35
+ } : {}
36
+ };
37
+ }
38
+ function notifyFromDocument(raw) {
39
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
40
+ const d = raw;
41
+ const n = {
42
+ webhookUrl: text(d["webhook_url"]),
43
+ webhookAutomationId: text(d["webhook_automation_id"]),
44
+ emailRespondent: d["email_respondent"] === true ? true : null,
45
+ respondentField: text(d["respondent_field"])
46
+ };
47
+ return n.webhookUrl || n.webhookAutomationId || n.emailRespondent ? n : null;
48
+ }
49
+ var bool = (v) => typeof v === "boolean" ? v : null;
50
+ function answerRefusal(kind, required, value) {
51
+ const empty = value === void 0 || value === null || value === "" || Array.isArray(value) && value.length === 0;
52
+ if (empty) return required ? "This field is required" : null;
53
+ if (typeof value !== "string") return null;
54
+ const v = value.trim();
55
+ if (kind === "email" && !/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(v)) return "Enter a valid email";
56
+ if (kind === "phone" && (v.replace(/\D/g, "").length < 7 || /[^\d\s()+.\-x]/i.test(v))) return "Enter a valid phone number";
57
+ if (kind === "url" && !/^(https?:\/\/)?[^\s.\/]+(\.[^\s.\/]+)+(\/\S*)?$/i.test(v)) return "Enter a valid link";
58
+ return null;
59
+ }
60
+ function pipeAnswers(text2, answers, questions, labelOf) {
61
+ if (!text2.includes("{{")) return text2;
62
+ return text2.replace(/\{\{\s*([^{}]+?)\s*\}\}/g, (_all, name) => {
63
+ const wanted = name.trim().toLowerCase();
64
+ const q = questions.find((x) => x.key.toLowerCase() === wanted) ?? questions.find((x) => (x.ask ?? "").trim().toLowerCase() === wanted);
65
+ if (!q) return "";
66
+ const value = answers[q.key];
67
+ if (value === void 0 || value === null) return "";
68
+ const one = (v) => labelOf?.(q.key, v) ?? String(v);
69
+ return Array.isArray(value) ? value.map(one).join(", ") : one(value);
70
+ });
71
+ }
72
+ var text = (v) => typeof v === "string" && v.trim() !== "" ? v.trim() : null;
73
+ var num = (v) => typeof v === "number" && Number.isFinite(v) ? v : null;
74
+ var strip = (doc) => Object.fromEntries(Object.entries(doc).filter(([, v]) => v !== null && v !== void 0));
75
+ function themeDocument(theme) {
76
+ if (!theme) return {};
77
+ return strip({
78
+ accent: theme.accent ?? null,
79
+ align: theme.align ?? null,
80
+ font: text(theme.font),
81
+ button: text(theme.button),
82
+ background: text(theme.background),
83
+ background_file_id: text(theme.backgroundFileId)
84
+ });
85
+ }
86
+ function themeFromDocument(raw) {
87
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
88
+ const d = raw;
89
+ const align = d["align"] === "center" ? "center" : d["align"] === "left" ? "left" : null;
90
+ return {
91
+ accent: text(d["accent"]),
92
+ align,
93
+ font: text(d["font"]),
94
+ button: text(d["button"]),
95
+ background: text(d["background"]),
96
+ backgroundFileId: text(d["background_file_id"]),
97
+ backgroundUrl: text(d["background_url"])
98
+ };
99
+ }
100
+ function welcomeDocument(welcome) {
101
+ if (!welcome) return null;
102
+ const doc = strip({
103
+ title: text(welcome.title),
104
+ body: text(welcome.body),
105
+ picture_file_id: text(welcome.pictureFileId),
106
+ button_label: text(welcome.buttonLabel)
107
+ });
108
+ return Object.keys(doc).length > 0 ? doc : null;
109
+ }
110
+ function welcomeFromDocument(raw) {
111
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
112
+ const d = raw;
113
+ const w = {
114
+ title: text(d["title"]),
115
+ body: text(d["body"]),
116
+ pictureFileId: text(d["picture_file_id"]),
117
+ pictureUrl: text(d["picture_url"]),
118
+ buttonLabel: text(d["button_label"])
119
+ };
120
+ return w.title || w.body || w.pictureFileId || w.pictureUrl || w.buttonLabel ? w : null;
121
+ }
122
+ function endingDocument(ending) {
123
+ return strip({
124
+ id: ending.id,
125
+ title: text(ending.title),
126
+ body: text(ending.body),
127
+ picture_file_id: text(ending.pictureFileId),
128
+ button_label: text(ending.buttonLabel),
129
+ redirect_url: text(ending.redirectUrl),
130
+ when: ending.when ?? null,
131
+ score_min: num(ending.scoreMin),
132
+ score_max: num(ending.scoreMax)
133
+ });
134
+ }
135
+ function endingsFromDocument(raw) {
136
+ if (!Array.isArray(raw)) return [];
137
+ return raw.filter((e) => !!e && typeof e === "object" && typeof e["id"] === "string").map((d) => ({
138
+ id: String(d["id"]),
139
+ title: text(d["title"]),
140
+ body: text(d["body"]),
141
+ pictureFileId: text(d["picture_file_id"]),
142
+ pictureUrl: text(d["picture_url"]),
143
+ buttonLabel: text(d["button_label"]),
144
+ redirectUrl: text(d["redirect_url"]),
145
+ when: d["when"] ?? null,
146
+ scoreMin: num(d["score_min"]),
147
+ scoreMax: num(d["score_max"])
148
+ }));
149
+ }
150
+ function hiddenFromLink(hiddenFields, params) {
151
+ const out = {};
152
+ for (const key of hiddenFields ?? []) {
153
+ const raw = params[key];
154
+ const one = Array.isArray(raw) ? raw[raw.length - 1] : raw;
155
+ if (typeof one === "string" && one.trim() !== "") out[key] = one.trim().slice(0, 500);
156
+ }
157
+ return out;
158
+ }
159
+ function formRoutes(form) {
160
+ return (form.questions ?? []).some((q) => q.showIf || q.jumps && q.jumps.length > 0 || q.points) || (form.endings ?? []).length > 0;
161
+ }
162
+ var FORM_EMBEDS = ["inline", "popup", "slider"];
163
+ function formEmbedSnippet(kind, formUrl, options) {
164
+ const url = new URL(formUrl);
165
+ url.searchParams.set("embed", "1");
166
+ const src = url.toString();
167
+ const label = (options?.label ?? "").trim() || "Open the form";
168
+ const esc = (v) => v.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
169
+ if (kind === "inline") {
170
+ const height = options?.height && options.height > 0 ? Math.round(options.height) : 640;
171
+ return `<iframe src="${esc(src)}" title="${esc(label)}" style="width:100%;height:${height}px;border:0" loading="lazy" allow="clipboard-write"></iframe>`;
172
+ }
173
+ const script = `${url.origin}/embed/form.js`;
174
+ return `<button type="button" data-matrx-form="${esc(src)}" data-matrx-form-mode="${kind}">${esc(label)}</button>
175
+ <script src="${esc(script)}" async><\/script>`;
176
+ }
177
+ function formLookDocument(look) {
178
+ if (!look) return null;
179
+ const out = {};
180
+ if (look.accent) out["accent"] = look.accent;
181
+ if (look.logoFileId) out["logo_file_id"] = look.logoFileId;
182
+ if (look.coverFileId) out["cover_file_id"] = look.coverFileId;
183
+ return Object.keys(out).length > 0 ? out : null;
184
+ }
185
+ function formLookFromDocument(raw) {
186
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
187
+ const doc = raw;
188
+ const id = (v) => typeof v === "string" && v.trim() !== "" ? v : null;
189
+ const look = {
190
+ accent: isPortalAccent(doc["accent"]) ? doc["accent"] : null,
191
+ logoFileId: id(doc["logo_file_id"]),
192
+ coverFileId: id(doc["cover_file_id"])
193
+ };
194
+ return look.accent || look.logoFileId || look.coverFileId ? look : null;
195
+ }
196
+ function thankYouDocument(thankYou) {
197
+ if (!thankYou) return null;
198
+ const title = thankYou.title?.trim() || null;
199
+ const body = thankYou.body?.trim() || null;
200
+ const redirect = thankYou.redirectUrl?.trim() || null;
201
+ if (!title && !body && !redirect) return null;
202
+ return { title, body, ...redirect ? { redirect_url: redirect } : {} };
203
+ }
204
+ function thankYouFromDocument(raw) {
205
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
206
+ const doc = raw;
207
+ const text2 = (v) => typeof v === "string" && v.trim() !== "" ? v : null;
208
+ const thankYou = {
209
+ title: text2(doc["title"]),
210
+ body: text2(doc["body"]),
211
+ redirectUrl: text2(doc["redirect_url"] ?? doc["redirectUrl"])
212
+ };
213
+ return thankYou.title || thankYou.body || thankYou.redirectUrl ? thankYou : null;
214
+ }
215
+ function formDeclareArgs(form) {
216
+ const saved = form;
217
+ return {
218
+ table_id: form.subject,
219
+ title: form.name,
220
+ questions: form.questions ?? [],
221
+ presentation: formPresentation(form),
222
+ submission_cap: saved.submissionCap ?? null,
223
+ quarantine_rule_id: saved.quarantineRuleId ?? null,
224
+ notify_rule_id: saved.notifyRuleId ?? null,
225
+ form_id: saved.id ?? null,
226
+ slug: saved.slug ?? null
227
+ };
228
+ }
229
+ function formFromSummary(summary) {
230
+ const presentation = summary.presentation ?? {};
231
+ const flow = String(presentation["flow"] ?? "one-at-a-time");
232
+ return {
233
+ id: summary.form_id,
234
+ name: summary.title ?? summary.slug,
235
+ subject: summary.table_id,
236
+ intro: presentation["intro"] ?? null,
237
+ questions: readQuestions(presentation["questions"]),
238
+ flow: FORM_FLOWS.includes(flow) ? flow : "one-at-a-time",
239
+ theme: themeFromDocument(presentation["theme"]),
240
+ welcome: welcomeFromDocument(presentation["welcome"]),
241
+ endings: endingsFromDocument(presentation["endings"]),
242
+ hiddenFields: Array.isArray(presentation["hidden_fields"]) ? presentation["hidden_fields"].filter((k) => typeof k === "string") : [],
243
+ thankYou: thankYouFromDocument(presentation["thank_you"]),
244
+ look: formLookFromDocument(presentation["look"]),
245
+ submitLabel: presentation["submit_label"] ?? null,
246
+ showOwnerHeader: bool(presentation["show_owner_header"]),
247
+ autoAdvance: bool(presentation["auto_advance"]),
248
+ notify: notifyFromDocument(presentation["notify"]),
249
+ // A form is public when the store says it is OPEN. A screen never decides
250
+ // this from a flag of its own: the link answers or it does not.
251
+ isPublic: summary.state === "open",
252
+ state: summary.state,
253
+ slug: summary.slug,
254
+ submissionCap: summary.submission_cap,
255
+ quarantineRuleId: summary.quarantine_rule_id ?? null,
256
+ notifyRuleId: summary.notify_rule_id ?? null
257
+ };
258
+ }
259
+ function readQuestions(raw) {
260
+ if (!Array.isArray(raw)) return [];
261
+ const out = [];
262
+ for (const entry of raw) {
263
+ if (!entry || typeof entry !== "object") continue;
264
+ const q = entry;
265
+ const field = String(q["field"] ?? q["key"] ?? "").trim();
266
+ if (field === "") continue;
267
+ out.push({
268
+ field,
269
+ ask: q["ask"] ?? null,
270
+ help: q["help"] ?? null,
271
+ required: q["required"] ?? null,
272
+ showIf: q["showIf"] ?? q["show_if"] ?? null,
273
+ jumps: Array.isArray(q["jumps"]) ? q["jumps"] : null,
274
+ points: q["points"] && typeof q["points"] === "object" && !Array.isArray(q["points"]) ? q["points"] : null
275
+ });
276
+ }
277
+ return out;
278
+ }
279
+ function submissionStamp(args) {
280
+ return {
281
+ _source: {
282
+ via: "form",
283
+ form_id: args.formId,
284
+ form_name: args.formName,
285
+ at: (/* @__PURE__ */ new Date()).toISOString(),
286
+ actor: args.actor,
287
+ ...args.onBehalfOf ? { on_behalf_of: args.onBehalfOf } : {}
288
+ }
289
+ };
290
+ }
291
+
292
+ // src/forms/publicBinding.ts
293
+ function publicReadOf(binding) {
294
+ if (binding.kind === "table" && binding.table_id) return { read: "table", tableId: binding.table_id };
295
+ if (binding.kind === "view" && binding.table_id) {
296
+ return { read: "view", tableId: binding.table_id, viewId: binding.resource_id };
297
+ }
298
+ if (binding.kind === "record") return { read: "record", recordId: binding.resource_id };
299
+ return { read: "unknown" };
300
+ }
301
+
302
+ // src/button.ts
303
+ function isButtonField(field) {
304
+ return field?.type === "text" && String(field.format ?? "") === "button";
305
+ }
306
+
307
+ // src/newerKinds.ts
308
+ import { formatDurationSeconds } from "@ai-matrx/kit/format";
309
+ function newerKindOf(field) {
310
+ const format = String(field.format ?? "");
311
+ const config = field.config ?? {};
312
+ switch (field.type) {
313
+ case "range":
314
+ if (format === "rating") return "rating";
315
+ if (format === "duration") return "duration";
316
+ return null;
317
+ case "text":
318
+ if (format === "address") return "address";
319
+ if (format === "barcode") return "barcode";
320
+ if (format === "rich_text") return "rich_text";
321
+ return null;
322
+ case "list":
323
+ return format === "status" ? "status" : null;
324
+ case "formula":
325
+ if (format === "count" && config["agg"] === "count") return "count";
326
+ if (config["system"] === "created_by") return "created_by";
327
+ if (config["system"] === "modified_by") return "modified_by";
328
+ return null;
329
+ default:
330
+ return null;
331
+ }
332
+ }
333
+ function ratingMaxOf(field) {
334
+ const rules = field.rules ?? [];
335
+ const max = rules.find((r) => r?.kind === "max")?.value;
336
+ const n = typeof max === "number" ? max : typeof max === "string" ? Number(max) : NaN;
337
+ return Number.isInteger(n) && n >= 1 && n <= 10 ? n : 5;
338
+ }
339
+ function readTypedDuration(text2) {
340
+ const t = text2.trim().toLowerCase();
341
+ if (t === "") return null;
342
+ const clock = /^(\d+):([0-5]?\d)(?::([0-5]?\d))?$/.exec(t);
343
+ if (clock) return Number(clock[1]) * 3600 + Number(clock[2]) * 60 + Number(clock[3] ?? 0);
344
+ if (/^\d+(\.\d+)?$/.test(t)) return Math.round(Number(t) * 60);
345
+ const unitRe = /(\d+(?:\.\d+)?)\s*(h|hr|hrs|hour|hours|m|min|mins|minute|minutes|s|sec|secs|second|seconds)\b/g;
346
+ let total = 0;
347
+ let matched = "";
348
+ for (const m of t.matchAll(unitRe)) {
349
+ const n = Number(m[1]);
350
+ const u = m[2];
351
+ total += u.startsWith("h") ? n * 3600 : u.startsWith("m") ? n * 60 : n;
352
+ matched += m[0];
353
+ }
354
+ if (matched === "" || t.replace(unitRe, "").replace(/[\s,and]+/g, "") !== "") return null;
355
+ return Math.round(total);
356
+ }
357
+ function addressFromText(text2) {
358
+ const t = text2.trim();
359
+ if (t === "") return null;
360
+ const bits = t.split(",").map((b) => b.trim()).filter(Boolean);
361
+ if (bits.length >= 3) {
362
+ const [street, city, regionPostal, ...rest] = bits;
363
+ const rp = /^(.*?)\s*([A-Za-z0-9-]*\d[A-Za-z0-9 -]*)?$/.exec(regionPostal ?? "");
364
+ const out = { street, city };
365
+ if (rp?.[1]) out.region = rp[1];
366
+ if (rp?.[2]) out.postal_code = rp[2].trim();
367
+ if (rest.length > 0) out.country = rest.join(", ");
368
+ return out;
369
+ }
370
+ return { street: t };
371
+ }
372
+
373
+ // src/typedDate.ts
374
+ var MONTHS = {
375
+ jan: 1,
376
+ january: 1,
377
+ feb: 2,
378
+ february: 2,
379
+ mar: 3,
380
+ march: 3,
381
+ apr: 4,
382
+ april: 4,
383
+ may: 5,
384
+ jun: 6,
385
+ june: 6,
386
+ jul: 7,
387
+ july: 7,
388
+ aug: 8,
389
+ august: 8,
390
+ sep: 9,
391
+ sept: 9,
392
+ september: 9,
393
+ oct: 10,
394
+ october: 10,
395
+ nov: 11,
396
+ november: 11,
397
+ dec: 12,
398
+ december: 12
399
+ };
400
+ var WEEKDAYS = /\b(mon|tue|tues|wed|thu|thur|thurs|fri|sat|sun)(day|nesday|sday|urday)?\b\.?,?/gi;
401
+ function localeDateOrder(locale) {
402
+ try {
403
+ const parts = new Intl.DateTimeFormat(locale, { year: "numeric", month: "2-digit", day: "2-digit" }).formatToParts(
404
+ new Date(2026, 10, 22)
405
+ );
406
+ const order = parts.filter((p) => p.type === "year" || p.type === "month" || p.type === "day").map((p) => p.type.charAt(0)).join("");
407
+ if (order === "mdy" || order === "dmy" || order === "ymd") return order;
408
+ } catch {
409
+ }
410
+ return "mdy";
411
+ }
412
+ function pad(n, width = 2) {
413
+ return String(n).padStart(width, "0");
414
+ }
415
+ function daysIn(year, month) {
416
+ return new Date(Date.UTC(year, month, 0)).getUTCDate();
417
+ }
418
+ var HOW = "Write it as 2026-10-03, or with the year in full the way your spreadsheet writes dates.";
419
+ function twoDigitYear(yy) {
420
+ return yy < 30 ? 2e3 + yy : 1900 + yy;
421
+ }
422
+ function checkDay(year, month, day, raw) {
423
+ if (year < 1e3 || year > 9999) return `\u201C${raw}\u201D has no four-digit year. ${HOW}`;
424
+ if (month < 1 || month > 12) return `\u201C${raw}\u201D names no month (month ${month}). ${HOW}`;
425
+ if (day < 1 || day > daysIn(year, month)) return `\u201C${raw}\u201D is not a day on the calendar. ${HOW}`;
426
+ return null;
427
+ }
428
+ var TIME_HOW = "Write it like 2:30 PM or 14:30.";
429
+ var MERIDIEM = String.raw`([ap])\.?\s*(?:m\.?)?`;
430
+ var CLOCK = new RegExp(String.raw`^(\d{1,2})(?::(\d{2})(?::(\d{2}))?)?\s*(?:${MERIDIEM})?$`, "i");
431
+ var COMPACT = new RegExp(String.raw`^(\d{3,4})\s*(?:${MERIDIEM})?$`, "i");
432
+ function readTypedTime(raw) {
433
+ const text2 = raw.trim();
434
+ if (text2 === "") return { ok: false, why: "Nothing was typed." };
435
+ let h;
436
+ let m;
437
+ let s = null;
438
+ let meridiem;
439
+ const clock = CLOCK.exec(text2);
440
+ const compact = clock ? null : COMPACT.exec(text2);
441
+ if (clock) {
442
+ h = Number(clock[1]);
443
+ meridiem = clock[4]?.toLowerCase();
444
+ if (clock[2] === void 0 && !meridiem) {
445
+ return { ok: false, why: `\u201C${raw}\u201D could be in the morning or the evening. ${TIME_HOW}` };
446
+ }
447
+ m = clock[2] === void 0 ? 0 : Number(clock[2]);
448
+ if (clock[3] !== void 0) s = Number(clock[3]);
449
+ } else if (compact) {
450
+ const digits = compact[1];
451
+ meridiem = compact[2]?.toLowerCase();
452
+ if (digits.length === 3 && !meridiem) {
453
+ return { ok: false, why: `\u201C${raw}\u201D could be in the morning or the evening. ${TIME_HOW}` };
454
+ }
455
+ h = Number(digits.slice(0, digits.length - 2));
456
+ m = Number(digits.slice(-2));
457
+ } else {
458
+ return { ok: false, why: `\u201C${raw}\u201D is not a time of day. ${TIME_HOW}` };
459
+ }
460
+ if (meridiem) {
461
+ if (h < 1 || h > 12) return { ok: false, why: `\u201C${raw}\u201D is not a time of day. ${TIME_HOW}` };
462
+ if (meridiem === "p" && h !== 12) h += 12;
463
+ if (meridiem === "a" && h === 12) h = 0;
464
+ }
465
+ if (h > 23 || m > 59 || s !== null && s > 59) return { ok: false, why: `\u201C${raw}\u201D is not a time of day. ${TIME_HOW}` };
466
+ return { ok: true, value: [h, m, ...s === null ? [] : [s]].map((n) => pad(n)).join(":") };
467
+ }
468
+ function splitTime(text2) {
469
+ const match = /(?:^|[\sT,]+)(?:at\s+)?(\d{1,4}(?::\d{2}){0,2}\s*(?:[ap]\.?\s*(?:m\.?)?)?)$/i.exec(text2);
470
+ if (!match) return { date: text2, time: null, bad: false };
471
+ const token = match[1];
472
+ if (!/:|[ap]\.?\s*(?:m\.?)?$/i.test(token)) return { date: text2, time: null, bad: false };
473
+ const read = readTypedTime(token);
474
+ if (!read.ok) return { date: text2, time: null, bad: true };
475
+ const [h, m, s] = read.value.split(":").map(Number);
476
+ return { date: text2.slice(0, match.index).replace(/[\s,]+$/, ""), time: { h, m, s: s ?? 0 }, bad: false };
477
+ }
478
+ function readTypedDate(raw, opts = {}) {
479
+ const text2 = raw.trim();
480
+ if (text2 === "") return { ok: false, why: "Nothing was typed." };
481
+ const iso = /^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})(?::(\d{2})(?:\.\d+)?)?(Z|[+-]\d{2}:?\d{2})$/i.exec(text2);
482
+ if (iso) {
483
+ const bad2 = checkDay(Number(iso[1]), Number(iso[2]), Number(iso[3]), raw);
484
+ if (bad2) return { ok: false, why: bad2 };
485
+ const at = new Date(Date.parse(text2));
486
+ if (Number.isNaN(at.getTime())) return { ok: false, why: `\u201C${raw}\u201D is not a moment this can read. ${HOW}` };
487
+ const day2 = `${at.getFullYear()}-${pad(at.getMonth() + 1)}-${pad(at.getDate())}`;
488
+ return { ok: true, day: day2, instant: opts.withTime ? at.toISOString() : null };
489
+ }
490
+ const split = splitTime(text2.replace(/(\d)T(\d)/, "$1 $2"));
491
+ if (split.bad) return { ok: false, why: `\u201C${raw}\u201D has a time of day that is not one. Write it as 14:30 or 2:30 PM.` };
492
+ const dateText = split.date.replace(WEEKDAYS, " ").replace(/\s+/g, " ").trim();
493
+ let year = null;
494
+ let month = null;
495
+ let day = null;
496
+ const numeric = /^(\d{1,4})[/.\-](\d{1,2})[/.\-](\d{1,4})$/.exec(dateText);
497
+ const compact = /^(\d{8}|\d{6})$/.exec(dateText);
498
+ if (compact) {
499
+ const digits = compact[1];
500
+ const order = localeDateOrder(opts.locale);
501
+ const first2 = Number(digits.slice(0, 2));
502
+ const mid2 = Number(digits.slice(2, 4));
503
+ if (digits.length === 6) {
504
+ year = twoDigitYear(Number(digits.slice(4)));
505
+ if (order === "dmy") {
506
+ day = first2;
507
+ month = mid2;
508
+ } else {
509
+ month = first2;
510
+ day = mid2;
511
+ }
512
+ } else {
513
+ const yearLast = Number(digits.slice(4));
514
+ const yearFirst = Number(digits.slice(0, 4));
515
+ const localeReading = () => order === "dmy" ? [yearLast, mid2, first2] : [yearLast, first2, mid2];
516
+ const [ly, lm, ld] = localeReading();
517
+ const isoReading = [yearFirst, Number(digits.slice(4, 6)), Number(digits.slice(6))];
518
+ const fits = (y, m, d) => checkDay(y, m, d, raw) === null;
519
+ const useIso = order === "ymd" || !fits(ly, lm, ld) && fits(...isoReading);
520
+ [year, month, day] = useIso ? isoReading : [ly, lm, ld];
521
+ }
522
+ } else if (numeric) {
523
+ const [a, b, c] = [numeric[1], numeric[2], numeric[3]];
524
+ if (a.length === 4) {
525
+ year = Number(a);
526
+ month = Number(b);
527
+ day = Number(c);
528
+ } else {
529
+ if (c.length !== 4 && c.length !== 2) {
530
+ return { ok: false, why: `\u201C${raw}\u201D has no four-digit year. ${HOW}` };
531
+ }
532
+ const order = localeDateOrder(opts.locale);
533
+ year = c.length === 2 ? twoDigitYear(Number(c)) : Number(c);
534
+ if (order === "dmy") {
535
+ day = Number(a);
536
+ month = Number(b);
537
+ } else {
538
+ month = Number(a);
539
+ day = Number(b);
540
+ }
541
+ }
542
+ } else {
543
+ const tokens = dateText.toLowerCase().replace(/,/g, " ").split(/[\s/.\-]+/).filter(Boolean);
544
+ const numbers = [];
545
+ for (const token of tokens) {
546
+ const word = token.replace(/\.$/, "");
547
+ if (/^\d+(st|nd|rd|th)?$/.test(word)) numbers.push(word.replace(/\D/g, ""));
548
+ else if (word in MONTHS) {
549
+ if (month !== null) return { ok: false, why: `\u201C${raw}\u201D names two months. ${HOW}` };
550
+ month = MONTHS[word];
551
+ } else if (word !== "of" && word !== "the") {
552
+ return { ok: false, why: `\u201C${raw}\u201D is not a date this can read. ${HOW}` };
553
+ }
554
+ }
555
+ if (month === null) return { ok: false, why: `\u201C${raw}\u201D is not a date this can read. ${HOW}` };
556
+ const years = numbers.filter((n) => n.length === 4);
557
+ const days = numbers.filter((n) => n.length <= 2);
558
+ if (years.length === 0) {
559
+ return {
560
+ ok: false,
561
+ why: numbers.some((n) => n.length === 3) ? `\u201C${raw}\u201D has no four-digit year. ${HOW}` : `\u201C${raw}\u201D has no year, and which year it means is a guess. Write the year too (${days[0] ?? "3"} ${Object.keys(MONTHS).find((k) => MONTHS[k] === month && k.length > 3) ?? "October"} 2026).`
562
+ };
563
+ }
564
+ if (years.length > 1 || days.length !== 1 || numbers.length !== 2) {
565
+ return { ok: false, why: `\u201C${raw}\u201D is not a date this can read. ${HOW}` };
566
+ }
567
+ year = Number(years[0]);
568
+ day = Number(days[0]);
569
+ }
570
+ const bad = checkDay(year, month, day, raw);
571
+ if (bad) return { ok: false, why: bad };
572
+ const dayText = `${pad(year, 4)}-${pad(month)}-${pad(day)}`;
573
+ if (!opts.withTime) return { ok: true, day: dayText, instant: null };
574
+ const t = split.time ?? { h: 0, m: 0, s: 0 };
575
+ const local = new Date(year, month - 1, day, t.h, t.m, t.s);
576
+ return { ok: true, day: dayText, instant: local.toISOString() };
577
+ }
578
+
579
+ // src/typedNumber.ts
580
+ function readTypedNumber(raw) {
581
+ const text2 = raw.trim();
582
+ if (text2 === "") return { ok: false, why: "Nothing was typed." };
583
+ const bracketed = /^\(.*\)$/.test(text2);
584
+ const inner = (bracketed ? text2.slice(1, -1) : text2).replace(/[\s,]/g, "");
585
+ const head = /^[^\d.]*/.exec(inner)?.[0] ?? "";
586
+ const bare = inner.slice(head.length).replace(/[^\d.eE+-]+$/, "");
587
+ const minus = (head.match(/[-−]/g) ?? []).length % 2 === 1;
588
+ const n = Number(bare);
589
+ if (bare === "" || Number.isNaN(n) || /[\d]/.test(head)) {
590
+ return {
591
+ ok: false,
592
+ why: `\u201C${raw}\u201D is not a number. Write digits, like 1,250.50 \u2014 a currency symbol, a percent sign, commas and a minus sign or brackets for a negative are all fine.`
593
+ };
594
+ }
595
+ return { ok: true, value: bracketed !== minus ? -n : n };
596
+ }
597
+
598
+ // src/coerce.ts
599
+ import { isUuidShape } from "@ai-matrx/kit/uuid";
600
+ function fieldKindFor(field, kernel) {
601
+ const format = (field.format ?? "").toLowerCase();
602
+ const config = field.config ?? {};
603
+ switch (field.type) {
604
+ case "boolean":
605
+ return "checkbox";
606
+ case "text":
607
+ if (format === "email") return "email";
608
+ if (format === "phone") return "phone";
609
+ if (format === "url") return "url";
610
+ if (format === "json" || config["json"] === true) return "json";
611
+ if (format === "long" || config["multiline"] === true) return "long_text";
612
+ return "text";
613
+ case "range": {
614
+ const kind = String(config["kind"] ?? "number");
615
+ if (kind === "date" || kind === "datetime") return "datetime";
616
+ if (format === "currency") return "currency";
617
+ if (format === "percent" || field.unit === "%") return "percent";
618
+ return "number";
619
+ }
620
+ case "list":
621
+ return field.multi ? "multi_select" : "select";
622
+ case "relation": {
623
+ if (isEntityReferenceConfig(config)) return "entity_reference";
624
+ const parity = String(config["parity"] ?? "");
625
+ if (parity === "attachment" || parity === "member") return parity;
626
+ if (kernel?.file && field.relation_target === kernel.file) return "attachment";
627
+ if (kernel?.person && field.relation_target === kernel.person) return "member";
628
+ return "relation";
629
+ }
630
+ case "formula": {
631
+ const via = config["via"];
632
+ if (via && config["agg"]) return "rollup";
633
+ if (via) return "lookup";
634
+ return "formula";
635
+ }
636
+ default:
637
+ return "text";
638
+ }
639
+ }
640
+ function relationNameKey(name) {
641
+ return name.trim().replace(/\s+/g, " ").toLowerCase();
642
+ }
643
+ function splitListCell(text2) {
644
+ return text2.split(/\s*[;,|]\s*/).map((w) => w.trim()).filter((w) => w.length > 0);
645
+ }
646
+ var MONEY_SYMBOLS = { $: "USD", "\u20AC": "EUR", "\xA3": "GBP", "\u20B9": "INR", "\u20A9": "KRW", "\u20AA": "ILS", "\u20BA": "TRY", "\u20BD": "RUB" };
647
+ function columnCurrency(unit) {
648
+ const u = (unit ?? "").trim();
649
+ if (u === "") return null;
650
+ if (MONEY_SYMBOLS[u]) return MONEY_SYMBOLS[u];
651
+ return /^[A-Za-z]{3}$/.test(u) ? u.toUpperCase() : null;
652
+ }
653
+ function foreignUnit(kind, text2, label, raw, unit, showsAsPercent) {
654
+ const hasPercent = /%/.test(text2);
655
+ const symbols = [...text2].filter((ch) => ch in MONEY_SYMBOLS);
656
+ if (kind === "percent" || showsAsPercent) {
657
+ if (symbols.length > 0) return `${label} holds a percent, and \u201C${raw}\u201D is an amount of money. Write it like 45%.`;
658
+ return null;
659
+ }
660
+ if (hasPercent) {
661
+ return kind === "currency" ? `${label} holds an amount of money, and \u201C${raw}\u201D is a percent. Write the amount, like $1,250.50.` : `${label} holds a number, and \u201C${raw}\u201D is a percent, which this column does not show. Write 45 or 0.45 \u2014 or make the column show as a percent.`;
662
+ }
663
+ if (kind === "currency") {
664
+ const own = columnCurrency(unit);
665
+ const typed = symbols.map((ch) => MONEY_SYMBOLS[ch]);
666
+ if (own && typed.some((code) => code !== own)) {
667
+ return `${label} holds an amount in ${own}, and \u201C${raw}\u201D is written in ${typed.find((code) => code !== own)}. Write the amount in ${own}.`;
668
+ }
669
+ }
670
+ return null;
671
+ }
672
+ function isEntityReferenceConfig(config) {
673
+ const allowed = config?.["allowed_types"];
674
+ return Array.isArray(allowed) && allowed.length > 0;
675
+ }
676
+ function entityReferenceKindsOf(field) {
677
+ const allowed = field.config?.["allowed_types"];
678
+ return Array.isArray(allowed) ? allowed.map((k) => String(k).trim().toLowerCase()).filter(Boolean) : [];
679
+ }
680
+ var TRUE_WORDS = /* @__PURE__ */ new Set(["true", "yes", "y", "1", "\u2713", "x", "checked", "on"]);
681
+ var FALSE_WORDS = /* @__PURE__ */ new Set(["false", "no", "n", "0", "", "unchecked", "off"]);
682
+ function coercionLabel(field) {
683
+ const label = (field.label ?? "").trim();
684
+ if (label !== "") return label;
685
+ const name = (field.name ?? "").trim();
686
+ if (name !== "") return name;
687
+ return field.key.replace(/[_-]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()).trim();
688
+ }
689
+ function optionsFromPublicChoices(field) {
690
+ const offered = field.public_choices;
691
+ if (!Array.isArray(offered) || offered.length === 0) return null;
692
+ const byWord = /* @__PURE__ */ new Map();
693
+ const labels = [];
694
+ for (const c of offered) {
695
+ const key = typeof c?.key === "string" ? c.key : null;
696
+ if (!key) continue;
697
+ const words = c.label;
698
+ const choiceLabel = typeof words === "string" && words.trim() !== "" ? words.trim() : key;
699
+ byWord.set(key.toLowerCase(), key);
700
+ byWord.set(choiceLabel.toLowerCase(), key);
701
+ labels.push(choiceLabel);
702
+ }
703
+ return labels.length > 0 ? { byWord, labels } : null;
704
+ }
705
+ function coerceTypedAnswer(field, raw, opts = {}) {
706
+ const label = opts.label ?? coercionLabel(field);
707
+ const text2 = raw.trim();
708
+ const kind = fieldKindFor(field, opts.kernel);
709
+ if (text2 === "" && kind !== "checkbox") return { value: null };
710
+ if (isButtonField(field)) return { refusal: `${label} is a button: it is pressed, and holds nothing to type.` };
711
+ const newer = newerKindOf(field);
712
+ if (newer === "rating") {
713
+ const max = ratingMaxOf(field);
714
+ const n = Number(text2);
715
+ if (Number.isInteger(n) && n >= 0 && n <= max) return { value: n };
716
+ return { refusal: `${label} is a whole number of stars from 0 to ${max}, and \u201C${raw}\u201D is not one.` };
717
+ }
718
+ if (newer === "duration") {
719
+ const seconds = readTypedDuration(text2);
720
+ if (seconds === null || seconds < 0) {
721
+ return { refusal: `${label} is a length of time, and \u201C${raw}\u201D is not one. Write it like 45 min, 1:30 or 1h 30m.` };
722
+ }
723
+ if (seconds > 315576e3) return { refusal: `${label} is longer than ten years, and a length of time here is at most ten years.` };
724
+ return { value: seconds };
725
+ }
726
+ if (newer === "address") return { value: addressFromText(text2) };
727
+ if (newer === "barcode") return { value: text2 };
728
+ switch (kind) {
729
+ case "checkbox": {
730
+ const word = text2.toLowerCase();
731
+ if (TRUE_WORDS.has(word)) return { value: true };
732
+ if (FALSE_WORDS.has(word)) return { value: word === "" ? null : false };
733
+ return {
734
+ refusal: `${label} is a tick box, so it takes yes or no \u2014 \u201C${raw}\u201D is neither. Write TRUE/FALSE, yes/no or 1/0.`
735
+ };
736
+ }
737
+ case "number":
738
+ case "currency":
739
+ case "percent": {
740
+ const read = readTypedNumber(text2);
741
+ if (!read.ok) {
742
+ return {
743
+ refusal: `${label} holds a number, and \u201C${raw}\u201D is not one. Write digits, like 1,250.50 \u2014 a currency symbol, a percent sign, commas and a minus sign or brackets for a negative are all fine.`
744
+ };
745
+ }
746
+ const foreign = foreignUnit(kind, text2, label, raw, field.unit, opts.showsAsPercent === true);
747
+ if (foreign) return { refusal: foreign };
748
+ return { value: read.value };
749
+ }
750
+ case "datetime": {
751
+ const wantsTime = String(field.config?.["kind"] ?? "date") === "datetime";
752
+ const read = readTypedDate(text2, { withTime: wantsTime, ...opts.locale ? { locale: opts.locale } : {} });
753
+ if (!read.ok) return { refusal: `${label} holds a date, and ${read.why}` };
754
+ return { value: wantsTime ? read.instant : read.day };
755
+ }
756
+ case "json": {
757
+ try {
758
+ return { value: JSON.parse(text2) };
759
+ } catch {
760
+ return {
761
+ refusal: `${label} holds a document, and \u201C${raw}\u201D is not valid JSON. Write the whole object, braces and all.`
762
+ };
763
+ }
764
+ }
765
+ case "select":
766
+ case "multi_select": {
767
+ const options = opts.options ?? optionsFromPublicChoices(field);
768
+ if (!options) {
769
+ return {
770
+ refusal: `${label} is a list and its options have not been read yet, so nothing can be matched to \u201C${raw}\u201D. Try again once the column's choices have loaded.`
771
+ };
772
+ }
773
+ const wanted = kind === "multi_select" ? text2.split(/[,;]/).map((w) => w.trim()).filter((w) => w.length > 0) : [text2];
774
+ if (wanted.length === 0) return { value: kind === "multi_select" ? [] : null };
775
+ const matched = [];
776
+ const unmatched = [];
777
+ for (const word of wanted) {
778
+ const hit = options.byWord.get(word.toLowerCase());
779
+ if (hit === void 0) unmatched.push(word);
780
+ else matched.push(hit);
781
+ }
782
+ if (unmatched.length > 0) {
783
+ const offered = options.labels.length > 0 ? options.labels.join(", ") : "it has none yet";
784
+ return {
785
+ refusal: `${label} does not have ${unmatched.map((w) => `\u201C${w}\u201D`).join(" or ")} among its options. It offers: ${offered}.`,
786
+ unmatched
787
+ };
788
+ }
789
+ return { value: kind === "multi_select" ? matched : matched[0] };
790
+ }
791
+ // AN IDENTITY IS NOT A WORD. Writing the word would put a string where an
792
+ // id belongs, and every filter, colour and rollup built on that column
793
+ // would stop agreeing with it. So it is refused and the person is told
794
+ // which control does hold it.
795
+ //
796
+ // BUT AN ID IS AN IDENTITY (RELATION-TARGETS, 2026-09-23). A Person or File
797
+ // column holds the kernel Person / File record, and the store itself now
798
+ // turns the id a writer actually holds — a member's user id, an uploaded
799
+ // file's id, or the record's own id — into that record at every write door
800
+ // (`custom.relation_kernel_record`), refusing anything else in its own
801
+ // words ("… names someone who is not a member of …"). So an import column
802
+ // of member ids, or an agent handing over the file it just uploaded, is a
803
+ // value and not a refusal; the store, not this mirror, decides whether the
804
+ // id is somebody here.
805
+ case "member":
806
+ if (isUuidShape(text2)) return { value: text2.toLowerCase() };
807
+ return {
808
+ refusal: `${label} names a person in this organization, so it is chosen rather than typed. Open it and pick them.`
809
+ };
810
+ case "attachment":
811
+ if (isUuidShape(text2)) return { value: text2.toLowerCase() };
812
+ return {
813
+ refusal: `${label} holds a file, which cannot arrive as typed-in text. Open it and attach the file.`
814
+ };
815
+ case "relation": {
816
+ const parts = field.multi ? splitListCell(text2) : [text2];
817
+ const ids = [];
818
+ const words = [];
819
+ const none = [];
820
+ const many = [];
821
+ for (const part of parts) {
822
+ if (isUuidShape(part)) {
823
+ if (!ids.includes(part.toLowerCase())) ids.push(part.toLowerCase());
824
+ continue;
825
+ }
826
+ if (!opts.relationNames) {
827
+ return {
828
+ refusal: `${label} points at another record, so it is chosen rather than typed. Open it and pick the record.`
829
+ };
830
+ }
831
+ const hits = opts.relationNames.byName.get(relationNameKey(part)) ?? [];
832
+ if (hits.length === 0) none.push(part);
833
+ else if (hits.length > 1) many.push(part);
834
+ else if (!ids.includes(hits[0].id)) {
835
+ ids.push(hits[0].id);
836
+ words.push(hits[0].words);
837
+ }
838
+ }
839
+ if (none.length > 0 || many.length > 0) {
840
+ const table = opts.relationNames?.tableName ?? "that table";
841
+ const said = many.length > 0 ? `${many.map((w) => `\u201C${w}\u201D`).join(", ")} names more than one record in ${table}.` : `${table} has no record called ${none.map((w) => `\u201C${w}\u201D`).join(" or ")}.`;
842
+ return { refusal: `${label}: ${said}`, unmatchedNames: none, ambiguousNames: many };
843
+ }
844
+ if (ids.length === 0) return { value: field.multi ? [] : null };
845
+ return { value: field.multi ? ids : ids[0], ...words.length > 0 ? { words: words.join(", ") } : {} };
846
+ }
847
+ // SC-R / P12. An entity reference names a platform thing by kind and id.
848
+ // An import or an agent may hand over `note:<id>` — or a bare id when the
849
+ // column allows exactly one kind; the store then decides whether the thing
850
+ // is there and whether the writer may open it. Words are chosen, not typed.
851
+ case "entity_reference": {
852
+ const allowed = entityReferenceKindsOf(field);
853
+ const [maybeToken, maybeId] = text2.includes(":") ? text2.split(/:(.*)/s, 2) : ["", text2];
854
+ const token = (maybeToken ?? "").trim().toLowerCase() || (allowed.length === 1 ? allowed[0] : "");
855
+ const id = (maybeId ?? "").trim();
856
+ if (token && isUuidShape(id)) {
857
+ const ref = { token, id: id.toLowerCase() };
858
+ return { value: field.multi ? [ref] : ref };
859
+ }
860
+ return {
861
+ refusal: `${label} points at something on the platform, so it is chosen rather than typed. Open it and pick it.`
862
+ };
863
+ }
864
+ case "formula":
865
+ case "lookup":
866
+ case "rollup":
867
+ return {
868
+ refusal: `${label} is worked out by the system, so nothing can be written into it.`
869
+ };
870
+ // A LINK AND AN EMAIL ARE READ, THE WAY AIRTABLE READS THEM (merged-grid review 2, A9).
871
+ // "company.com" is a link a person meant: it is kept as https://company.com. Words that
872
+ // cannot be one are refused NAMING THE KIND, WITH ONE EXAMPLE — never "not written the way
873
+ // this field expects". Any further format Rule on the column is still the store's to judge.
874
+ case "url": {
875
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(text2) || /^mailto:/i.test(text2)) return { value: text2 };
876
+ if (!/\s/.test(text2) && /^[^./@\s]+(\.[^./\s]+)+(\/.*)?$/.test(text2)) return { value: `https://${text2}` };
877
+ return { refusal: `${label} holds a link, and \u201C${raw}\u201D is not one. A link looks like company.com or https://company.com/page.` };
878
+ }
879
+ case "email": {
880
+ if (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(text2)) return { value: text2 };
881
+ return { refusal: `${label} holds an email address, and \u201C${raw}\u201D is not one. An email looks like name@company.com.` };
882
+ }
883
+ default:
884
+ return { value: raw };
885
+ }
886
+ }
887
+ function coerceTypedAnswers(fields, answers, opts = {}) {
888
+ const byKey = new Map(fields.map((f) => [f.key, f]));
889
+ const values = {};
890
+ const refusals = {};
891
+ const unknownKeys = [];
892
+ for (const [key, given] of Object.entries(answers)) {
893
+ const field = byKey.get(key);
894
+ if (!field) {
895
+ unknownKeys.push(key);
896
+ values[key] = given;
897
+ continue;
898
+ }
899
+ if (typeof given !== "string") {
900
+ values[key] = given;
901
+ continue;
902
+ }
903
+ const opt = {};
904
+ const choices = opts.options?.get(key);
905
+ if (choices) opt.options = choices;
906
+ const named = opts.relationNames?.get(key);
907
+ if (named) opt.relationNames = named;
908
+ if (opts.kernel) opt.kernel = opts.kernel;
909
+ const label = opts.labels?.[key];
910
+ if (label) opt.label = label;
911
+ const answered = coerceTypedAnswer(field, given, opt);
912
+ if ("refusal" in answered) refusals[key] = answered.refusal;
913
+ else values[key] = answered.value;
914
+ }
915
+ return { values, refusals, unknownKeys };
916
+ }
917
+
918
+ // src/forms/typedAnswers.ts
919
+ function typedAnswersFor(fields, values) {
920
+ if (!Array.isArray(fields) || fields.length === 0) {
921
+ return { values, refusal: null, byKey: {} };
922
+ }
923
+ const coerced = coerceTypedAnswers(fields, values);
924
+ const keys = Object.keys(coerced.refusals);
925
+ if (keys.length === 0) return { values: coerced.values, refusal: null, byKey: {} };
926
+ const order = new Map(fields.map((f, at) => [String(f.key ?? ""), at]));
927
+ keys.sort((a, b) => (order.get(a) ?? 0) - (order.get(b) ?? 0));
928
+ return {
929
+ values: coerced.values,
930
+ refusal: keys.map((k) => coerced.refusals[k]).join(" "),
931
+ byKey: coerced.refusals
932
+ };
933
+ }
934
+ export {
935
+ FORM_EMBEDS,
936
+ FORM_FLOWS,
937
+ FORM_STATE_WORDS,
938
+ answerRefusal,
939
+ endingDocument,
940
+ endingsFromDocument,
941
+ formDeclareArgs,
942
+ formEmbedSnippet,
943
+ formFromSummary,
944
+ formLookDocument,
945
+ formLookFromDocument,
946
+ formPresentation,
947
+ formRoutes,
948
+ hiddenFromLink,
949
+ notifyFromDocument,
950
+ pipeAnswers,
951
+ publicReadOf,
952
+ submissionStamp,
953
+ thankYouDocument,
954
+ thankYouFromDocument,
955
+ themeDocument,
956
+ themeFromDocument,
957
+ typedAnswersFor,
958
+ welcomeDocument,
959
+ welcomeFromDocument
960
+ };
961
+ //# sourceMappingURL=index.js.map