@ai-matrx/records 0.44.1 → 0.46.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.
@@ -460,6 +460,9 @@ var STORE_DOORS = [
460
460
  { name: "parity_values", args: "p_organization_id uuid, p_record_id uuid", returns: "TABLE(field_key text, field_id uuid, parity_type text, behavior text, unit text, format text, value jsonb, value_version integer, actor text, written_at timestamp with time zone)", security: "invoker" },
461
461
  { name: "per_value_access_words", args: "", returns: "text[]", security: "invoker" },
462
462
  { name: "person_kernel_id", args: "", returns: "uuid", security: "invoker" },
463
+ { name: "phone_canonical", args: "p_text text", returns: "text", security: "invoker" },
464
+ { name: "phone_display", args: "p_text text", returns: "text", security: "invoker" },
465
+ { name: "phone_pattern", args: "", returns: "text", security: "invoker" },
463
466
  { name: "pin_agent_cells", args: "p_new jsonb, p_old jsonb, p_agent_keys text[]", returns: "jsonb", security: "invoker" },
464
467
  { name: "pipeline_board", args: "p_organization_id uuid, p_table_id uuid, p_measure text DEFAULT NULL::text", returns: "TABLE(stage_key text, stage_label text, stage_position integer, cards bigint, total numeric, wip_limit integer, over_limit boolean)", security: "definer" },
465
468
  { name: "pipeline_declare", args: "p_organization_id uuid, p_table_id uuid, p_spec jsonb", returns: "jsonb", security: "definer" },
@@ -481,6 +484,7 @@ var STORE_DOORS = [
481
484
  { name: "portal_record_title", args: "p_organization_id uuid, p_record_id uuid", returns: "text", security: "definer" },
482
485
  { name: "portal_revoke", args: "p_organization_id uuid, p_portal_id uuid, p_principal_id uuid", returns: "jsonb", security: "definer" },
483
486
  { name: "portal_slug", args: "p_organization_id uuid, p_title text, p_portal_id uuid DEFAULT NULL::uuid", returns: "text", security: "definer" },
487
+ { name: "portal_tables", args: "p_organization_id uuid", returns: "TABLE(portal_id uuid, table_id uuid, name text)", security: "definer" },
484
488
  { name: "portals", args: "p_organization_id uuid", returns: "TABLE(portal_id uuid, title text, slug text, client_table_id uuid, client_table text, is_active boolean, tables integer, invited integer, signed_in integer, sign_in_method text, opened_at timestamp with time zone)", security: "definer" },
485
489
  { name: "presentation_kernel_id", args: "", returns: "uuid", security: "invoker" },
486
490
  { name: "promote_field", args: "p_organization_id uuid, p_table_id uuid, p_field_id uuid", returns: "jsonb", security: "invoker" },
@@ -759,7 +763,7 @@ var STORE_REFUSAL_CODES = [
759
763
  { code: "22004", raised_by: "_entity_custom_fields_guard,_value_envelope,actor_word,agg_assert_key,agg_sql,anon_capture,anon_token_issue,anon_write,assert_client_may_reach,booking_declare,capture_submit,checklist_declare,context_resolve,dashboard_block_normalize,dashboard_declare,dashboard_window_sql,doc_render_write,doc_sign,doc_signature_write,doc_template_save,enrich_land,enrich_normalize,external_history_event,external_rows,external_source_declare,external_stub_upsert,external_write_through,external_writes_set,field_history,form_declare,form_submit,home_add,io_comment_write,io_import_begin,io_infer_column,io_outbox_drain,io_restore,migrate_choice_keys,migrate_purge,migrate_reclass,migrate_split,migrate_undo,organization_clear,organization_contents,pipeline_declare,pipeline_gate_preview,portal_declare,portal_invite,portal_principal_bind,provenance_retention_days,provenance_write,query_across_homes,query_by_coordinates,query_rollup,record_delete,record_reparent,record_resolve,record_restore,record_update,record_write,record_write_many,relation_carry,relation_own,rule_declare,share_grant,sign_request_create,sign_request_sign,subscription_declare,table_declare,table_share_outside_invite,value_restore,view_declare,work_approval_decide,work_approval_request,work_template_declare" },
760
764
  { code: "22007", raised_by: "dashboard_window_sql" },
761
765
  { code: "22012", raised_by: "rule_eval" },
762
- { code: "22023", raised_by: "_booking_availability,_entity_custom_fields_guard,_take_op_id,_value_envelope,_work_shape_guard,actor_word,agg_assert_key,agg_sql,anon_token_issue,booking_declare,containment_parent,dashboard_block_normalize,dashboard_window_sql,doc_render_body,doc_render_write,doc_signature_write,enrich_declare,enrich_land,enrich_normalize,enrich_pin,entity_value_write,external_history_event,external_source_declare,external_stub_upsert,external_write_through,field_history,io_export,io_import_begin,io_import_finish,io_import_plan,io_import_report,io_proposal_accept,migrate_merge,migrate_reclass,my_levels,organization_clear,page_size,pin_agent_cells,pipeline_move,portal_declare,provenance_write,query_by_coordinates,query_rollup,record_update,record_write_many,relation_carry,rule_eval,rule_members,share_grant,share_lane_set,sign_request_create,sign_request_sign,subscription_declare,table_rules,visibility_as_of,work_approval_request,work_list,work_slot_hold" },
766
+ { code: "22023", raised_by: "_booking_availability,_entity_custom_fields_guard,_take_op_id,_value_envelope,_work_shape_guard,actor_word,agg_assert_key,agg_sql,anon_token_issue,booking_declare,containment_parent,dashboard_block_normalize,dashboard_window_sql,doc_render_body,doc_render_write,doc_signature_write,enrich_declare,enrich_land,enrich_normalize,enrich_pin,entity_value_write,external_history_event,external_source_declare,external_stub_upsert,external_write_through,field_history,field_update,io_export,io_import_begin,io_import_finish,io_import_plan,io_import_report,io_proposal_accept,migrate_merge,migrate_reclass,my_levels,organization_clear,page_size,pin_agent_cells,pipeline_move,portal_declare,provenance_write,query_by_coordinates,query_rollup,record_update,record_write_many,relation_carry,rule_eval,rule_members,share_grant,share_lane_set,sign_request_create,sign_request_sign,subscription_declare,table_rules,visibility_as_of,work_approval_request,work_list,work_slot_hold" },
763
767
  { code: "23503", raised_by: "_booking_availability,_checklist_instantiate,_containment_guard,_display_spec_for,_field_document_for,_pipeline_stage_key,_record_rule_uses,agg_digest_assemble,anon_publish,anon_rate_take,anon_token_issue,anon_write,assert_organization_wall,booking_cancel,booking_confirm,booking_declare,booking_hold,booking_reschedule,capture_publish,checklist_declare,checklist_steps_table,comment_write,dashboard_declare,delete_rule,doc_sign,doc_template_save,enrich_declare,enrich_due,enrich_land,form_declare,form_submit,home_add,inbound_declare,inbound_mail_land,inbound_set,io_comment_write,io_import_begin,io_import_finish,io_import_report,io_import_rows,io_proposal_accept,migrate_demote,migrate_promote,pipeline_board,pipeline_declare,pipeline_move,pipeline_read,pipeline_transition_refusal,promote_field,promote_table,record_applicability,record_reparent,relation_carry,relation_own,resolve_first_match,rule_applies,rule_declare,rule_eval,rule_members,rule_run,sign_request_create,subscription_mute,subscription_preview,view_declare,work_set_state,work_take_assignment,work_template_instantiate" },
764
768
  { code: "23505", raised_by: "_unique_rule_holds,doc_sign,doc_signature_write,entity_field_declare,field_declare,home_add,inbound_declare,io_proposal_accept,relation_carry,relation_own,share_grant,sign_request_cancel,sign_request_create,table_share_outside_invite,table_share_outside_resend,work_approval_decide,work_slot_hold" },
765
769
  { code: "23514", raised_by: "_checklist_instantiate,_checklist_step_guard,_containment_guard,_dated_values_guard,_derived_fields,_display_spec_for,_entity_custom_fields_guard,_field_class_guard,_field_document_for,_field_shape_guard,_field_type_parity_guard,_merge_field_shape_guard,_merge_field_temporal_guard,_promoted_field_cap_guard,_record_rule_uses,_resolve_choice_words,_rule_shape_guard,_rule_topology_guard,_store_relation_edge_names_its_field,_table_columns_word_guard,_table_shape_guard,_undeclared_key_guard,_value_envelope,_with_display,_work_shape_guard,_workdoors_approval_guard,assert_columns_are_defined,assert_entity_is_organization_scoped,booking_declare,capture_publish,capture_sheet_declare,checklist_declare,checklist_step_complete,doc_sign,doc_signature_write,doc_template_save,entity_field_declare,entity_field_retire,entity_field_update,entity_records_find,entity_table,field_declare,field_retire,field_update,home_add,io_import_rows,migrate_merge,relation_carry,relation_own,sign_request_create,sign_request_sign,table_declare,validate_value_envelope,validate_values,work_set_state,work_slots_declare,work_take_assignment,work_template_instantiate" },
@@ -1659,6 +1663,7 @@ var ENTITY_TOKENS = [
1659
1663
  "template_context_items",
1660
1664
  "template_scope_types",
1661
1665
  "templates",
1666
+ "test_handset_inbox",
1662
1667
  "thread",
1663
1668
  "tool",
1664
1669
  "tool_binding",
@@ -2023,6 +2028,14 @@ var DOORS = {
2023
2028
  // pair (`custom.portal_public`, `custom.portal_invitation`, `custom.portal_me`)
2024
2029
  // is deliberately absent here: those belong to the outsider's own shell.
2025
2030
  portals: "portals",
2031
+ // WHICH TABLES EACH PORTAL SHOWS (TAILS-3). The Portals RAIL lives on a
2032
+ // Table's page beside Forms, Bookings and Dashboards, all of which are
2033
+ // table-scoped; `custom.portals` takes no table, so an organization with one
2034
+ // portal over Jobs saw that portal's cards on its Parts table, the empty
2035
+ // state never rendered, and the whole build/ask offer was unreachable
2036
+ // everywhere in that organization. This is the one question the rail has to
2037
+ // ask, under the same wall and visibility gate as `portals` itself.
2038
+ portalTables: "portal_tables",
2026
2039
  portalCard: "portal_card",
2027
2040
  portalDeclare: "portal_declare",
2028
2041
  portalInvite: "portal_invite",
@@ -3080,6 +3093,9 @@ function createRecordsClient(config) {
3080
3093
  async portals() {
3081
3094
  return callDoor(DOORS.portals, { p_organization_id: org }, "portals");
3082
3095
  },
3096
+ async portalTables() {
3097
+ return callDoor(DOORS.portalTables, { p_organization_id: org }, "portalTables");
3098
+ },
3083
3099
  async portalCard({ portal_id }) {
3084
3100
  return callDoor(
3085
3101
  DOORS.portalCard,
@@ -4716,6 +4732,209 @@ function looksLikeAnId(value) {
4716
4732
  return typeof value === "string" && UUID.test(value);
4717
4733
  }
4718
4734
 
4735
+ // src/coerce.ts
4736
+ var FIELD_KINDS = [
4737
+ "attachment",
4738
+ "checkbox",
4739
+ "currency",
4740
+ "datetime",
4741
+ "email",
4742
+ "formula",
4743
+ "json",
4744
+ "long_text",
4745
+ "lookup",
4746
+ "member",
4747
+ "multi_select",
4748
+ "number",
4749
+ "percent",
4750
+ "phone",
4751
+ "relation",
4752
+ "rollup",
4753
+ "select",
4754
+ "text",
4755
+ "url"
4756
+ ];
4757
+ function fieldKindFor(field, kernel) {
4758
+ const format = (field.format ?? "").toLowerCase();
4759
+ const config = field.config ?? {};
4760
+ switch (field.type) {
4761
+ case "boolean":
4762
+ return "checkbox";
4763
+ case "text":
4764
+ if (format === "email") return "email";
4765
+ if (format === "phone") return "phone";
4766
+ if (format === "url") return "url";
4767
+ if (format === "json" || config["json"] === true) return "json";
4768
+ if (format === "long" || config["multiline"] === true) return "long_text";
4769
+ return "text";
4770
+ case "range": {
4771
+ const kind = String(config["kind"] ?? "number");
4772
+ if (kind === "date" || kind === "datetime") return "datetime";
4773
+ if (format === "currency") return "currency";
4774
+ if (format === "percent" || field.unit === "%") return "percent";
4775
+ return "number";
4776
+ }
4777
+ case "list":
4778
+ return field.multi ? "multi_select" : "select";
4779
+ case "relation": {
4780
+ const parity = String(config["parity"] ?? "");
4781
+ if (parity === "attachment" || parity === "member") return parity;
4782
+ if (kernel?.file && field.relation_target === kernel.file) return "attachment";
4783
+ if (kernel?.person && field.relation_target === kernel.person) return "member";
4784
+ return "relation";
4785
+ }
4786
+ case "formula": {
4787
+ const via = config["via"];
4788
+ if (via && config["agg"]) return "rollup";
4789
+ if (via) return "lookup";
4790
+ return "formula";
4791
+ }
4792
+ default:
4793
+ return "text";
4794
+ }
4795
+ }
4796
+ var TRUE_WORDS = /* @__PURE__ */ new Set(["true", "yes", "y", "1", "\u2713", "x", "checked", "on"]);
4797
+ var FALSE_WORDS = /* @__PURE__ */ new Set(["false", "no", "n", "0", "", "unchecked", "off"]);
4798
+ function coercionLabel(field) {
4799
+ const label = (field.label ?? "").trim();
4800
+ if (label !== "") return label;
4801
+ const name = (field.name ?? "").trim();
4802
+ if (name !== "") return name;
4803
+ return field.key.replace(/[_-]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()).trim();
4804
+ }
4805
+ function coerceTypedAnswer(field, raw, opts = {}) {
4806
+ const label = opts.label ?? coercionLabel(field);
4807
+ const text = raw.trim();
4808
+ const kind = fieldKindFor(field, opts.kernel);
4809
+ if (text === "" && kind !== "checkbox") return { value: null };
4810
+ switch (kind) {
4811
+ case "checkbox": {
4812
+ const word = text.toLowerCase();
4813
+ if (TRUE_WORDS.has(word)) return { value: true };
4814
+ if (FALSE_WORDS.has(word)) return { value: word === "" ? null : false };
4815
+ return {
4816
+ refusal: `${label} is a tick box, so it takes yes or no \u2014 \u201C${raw}\u201D is neither. Write TRUE/FALSE, yes/no or 1/0.`
4817
+ };
4818
+ }
4819
+ case "number":
4820
+ case "currency":
4821
+ case "percent": {
4822
+ const negative = /^\(.*\)$/.test(text);
4823
+ const bare = text.replace(/^\(|\)$/g, "").replace(/[\s,]/g, "").replace(/^[^\d.\-+]+/, "").replace(/[^\d.\-+eE]+$/, "");
4824
+ const n = Number(bare);
4825
+ if (bare === "" || Number.isNaN(n)) {
4826
+ return {
4827
+ refusal: `${label} holds a number, and \u201C${raw}\u201D is not one. Write digits \u2014 a currency symbol, a percent sign, commas and brackets for a negative are all fine.`
4828
+ };
4829
+ }
4830
+ return { value: negative ? -n : n };
4831
+ }
4832
+ case "datetime": {
4833
+ const wantsTime = String(field.config?.["kind"] ?? "date") === "datetime";
4834
+ const plainDay = /^\s*(\d{4})-(\d{2})-(\d{2})\s*$/.exec(raw);
4835
+ if (!wantsTime && plainDay) return { value: `${plainDay[1]}-${plainDay[2]}-${plainDay[3]}` };
4836
+ const at = Date.parse(text);
4837
+ if (Number.isNaN(at)) {
4838
+ return {
4839
+ refusal: `${label} holds a date, and \u201C${raw}\u201D is not one this can read. Write it as 2026-09-21, or the way your spreadsheet writes dates.`
4840
+ };
4841
+ }
4842
+ const parsed = new Date(at);
4843
+ if (wantsTime) return { value: parsed.toISOString() };
4844
+ const y = parsed.getFullYear();
4845
+ const m = String(parsed.getMonth() + 1).padStart(2, "0");
4846
+ const d = String(parsed.getDate()).padStart(2, "0");
4847
+ return { value: `${y}-${m}-${d}` };
4848
+ }
4849
+ case "json": {
4850
+ try {
4851
+ return { value: JSON.parse(text) };
4852
+ } catch {
4853
+ return {
4854
+ refusal: `${label} holds a document, and \u201C${raw}\u201D is not valid JSON. Write the whole object, braces and all.`
4855
+ };
4856
+ }
4857
+ }
4858
+ case "select":
4859
+ case "multi_select": {
4860
+ if (!opts.options) {
4861
+ return {
4862
+ refusal: `${label} is a list and its options have not been read yet, so nothing can be matched to \u201C${raw}\u201D. Try again once the column's choices have loaded.`
4863
+ };
4864
+ }
4865
+ const wanted = kind === "multi_select" ? text.split(/[,;]/).map((w) => w.trim()).filter((w) => w.length > 0) : [text];
4866
+ if (wanted.length === 0) return { value: kind === "multi_select" ? [] : null };
4867
+ const matched = [];
4868
+ const unmatched = [];
4869
+ for (const word of wanted) {
4870
+ const hit = opts.options.byWord.get(word.toLowerCase());
4871
+ if (hit === void 0) unmatched.push(word);
4872
+ else matched.push(hit);
4873
+ }
4874
+ if (unmatched.length > 0) {
4875
+ const offered = opts.options.labels.length > 0 ? opts.options.labels.join(", ") : "it has none yet";
4876
+ return {
4877
+ refusal: `${label} does not have ${unmatched.map((w) => `\u201C${w}\u201D`).join(" or ")} among its options. It offers: ${offered}.`,
4878
+ unmatched
4879
+ };
4880
+ }
4881
+ return { value: kind === "multi_select" ? matched : matched[0] };
4882
+ }
4883
+ // AN IDENTITY IS NOT A WORD. Writing the word would put a string where an
4884
+ // id belongs, and every filter, colour and rollup built on that column
4885
+ // would stop agreeing with it. So it is refused and the person is told
4886
+ // which control does hold it.
4887
+ case "member":
4888
+ return {
4889
+ refusal: `${label} names a person in this organization, so it is chosen rather than typed. Open it and pick them.`
4890
+ };
4891
+ case "attachment":
4892
+ return {
4893
+ refusal: `${label} holds a file, which cannot arrive as typed-in text. Open it and attach the file.`
4894
+ };
4895
+ case "relation":
4896
+ return {
4897
+ refusal: `${label} points at another record, so it is chosen rather than typed. Open it and pick the record.`
4898
+ };
4899
+ case "formula":
4900
+ case "lookup":
4901
+ case "rollup":
4902
+ return {
4903
+ refusal: `${label} is worked out by the system, so nothing can be written into it.`
4904
+ };
4905
+ default:
4906
+ return { value: raw };
4907
+ }
4908
+ }
4909
+ function coerceTypedAnswers(fields, answers, opts = {}) {
4910
+ const byKey = new Map(fields.map((f) => [f.key, f]));
4911
+ const values = {};
4912
+ const refusals = {};
4913
+ const unknownKeys = [];
4914
+ for (const [key, given] of Object.entries(answers)) {
4915
+ const field = byKey.get(key);
4916
+ if (!field) {
4917
+ unknownKeys.push(key);
4918
+ values[key] = given;
4919
+ continue;
4920
+ }
4921
+ if (typeof given !== "string") {
4922
+ values[key] = given;
4923
+ continue;
4924
+ }
4925
+ const opt = {};
4926
+ const choices = opts.options?.get(key);
4927
+ if (choices) opt.options = choices;
4928
+ if (opts.kernel) opt.kernel = opts.kernel;
4929
+ const label = opts.labels?.[key];
4930
+ if (label) opt.label = label;
4931
+ const answered = coerceTypedAnswer(field, given, opt);
4932
+ if ("refusal" in answered) refusals[key] = answered.refusal;
4933
+ else values[key] = answered.value;
4934
+ }
4935
+ return { values, refusals, unknownKeys };
4936
+ }
4937
+
4719
4938
  // src/pipeline.ts
4720
4939
  var PIPELINE_RULE_KINDS = ["moves", "requires", "who", "limit", "entry"];
4721
4940
  var STAGE_RULE_ON_FAIL = ["refuse", "require_approval"];
@@ -5032,6 +5251,7 @@ export {
5032
5251
  CONTEXT_POLICIES,
5033
5252
  DELIVERIES,
5034
5253
  ENTITY_TOKENS,
5254
+ FIELD_KINDS,
5035
5255
  FIELD_SENSITIVITIES,
5036
5256
  FIELD_SOURCES,
5037
5257
  FRESHNESS,
@@ -5070,7 +5290,11 @@ export {
5070
5290
  choiceSlug,
5071
5291
  choiceValuesOf,
5072
5292
  choicesOf,
5293
+ coerceTypedAnswer,
5294
+ coerceTypedAnswers,
5295
+ coercionLabel,
5073
5296
  err,
5297
+ fieldKindFor,
5074
5298
  forgetAllOps,
5075
5299
  highestLevel,
5076
5300
  holdsARetiredChoice,