@aglyn/aglyn 1.0.0-beta.219 → 1.0.0-beta.221

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 (188) hide show
  1. package/package.json +12 -12
  2. package/src/lib/app-utils/actions.d.ts +1 -59
  3. package/src/lib/app-utils/actions.js +7 -72
  4. package/src/lib/app-utils/actions.js.map +1 -1
  5. package/src/lib/app-utils/consent-groups.d.ts +1 -1
  6. package/src/lib/app-utils/consent-groups.js.map +1 -1
  7. package/src/lib/app-utils/console-routes.d.ts +0 -36
  8. package/src/lib/app-utils/console-routes.js +2 -37
  9. package/src/lib/app-utils/console-routes.js.map +1 -1
  10. package/src/lib/app-utils/contacts.d.ts +1 -1
  11. package/src/lib/app-utils/contacts.js.map +1 -1
  12. package/src/lib/app-utils/container-membership.d.ts +93 -0
  13. package/src/lib/app-utils/container-membership.js +165 -0
  14. package/src/lib/app-utils/container-membership.js.map +1 -0
  15. package/src/lib/app-utils/crm-task-reminders.d.ts +23 -6
  16. package/src/lib/app-utils/crm-task-reminders.js +32 -11
  17. package/src/lib/app-utils/crm-task-reminders.js.map +1 -1
  18. package/src/lib/app-utils/crm.d.ts +24 -4
  19. package/src/lib/app-utils/crm.js +23 -0
  20. package/src/lib/app-utils/crm.js.map +1 -1
  21. package/src/lib/app-utils/csv-upload.d.ts +1 -1
  22. package/src/lib/app-utils/csv-upload.js +1 -1
  23. package/src/lib/app-utils/csv-upload.js.map +1 -1
  24. package/src/lib/app-utils/{dataset-csv.d.ts → csv.d.ts} +10 -43
  25. package/src/lib/app-utils/{dataset-csv.js → csv.js} +10 -81
  26. package/src/lib/app-utils/csv.js.map +1 -0
  27. package/src/lib/app-utils/datasets.d.ts +0 -19
  28. package/src/lib/app-utils/datasets.js +0 -13
  29. package/src/lib/app-utils/datasets.js.map +1 -1
  30. package/src/lib/app-utils/docs-help.generated.d.ts +4 -4
  31. package/src/lib/app-utils/docs-help.generated.js +5 -0
  32. package/src/lib/app-utils/docs-help.generated.js.map +1 -1
  33. package/src/lib/app-utils/docs-index.generated.js +71 -36
  34. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  35. package/src/lib/app-utils/dynamic-list-rule.js +3 -3
  36. package/src/lib/app-utils/dynamic-list-rule.js.map +1 -1
  37. package/src/lib/app-utils/email-text.d.ts +80 -0
  38. package/src/lib/app-utils/email-text.js +189 -0
  39. package/src/lib/app-utils/email-text.js.map +1 -0
  40. package/src/lib/app-utils/{list-assignment-policy.d.ts → enrollment-basis.d.ts} +4 -0
  41. package/src/lib/app-utils/{list-assignment-policy.js → enrollment-basis.js} +5 -1
  42. package/src/lib/app-utils/enrollment-basis.js.map +1 -0
  43. package/src/lib/app-utils/expand-repeatables.js +4 -4
  44. package/src/lib/app-utils/expand-repeatables.js.map +1 -1
  45. package/src/lib/app-utils/forms.js +2 -2
  46. package/src/lib/app-utils/forms.js.map +1 -1
  47. package/src/lib/app-utils/health-report.d.ts +2 -3
  48. package/src/lib/app-utils/health-report.js +30 -6
  49. package/src/lib/app-utils/health-report.js.map +1 -1
  50. package/src/lib/app-utils/inbound-mail-domain.d.ts +27 -0
  51. package/src/lib/app-utils/inbound-mail-domain.js +31 -0
  52. package/src/lib/app-utils/inbound-mail-domain.js.map +1 -0
  53. package/src/lib/app-utils/leaving-notice.d.ts +96 -0
  54. package/src/lib/app-utils/leaving-notice.js +157 -0
  55. package/src/lib/app-utils/leaving-notice.js.map +1 -0
  56. package/src/lib/app-utils/lockdown.d.ts +11 -0
  57. package/src/lib/app-utils/lockdown.js +33 -3
  58. package/src/lib/app-utils/lockdown.js.map +1 -1
  59. package/src/lib/app-utils/member-email-aliases.d.ts +25 -0
  60. package/src/lib/app-utils/member-email-aliases.js +29 -0
  61. package/src/lib/app-utils/member-email-aliases.js.map +1 -1
  62. package/src/lib/app-utils/notifications.d.ts +69 -0
  63. package/src/lib/app-utils/notifications.js +119 -0
  64. package/src/lib/app-utils/notifications.js.map +1 -1
  65. package/src/lib/app-utils/operator-alerts.d.ts +15 -1
  66. package/src/lib/app-utils/operator-alerts.js +64 -10
  67. package/src/lib/app-utils/operator-alerts.js.map +1 -1
  68. package/src/lib/app-utils/plan-entitlements.d.ts +164 -178
  69. package/src/lib/app-utils/plan-entitlements.js +200 -224
  70. package/src/lib/app-utils/plan-entitlements.js.map +1 -1
  71. package/src/lib/app-utils/platform-marketing-consent.d.ts +56 -2
  72. package/src/lib/app-utils/platform-marketing-consent.js +6 -5
  73. package/src/lib/app-utils/platform-marketing-consent.js.map +1 -1
  74. package/src/lib/app-utils/plugin-starter-templates.generated.d.ts +12 -0
  75. package/src/lib/app-utils/plugin-starter-templates.generated.js +711 -0
  76. package/src/lib/app-utils/plugin-starter-templates.generated.js.map +1 -0
  77. package/src/lib/app-utils/repeat-query.d.ts +56 -0
  78. package/src/lib/app-utils/{dataset-query.js → repeat-query.js} +16 -16
  79. package/src/lib/app-utils/repeat-query.js.map +1 -0
  80. package/src/lib/app-utils/screen-link-context-value.d.ts +7 -0
  81. package/src/lib/app-utils/screen-link-context-value.js.map +1 -1
  82. package/src/lib/app-utils/screen-link-context.js +14 -2
  83. package/src/lib/app-utils/screen-link-context.js.map +1 -1
  84. package/src/lib/app-utils/screen-route.d.ts +3 -0
  85. package/src/lib/app-utils/screen-route.js +6 -1
  86. package/src/lib/app-utils/screen-route.js.map +1 -1
  87. package/src/lib/app-utils/server.d.ts +7 -9
  88. package/src/lib/app-utils/server.js +18 -26
  89. package/src/lib/app-utils/server.js.map +1 -1
  90. package/src/lib/app-utils/starter-template-nodes.d.ts +137 -0
  91. package/src/lib/app-utils/starter-template-nodes.js +129 -0
  92. package/src/lib/app-utils/starter-template-nodes.js.map +1 -0
  93. package/src/lib/app-utils/starter-templates.d.ts +11 -34
  94. package/src/lib/app-utils/starter-templates.js +19 -230
  95. package/src/lib/app-utils/starter-templates.js.map +1 -1
  96. package/src/lib/app-utils/{email-topics.d.ts → subscription-topics.d.ts} +64 -48
  97. package/src/lib/app-utils/{email-topics.js → subscription-topics.js} +39 -109
  98. package/src/lib/app-utils/subscription-topics.js.map +1 -0
  99. package/src/lib/app-utils/usage-budget.d.ts +73 -188
  100. package/src/lib/app-utils/usage-budget.js +58 -194
  101. package/src/lib/app-utils/usage-budget.js.map +1 -1
  102. package/src/lib/app-utils/utm-attribution.d.ts +1 -1
  103. package/src/lib/app-utils/utm-attribution.js +1 -1
  104. package/src/lib/app-utils/utm-attribution.js.map +1 -1
  105. package/src/lib/app-utils/{campaign-touch.d.ts → utm-touch.d.ts} +21 -13
  106. package/src/lib/app-utils/{campaign-touch.js → utm-touch.js} +30 -22
  107. package/src/lib/app-utils/utm-touch.js.map +1 -0
  108. package/src/lib/app-utils/variables.d.ts +9 -9
  109. package/src/lib/app-utils/variables.js.map +1 -1
  110. package/src/lib/app-utils/visitor-record-ceiling.d.ts +32 -0
  111. package/src/lib/app-utils/visitor-record-ceiling.js +39 -0
  112. package/src/lib/app-utils/visitor-record-ceiling.js.map +1 -1
  113. package/src/lib/foundation/definitions/org-billing.types.d.ts +2 -2
  114. package/src/lib/foundation/definitions/org-billing.types.js.map +1 -1
  115. package/src/lib/plugin-manager/feature-plugins.d.ts +80 -79
  116. package/src/lib/plugin-manager/feature-plugins.js +22 -35
  117. package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
  118. package/src/lib/plugin-manager/first-party-plugins.generated.d.ts +18 -1
  119. package/src/lib/plugin-manager/first-party-plugins.generated.js +62 -2
  120. package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
  121. package/src/lib/plugin-manager/index.d.ts +0 -1
  122. package/src/lib/plugin-manager/index.js +0 -1
  123. package/src/lib/plugin-manager/index.js.map +1 -1
  124. package/src/lib/plugin-manager/plugin-contact-capture.d.ts +9 -4
  125. package/src/lib/plugin-manager/plugin-contact-capture.js.map +1 -1
  126. package/src/lib/plugin-manager/plugin-containers.d.ts +68 -0
  127. package/src/lib/plugin-manager/plugin-containers.generated.d.ts +10 -0
  128. package/src/lib/plugin-manager/plugin-containers.generated.js +20 -0
  129. package/src/lib/plugin-manager/plugin-containers.generated.js.map +1 -0
  130. package/src/lib/plugin-manager/plugin-containers.js +27 -0
  131. package/src/lib/plugin-manager/plugin-containers.js.map +1 -0
  132. package/src/lib/plugin-manager/plugin-entitlements.d.ts +8 -0
  133. package/src/lib/plugin-manager/plugin-entitlements.js.map +1 -1
  134. package/src/lib/plugin-manager/plugin-membership-detach.js +1 -1
  135. package/src/lib/plugin-manager/plugin-membership-detach.js.map +1 -1
  136. package/src/lib/plugin-manager/plugin-record-email-state.d.ts +69 -0
  137. package/src/lib/plugin-manager/plugin-record-email-state.js +32 -0
  138. package/src/lib/plugin-manager/plugin-record-email-state.js.map +1 -1
  139. package/src/lib/plugin-manager/plugin-record-lists.d.ts +115 -0
  140. package/src/lib/plugin-manager/plugin-record-lists.js +75 -0
  141. package/src/lib/plugin-manager/plugin-record-lists.js.map +1 -0
  142. package/src/lib/plugin-manager/plugin-record-timeline.d.ts +42 -8
  143. package/src/lib/plugin-manager/plugin-record-timeline.js.map +1 -1
  144. package/src/lib/plugin-manager/plugin-server-steps.d.ts +170 -0
  145. package/src/lib/plugin-manager/plugin-server-steps.js +116 -0
  146. package/src/lib/plugin-manager/plugin-server-steps.js.map +1 -0
  147. package/src/lib/plugin-manager/plugin-site-beacons.d.ts +71 -0
  148. package/src/lib/plugin-manager/plugin-site-beacons.js +54 -0
  149. package/src/lib/plugin-manager/plugin-site-beacons.js.map +1 -0
  150. package/src/lib/plugin-manager/plugin-subscription-topics.generated.d.ts +11 -0
  151. package/src/lib/plugin-manager/plugin-subscription-topics.generated.js +41 -0
  152. package/src/lib/plugin-manager/plugin-subscription-topics.generated.js.map +1 -0
  153. package/src/lib/plugin-manager/plugin-usage-axes.d.ts +212 -0
  154. package/src/lib/plugin-manager/plugin-usage-axes.js +82 -3
  155. package/src/lib/plugin-manager/plugin-usage-axes.js.map +1 -1
  156. package/src/lib/plugin-manager/plugin-usage-meters.d.ts +108 -0
  157. package/src/lib/plugin-manager/plugin-usage-meters.js +105 -0
  158. package/src/lib/plugin-manager/plugin-usage-meters.js.map +1 -0
  159. package/src/lib/plugin-manager/usage-alert-contributors.d.ts +76 -6
  160. package/src/lib/plugin-manager/usage-alert-contributors.js +23 -10
  161. package/src/lib/plugin-manager/usage-alert-contributors.js.map +1 -1
  162. package/src/lib/app-utils/assist-credits.d.ts +0 -482
  163. package/src/lib/app-utils/assist-credits.js +0 -510
  164. package/src/lib/app-utils/assist-credits.js.map +0 -1
  165. package/src/lib/app-utils/campaign-membership.d.ts +0 -121
  166. package/src/lib/app-utils/campaign-membership.js +0 -189
  167. package/src/lib/app-utils/campaign-membership.js.map +0 -1
  168. package/src/lib/app-utils/campaign-touch.js.map +0 -1
  169. package/src/lib/app-utils/console-record-links.d.ts +0 -84
  170. package/src/lib/app-utils/console-record-links.js +0 -76
  171. package/src/lib/app-utils/console-record-links.js.map +0 -1
  172. package/src/lib/app-utils/crm-booking.d.ts +0 -75
  173. package/src/lib/app-utils/crm-booking.js +0 -133
  174. package/src/lib/app-utils/crm-booking.js.map +0 -1
  175. package/src/lib/app-utils/crm-inbound.d.ts +0 -245
  176. package/src/lib/app-utils/crm-inbound.js +0 -407
  177. package/src/lib/app-utils/crm-inbound.js.map +0 -1
  178. package/src/lib/app-utils/crm-reports.d.ts +0 -479
  179. package/src/lib/app-utils/crm-reports.js +0 -680
  180. package/src/lib/app-utils/crm-reports.js.map +0 -1
  181. package/src/lib/app-utils/dataset-csv.js.map +0 -1
  182. package/src/lib/app-utils/dataset-query.d.ts +0 -57
  183. package/src/lib/app-utils/dataset-query.js.map +0 -1
  184. package/src/lib/app-utils/email-topics.js.map +0 -1
  185. package/src/lib/app-utils/list-assignment-policy.js.map +0 -1
  186. package/src/lib/plugin-manager/plugin-metered-lines.d.ts +0 -97
  187. package/src/lib/plugin-manager/plugin-metered-lines.js +0 -123
  188. package/src/lib/plugin-manager/plugin-metered-lines.js.map +0 -1
