@fieldia/core 0.23.0 → 0.25.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 (221) hide show
  1. package/README.md +211 -3
  2. package/package.json +1 -1
  3. package/schema/page.schema.json +1051 -325
  4. package/src/index.d.mts +9 -2
  5. package/src/index.d.ts +9 -2
  6. package/src/index.js +31 -1
  7. package/src/index.js.map +1 -1
  8. package/src/index.mjs +12 -2
  9. package/src/index.mjs.map +1 -1
  10. package/src/lib/format/actions.d.mts +31 -1
  11. package/src/lib/format/actions.d.ts +31 -1
  12. package/src/lib/format/actions.js +1 -0
  13. package/src/lib/format/actions.js.map +1 -1
  14. package/src/lib/format/actions.mjs +1 -0
  15. package/src/lib/format/actions.mjs.map +1 -1
  16. package/src/lib/format/autofill-group.d.mts +12 -0
  17. package/src/lib/format/autofill-group.d.ts +12 -0
  18. package/src/lib/format/autofill-group.js +23 -0
  19. package/src/lib/format/autofill-group.js.map +1 -0
  20. package/src/lib/format/autofill-group.mjs +19 -0
  21. package/src/lib/format/autofill-group.mjs.map +1 -0
  22. package/src/lib/format/autofill.d.mts +14 -0
  23. package/src/lib/format/autofill.d.ts +14 -0
  24. package/src/lib/format/autofill.js +54 -0
  25. package/src/lib/format/autofill.js.map +1 -0
  26. package/src/lib/format/autofill.mjs +48 -0
  27. package/src/lib/format/autofill.mjs.map +1 -0
  28. package/src/lib/format/bulk-edit.d.mts +18 -0
  29. package/src/lib/format/bulk-edit.d.ts +18 -0
  30. package/src/lib/format/bulk-edit.js +26 -0
  31. package/src/lib/format/bulk-edit.js.map +1 -0
  32. package/src/lib/format/bulk-edit.mjs +21 -0
  33. package/src/lib/format/bulk-edit.mjs.map +1 -0
  34. package/src/lib/format/duplicate-kinds.d.mts +10 -0
  35. package/src/lib/format/duplicate-kinds.d.ts +10 -0
  36. package/src/lib/format/duplicate-kinds.js +14 -0
  37. package/src/lib/format/duplicate-kinds.js.map +1 -0
  38. package/src/lib/format/duplicate-kinds.mjs +10 -0
  39. package/src/lib/format/duplicate-kinds.mjs.map +1 -0
  40. package/src/lib/format/duplicates.d.mts +22 -0
  41. package/src/lib/format/duplicates.d.ts +22 -0
  42. package/src/lib/format/duplicates.js +24 -0
  43. package/src/lib/format/duplicates.js.map +1 -0
  44. package/src/lib/format/duplicates.mjs +21 -0
  45. package/src/lib/format/duplicates.mjs.map +1 -0
  46. package/src/lib/format/field.d.mts +291 -34
  47. package/src/lib/format/field.d.ts +291 -34
  48. package/src/lib/format/field.js +19 -4
  49. package/src/lib/format/field.js.map +1 -1
  50. package/src/lib/format/field.mjs +17 -2
  51. package/src/lib/format/field.mjs.map +1 -1
  52. package/src/lib/format/import-fields.d.mts +18 -0
  53. package/src/lib/format/import-fields.d.ts +18 -0
  54. package/src/lib/format/import-fields.js +26 -0
  55. package/src/lib/format/import-fields.js.map +1 -0
  56. package/src/lib/format/import-fields.mjs +21 -0
  57. package/src/lib/format/import-fields.mjs.map +1 -0
  58. package/src/lib/format/layout.d.mts +7357 -3127
  59. package/src/lib/format/layout.d.ts +7357 -3127
  60. package/src/lib/format/layout.js +108 -32
  61. package/src/lib/format/layout.js.map +1 -1
  62. package/src/lib/format/layout.mjs +107 -32
  63. package/src/lib/format/layout.mjs.map +1 -1
  64. package/src/lib/format/layout.types.d.mts +5 -2
  65. package/src/lib/format/layout.types.d.ts +5 -2
  66. package/src/lib/format/page.d.mts +1080 -106
  67. package/src/lib/format/page.d.ts +1080 -106
  68. package/src/lib/format/page.js +25 -0
  69. package/src/lib/format/page.js.map +1 -1
  70. package/src/lib/format/page.mjs +26 -1
  71. package/src/lib/format/page.mjs.map +1 -1
  72. package/src/lib/format/phone.d.mts +27 -0
  73. package/src/lib/format/phone.d.ts +27 -0
  74. package/src/lib/format/phone.js +43 -0
  75. package/src/lib/format/phone.js.map +1 -0
  76. package/src/lib/format/phone.mjs +39 -0
  77. package/src/lib/format/phone.mjs.map +1 -0
  78. package/src/lib/format/references.d.mts +39 -0
  79. package/src/lib/format/references.d.ts +39 -0
  80. package/src/lib/format/references.js +263 -20
  81. package/src/lib/format/references.js.map +1 -1
  82. package/src/lib/format/references.mjs +263 -20
  83. package/src/lib/format/references.mjs.map +1 -1
  84. package/src/lib/format/slots-options.d.mts +26 -0
  85. package/src/lib/format/slots-options.d.ts +26 -0
  86. package/src/lib/format/slots-options.js +35 -0
  87. package/src/lib/format/slots-options.js.map +1 -0
  88. package/src/lib/format/slots-options.mjs +31 -0
  89. package/src/lib/format/slots-options.mjs.map +1 -0
  90. package/src/lib/format/translate.js +8 -4
  91. package/src/lib/format/translate.js.map +1 -1
  92. package/src/lib/format/translate.mjs +8 -4
  93. package/src/lib/format/translate.mjs.map +1 -1
  94. package/src/lib/format/upgrade.d.mts +8 -3
  95. package/src/lib/format/upgrade.d.ts +8 -3
  96. package/src/lib/format/upgrade.js +8 -3
  97. package/src/lib/format/upgrade.js.map +1 -1
  98. package/src/lib/format/upgrade.mjs +8 -3
  99. package/src/lib/format/upgrade.mjs.map +1 -1
  100. package/src/lib/format/version.d.mts +2 -2
  101. package/src/lib/format/version.d.ts +2 -2
  102. package/src/lib/format/version.js +2 -2
  103. package/src/lib/format/version.js.map +1 -1
  104. package/src/lib/format/version.mjs +2 -2
  105. package/src/lib/format/version.mjs.map +1 -1
  106. package/src/lib/format/zoned.d.mts +8 -0
  107. package/src/lib/format/zoned.d.ts +8 -0
  108. package/src/lib/format/zoned.js +36 -0
  109. package/src/lib/format/zoned.js.map +1 -0
  110. package/src/lib/format/zoned.mjs +33 -0
  111. package/src/lib/format/zoned.mjs.map +1 -0
  112. package/src/lib/record/check.d.mts +6 -1
  113. package/src/lib/record/check.d.ts +6 -1
  114. package/src/lib/record/check.js +12 -4
  115. package/src/lib/record/check.js.map +1 -1
  116. package/src/lib/record/check.mjs +12 -4
  117. package/src/lib/record/check.mjs.map +1 -1
  118. package/src/lib/record/data-source.d.mts +309 -3
  119. package/src/lib/record/data-source.d.ts +309 -3
  120. package/src/lib/record/data-source.js +16 -0
  121. package/src/lib/record/data-source.js.map +1 -1
  122. package/src/lib/record/data-source.mjs +15 -0
  123. package/src/lib/record/data-source.mjs.map +1 -1
  124. package/src/lib/record/form.d.mts +116 -25
  125. package/src/lib/record/form.d.ts +116 -25
  126. package/src/lib/record/form.js +457 -78
  127. package/src/lib/record/form.js.map +1 -1
  128. package/src/lib/record/form.mjs +457 -78
  129. package/src/lib/record/form.mjs.map +1 -1
  130. package/src/lib/record/input-rules.d.mts +51 -9
  131. package/src/lib/record/input-rules.d.ts +51 -9
  132. package/src/lib/record/input-rules.js +119 -20
  133. package/src/lib/record/input-rules.js.map +1 -1
  134. package/src/lib/record/input-rules.mjs +118 -19
  135. package/src/lib/record/input-rules.mjs.map +1 -1
  136. package/src/lib/record/inputs.js +4 -2
  137. package/src/lib/record/inputs.js.map +1 -1
  138. package/src/lib/record/inputs.mjs +4 -2
  139. package/src/lib/record/inputs.mjs.map +1 -1
  140. package/src/lib/record/limits.d.mts +1 -1
  141. package/src/lib/record/limits.d.ts +1 -1
  142. package/src/lib/record/limits.js +4 -2
  143. package/src/lib/record/limits.js.map +1 -1
  144. package/src/lib/record/limits.mjs +4 -2
  145. package/src/lib/record/limits.mjs.map +1 -1
  146. package/src/lib/record/locales/ar.js +3 -2
  147. package/src/lib/record/locales/ar.js.map +1 -1
  148. package/src/lib/record/locales/ar.mjs +3 -2
  149. package/src/lib/record/locales/ar.mjs.map +1 -1
  150. package/src/lib/record/locales/de.js +3 -2
  151. package/src/lib/record/locales/de.js.map +1 -1
  152. package/src/lib/record/locales/de.mjs +3 -2
  153. package/src/lib/record/locales/de.mjs.map +1 -1
  154. package/src/lib/record/locales/fr.js +3 -2
  155. package/src/lib/record/locales/fr.js.map +1 -1
  156. package/src/lib/record/locales/fr.mjs +3 -2
  157. package/src/lib/record/locales/fr.mjs.map +1 -1
  158. package/src/lib/record/memory-data-source.d.mts +20 -2
  159. package/src/lib/record/memory-data-source.d.ts +20 -2
  160. package/src/lib/record/memory-data-source.js +153 -35
  161. package/src/lib/record/memory-data-source.js.map +1 -1
  162. package/src/lib/record/memory-data-source.mjs +154 -36
  163. package/src/lib/record/memory-data-source.mjs.map +1 -1
  164. package/src/lib/record/memory-duplicates.d.mts +19 -0
  165. package/src/lib/record/memory-duplicates.d.ts +19 -0
  166. package/src/lib/record/memory-duplicates.js +78 -0
  167. package/src/lib/record/memory-duplicates.js.map +1 -0
  168. package/src/lib/record/memory-duplicates.mjs +74 -0
  169. package/src/lib/record/memory-duplicates.mjs.map +1 -0
  170. package/src/lib/record/memory-schedules.d.mts +45 -0
  171. package/src/lib/record/memory-schedules.d.ts +45 -0
  172. package/src/lib/record/memory-schedules.js +81 -0
  173. package/src/lib/record/memory-schedules.js.map +1 -0
  174. package/src/lib/record/memory-schedules.mjs +78 -0
  175. package/src/lib/record/memory-schedules.mjs.map +1 -0
  176. package/src/lib/record/messages.d.mts +3 -1
  177. package/src/lib/record/messages.d.ts +3 -1
  178. package/src/lib/record/messages.js +3 -2
  179. package/src/lib/record/messages.js.map +1 -1
  180. package/src/lib/record/messages.mjs +3 -2
  181. package/src/lib/record/messages.mjs.map +1 -1
  182. package/src/lib/record/periods.d.mts +47 -0
  183. package/src/lib/record/periods.d.ts +47 -0
  184. package/src/lib/record/periods.js +86 -0
  185. package/src/lib/record/periods.js.map +1 -0
  186. package/src/lib/record/periods.mjs +79 -0
  187. package/src/lib/record/periods.mjs.map +1 -0
  188. package/src/lib/record/rules.js +5 -6
  189. package/src/lib/record/rules.js.map +1 -1
  190. package/src/lib/record/rules.mjs +5 -6
  191. package/src/lib/record/rules.mjs.map +1 -1
  192. package/src/lib/record/run.d.mts +75 -9
  193. package/src/lib/record/run.d.ts +75 -9
  194. package/src/lib/record/run.js +169 -26
  195. package/src/lib/record/run.js.map +1 -1
  196. package/src/lib/record/run.mjs +170 -27
  197. package/src/lib/record/run.mjs.map +1 -1
  198. package/src/lib/record/search.d.mts +13 -3
  199. package/src/lib/record/search.d.ts +13 -3
  200. package/src/lib/record/search.js +11 -4
  201. package/src/lib/record/search.js.map +1 -1
  202. package/src/lib/record/search.mjs +11 -4
  203. package/src/lib/record/search.mjs.map +1 -1
  204. package/src/lib/record/waits.d.mts +32 -0
  205. package/src/lib/record/waits.d.ts +32 -0
  206. package/src/lib/record/waits.js +89 -0
  207. package/src/lib/record/waits.js.map +1 -0
  208. package/src/lib/record/waits.mjs +85 -0
  209. package/src/lib/record/waits.mjs.map +1 -0
  210. package/src/lib/record/zones.d.mts +57 -0
  211. package/src/lib/record/zones.d.ts +57 -0
  212. package/src/lib/record/zones.js +147 -0
  213. package/src/lib/record/zones.js.map +1 -0
  214. package/src/lib/record/zones.mjs +132 -0
  215. package/src/lib/record/zones.mjs.map +1 -0
  216. package/src/runtime.d.mts +19 -9
  217. package/src/runtime.d.ts +19 -9
  218. package/src/runtime.js +37 -2
  219. package/src/runtime.js.map +1 -1
  220. package/src/runtime.mjs +9 -2
  221. package/src/runtime.mjs.map +1 -1