@@ -17,7 +17,7 @@
17
17
  /**
18
18
  * The most characters one uploaded CSV may carry, whoever reads it.
19
19
  *
20
- * A file a person uploads is parsed in one pass by `parseCsv` (`dataset-csv.ts`), in the
20
+ * A file a person uploads is parsed in one pass by `parseCsv` (`csv.ts`), in the
21
21
  * browser that picked it and again on the server that imports it. This bounds
22
22
  * that pass: it is checked before the parse rather than after, because the
23
23
  * parse is what the ceiling is protecting — a refusal answered on the byte
@@ -16,7 +16,7 @@
16
16
  */ /**
17
17
  * The most characters one uploaded CSV may carry, whoever reads it.
18
18
  *
19
- * A file a person uploads is parsed in one pass by `parseCsv` (`dataset-csv.ts`), in the
19
+ * A file a person uploads is parsed in one pass by `parseCsv` (`csv.ts`), in the
20
20
  * browser that picked it and again on the server that imports it. This bounds
21
21
  * that pass: it is checked before the parse rather than after, because the
22
22
  * parse is what the ceiling is protecting — a refusal answered on the byte
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/csv-upload.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The most characters one uploaded CSV may carry, whoever reads it.\n *\n * A file a person uploads is parsed in one pass by `parseCsv` (`dataset-csv.ts`), in the\n * browser that picked it and again on the server that imports it. This bounds\n * that pass: it is checked before the parse rather than after, because the\n * parse is what the ceiling is protecting — a refusal answered on the byte\n * count costs nothing, where discovering the file was too big by\n * materializing all of its cells has already spent the memory the limit\n * exists to bound. Every importer that reads a CSV in one go shares it, so a\n * file one importer accepts is never one another refuses for its size.\n */\nexport const CSV_UPLOAD_MAX_CHARACTERS = 8_000_000\n"],"names":["CSV_UPLOAD_MAX_CHARACTERS"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;CAWC,GACD,OAAO,MAAMA,4BAA4B,QAAS"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/csv-upload.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The most characters one uploaded CSV may carry, whoever reads it.\n *\n * A file a person uploads is parsed in one pass by `parseCsv` (`csv.ts`), in the\n * browser that picked it and again on the server that imports it. This bounds\n * that pass: it is checked before the parse rather than after, because the\n * parse is what the ceiling is protecting — a refusal answered on the byte\n * count costs nothing, where discovering the file was too big by\n * materializing all of its cells has already spent the memory the limit\n * exists to bound. Every importer that reads a CSV in one go shares it, so a\n * file one importer accepts is never one another refuses for its size.\n */\nexport const CSV_UPLOAD_MAX_CHARACTERS = 8_000_000\n"],"names":["CSV_UPLOAD_MAX_CHARACTERS"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;CAWC,GACD,OAAO,MAAMA,4BAA4B,QAAS"}
@@ -15,49 +15,18 @@
15
15
  * limitations under the License.
16
16
  */
17
17
  /**
18
- * Dataset CSV/JSON serialization (AGL-182). The predictable inverse of
19
- * `coerceDocumentValues`: ISO dates, `lat, lon` coordinates, comma-joined
20
- * lists, JSON maps, reference ids — so an export re-imports losslessly
21
- * through the same coercion.
18
+ * CSV, read and written the one way the platform does it (AGL-2335).
22
19
  *
23
- * **Why this lives in core rather than in `@aglyn/plugins-data`** (AGL-2335).
24
- * AGL-413 moved dataset io out to the plugin, and the dataset-shaped half of
25
- * the import machinery — column mapping against a `DatasetModel` — is still
26
- * there, correctly: nothing outside that plugin maps a file onto a dataset.
27
- *
28
- * Writing one is different now. `/api/orgs/datasets/export` streams a
29
- * dataset from the server, and `scope:app` may not import a lib tagged
30
- * `aglyn:addons` — apps reach plugins only through the generated loader
31
- * manifests (AGL-417/419). The choice was a second copy of the escaper in
32
- * the console or one copy somewhere both may reach. There are already five
33
- * divergent hand-rolled CSV escapers in this repo; this is not becoming the
34
- * sixth. `dataset-io.ts` re-exports every name below, so the plugin's own
35
- * imports are unchanged.
36
- *
37
- * READING one is shared for the same reason. The email list importer parses
38
- * a merchant's contact file, and it lives in `@aglyn/plugins-email`, which
39
- * may not import `@aglyn/plugins-data` either. So {@link parseCsv} sits here
40
- * beside the escaper that is its inverse, and `dataset-io.ts` re-exports it
41
- * exactly as it re-exports the writing half.
42
- */
43
- import type { DatasetFieldDefinition, DatasetModel } from './dataset-models';
44
- /** Storage value → portable string (CSV cell / JSON value). */
45
- export declare function serializeDatasetValue(field: DatasetFieldDefinition, value: unknown): string;
46
- /**
47
- * The header line — field ids, in model order (AGL-2335).
48
- *
49
- * Split out of {@link datasetRecordsToCsv} so a server export can emit the
50
- * file a page at a time. The whole point of that export is that it is not
51
- * bounded by what one process can hold in memory, and a helper that only
52
- * takes a complete `rows` array cannot serve it. Everything below shares
53
- * one escaper: there are already five divergent hand-rolled CSV escapers in
54
- * this repo and this is not becoming the sixth.
20
+ * There were five divergent hand-rolled CSV escapers in this repo; this is
21
+ * the one escaper and the one parser every plugin and app reaches — the data
22
+ * plugin's dataset export and import, the email list import, the commerce
23
+ * catalog import, the CRM's contact import — and the check a download runs
24
+ * against the row count its server promised. Nothing here knows what the rows
25
+ * are; a plugin that writes its own records into a file formats each value
26
+ * itself and escapes it here.
55
27
  */
56
- export declare function datasetCsvHeader(model: DatasetModel): string;
57
- /** One record's values (storage form) → one CSV line, no terminator. */
58
- export declare function datasetCsvRow(model: DatasetModel, row: Record<string, unknown>): string;
59
- /** One record's values → the portable JSON object the JSON export emits. */
60
- export declare function datasetRecordToJson(model: DatasetModel, row: Record<string, unknown>): Record<string, string>;
28
+ /** One cell, quoted when it has to be: a comma, a quote or a line break. */
29
+ export declare const escapeCsvCell: (cell: string) => string;
61
30
  /**
62
31
  * Data rows in a CSV document — the header excluded, quoted newlines not
63
32
  * counted as row breaks (AGL-2335).
@@ -93,8 +62,6 @@ export declare function exportShortfall(promisedHeader: string | null | undefine
93
62
  received: number;
94
63
  short: boolean;
95
64
  };
96
- /** Rows (storage-form value maps) → CSV with a fieldId header row. */
97
- export declare function datasetRecordsToCsv(model: DatasetModel, rows: Array<Record<string, unknown>>): string;
98
65
  /**
99
66
  * Minimal RFC-4180 CSV parser (quoted fields, escaped quotes, CRLF).
100
67
  *
@@ -14,81 +14,16 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */ /**
17
- * Dataset CSV/JSON serialization (AGL-182). The predictable inverse of
18
- * `coerceDocumentValues`: ISO dates, `lat, lon` coordinates, comma-joined
19
- * lists, JSON maps, reference ids — so an export re-imports losslessly
20
- * through the same coercion.
17
+ * CSV, read and written the one way the platform does it (AGL-2335).
21
18
  *
22
- * **Why this lives in core rather than in `@aglyn/plugins-data`** (AGL-2335).
23
- * AGL-413 moved dataset io out to the plugin, and the dataset-shaped half of
24
- * the import machinery — column mapping against a `DatasetModel` — is still
25
- * there, correctly: nothing outside that plugin maps a file onto a dataset.
26
- *
27
- * Writing one is different now. `/api/orgs/datasets/export` streams a
28
- * dataset from the server, and `scope:app` may not import a lib tagged
29
- * `aglyn:addons` — apps reach plugins only through the generated loader
30
- * manifests (AGL-417/419). The choice was a second copy of the escaper in
31
- * the console or one copy somewhere both may reach. There are already five
32
- * divergent hand-rolled CSV escapers in this repo; this is not becoming the
33
- * sixth. `dataset-io.ts` re-exports every name below, so the plugin's own
34
- * imports are unchanged.
35
- *
36
- * READING one is shared for the same reason. The email list importer parses
37
- * a merchant's contact file, and it lives in `@aglyn/plugins-email`, which
38
- * may not import `@aglyn/plugins-data` either. So {@link parseCsv} sits here
39
- * beside the escaper that is its inverse, and `dataset-io.ts` re-exports it
40
- * exactly as it re-exports the writing half.
41
- */ /** Storage value → portable string (CSV cell / JSON value). */ export function serializeDatasetValue(field, value) {
42
- if (value == null) return '';
43
- switch(field.type){
44
- case 'timestamp':
45
- return typeof value === 'number' && Number.isFinite(value) ? new Date(value).toISOString() : String(value);
46
- case 'coordinates':
47
- {
48
- const coordinates = value;
49
- return coordinates && typeof coordinates.latitude === 'number' && typeof coordinates.longitude === 'number' ? `${coordinates.latitude}, ${coordinates.longitude}` : String(value);
50
- }
51
- case 'sorted':
52
- case 'reference':
53
- return Array.isArray(value) ? value.join(', ') : String(value);
54
- case 'map':
55
- try {
56
- return JSON.stringify(value);
57
- } catch (unused) {
58
- return String(value);
59
- }
60
- default:
61
- return String(value);
62
- }
63
- }
64
- const csvEscape = (cell)=>/[",\r\n]/.test(cell) ? `"${cell.replace(/"/g, '""')}"` : cell;
65
- /**
66
- * The header line — field ids, in model order (AGL-2335).
67
- *
68
- * Split out of {@link datasetRecordsToCsv} so a server export can emit the
69
- * file a page at a time. The whole point of that export is that it is not
70
- * bounded by what one process can hold in memory, and a helper that only
71
- * takes a complete `rows` array cannot serve it. Everything below shares
72
- * one escaper: there are already five divergent hand-rolled CSV escapers in
73
- * this repo and this is not becoming the sixth.
74
- */ export function datasetCsvHeader(model) {
75
- return model.order.map(csvEscape).join(',');
76
- }
77
- /** One record's values (storage form) → one CSV line, no terminator. */ export function datasetCsvRow(model, row) {
78
- return model.order.map((fieldId)=>{
79
- const field = model.fields[fieldId];
80
- return csvEscape(field ? serializeDatasetValue(field, row[fieldId]) : '');
81
- }).join(',');
82
- }
83
- /** One record's values → the portable JSON object the JSON export emits. */ export function datasetRecordToJson(model, row) {
84
- return Object.fromEntries(model.order.map((fieldId)=>{
85
- const field = model.fields[fieldId];
86
- return [
87
- fieldId,
88
- field ? serializeDatasetValue(field, row[fieldId]) : ''
89
- ];
90
- }));
91
- }
19
+ * There were five divergent hand-rolled CSV escapers in this repo; this is
20
+ * the one escaper and the one parser every plugin and app reaches — the data
21
+ * plugin's dataset export and import, the email list import, the commerce
22
+ * catalog import, the CRM's contact import — and the check a download runs
23
+ * against the row count its server promised. Nothing here knows what the rows
24
+ * are; a plugin that writes its own records into a file formats each value
25
+ * itself and escapes it here.
26
+ */ /** One cell, quoted when it has to be: a comma, a quote or a line break. */ export const escapeCsvCell = (cell)=>/[",\r\n]/.test(cell) ? `"${cell.replace(/"/g, '""')}"` : cell;
92
27
  /**
93
28
  * Data rows in a CSV document — the header excluded, quoted newlines not
94
29
  * counted as row breaks (AGL-2335).
@@ -173,12 +108,6 @@ const csvEscape = (cell)=>/[",\r\n]/.test(cell) ? `"${cell.replace(/"/g, '""')}"
173
108
  short: Number.isFinite(promised) && promisedHeader != null && received < promised
174
109
  };
175
110
  }
176
- /** Rows (storage-form value maps) → CSV with a fieldId header row. */ export function datasetRecordsToCsv(model, rows) {
177
- return [
178
- datasetCsvHeader(model),
179
- ...rows.map((row)=>datasetCsvRow(model, row))
180
- ].join('\n');
181
- }
182
111
  /**
183
112
  * Minimal RFC-4180 CSV parser (quoted fields, escaped quotes, CRLF).
184
113
  *
@@ -232,4 +161,4 @@ const csvEscape = (cell)=>/[",\r\n]/.test(cell) ? `"${cell.replace(/"/g, '""')}"
232
161
  return rows.filter((cells)=>cells.some((value)=>value.trim() !== ''));
233
162
  }
234
163
 
235
- //# sourceMappingURL=dataset-csv.js.map
164
+ //# sourceMappingURL=csv.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/csv.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * CSV, read and written the one way the platform does it (AGL-2335).\n *\n * There were five divergent hand-rolled CSV escapers in this repo; this is\n * the one escaper and the one parser every plugin and app reaches — the data\n * plugin's dataset export and import, the email list import, the commerce\n * catalog import, the CRM's contact import — and the check a download runs\n * against the row count its server promised. Nothing here knows what the rows\n * are; a plugin that writes its own records into a file formats each value\n * itself and escapes it here.\n */\n\n/** One cell, quoted when it has to be: a comma, a quote or a line break. */\nexport const escapeCsvCell = (cell: string): string =>\n /[\",\\r\\n]/.test(cell) ? `\"${cell.replace(/\"/g, '\"\"')}\"` : cell\n\n/**\n * Data rows in a CSV document — the header excluded, quoted newlines not\n * counted as row breaks (AGL-2335).\n *\n * This exists so a download can be CHECKED against the row count the server\n * promised, rather than trusted. A truncated export is the defect this whole\n * area is about, and a stream that dies halfway produces a perfectly\n * well-formed shorter file: nothing about the bytes says they are short.\n *\n * Single pass, no array building — {@link parseCsv} answers the same\n * question but materializes every cell, which is the wrong trade when the\n * file may be hundreds of thousands of rows and the only thing wanted is\n * how many there are.\n */\nexport function countCsvDataRows(text: string): number {\n const source = String(text ?? '')\n if (!source) return 0\n let rows = 0\n let quoted = false\n let cellsSeen = false\n for (let index = 0; index < source.length; index += 1) {\n const char = source[index]\n if (quoted) {\n if (char === '\"') {\n if (source[index + 1] === '\"') index += 1\n else quoted = false\n }\n cellsSeen = true\n continue\n }\n if (char === '\"') {\n quoted = true\n cellsSeen = true\n continue\n }\n if (char === '\\n') {\n rows += 1\n cellsSeen = false\n continue\n }\n if (char !== '\\r') cellsSeen = true\n }\n // A file that does not end in a newline still has a final row.\n if (cellsSeen) rows += 1\n // The header is not data.\n return Math.max(0, rows - 1)\n}\n\n/**\n * Did a downloaded export arrive whole? (AGL-2335)\n *\n * The server streams and reports its `count()` aggregate in a header, so the\n * client can CHECK rather than trust. This is the check, kept here as a pure\n * function because a guard living inline in a component is a guard nobody\n * can force red — and this one exists precisely to catch a silent shortfall.\n *\n * `short` is only true when the count is both KNOWN and lower. A missing or\n * unparseable header is not evidence of truncation, and refusing a download\n * on the strength of a header that never arrived would fail the user for the\n * server's bookkeeping. More rows than promised is not short either — the\n * count is a snapshot taken before the first page, so a concurrent insert\n * can legitimately overtake it.\n */\nexport function exportShortfall(\n promisedHeader: string | null | undefined,\n body: string,\n format: 'csv' | 'json',\n): { promised: number; received: number; short: boolean } {\n const promised = Number(promisedHeader)\n let received: number\n if (format === 'csv') {\n received = countCsvDataRows(body)\n } else {\n try {\n const parsed = JSON.parse(body) as unknown\n received = Array.isArray(parsed) ? parsed.length : 0\n } catch {\n // A body that is not JSON at all did not arrive whole.\n return { promised, received: 0, short: true }\n }\n }\n return {\n promised,\n received,\n short: Number.isFinite(promised) && promisedHeader != null && received < promised,\n }\n}\n\n/**\n * Minimal RFC-4180 CSV parser (quoted fields, escaped quotes, CRLF).\n *\n * The inverse of the escaper above, and the one parser in the repo — see the\n * module note for why the email list importer reaches this rather than\n * writing its own.\n *\n * A wholly blank row is dropped. Every spreadsheet writes a trailing newline\n * and a row of empty cells is not a record; carrying it through would give\n * every caller the same off-by-one to remember.\n */\nexport function parseCsv(text: string): string[][] {\n const rows: string[][] = []\n let row: string[] = []\n let cell = ''\n let quoted = false\n const source = String(text ?? '')\n for (let index = 0; index < source.length; index += 1) {\n const char = source[index]\n if (quoted) {\n if (char === '\"') {\n if (source[index + 1] === '\"') {\n cell += '\"'\n index += 1\n } else {\n quoted = false\n }\n } else {\n cell += char\n }\n continue\n }\n if (char === '\"') {\n quoted = true\n } else if (char === ',') {\n row.push(cell)\n cell = ''\n } else if (char === '\\n' || char === '\\r') {\n if (char === '\\r' && source[index + 1] === '\\n') index += 1\n row.push(cell)\n cell = ''\n rows.push(row)\n row = []\n } else {\n cell += char\n }\n }\n if (cell !== '' || row.length) {\n row.push(cell)\n rows.push(row)\n }\n return rows.filter((cells) => cells.some((value) => value.trim() !== ''))\n}\n"],"names":["escapeCsvCell","cell","test","replace","countCsvDataRows","text","source","String","rows","quoted","cellsSeen","index","length","char","Math","max","exportShortfall","promisedHeader","body","format","promised","Number","received","parsed","JSON","parse","Array","isArray","short","isFinite","parseCsv","row","push","filter","cells","some","value","trim"],"mappings":"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"}
@@ -167,24 +167,5 @@ export declare function datasetDisplayName(dataset: {
167
167
  displayName?: unknown;
168
168
  name?: unknown;
169
169
  } | null | undefined): string;