@@ -4,7 +4,8 @@ import type { Page } from '../format/page.mjs';
4
4
  import type { JsonValue } from '../format/json.mjs';
5
5
  import { type FieldTone, type LineState } from './cells.mjs';
6
6
  import { type Messages } from './messages.mjs';
7
- import { type DataSource, type RecordChanges, type RecordPermissions, type RecordVersion, type SaveProblem, type UploadFile } from './data-source.mjs';
7
+ import { type DataSource, type RecordChanges, type RecordPermissions, type RecordVersion, type ResolvedFilter, type SaveProblem, type BotReport, type SlotsResult, type UploadFile, type DuplicateMatch, type MergeResult } from './data-source.mjs';
8
+ import { type WaitsFor } from './waits.mjs';
8
9
  import { type FileUpload, type UploadHow } from './uploads.mjs';
9
10
  import { type Scheduler } from './scheduler.mjs';
10
11
  import { type ActionHost, type ActionRequest, type ActionResult, type OnAction, type PostedMessage, type RunContext, type RunResult } from './run.mjs';
@@ -61,11 +62,12 @@ export interface FormState {
61
62
  /**
62
63
  * Fields holding a change of the person's that the last finished save of
63
64
  * this record did not send: made while it was on its way, while the run that
64
- * saved went on (its afterSave run, a copy being made), or since. Worked out
65
- * from the record as that save left it, so a change back to it is none; and
66
- * before the record's first save, only a file the store holds and no save
67
- * sent. Never a value a step, a rule, a compute or the app set: each change
68
- * is told by its origin. The next record starts with none.
65
+ * saved went on (its afterSave run, a copy being made), or since — and text
66
+ * they left since in a box of it that does not read (`unread`), which no save
67
+ * can send. Worked out from the record as that save left it, so a change back
68
+ * to it is none; and before the record's first save, only a file the store
69
+ * holds and no save sent. Never a value a step, a rule, a compute or the app
70
+ * set: each change is told by its origin. The next record starts with none.
69
71
  */
70
72
  readonly unsent: readonly string[];
71
73
  /**
@@ -80,6 +82,15 @@ export interface FormState {
80
82
  */
81
83
  readonly unread: Readonly<Record<string, UnreadText>>;
82
84
  }