170
- /** One entry in a dataset select: stored by id, shown by name. */
171
- export interface DatasetPickerOption {
172
- id: string;
173
- name: string;
174
- }
175
- /**
176
- * The options a dataset select offers, sorted by the label shown.
177
- *
178
- * Deleted datasets are left out. Each is labeled by
179
- * {@link datasetDisplayName}; one whose document carries no name at all is
180
- * offered under its id, because a select that hides it leaves a dataset that
181
- * cannot be picked.
182
- */
183
- export declare function datasetPickerOptions(docs: ReadonlyArray<{
184
- $id?: string;
185
- deletedAt?: unknown;
186
- displayName?: unknown;
187
- name?: unknown;
188
- }> | null | undefined): DatasetPickerOption[];
189
170
  /** Records sorted by their editor order (then id for stability). */
190
171
  export declare function sortDatasetRecords<T extends HostDatasetRecord>(records: T[]): T[];
@@ -215,19 +215,6 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
215
215
  }
216
216
  return '';
217
217
  }
218
- /**
219
- * The options a dataset select offers, sorted by the label shown.
220
- *
221
- * Deleted datasets are left out. Each is labeled by
222
- * {@link datasetDisplayName}; one whose document carries no name at all is
223
- * offered under its id, because a select that hides it leaves a dataset that
224
- * cannot be picked.
225
- */ export function datasetPickerOptions(docs) {
226
- return (docs != null ? docs : []).filter((dataset)=>!dataset.deletedAt && dataset.$id).map((dataset)=>({
227
- id: dataset.$id,
228
- name: datasetDisplayName(dataset) || dataset.$id
229
- })).sort((a, b)=>a.name.localeCompare(b.name));
230
- }
231
218
  /** Records sorted by their editor order (then id for stability). */ export function sortDatasetRecords(records) {
232
219
  return [
233
220
  ...records
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/datasets.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Datasets (AGL-102): tabular data at `orgs/{orgId}/datasets/{id}` with a\n * `records` subcollection, edited in the console Data card and consumed by\n * repeatable components (AGL-103) via `{{item.field}}` bindings.\n */\n\nimport {\n coerceDocumentValues,\n type DatasetModel,\n effectiveDatasetModel,\n validateDocument,\n} from './dataset-models'\n\n/** Dataset field name: starts with a letter; letters/digits/underscores. */\nexport const DATASET_FIELD_PATTERN = /^[A-Za-z][A-Za-z0-9_]*$/\n\nexport interface HostDataset {\n $id?: string\n displayName?: string\n /** Column names, in display order (v1; superseded by `model`). */\n fields?: string[]\n /** Typed model (AGL-177); when absent, derive from `fields`. */\n model?: import('./dataset-models').DatasetModel\n /** dod.ts Schema.Name shape (AGL-178). */\n names?: { singular?: string; plural?: string }\n /**\n * Which sites may see this dataset (AGL-1037). Absent = org-wide, until\n * the AGL-1040 backfill stamps it. See `app-utils/scope-tokens`.\n */\n visibleTo?: import('../foundation').ScopeToken[]\n}\n\nexport interface HostDatasetRecord {\n $id?: string\n /**\n * Field → value, in storage form per the field's type (see\n * `dataset-models`). Rows written by a form or an automation step before\n * AGL-2773 hold every value as text; readers accept both.\n */\n values?: Record<string, unknown>\n /** Row position in the editor and in repeated output. */\n order?: number\n}\n\n/**\n * A human field entry from the quick creator: the stable id plus the\n * display name shown in table headers and bindings pickers (AGL-558).\n */\nexport interface DatasetFieldEntry {\n id: string\n name: string\n}\n\n/**\n * Stable field id from a human name: \"Roast preference\" → \"roast_preference\".\n * Mirrors DATASET_FIELD_PATTERN; returns '' when nothing salvageable.\n */\nexport function slugifyDatasetFieldId(name: string): string {\n const slug = name\n .trim()\n .toLowerCase()\n .replace(/[\\s-]+/g, '_')\n .replace(/[^a-z0-9_]/g, '')\n .replace(/^[0-9_]+/, '')\n .replace(/_+$/, '')\n return DATASET_FIELD_PATTERN.test(slug) ? slug : ''\n}\n\n/**\n * Default reference id for a new field (AGL-578): the `slugifyDatasetFieldId`\n * slug, suffixed (`_2`, `_3`, …) to stay unique within the dataset. `taken`\n * holds the ids already in use (compared case-insensitively). Returns '' when\n * the name yields nothing valid. The user can override the result.\n */\nexport function defaultDatasetFieldId(\n name: string,\n taken: ReadonlySet<string>,\n): string {\n const base = slugifyDatasetFieldId(name)\n if (!base) return ''\n const used = new Set([...taken].map((id) => id.toLowerCase()))\n let candidate = base\n let suffix = 2\n while (used.has(candidate.toLowerCase())) candidate = `${base}_${suffix++}`\n return candidate\n}\n\n/**\n * Validate a user-entered reference id (AGL-578): non-empty, matches\n * `DATASET_FIELD_PATTERN`, and unique within the dataset (`taken` = ids already\n * used, compared case-insensitively). Returns an error message, or null when\n * the id is usable.\n */\nexport function validateDatasetFieldId(\n id: string,\n taken: ReadonlySet<string>,\n): string | null {\n const trimmed = id.trim()\n if (!trimmed) return 'A reference ID is required'\n if (!DATASET_FIELD_PATTERN.test(trimmed))\n return 'Start with a letter; use only letters, numbers, and underscores'\n const used = new Set([...taken].map((existing) => existing.toLowerCase()))\n if (used.has(trimmed.toLowerCase()))\n return 'Another field already uses this reference ID'\n return null\n}\n\n/** Display fallback for raw ids: \"roast_preference\" → \"Roast preference\". */\nexport function humanizeDatasetFieldId(id: string): string {\n const words = id.replace(/_/g, ' ').trim()\n return words ? words.charAt(0).toUpperCase() + words.slice(1) : id\n}\n\n/**\n * Parses a comma/newline separated list of HUMAN field names into\n * {id, name} entries — \"Roast preference\" keeps its pretty name and gets\n * the stable id `roast_preference` (AGL-558). Plain snake_case keys\n * still work and pick up a humanized display name. Duplicate ids and\n * unsalvageable entries are dropped rather than failing the set.\n */\nexport function parseDatasetFieldEntries(input: string): DatasetFieldEntry[] {\n const seen = new Set<string>()\n const entries: DatasetFieldEntry[] = []\n for (const raw of input.split(/[,\\n]/)) {\n const trimmed = raw.trim()\n if (!trimmed) continue\n const id = slugifyDatasetFieldId(trimmed)\n if (!id || seen.has(id)) continue\n seen.add(id)\n entries.push({\n id,\n name: DATASET_FIELD_PATTERN.test(trimmed)\n ? humanizeDatasetFieldId(trimmed)\n : trimmed,\n })\n }\n return entries\n}\n\n/**\n * Parses a comma/newline separated field list into valid unique names;\n * invalid entries are dropped rather than failing the whole set.\n */\nexport function parseDatasetFields(input: string): string[] {\n const seen = new Set<string>()\n const fields: string[] = []\n for (const raw of input.split(/[,\\n]/)) {\n const name = raw.trim()\n if (!DATASET_FIELD_PATTERN.test(name)) continue\n const key = name.toLowerCase()\n if (seen.has(key)) continue\n seen.add(key)\n fields.push(name)\n }\n return fields\n}\n\n/** Record values restricted to the dataset's declared fields. */\nexport function sanitizeRecordValues(\n fields: string[],\n input: Record<string, unknown> | null | undefined,\n): Record<string, string> {\n const values: Record<string, string> = {}\n for (const field of fields) {\n const value = input?.[field]\n if (value == null) continue\n values[field] = String(value)\n }\n return values\n}\n\n/**\n * The submitted values a dataset write would store, picked by field id and\n * left as they arrived (AGL-556). With a `fieldMap` (submitted key → stable\n * model fieldId), mapped values land under the mapped fieldId — so renamed\n * schema fields keep receiving data — and entries whose fieldId isn't in the\n * model are dropped. Submitted keys without a mapping (and calls without a\n * map) fall back to the legacy name-intersection against the model's field\n * ids, matching `sanitizeRecordValues`.\n */\nexport function pickDatasetRecordInput(\n dataset: { model?: DatasetModel; fields?: string[] },\n input: Record<string, unknown> | null | undefined,\n fieldMap?: Record<string, string> | null,\n): Record<string, unknown> {\n const model = effectiveDatasetModel(dataset)\n const map = fieldMap ?? {}\n const values: Record<string, unknown> = {}\n for (const fieldId of model.order) {\n if (!model.fields[fieldId]) continue\n // A submitted key with an explicit mapping never doubles as a\n // name-match — the mapping decides where it lands.\n if (fieldId in map) continue\n const value = input?.[fieldId]\n if (value == null) continue\n values[fieldId] = value\n }\n for (const [submittedKey, fieldId] of Object.entries(map)) {\n if (typeof fieldId !== 'string' || !model.fields[fieldId]) continue\n const value = input?.[submittedKey]\n if (value == null) continue\n values[fieldId] = value\n }\n return values\n}\n\n/** A dataset record write, checked against the dataset's model. */\nexport interface DatasetRecordWrite {\n /**\n * The model fields this write supplied a value for, before coercion. Empty\n * means nothing submitted matched a field of the dataset.\n */\n matched: string[]\n /**\n * The values to store, in storage form (see `dataset-models`): numbers,\n * booleans, epoch millis and so on per the field's type. For a merge, the\n * existing values with this write's laid over them.\n */\n values: Record<string, unknown>\n /** fieldId → the reason it was refused. Empty when the write may land. */\n errors: Record<string, string>\n}\n\n/**\n * A text field takes what an event payload carries as a number or a boolean\n * as the text it reads as, which is what these writes always stored.\n */\nfunction asTextInput(\n model: DatasetModel,\n picked: Record<string, unknown>,\n): Record<string, unknown> {\n const values: Record<string, unknown> = {}\n for (const [fieldId, value] of Object.entries(picked)) {\n const type = model.fields[fieldId]?.type\n values[fieldId] =\n (type === 'text' || type === 'reference') &&\n (typeof value === 'number' || typeof value === 'boolean')\n ? String(value)\n : value\n }\n return values\n}\n\n/**\n * A record write from a form submission or an automation step, coerced and\n * validated against the dataset's model (AGL-2773, option B).\n *\n * These writes used to store `String(value)` for every field and check\n * nothing, so a form could put `abc` in a number column and skip a required\n * one. They now run the same `coerceDocumentValues` + `validateDocument` pair\n * the console and `/v1` do: a value is stored in its field's type, and a\n * record with any refused field is not written at all. The caller decides\n * what a refusal costs — a form keeps the Inbox submission, a run records a\n * failed step.\n *\n * `existing` makes it a merge (update-or-append's update leg): this write's\n * values are laid over the stored ones, and only the fields THIS write\n * supplied are held to the model. A stored row written as text before this\n * change keeps its text values and is not refused for them — the reads have\n * always rendered them, and a merge that touched `email` must not fail over a\n * legacy `price` of `\"12\"` it never sent.\n *\n * Custom field types validate only once registered: call\n * `ensureDeclaredCustomFieldTypes(model)` before this on a server path.\n */\nexport function prepareDatasetRecordWrite(\n dataset: { model?: DatasetModel; fields?: string[] },\n input: Record<string, unknown> | null | undefined,\n options: {\n fieldMap?: Record<string, string> | null\n existing?: Record<string, unknown> | null\n } = {},\n): DatasetRecordWrite {\n const model = effectiveDatasetModel(dataset)\n const picked = pickDatasetRecordInput(dataset, input, options.fieldMap)\n const matched = Object.keys(picked)\n const coerced = coerceDocumentValues(model, asTextInput(model, picked))\n if (!options.existing) {\n return {\n matched,\n values: coerced,\n errors: validateDocument(model, coerced),\n }\n }\n const values = { ...options.existing, ...coerced }\n const errors: Record<string, string> = {}\n for (const [fieldId, error] of Object.entries(\n validateDocument(model, values),\n )) {\n if (fieldId in coerced) errors[fieldId] = error\n }\n return { matched, values, errors }\n}\n\n/**\n * A refused write's errors as one line, in the model's field order:\n * `Stars must be a whole number; Email is required`.\n */\nexport function describeDatasetRecordErrors(\n errors: Record<string, string>,\n): string {\n return Object.values(errors).join('; ')\n}\n\n/**\n * The name a dataset is shown under: `displayName`, which is what every create\n * path writes (AGL-536), then the pre-migration `name`. Blank when the document\n * carries neither.\n */\nexport function datasetDisplayName(\n dataset: { displayName?: unknown; name?: unknown } | null | undefined,\n): string {\n for (const candidate of [dataset?.displayName, dataset?.name]) {\n if (typeof candidate === 'string' && candidate.trim()) {\n return candidate.trim()\n }\n }\n return ''\n}\n\n/** One entry in a dataset select: stored by id, shown by name. */\nexport interface DatasetPickerOption {\n id: string\n name: string\n}\n\n/**\n * The options a dataset select offers, sorted by the label shown.\n *\n * Deleted datasets are left out. Each is labeled by\n * {@link datasetDisplayName}; one whose document carries no name at all is\n * offered under its id, because a select that hides it leaves a dataset that\n * cannot be picked.\n */\nexport function datasetPickerOptions(\n docs:\n | ReadonlyArray<{\n $id?: string\n deletedAt?: unknown\n displayName?: unknown\n name?: unknown\n }>\n | null\n | undefined,\n): DatasetPickerOption[] {\n return (docs ?? [])\n .filter((dataset) => !dataset.deletedAt && dataset.$id)\n .map((dataset) => ({\n id: dataset.$id as string,\n name: datasetDisplayName(dataset) || (dataset.$id as string),\n }))\n .sort((a, b) => a.name.localeCompare(b.name))\n}\n\n/** Records sorted by their editor order (then id for stability). */\nexport function sortDatasetRecords<T extends HostDatasetRecord>(\n records: T[],\n): T[] {\n return [...records].sort(\n (a, b) =>\n (a.order ?? Number.MAX_SAFE_INTEGER) -\n (b.order ?? Number.MAX_SAFE_INTEGER) ||\n String(a.$id ?? '').localeCompare(String(b.$id ?? '')),\n )\n}\n"],"names":["coerceDocumentValues","effectiveDatasetModel","validateDocument","DATASET_FIELD_PATTERN","slugifyDatasetFieldId","name","slug","trim","toLowerCase","replace","test","defaultDatasetFieldId","taken","base","used","Set","map","id","candidate","suffix","has","validateDatasetFieldId","trimmed","existing","humanizeDatasetFieldId","words","charAt","toUpperCase","slice","parseDatasetFieldEntries","input","seen","entries","raw","split","add","push","parseDatasetFields","fields","key","sanitizeRecordValues","values","field","value","String","pickDatasetRecordInput","dataset","fieldMap","model","fieldId","order","submittedKey","Object","asTextInput","picked","type","prepareDatasetRecordWrite","options","matched","keys","coerced","errors","error","describeDatasetRecordErrors","join","datasetDisplayName","displayName","datasetPickerOptions","docs","filter","deletedAt","$id","sort","a","b","localeCompare","sortDatasetRecords","records","Number","MAX_SAFE_INTEGER"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/datasets.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Datasets (AGL-102): tabular data at `orgs/{orgId}/datasets/{id}` with a\n * `records` subcollection, edited in the console Data card and consumed by\n * repeatable components (AGL-103) via `{{item.field}}` bindings.\n */\n\nimport {\n coerceDocumentValues,\n type DatasetModel,\n effectiveDatasetModel,\n validateDocument,\n} from './dataset-models'\n\n/** Dataset field name: starts with a letter; letters/digits/underscores. */\nexport const DATASET_FIELD_PATTERN = /^[A-Za-z][A-Za-z0-9_]*$/\n\nexport interface HostDataset {\n $id?: string\n displayName?: string\n /** Column names, in display order (v1; superseded by `model`). */\n fields?: string[]\n /** Typed model (AGL-177); when absent, derive from `fields`. */\n model?: import('./dataset-models').DatasetModel\n /** dod.ts Schema.Name shape (AGL-178). */\n names?: { singular?: string; plural?: string }\n /**\n * Which sites may see this dataset (AGL-1037). Absent = org-wide, until\n * the AGL-1040 backfill stamps it. See `app-utils/scope-tokens`.\n */\n visibleTo?: import('../foundation').ScopeToken[]\n}\n\nexport interface HostDatasetRecord {\n $id?: string\n /**\n * Field → value, in storage form per the field's type (see\n * `dataset-models`). Rows written by a form or an automation step before\n * AGL-2773 hold every value as text; readers accept both.\n */\n values?: Record<string, unknown>\n /** Row position in the editor and in repeated output. */\n order?: number\n}\n\n/**\n * A human field entry from the quick creator: the stable id plus the\n * display name shown in table headers and bindings pickers (AGL-558).\n */\nexport interface DatasetFieldEntry {\n id: string\n name: string\n}\n\n/**\n * Stable field id from a human name: \"Roast preference\" → \"roast_preference\".\n * Mirrors DATASET_FIELD_PATTERN; returns '' when nothing salvageable.\n */\nexport function slugifyDatasetFieldId(name: string): string {\n const slug = name\n .trim()\n .toLowerCase()\n .replace(/[\\s-]+/g, '_')\n .replace(/[^a-z0-9_]/g, '')\n .replace(/^[0-9_]+/, '')\n .replace(/_+$/, '')\n return DATASET_FIELD_PATTERN.test(slug) ? slug : ''\n}\n\n/**\n * Default reference id for a new field (AGL-578): the `slugifyDatasetFieldId`\n * slug, suffixed (`_2`, `_3`, …) to stay unique within the dataset. `taken`\n * holds the ids already in use (compared case-insensitively). Returns '' when\n * the name yields nothing valid. The user can override the result.\n */\nexport function defaultDatasetFieldId(\n name: string,\n taken: ReadonlySet<string>,\n): string {\n const base = slugifyDatasetFieldId(name)\n if (!base) return ''\n const used = new Set([...taken].map((id) => id.toLowerCase()))\n let candidate = base\n let suffix = 2\n while (used.has(candidate.toLowerCase())) candidate = `${base}_${suffix++}`\n return candidate\n}\n\n/**\n * Validate a user-entered reference id (AGL-578): non-empty, matches\n * `DATASET_FIELD_PATTERN`, and unique within the dataset (`taken` = ids already\n * used, compared case-insensitively). Returns an error message, or null when\n * the id is usable.\n */\nexport function validateDatasetFieldId(\n id: string,\n taken: ReadonlySet<string>,\n): string | null {\n const trimmed = id.trim()\n if (!trimmed) return 'A reference ID is required'\n if (!DATASET_FIELD_PATTERN.test(trimmed))\n return 'Start with a letter; use only letters, numbers, and underscores'\n const used = new Set([...taken].map((existing) => existing.toLowerCase()))\n if (used.has(trimmed.toLowerCase()))\n return 'Another field already uses this reference ID'\n return null\n}\n\n/** Display fallback for raw ids: \"roast_preference\" → \"Roast preference\". */\nexport function humanizeDatasetFieldId(id: string): string {\n const words = id.replace(/_/g, ' ').trim()\n return words ? words.charAt(0).toUpperCase() + words.slice(1) : id\n}\n\n/**\n * Parses a comma/newline separated list of HUMAN field names into\n * {id, name} entries — \"Roast preference\" keeps its pretty name and gets\n * the stable id `roast_preference` (AGL-558). Plain snake_case keys\n * still work and pick up a humanized display name. Duplicate ids and\n * unsalvageable entries are dropped rather than failing the set.\n */\nexport function parseDatasetFieldEntries(input: string): DatasetFieldEntry[] {\n const seen = new Set<string>()\n const entries: DatasetFieldEntry[] = []\n for (const raw of input.split(/[,\\n]/)) {\n const trimmed = raw.trim()\n if (!trimmed) continue\n const id = slugifyDatasetFieldId(trimmed)\n if (!id || seen.has(id)) continue\n seen.add(id)\n entries.push({\n id,\n name: DATASET_FIELD_PATTERN.test(trimmed)\n ? humanizeDatasetFieldId(trimmed)\n : trimmed,\n })\n }\n return entries\n}\n\n/**\n * Parses a comma/newline separated field list into valid unique names;\n * invalid entries are dropped rather than failing the whole set.\n */\nexport function parseDatasetFields(input: string): string[] {\n const seen = new Set<string>()\n const fields: string[] = []\n for (const raw of input.split(/[,\\n]/)) {\n const name = raw.trim()\n if (!DATASET_FIELD_PATTERN.test(name)) continue\n const key = name.toLowerCase()\n if (seen.has(key)) continue\n seen.add(key)\n fields.push(name)\n }\n return fields\n}\n\n/** Record values restricted to the dataset's declared fields. */\nexport function sanitizeRecordValues(\n fields: string[],\n input: Record<string, unknown> | null | undefined,\n): Record<string, string> {\n const values: Record<string, string> = {}\n for (const field of fields) {\n const value = input?.[field]\n if (value == null) continue\n values[field] = String(value)\n }\n return values\n}\n\n/**\n * The submitted values a dataset write would store, picked by field id and\n * left as they arrived (AGL-556). With a `fieldMap` (submitted key → stable\n * model fieldId), mapped values land under the mapped fieldId — so renamed\n * schema fields keep receiving data — and entries whose fieldId isn't in the\n * model are dropped. Submitted keys without a mapping (and calls without a\n * map) fall back to the legacy name-intersection against the model's field\n * ids, matching `sanitizeRecordValues`.\n */\nexport function pickDatasetRecordInput(\n dataset: { model?: DatasetModel; fields?: string[] },\n input: Record<string, unknown> | null | undefined,\n fieldMap?: Record<string, string> | null,\n): Record<string, unknown> {\n const model = effectiveDatasetModel(dataset)\n const map = fieldMap ?? {}\n const values: Record<string, unknown> = {}\n for (const fieldId of model.order) {\n if (!model.fields[fieldId]) continue\n // A submitted key with an explicit mapping never doubles as a\n // name-match — the mapping decides where it lands.\n if (fieldId in map) continue\n const value = input?.[fieldId]\n if (value == null) continue\n values[fieldId] = value\n }\n for (const [submittedKey, fieldId] of Object.entries(map)) {\n if (typeof fieldId !== 'string' || !model.fields[fieldId]) continue\n const value = input?.[submittedKey]\n if (value == null) continue\n values[fieldId] = value\n }\n return values\n}\n\n/** A dataset record write, checked against the dataset's model. */\nexport interface DatasetRecordWrite {\n /**\n * The model fields this write supplied a value for, before coercion. Empty\n * means nothing submitted matched a field of the dataset.\n */\n matched: string[]\n /**\n * The values to store, in storage form (see `dataset-models`): numbers,\n * booleans, epoch millis and so on per the field's type. For a merge, the\n * existing values with this write's laid over them.\n */\n values: Record<string, unknown>\n /** fieldId → the reason it was refused. Empty when the write may land. */\n errors: Record<string, string>\n}\n\n/**\n * A text field takes what an event payload carries as a number or a boolean\n * as the text it reads as, which is what these writes always stored.\n */\nfunction asTextInput(\n model: DatasetModel,\n picked: Record<string, unknown>,\n): Record<string, unknown> {\n const values: Record<string, unknown> = {}\n for (const [fieldId, value] of Object.entries(picked)) {\n const type = model.fields[fieldId]?.type\n values[fieldId] =\n (type === 'text' || type === 'reference') &&\n (typeof value === 'number' || typeof value === 'boolean')\n ? String(value)\n : value\n }\n return values\n}\n\n/**\n * A record write from a form submission or an automation step, coerced and\n * validated against the dataset's model (AGL-2773, option B).\n *\n * These writes used to store `String(value)` for every field and check\n * nothing, so a form could put `abc` in a number column and skip a required\n * one. They now run the same `coerceDocumentValues` + `validateDocument` pair\n * the console and `/v1` do: a value is stored in its field's type, and a\n * record with any refused field is not written at all. The caller decides\n * what a refusal costs — a form keeps the Inbox submission, a run records a\n * failed step.\n *\n * `existing` makes it a merge (update-or-append's update leg): this write's\n * values are laid over the stored ones, and only the fields THIS write\n * supplied are held to the model. A stored row written as text before this\n * change keeps its text values and is not refused for them — the reads have\n * always rendered them, and a merge that touched `email` must not fail over a\n * legacy `price` of `\"12\"` it never sent.\n *\n * Custom field types validate only once registered: call\n * `ensureDeclaredCustomFieldTypes(model)` before this on a server path.\n */\nexport function prepareDatasetRecordWrite(\n dataset: { model?: DatasetModel; fields?: string[] },\n input: Record<string, unknown> | null | undefined,\n options: {\n fieldMap?: Record<string, string> | null\n existing?: Record<string, unknown> | null\n } = {},\n): DatasetRecordWrite {\n const model = effectiveDatasetModel(dataset)\n const picked = pickDatasetRecordInput(dataset, input, options.fieldMap)\n const matched = Object.keys(picked)\n const coerced = coerceDocumentValues(model, asTextInput(model, picked))\n if (!options.existing) {\n return {\n matched,\n values: coerced,\n errors: validateDocument(model, coerced),\n }\n }\n const values = { ...options.existing, ...coerced }\n const errors: Record<string, string> = {}\n for (const [fieldId, error] of Object.entries(\n validateDocument(model, values),\n )) {\n if (fieldId in coerced) errors[fieldId] = error\n }\n return { matched, values, errors }\n}\n\n/**\n * A refused write's errors as one line, in the model's field order:\n * `Stars must be a whole number; Email is required`.\n */\nexport function describeDatasetRecordErrors(\n errors: Record<string, string>,\n): string {\n return Object.values(errors).join('; ')\n}\n\n/**\n * The name a dataset is shown under: `displayName`, which is what every create\n * path writes (AGL-536), then the pre-migration `name`. Blank when the document\n * carries neither.\n */\nexport function datasetDisplayName(\n dataset: { displayName?: unknown; name?: unknown } | null | undefined,\n): string {\n for (const candidate of [dataset?.displayName, dataset?.name]) {\n if (typeof candidate === 'string' && candidate.trim()) {\n return candidate.trim()\n }\n }\n return ''\n}\n\n/** Records sorted by their editor order (then id for stability). */\nexport function sortDatasetRecords<T extends HostDatasetRecord>(\n records: T[],\n): T[] {\n return [...records].sort(\n (a, b) =>\n (a.order ?? Number.MAX_SAFE_INTEGER) -\n (b.order ?? Number.MAX_SAFE_INTEGER) ||\n String(a.$id ?? '').localeCompare(String(b.$id ?? '')),\n )\n}\n"],"names":["coerceDocumentValues","effectiveDatasetModel","validateDocument","DATASET_FIELD_PATTERN","slugifyDatasetFieldId","name","slug","trim","toLowerCase","replace","test","defaultDatasetFieldId","taken","base","used","Set","map","id","candidate","suffix","has","validateDatasetFieldId","trimmed","existing","humanizeDatasetFieldId","words","charAt","toUpperCase","slice","parseDatasetFieldEntries","input","seen","entries","raw","split","add","push","parseDatasetFields","fields","key","sanitizeRecordValues","values","field","value","String","pickDatasetRecordInput","dataset","fieldMap","model","fieldId","order","submittedKey","Object","asTextInput","picked","type","prepareDatasetRecordWrite","options","matched","keys","coerced","errors","error","describeDatasetRecordErrors","join","datasetDisplayName","displayName","sortDatasetRecords","records","sort","a","b","Number","MAX_SAFE_INTEGER","$id","localeCompare"],"mappings":"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"}
@@ -286,7 +286,7 @@ export declare const PLUGIN_DOCS: {
286
286
  };
287
287
  export type PluginDocsKey = keyof typeof PLUGIN_DOCS;
288
288
  export declare const PLUGIN_DOCS_ANCHORS: {
289
- readonly abuseReports: readonly ["#fraud-and-risk-alerts-by-email", "#where-reports-come-from", "#held-outbound-email", "#what-is-screened", "#tiers", "#deciding-a-held-row", "#names-and-domains", "#stripe-fraud-signals", "#seller-fraud-pattern", "#card-testing-velocity", "#marketplace", "#risk-notices", "#triage-by-severity", "#csam", "#which-lever", "#statuses", "#disclosure", "#dmca", "#counter-notices", "#counter-notice-clock", "#counter-notice-steps", "#repeat-infringers", "#repeat-infringer-threshold", "#known-gaps", "#related"];
289
+ readonly abuseReports: readonly ["#fraud-and-risk-alerts-by-email", "#where-reports-come-from", "#held-outbound-email", "#what-is-screened", "#tiers", "#web-risk", "#deciding-a-held-row", "#security-hold", "#names-and-domains", "#stripe-fraud-signals", "#seller-fraud-pattern", "#card-testing-velocity", "#marketplace", "#risk-notices", "#triage-by-severity", "#csam", "#which-lever", "#statuses", "#disclosure", "#dmca", "#counter-notices", "#counter-notice-clock", "#counter-notice-steps", "#repeat-infringers", "#repeat-infringer-threshold", "#known-gaps", "#related"];
290
290
  readonly actionsBuilder: readonly ["#create-an-action", "#recipes", "#describe-it", "#triggers", "#crm-events", "#only-run-when-a-field-matches", "#chain-multiple-conditions-andor", "#steps", "#crm-steps", "#step-conditions", "#sequences", "#run-history", "#what-is-and-isnt-recorded", "#interactions-from-the-besigner", "#when-to-use-which", "#related"];
291
291
  readonly aglynAssist: readonly ["#what-it-can-do", "#aglyn-ai", "#answers-for-beginners-and-developers", "#offers-to-open-a-page", "#edits-in-the-besigner", "#where-an-answer-came-from", "#answers-straight-from-the-documentation", "#message-limits", "#feedback", "#privacy"];
292
292
  readonly aiAutomations: readonly ["#draft", "#explain", "#why-a-run-failed", "#what-is-sent", "#who-can-use-it", "#related"];
@@ -297,7 +297,7 @@ export declare const PLUGIN_DOCS_ANCHORS: {
297
297
  readonly aiSeo: readonly ["#write-a-pages-listing", "#write-a-products-listing", "#fix-what-the-seo-check-finds", "#apply-all-as-drafts", "#structured-data-and-llmstxt", "#related"];
298
298
  readonly aiThemes: readonly ["#change-what-you-describe-or-design-a-new-theme", "#match-your-brand", "#review-the-proposal", "#save-it-or-dont", "#who-can-use-it", "#related"];
299
299
  readonly assistSignals: readonly ["#the-workflow-this-board-exists-for", "#fleet", "#the-cache-read-rate-and-what-a-bad-number-looks-like", "#where-the-money-goes", "#tokens-by-kind", "#docs-gaps", "#questions-the-docs-could-not-answer", "#what-people-actually-asked", "#what-assist-costs-by-workspace", "#reading-the-sample-honestly", "#related"];
300
- readonly billing: readonly ["#tiers--entitlements", "#enterprise", "#single-sign-on-and-enforcement", "#usage-meters", "#who-is-generating-what", "#ai-allotments", "#storage-overage", "#if-you-would-rather-uploads-stopped", "#assist-overage", "#stop-ai-assist-at-the-included-band", "#ai-overage-ceiling", "#free-ai-credits", "#ai-credit-alerts", "#usage-budget", "#seats", "#crm-records", "#the-crm-suite", "#one-to-one-email", "#organization-data", "#api-access", "#payments", "#outstanding", "#plan-total", "#billing-email", "#payment-methods", "#billing-address", "#tax-ids", "#sales-tax", "#platform-fees", "#related"];
300
+ readonly billing: readonly ["#tiers--entitlements", "#leaving-notice", "#enterprise", "#single-sign-on-and-enforcement", "#usage-meters", "#who-is-generating-what", "#ai-allotments", "#storage-overage", "#if-you-would-rather-uploads-stopped", "#assist-overage", "#stop-ai-assist-at-the-included-band", "#ai-overage-ceiling", "#free-ai-credits", "#ai-credit-alerts", "#usage-budget", "#seats", "#crm-records", "#the-crm-suite", "#one-to-one-email", "#organization-data", "#api-access", "#payments", "#outstanding", "#plan-total", "#billing-email", "#payment-methods", "#billing-address", "#tax-ids", "#sales-tax", "#platform-fees", "#related"];
301
301
  readonly bindings: readonly ["#binding-tokens", "#rename-safe-id-tokens", "#insert-a-variable", "#token-pills", "#in-the-canvas-text-editor", "#site-details", "#typed-variables", "#no-code-functions", "#parameters-a-visitor-can-answer", "#a-calculator-you-lay-out", "#where-used--safety", "#workflows", "#related"];
302
302
  readonly bookings: readonly ["#set-up-bookings", "#taking-bookings", "#reminders", "#payments-and-fees", "#service-tax", "#manage", "#booking-from-the-crm", "#canceling-and-refunding", "#related"];
303
303
  readonly buildAWorkflow: readonly ["#1-open-the-workflows-page", "#2-choose-a-trigger", "#3-add-steps", "#waiting", "#4-save-and-test", "#duplicate-a-workflow", "#tips", "#related"];
@@ -305,13 +305,13 @@ export declare const PLUGIN_DOCS_ANCHORS: {
305
305
  readonly commerce: readonly ["#products-hub", "#inventory", "#reserved-stock", "#stock-movements", "#gift-cards", "#recovery-and-alerts", "#orders", "#orders-screen", "#order-statuses", "#order-money-tiles", "#a-lost-dispute", "#shipping--taxes", "#lodging-tax-on-reservations", "#storefront-sales-tax", "#destination-coverage", "#dropshipping", "#related"];
306
306
  readonly commerceEndToEnd: readonly ["#1-connect-payments", "#2-create-products", "#3-design-the-storefront", "#catalog-search-filters-and-sort", "#category-pages", "#the-product-page-template", "#4-what-checkout-does", "#paying-without-leaving-your-site", "#5-run-orders-from-the-console", "#6-subscriptions--the-stripe-portal", "#related"];
307
307
  readonly companies: readonly ["#the-companies-list", "#create-a-company", "#a-companys-page", "#contacts-at-a-company", "#linked-on-capture", "#import-from-csv", "#export-csv", "#deleting-a-company", "#who-can-see-a-company", "#files", "#related"];
308
- readonly consoleTour: readonly ["#the-app-bar", "#in-context-help", "#filter-and-search", "#primary-navigation", "#editing-vs-managing", "#the-sites-list", "#the-status-pill", "#how-the-pill-is-decided", "#your-site-allowance", "#a-sites-dashboard", "#next", "#workspace-settings--notifications", "#the-notifications-feed", "#notification-settings", "#one-kind-at-a-time", "#workspace-and-site-overrides", "#daily-digests", "#alerts-on-this-device"];
308
+ readonly consoleTour: readonly ["#the-app-bar", "#in-context-help", "#filter-and-search", "#primary-navigation", "#editing-vs-managing", "#the-sites-list", "#the-status-pill", "#how-the-pill-is-decided", "#your-site-allowance", "#a-sites-dashboard", "#next", "#workspace-settings--notifications", "#the-notifications-feed", "#notification-levels", "#notification-settings", "#one-kind-at-a-time", "#workspace-and-site-overrides", "#daily-digests", "#alerts-on-this-device"];
309
309
  readonly contactActivities: readonly ["#four-kinds-of-history", "#reading-the-timeline", "#campaign-email", "#logging-an-activity", "#meeting-from-a-booking", "#click-to-call", "#sending-an-email", "#delivery-states", "#captured-email", "#where-an-activity-is-visible", "#the-recent-activity-feed", "#related"];
310
310
  readonly contactFields: readonly ["#define-a-field", "#fields-per-record", "#where-values-show", "#save-a-form-field", "#lead-source-values", "#over-the-api", "#retire-restore-delete", "#recompute-next-activity", "#related"];
311
311
  readonly contactRecord: readonly ["#adding-a-contact-by-hand", "#the-record-page", "#deleting-and-erasing", "#what-each-site-keeps-to-itself", "#when-sites-join-or-leave-a-group", "#merging-two-records", "#likely-duplicates", "#owner", "#last-engaged", "#lifecycle-stages", "#where-the-persons-lead-is", "#finding-a-contact", "#files", "#related"];
312
312
  readonly contacts: readonly ["#whats-in-the-crm-area", "#unified-ingestion", "#what-each-plan-includes", "#the-contacts-page", "#import-from-csv", "#segments", "#everywhere-the-crm-shows-up", "#capture-replies", "#at-the-organization-level", "#who-can-open-the-crm", "#one-sender-one-crm", "#related"];
313
313
  readonly crmEmailTemplates: readonly ["#templates-and-snippets", "#merge-fields", "#saving", "#managing-templates", "#duplicate-a-template", "#shared-or-personal", "#over-the-rest-api", "#related"];
314
- readonly crmLeads: readonly ["#what-makes-a-lead", "#what-a-lead-holds", "#adding-a-lead-by-hand", "#the-leads-list", "#filter-the-leads", "#working-a-lead-from-the-row", "#several-leads-at-once", "#export-csv", "#import-from-csv", "#who-owns-a-lead", "#a-leads-page", "#email-state", "#converting-a-lead", "#unqualifying-a-lead", "#erasing-the-person", "#who-can-do-this", "#related"];
314
+ readonly crmLeads: readonly ["#what-makes-a-lead", "#what-a-lead-holds", "#adding-a-lead-by-hand", "#the-leads-list", "#lead-statuses", "#filter-the-leads", "#working-a-lead-from-the-row", "#several-leads-at-once", "#export-csv", "#import-from-csv", "#who-owns-a-lead", "#a-leads-page", "#email-state", "#converting-a-lead", "#unqualifying-a-lead", "#erasing-the-person", "#who-can-do-this", "#related"];
315
315
  readonly crmReports: readonly ["#choosing-a-period", "#contacts", "#sources-and-lifecycle", "#conversion-by-source", "#lead-funnel", "#pipeline", "#forecast-by-close-month", "#won-and-lost", "#won-and-lost-by-owner", "#activity-by-teammate", "#tasks", "#exporting-a-table", "#crm-at-a-glance", "#how-the-numbers-are-counted", "#related"];
316
316
  readonly crmSettings: readonly ["#companies", "#create-companies-from-work-email-domains", "#default-owner", "#assignment-rules", "#round-robin", "#email-templates", "#email-capture", "#your-sending-addresses", "#recipes", "#related"];
317
317
  readonly crmSharing: readonly ["#what-a-shared-record-looks-like", "#share-a-record-by-hand", "#several-records-at-once", "#sharing-rules", "#access-read-only-or-read-and-edit", "#sharing-is-not-consent", "#who-can-do-this", "#related"];
@@ -288,7 +288,9 @@ export const PLUGIN_DOCS_ANCHORS = {
288
288
  '#held-outbound-email',
289
289
  '#what-is-screened',
290
290
  '#tiers',
291
+ '#web-risk',
291
292
  '#deciding-a-held-row',
293
+ '#security-hold',
292
294
  '#names-and-domains',
293
295
  '#stripe-fraud-signals',
294
296
  '#seller-fraud-pattern',
@@ -419,6 +421,7 @@ export const PLUGIN_DOCS_ANCHORS = {
419
421
  ],
420
422
  billing: [
421
423
  '#tiers--entitlements',
424
+ '#leaving-notice',
422
425
  '#enterprise',
423
426
  '#single-sign-on-and-enforcement',
424
427
  '#usage-meters',
@@ -554,6 +557,7 @@ export const PLUGIN_DOCS_ANCHORS = {
554
557
  '#next',
555
558
  '#workspace-settings--notifications',
556
559
  '#the-notifications-feed',
560
+ '#notification-levels',
557
561
  '#notification-settings',
558
562
  '#one-kind-at-a-time',
559
563
  '#workspace-and-site-overrides',
@@ -630,6 +634,7 @@ export const PLUGIN_DOCS_ANCHORS = {
630
634
  '#what-a-lead-holds',
631
635
  '#adding-a-lead-by-hand',
632
636
  '#the-leads-list',
637
+ '#lead-statuses',
633
638
  '#filter-the-leads',
634
639
  '#working-a-lead-from-the-row',
635
640
  '#several-leads-at-once',