85
+ /** How a search for a link's records is asked (`form.search`, `form.searchLine`). */
86
+ export interface SearchOptions {
87
+ /** The model searched: a reference's, or the model structured data names records of. */
88
+ model?: string;
89
+ /** Those records alone, for their names. */
90
+ ids?: RecordId[];
91
+ /** Only the records named exactly the query, in any case (`SearchRequest.exact`): a typed name read as a record. */
92
+ exact?: boolean;
93
+ }
83
94
  /** Text left in a box that does not read, and why, as `FormState.unread` keeps it. */
84
95
  export interface UnreadText {
85
96
  readonly text: string;
@@ -110,12 +121,15 @@ export interface FormConflict {
110
121
  export declare function unsavedWork(state: FormState): boolean;
111
122
  /**
112
123
  * Whether a way out would lose the person's work without their knowing: a
113
- * change of theirs the last save did not send (`unsent`), files still on
114
- * their way, changes Reload set aside — and, `how.changes`, any change not
115
- * saved. What `form.leave()` — the one check every way out goes through —
116
- * asks about, once the save on its way and the files on theirs settled.
117
- * Before a record's first save, a change not saved is not at risk: as in
118
- * 0.20, each way out decides.
124
+ * change of theirs the last save did not send, or text they left since that
125
+ * does not read ("12a" counts as "12" does: `unsent`), files still on
126
+ * their way, changes Reload set aside, the changes of a save refused for a
127
+ * conflict, waiting for Reload or Keep mine (only a source that keeps
128
+ * versions has one) — and, `how.changes`, any change not saved. What
129
+ * `form.leave()` — the one check every way out goes through — asks about,
130
+ * once the save on its way and the files on theirs settled. Before a
131
+ * record's first save, a change not saved is not at risk: as in 0.20, each
132
+ * way out decides.
119
133
  */
120
134
  export declare function workAtRisk(state: FormState, how?: LeaveHow): boolean;
121
135
  /**
@@ -169,14 +183,15 @@ export interface FormEvents {
169
183
  values: Readonly<Values>;
170
184
  by: ChangeBy;
171
185
  };
172
- /** A record was saved. */
186
+ /** A record was saved: its values as the save left it — what it sent and what came back; a change made meanwhile is not among them. */
173
187
  save: {
174
188
  recordId: RecordId | null;
175
189
  values: Readonly<Values>;
176
190
  };
177
- /** A page of responses sent its answers: those sent. */
191
+ /** A page of responses sent its answers: those sent, and on a page with a bot check (`botCheck`) what it saw. */
178
192
  send: {
179
193
  values: Values;
194
+ bot?: BotReport;
180
195
  };
181
196
  /** The app answered a call: with nothing (undefined), or an `ActionResult`. */
182
197
  action: {
@@ -313,6 +328,14 @@ export interface Form {
313
328
  fieldReadonly(name: string): boolean;
314
329
  /** What a field would be told if the form were checked now, or null when it passes. Nothing is shown. */
315
330
  problem(field: string): string | null;
331
+ /**
332
+ * What a value worked out from others waits for while one of them holds text that does not read (`unread`): a
333
+ * total of the page's own (`compute`) over a table whose line says "abc" in its Quantity, or an amount the app
334
+ * works out as the lines change (read-only, from a source with an onchange) — the line, as people count them, and
335
+ * its cell. Shown, it is a dash said in words, as the table's own total waits: never an amount worked out from the
336
+ * value that text replaced. Null when it shows its value.
337
+ */
338
+ waitsFor(field: string): WaitsFor | null;
316
339
  /**
317
340
  * Keep text a person left in a box that does not read (`FormState.unread`), by its path — `field`, or a line's
318
341
  * `field.lineKey.subfield` — with why in words; null once it reads, or is let go. Nothing is shown until the box
@@ -341,6 +364,15 @@ export interface Form {
341
364
  * Resolves once every reading that waits (a name looked up) has ended.
342
365
  */
343
366
  settle(): Promise<void>;
367
+ /**
368
+ * Tell a public page's bot check (`page.botCheck`) what was seen: its hidden box filled (`honeypot`, the viewer's), or
369
+ * the captcha's token as the app's widget gives it (`captcha`; null once it expires). Each send carries them, with
370
+ * the time the answers took, as `bot` (`SubmitRequest`); a token goes once it is sent, as a captcha's is spent.
371
+ */
372
+ botCheck(seen: {
373
+ honeypot?: boolean;
374
+ captcha?: string | null;
375
+ }): void;
344
376
  /**
345
377
  * Whether a box's reading of what was typed still waits (a name being looked up): a check or a step forward made
346
378
  * now refuses its text with "still looking", so a caller that can wait for it (`settle`) checks again after.
@@ -390,6 +422,23 @@ export interface Form {
390
422
  * first, so a way out with nothing to wait for is not put off.
391
423
  */
392
424
  leaveNow(how?: LeaveHow): boolean;
425
+ /**
426
+ * The record shown now, for an answer asked for it that comes later: the
427
+ * function returned is true while that record is still the one shown, as it
428
+ * was — no other opened, it not loaded again, no new one started, the
429
+ * person's Discard not pressed, the form not closed. What writes on the
430
+ * form, or acts on its record, once a wait ends — a name looked up, a
431
+ * record made from a link, a line's dialog, a step's call to the source —
432
+ * asks it first, so an answer is laid only on the record it was asked for
433
+ * (docs/plans/wave1-contract.md, section 10, part 2).
434
+ */
435
+ recordShown(): () => boolean;
436
+ /**
437
+ * What a related list (a `related` part, by its id) asks the data source's `list` for, on the record shown: the
438
+ * records whose link is this record, and that its own `filter` lets through, each `valueFrom` read from this record.
439
+ * Null before the record's first save — nothing points at it yet — and for no related list of that id.
440
+ */
441
+ relatedFilter(id: string): ResolvedFilter[] | null;
393
442
  /**
394
443
  * Post words in the record's conversation, as a `post` step does: through
395
444
  * the form's host when it has a way to, else told to the form's own
@@ -414,17 +463,12 @@ export interface Form {
414
463
  * Records a many2one, many2many or reference may point to. A reference needs
415
464
  * `options.model`, and so does structured data naming records of a model (a
416
465
  * json field, such as an analytic distribution's accounts); `options.ids`
417
- * finds those records alone, for their names.
466
+ * finds those records alone, for their names; `options.exact`, those named
467
+ * exactly the query (`SearchRequest.exact`).
418
468
  */
419
- search(field: string, query: string, limit?: number, options?: {
420
- model?: string;
421
- ids?: RecordId[];
422
- }): Promise<RelatedRecord[]>;
469
+ search(field: string, query: string, limit?: number, options?: SearchOptions): Promise<RelatedRecord[]>;
423
470
  /** The same, for a relation inside a one2many line, filtered by that line's values — or structured data of a line naming records of a model (`options.model`). */
424
- searchLine(field: string, key: string, subfield: string, query: string, limit?: number, options?: {
425
- model?: string;
426
- ids?: RecordId[];
427
- }): Promise<RelatedRecord[]>;
471
+ searchLine(field: string, key: string, subfield: string, query: string, limit?: number, options?: SearchOptions): Promise<RelatedRecord[]>;
428
472
  /**
429
473
  * The values of records a many2many links to, for a table of them: `fields`
430
474
  * of the records with these ids, in the order asked, read with the data
@@ -460,7 +504,9 @@ export interface Form {
460
504
  /**
461
505
  * Press a button: ask its `confirm` first, run its `steps`, then call its
462
506
  * `action`, when it names one, as a final `call`. A list's button passes
463
- * the records chosen in it, and every call of the run carries them.
507
+ * the records chosen in it, and every call of the run carries them. What is
508
+ * typed is read before its first step that reads the form or hands its
509
+ * values on, as a save reads it: text that does not read stops the run.
464
510
  */
465
511
  runAction(id: string, chosen?: {
466
512
  recordIds: RecordId[];
@@ -485,7 +531,7 @@ export interface Form {
485
531
  * carrying the lines. Hidden, or with no line chosen, it does not run.
486
532
  */
487
533
  runLinesAction(nodeId: string, buttonId: string, keys: readonly string[]): Promise<RunResult>;
488
- /** Run steps on this form, as a button would: `context.id` is what their calls carry (`run` when left out). */
534
+ /** Run steps on this form, as a button would — what is typed read first: `context.id` is what their calls carry (`run` when left out). */
489
535
  run(steps: readonly ActionStep[], context?: RunContext): Promise<RunResult>;
490
536
  /** A tab was shown: the viewer says so, and the page's `show` steps for it run. */
491
537
  shown(tab: string): void;
@@ -499,6 +545,51 @@ export interface Form {
499
545
  * travels inside the record as base64, as it always has.
500
546
  */
501
547
  canUpload(): boolean;
548
+ /** Today, as 2026-10-15, on the clock of the page's time zone — the browser's own where it names none — by the form's clock (`now`). */
549
+ today(): string;
550
+ /**
551
+ * A record of the page's model as conditions read it, the form's own way: its values (a link as its id), its `id`,
552
+ * the person using the form (`user`), the app's `context`, and `editing` false — for what reads conditions on records
553
+ * the form does not hold, such as a list's rows (their `rowTones`, `rowBold` and `rowButtons`). Of another model — a
554
+ * related list's rows — with that model's `fields` (the related list's own).
555
+ */
556
+ recordReads(id: RecordId | null, values: Values, fields?: Record<string, Field | LineField>): Record<string, unknown>;
557
+ /** Whether a date and time drawn as booking slots can ask for its free times: the data source has `slots`. */
558
+ canFindSlots(): boolean;
559
+ /**
560
+ * The free times of a date and time drawn as booking slots, asked of the data source's `slots` for the days `from`
561
+ * to `to` — in the field's time zone: its own, its page's, or the browser's — with the record, its values and the
562
+ * zone filled in. Rejects where the source has no `slots`.
563
+ */
564
+ findSlots(field: string, ask: {
565
+ from: string;
566
+ to: string;
567
+ schedule?: string;
568
+ length?: number;
569
+ between?: number;
570
+ }): Promise<SlotsResult>;
571
+ /** Whether the page asks for possible duplicates: a record's page naming fields in `duplicates`, its source with `duplicates`. */
572
+ canFindDuplicates(): boolean;
573
+ /**
574
+ * Records like the one shown, asked of the data source's `duplicates` with the fields the page names that hold a
575
+ * value now — none, and nothing is asked — and every value of the form: the matches, the likest first where the
576
+ * source scores them, never the record itself. Rejects where the source has no `duplicates`. Its answer is for the
577
+ * record shown as it was asked (`recordShown`).
578
+ */
579
+ findDuplicates(): Promise<DuplicateMatch[]>;
580
+ /** Whether records of the page's model can be merged: its source has `merge`, and `load` to read the others. */
581
+ canMerge(): boolean;
582
+ /** Another record of the page's model, as its source loads it, its values alone: the form is left as it is. For a merge's screen. */
583
+ readRecord(id: RecordId): Promise<Values>;
584
+ /**
585
+ * Merge records of the page's model into one through the source's `merge`: `keepId` kept with `values`, `mergeIds`
586
+ * gone. The form is left as it is: show the record kept (`openRecord`, `load`) once it is done.
587
+ */
588
+ merge(request: {
589
+ keepId: RecordId;
590
+ mergeIds: RecordId[];
591
+ values: Values;
592
+ }): Promise<MergeResult | void>;
502
593
  /**
503
594
  * Store files picked for a file or picture field — or a field on a table's
504
595
  * line (`how.line`) — through the data source's `upload`, each as soon as
@@ -4,7 +4,8 @@ import type { Page } from '../format/page';
4
4
  import type { JsonValue } from '../format/json';
5
5
  import { type FieldTone, type LineState } from './cells';
6
6
  import { type Messages } from './messages';
7
- import { type DataSource, type RecordChanges, type RecordPermissions, type RecordVersion, type SaveProblem, type UploadFile } from './data-source';
7
+ import { type DataSource, type RecordChanges, type RecordPermissions, type RecordVersion, type ResolvedFilter, type SaveProblem, type BotReport, type SlotsResult, type UploadFile, type DuplicateMatch, type MergeResult } from './data-source';
8
+ import { type WaitsFor } from './waits';
8
9
  import { type FileUpload, type UploadHow } from './uploads';
9
10
  import { type Scheduler } from './scheduler';
10
11
  import { type ActionHost, type ActionRequest, type ActionResult, type OnAction, type PostedMessage, type RunContext, type RunResult } from './run';
@@ -61,11 +62,12 @@ export interface FormState {
61
62
  /**
62
63
  * Fields holding a change of the person's that the last finished save of
63
64
  * this record did not send: made while it was on its way, while the run that
64
- * saved went on (its afterSave run, a copy being made), or since. Worked out
65
- * from the record as that save left it, so a change back to it is none; and
66
- * before the record's first save, only a file the store holds and no save
67
- * sent. Never a value a step, a rule, a compute or the app set: each change
68
- * is told by its origin. The next record starts with none.
65
+ * saved went on (its afterSave run, a copy being made), or since — and text
66
+ * they left since in a box of it that does not read (`unread`), which no save
67
+ * can send. Worked out from the record as that save left it, so a change back
68
+ * to it is none; and before the record's first save, only a file the store
69
+ * holds and no save sent. Never a value a step, a rule, a compute or the app
70
+ * set: each change is told by its origin. The next record starts with none.
69
71
  */
70
72
  readonly unsent: readonly string[];
71
73
  /**
@@ -80,6 +82,15 @@ export interface FormState {
80
82
  */
81
83
  readonly unread: Readonly<Record<string, UnreadText>>;
82
84
  }
85
+ /** How a search for a link's records is asked (`form.search`, `form.searchLine`). */
86
+ export interface SearchOptions {
87
+ /** The model searched: a reference's, or the model structured data names records of. */
88
+ model?: string;
89
+ /** Those records alone, for their names. */
90
+ ids?: RecordId[];
91
+ /** Only the records named exactly the query, in any case (`SearchRequest.exact`): a typed name read as a record. */
92
+ exact?: boolean;
93
+ }
83
94
  /** Text left in a box that does not read, and why, as `FormState.unread` keeps it. */
84
95
  export interface UnreadText {
85
96
  readonly text: string;
@@ -110,12 +121,15 @@ export interface FormConflict {
110
121
  export declare function unsavedWork(state: FormState): boolean;
111
122
  /**
112
123
  * Whether a way out would lose the person's work without their knowing: a
113
- * change of theirs the last save did not send (`unsent`), files still on
114
- * their way, changes Reload set aside — and, `how.changes`, any change not
115
- * saved. What `form.leave()` — the one check every way out goes through —
116
- * asks about, once the save on its way and the files on theirs settled.
117
- * Before a record's first save, a change not saved is not at risk: as in
118
- * 0.20, each way out decides.
124
+ * change of theirs the last save did not send, or text they left since that
125
+ * does not read ("12a" counts as "12" does: `unsent`), files still on
126
+ * their way, changes Reload set aside, the changes of a save refused for a
127
+ * conflict, waiting for Reload or Keep mine (only a source that keeps
128
+ * versions has one) — and, `how.changes`, any change not saved. What
129
+ * `form.leave()` — the one check every way out goes through — asks about,
130
+ * once the save on its way and the files on theirs settled. Before a
131
+ * record's first save, a change not saved is not at risk: as in 0.20, each
132
+ * way out decides.
119
133
  */
120
134
  export declare function workAtRisk(state: FormState, how?: LeaveHow): boolean;
121
135
  /**
@@ -169,14 +183,15 @@ export interface FormEvents {
169
183
  values: Readonly<Values>;
170
184
  by: ChangeBy;
171
185
  };
172
- /** A record was saved. */
186
+ /** A record was saved: its values as the save left it — what it sent and what came back; a change made meanwhile is not among them. */
173
187
  save: {
174
188
  recordId: RecordId | null;
175
189
  values: Readonly<Values>;
176
190
  };
177
- /** A page of responses sent its answers: those sent. */
191
+ /** A page of responses sent its answers: those sent, and on a page with a bot check (`botCheck`) what it saw. */
178
192
  send: {
179
193
  values: Values;
194
+ bot?: BotReport;
180
195
  };
181
196
  /** The app answered a call: with nothing (undefined), or an `ActionResult`. */
182
197
  action: {
@@ -313,6 +328,14 @@ export interface Form {
313
328
  fieldReadonly(name: string): boolean;
314
329
  /** What a field would be told if the form were checked now, or null when it passes. Nothing is shown. */
315
330
  problem(field: string): string | null;
331
+ /**
332
+ * What a value worked out from others waits for while one of them holds text that does not read (`unread`): a
333
+ * total of the page's own (`compute`) over a table whose line says "abc" in its Quantity, or an amount the app
334
+ * works out as the lines change (read-only, from a source with an onchange) — the line, as people count them, and
335
+ * its cell. Shown, it is a dash said in words, as the table's own total waits: never an amount worked out from the
336
+ * value that text replaced. Null when it shows its value.
337
+ */
338
+ waitsFor(field: string): WaitsFor | null;
316
339
  /**
317
340
  * Keep text a person left in a box that does not read (`FormState.unread`), by its path — `field`, or a line's
318
341
  * `field.lineKey.subfield` — with why in words; null once it reads, or is let go. Nothing is shown until the box
@@ -341,6 +364,15 @@ export interface Form {
341
364
  * Resolves once every reading that waits (a name looked up) has ended.
342
365
  */
343
366
  settle(): Promise<void>;
367
+ /**
368
+ * Tell a public page's bot check (`page.botCheck`) what was seen: its hidden box filled (`honeypot`, the viewer's), or
369
+ * the captcha's token as the app's widget gives it (`captcha`; null once it expires). Each send carries them, with
370
+ * the time the answers took, as `bot` (`SubmitRequest`); a token goes once it is sent, as a captcha's is spent.
371
+ */
372
+ botCheck(seen: {
373
+ honeypot?: boolean;
374
+ captcha?: string | null;
375
+ }): void;
344
376
  /**
345
377
  * Whether a box's reading of what was typed still waits (a name being looked up): a check or a step forward made
346
378
  * now refuses its text with "still looking", so a caller that can wait for it (`settle`) checks again after.
@@ -390,6 +422,23 @@ export interface Form {
390
422
  * first, so a way out with nothing to wait for is not put off.
391
423
  */
392
424
  leaveNow(how?: LeaveHow): boolean;
425
+ /**
426
+ * The record shown now, for an answer asked for it that comes later: the
427
+ * function returned is true while that record is still the one shown, as it
428
+ * was — no other opened, it not loaded again, no new one started, the
429
+ * person's Discard not pressed, the form not closed. What writes on the
430
+ * form, or acts on its record, once a wait ends — a name looked up, a
431
+ * record made from a link, a line's dialog, a step's call to the source —
432
+ * asks it first, so an answer is laid only on the record it was asked for
433
+ * (docs/plans/wave1-contract.md, section 10, part 2).
434
+ */
435
+ recordShown(): () => boolean;
436
+ /**
437
+ * What a related list (a `related` part, by its id) asks the data source's `list` for, on the record shown: the
438
+ * records whose link is this record, and that its own `filter` lets through, each `valueFrom` read from this record.
439
+ * Null before the record's first save — nothing points at it yet — and for no related list of that id.
440
+ */
441
+ relatedFilter(id: string): ResolvedFilter[] | null;
393
442
  /**
394
443
  * Post words in the record's conversation, as a `post` step does: through
395
444
  * the form's host when it has a way to, else told to the form's own
@@ -414,17 +463,12 @@ export interface Form {
414
463
  * Records a many2one, many2many or reference may point to. A reference needs
415
464
  * `options.model`, and so does structured data naming records of a model (a
416
465
  * json field, such as an analytic distribution's accounts); `options.ids`
417
- * finds those records alone, for their names.
466
+ * finds those records alone, for their names; `options.exact`, those named
467
+ * exactly the query (`SearchRequest.exact`).
418
468
  */
419
- search(field: string, query: string, limit?: number, options?: {
420
- model?: string;
421
- ids?: RecordId[];
422
- }): Promise<RelatedRecord[]>;
469
+ search(field: string, query: string, limit?: number, options?: SearchOptions): Promise<RelatedRecord[]>;
423
470
  /** The same, for a relation inside a one2many line, filtered by that line's values — or structured data of a line naming records of a model (`options.model`). */
424
- searchLine(field: string, key: string, subfield: string, query: string, limit?: number, options?: {
425
- model?: string;
426
- ids?: RecordId[];
427
- }): Promise<RelatedRecord[]>;
471
+ searchLine(field: string, key: string, subfield: string, query: string, limit?: number, options?: SearchOptions): Promise<RelatedRecord[]>;
428
472
  /**
429
473
  * The values of records a many2many links to, for a table of them: `fields`
430
474
  * of the records with these ids, in the order asked, read with the data
@@ -460,7 +504,9 @@ export interface Form {
460
504
  /**
461
505
  * Press a button: ask its `confirm` first, run its `steps`, then call its
462
506
  * `action`, when it names one, as a final `call`. A list's button passes
463
- * the records chosen in it, and every call of the run carries them.
507
+ * the records chosen in it, and every call of the run carries them. What is
508
+ * typed is read before its first step that reads the form or hands its
509
+ * values on, as a save reads it: text that does not read stops the run.
464
510
  */
465
511
  runAction(id: string, chosen?: {
466
512
  recordIds: RecordId[];
@@ -485,7 +531,7 @@ export interface Form {
485
531
  * carrying the lines. Hidden, or with no line chosen, it does not run.
486
532
  */
487
533
  runLinesAction(nodeId: string, buttonId: string, keys: readonly string[]): Promise<RunResult>;
488
- /** Run steps on this form, as a button would: `context.id` is what their calls carry (`run` when left out). */
534
+ /** Run steps on this form, as a button would — what is typed read first: `context.id` is what their calls carry (`run` when left out). */
489
535
  run(steps: readonly ActionStep[], context?: RunContext): Promise<RunResult>;
490
536
  /** A tab was shown: the viewer says so, and the page's `show` steps for it run. */
491
537
  shown(tab: string): void;
@@ -499,6 +545,51 @@ export interface Form {
499
545
  * travels inside the record as base64, as it always has.
500
546
  */
501
547
  canUpload(): boolean;
548
+ /** Today, as 2026-10-15, on the clock of the page's time zone — the browser's own where it names none — by the form's clock (`now`). */
549
+ today(): string;
550
+ /**
551
+ * A record of the page's model as conditions read it, the form's own way: its values (a link as its id), its `id`,
552
+ * the person using the form (`user`), the app's `context`, and `editing` false — for what reads conditions on records
553
+ * the form does not hold, such as a list's rows (their `rowTones`, `rowBold` and `rowButtons`). Of another model — a
554
+ * related list's rows — with that model's `fields` (the related list's own).
555
+ */
556
+ recordReads(id: RecordId | null, values: Values, fields?: Record<string, Field | LineField>): Record<string, unknown>;
557
+ /** Whether a date and time drawn as booking slots can ask for its free times: the data source has `slots`. */
558
+ canFindSlots(): boolean;
559
+ /**
560
+ * The free times of a date and time drawn as booking slots, asked of the data source's `slots` for the days `from`
561
+ * to `to` — in the field's time zone: its own, its page's, or the browser's — with the record, its values and the
562
+ * zone filled in. Rejects where the source has no `slots`.
563
+ */
564
+ findSlots(field: string, ask: {
565
+ from: string;
566
+ to: string;
567
+ schedule?: string;
568
+ length?: number;
569
+ between?: number;
570
+ }): Promise<SlotsResult>;
571
+ /** Whether the page asks for possible duplicates: a record's page naming fields in `duplicates`, its source with `duplicates`. */
572
+ canFindDuplicates(): boolean;
573
+ /**
574
+ * Records like the one shown, asked of the data source's `duplicates` with the fields the page names that hold a
575
+ * value now — none, and nothing is asked — and every value of the form: the matches, the likest first where the
576
+ * source scores them, never the record itself. Rejects where the source has no `duplicates`. Its answer is for the
577
+ * record shown as it was asked (`recordShown`).
578
+ */
579
+ findDuplicates(): Promise<DuplicateMatch[]>;
580
+ /** Whether records of the page's model can be merged: its source has `merge`, and `load` to read the others. */
581
+ canMerge(): boolean;
582
+ /** Another record of the page's model, as its source loads it, its values alone: the form is left as it is. For a merge's screen. */
583
+ readRecord(id: RecordId): Promise<Values>;
584
+ /**
585
+ * Merge records of the page's model into one through the source's `merge`: `keepId` kept with `values`, `mergeIds`
586
+ * gone. The form is left as it is: show the record kept (`openRecord`, `load`) once it is done.
587
+ */
588
+ merge(request: {
589
+ keepId: RecordId;
590
+ mergeIds: RecordId[];
591
+ values: Values;
592
+ }): Promise<MergeResult | void>;
502
593
  /**
503
594
  * Store files picked for a file or picture field — or a field on a table's
504
595
  * line (`how.line`) — through the data source's `upload`, each as soon as