@ai-matrx/records 0.96.0 → 0.96.2

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.
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/templates/index.ts
21
21
  var templates_exports = {};
22
22
  __export(templates_exports, {
23
+ AUTOMATION_EVENTS: () => AUTOMATION_EVENTS,
23
24
  BAD_OUTCOME: () => BAD_OUTCOME,
24
25
  BOOKING_AVAILABILITY_FACTS: () => BOOKING_AVAILABILITY_FACTS,
25
26
  CAPABILITY_WORDS: () => CAPABILITY_WORDS,
@@ -70,6 +71,7 @@ __export(templates_exports, {
70
71
  attachmentProblems: () => attachmentProblems,
71
72
  autofixDescribeSpec: () => autofixDescribeSpec,
72
73
  automationHeld: () => automationHeld,
74
+ automationSpec: () => automationSpec,
73
75
  blockTable: () => blockTable,
74
76
  bookingArgs: () => bookingArgs,
75
77
  bookingColumnsProblem: () => bookingColumnsProblem,
@@ -2714,7 +2716,21 @@ function checklistSpec(checklist, tables, forms, documents) {
2714
2716
  };
2715
2717
  }
2716
2718
  function automationHeld(a) {
2717
- return `The automation "${a.name}" is held for lane 11: no store door runs automations yet, so a template carrying one is held, never installed without it.`;
2719
+ return `The automation "${a.name}" has a trigger the install cannot set up (${a.when.event}); a template automation runs on created, due or overdue.`;
2720
+ }
2721
+ var AUTOMATION_EVENTS = ["created", "due", "overdue"];
2722
+ function automationSpec(a, fieldIds) {
2723
+ const text2 = (a.message?.trim() || a.then.trim()).slice(0, 600);
2724
+ const actions = [{ do: "notify", to: { author: true }, text: text2 }];
2725
+ const at = a.at && /^([01][0-9]|2[0-3]):[0-5][0-9]$/.test(a.at) ? a.at : "09:00";
2726
+ if (a.when.event === "created") return { name: a.name, trigger: { on: "row_added" }, actions };
2727
+ if (a.when.event === "due" || a.when.event === "overdue") {
2728
+ const field2 = a.when.field ? fieldIds[a.when.field] : void 0;
2729
+ if (!field2) throw new Error(`The automation "${a.name}" waits for "${a.when.field ?? "?"}", which "${a.when.table}" does not declare.`);
2730
+ const offset = a.when.event === "due" ? -Math.max(0, Math.round(a.when.daysBefore ?? 0)) : 1;
2731
+ return { name: a.name, trigger: { on: "date_arrives", field: field2, offset_days: offset, at }, actions };
2732
+ }
2733
+ throw new Error(automationHeld(a));
2718
2734
  }
2719
2735
  function bookingColumnsProblem(tables, booking) {
2720
2736
  const t = tables.find((x) => x.token === booking.table);
@@ -3216,6 +3232,7 @@ var DOOR_FOR = {
3216
3232
  document: "doc_template_save",
3217
3233
  "document.esign": "doc_sign",
3218
3234
  notification: "rule_declare",
3235
+ automation: "automation_declare",
3219
3236
  stage_rules: "pipeline_declare",
3220
3237
  row_action: "action_declare",
3221
3238
  "import.upsert": "io_import_begin",
@@ -3223,9 +3240,7 @@ var DOOR_FOR = {
3223
3240
  agent: "agent_context",
3224
3241
  "view.kanban.swimlanes": "view_declare"
3225
3242
  };
3226
- var OWED_BY = {
3227
- automation: "lane 11"
3228
- };
3243
+ var OWED_BY = {};
3229
3244
  var STORE_DOOR_NAMES = new Set(STORE_DOORS.map((d) => d.name));
3230
3245
  var STORE_FIELD_KINDS = new Set(BEHAVIOR_OF.keys());
3231
3246
  function capabilityStatus(word) {
@@ -3813,7 +3828,8 @@ function validateTemplate(raw, options = {}) {
3813
3828
  const e = effective(table, f);
3814
3829
  return e.contextPolicy !== "exclude" && e.sensitivity !== "restricted";
3815
3830
  };
3816
- if (own.length < TABLES_MIN || own.length > TABLES_MAX) say("tables", `A template installs ${TABLES_MIN}\u2013${TABLES_MAX} related tables; this one has ${own.length}.`);
3831
+ if (own.length === 0 || own.length > TABLES_MAX) say("tables", `A template installs ${TABLES_MIN}\u2013${TABLES_MAX} related tables; this one has ${own.length}.`);
3832
+ else if (own.length < TABLES_MIN) sayGallery("tables", `A template installs ${TABLES_MIN}\u2013${TABLES_MAX} related tables; this one has ${own.length}.`);
3817
3833
  const main = own[0];
3818
3834
  if (main && (main.rows.length < MAIN_TABLE_ROWS_MIN || main.rows.length > MAIN_TABLE_ROWS_MAX)) {
3819
3835
  say(`tables[0] (${main.token}).rows`, `The main table carries ${MAIN_TABLE_ROWS_MIN}\u2013${MAIN_TABLE_ROWS_MAX} realistic rows; it has ${main.rows.length}.`);
@@ -3851,12 +3867,12 @@ function validateTemplate(raw, options = {}) {
3851
3867
  if (seeds.length) {
3852
3868
  const hi = Math.max(...seeds);
3853
3869
  const lo = Math.min(...seeds);
3854
- if (new Set(seeds).size < 2) say(`${at}`, `Every seed row holds the same ${field2.label}; real records vary.`);
3870
+ if (new Set(seeds).size < 2) saySeed(`${at}`, `Every seed row holds the same ${field2.label}; real records vary.`);
3855
3871
  for (const rule of field2.rules ?? []) {
3856
3872
  const bound = Number(rule.value);
3857
- if (rule.kind === "max" && hi > 0 && bound > 100 * hi) say(`${at}.rules`, `The most "${field2.label}" allows (${rule.value}) is over 100\xD7 the largest real value (${hi}); write the real ceiling.`);
3858
- if (rule.kind === "min" && bound < -1e3 * Math.abs(lo)) say(`${at}.rules`, `The least "${field2.label}" allows (${rule.value}) is far below any real value (${lo}); write the real floor.`);
3859
- if (rule.kind === "min" && bound < 0 && lo >= 0) say(`${at}.rules`, `"${field2.label}" allows negative values, and no real one is negative; the floor is 0 or above.`);
3873
+ if (rule.kind === "max" && hi > 0 && bound > 100 * hi) saySeed(`${at}.rules`, `The most "${field2.label}" allows (${rule.value}) is over 100\xD7 the largest real value (${hi}); write the real ceiling.`);
3874
+ if (rule.kind === "min" && bound < -1e3 * Math.abs(lo)) saySeed(`${at}.rules`, `The least "${field2.label}" allows (${rule.value}) is far below any real value (${lo}); write the real floor.`);
3875
+ if (rule.kind === "min" && bound < 0 && lo >= 0) saySeed(`${at}.rules`, `"${field2.label}" allows negative values, and no real one is negative; the floor is 0 or above.`);
3860
3876
  }
3861
3877
  }
3862
3878
  }
@@ -4360,10 +4376,21 @@ function validateTemplate(raw, options = {}) {
4360
4376
  }
4361
4377
  } else if (!extra.prompt?.trim()) say(`${at}.prompt`, "An agent action says what the record chat opens with.");
4362
4378
  break;
4363
- case "automation":
4364
- if (needTable(extra.when.table, "when.table") && extra.when.field && !fieldOf(extra.when.table, extra.when.field)) say(`${at}.when.field`, `"${extra.when.table}" does not declare a field "${extra.when.field}".`);
4379
+ case "automation": {
4380
+ const ev = extra.when?.event;
4381
+ if (needTable(extra.when?.table, "when.table") && extra.when.field && !fieldOf(extra.when.table, extra.when.field)) say(`${at}.when.field`, `"${extra.when.table}" does not declare a field "${extra.when.field}".`);
4382
+ if (!AUTOMATION_EVENTS.includes(ev)) say(`${at}.when.event`, `An automation runs on ${AUTOMATION_EVENTS.join(", ")}; for "${ev}" use a notification.`);
4383
+ if (ev === "due" || ev === "overdue") {
4384
+ const f = extra.when.field ? fieldOf(extra.when.table, extra.when.field) : void 0;
4385
+ if (!extra.when.field) say(`${at}.when.field`, "A date reminder names the date field it waits for.");
4386
+ else if (f && f.parityType !== "datetime") say(`${at}.when.field`, `"${extra.when.field}" is not a date or datetime field, so nothing arrives to remind about.`);
4387
+ const d = extra.when.daysBefore;
4388
+ if (d != null && (!Number.isInteger(d) || d < 0 || d > 365)) say(`${at}.when.daysBefore`, "Say how many whole days before the date, 0 to 365.");
4389
+ }
4390
+ if (extra.at != null && !/^([01][0-9]|2[0-3]):[0-5][0-9]$/.test(extra.at)) say(`${at}.at`, "Say the time of day as HH:MM on a 24-hour clock, like 09:00.");
4365
4391
  if (!extra.then?.trim()) say(`${at}.then`, "Say what happens, in the business's words.");
4366
4392
  break;
4393
+ }
4367
4394
  default:
4368
4395
  say(at, `"${extra.kind}" is not an extra a template can carry.`);
4369
4396
  }
@@ -6044,7 +6071,8 @@ var TEMPLATE_PLAN_DOORS = [
6044
6071
  "rule_declare",
6045
6072
  "checklist_declare",
6046
6073
  "portal_declare",
6047
- "query_table_homes"
6074
+ "query_table_homes",
6075
+ "automation_declare"
6048
6076
  ];
6049
6077
  var ROW_BATCH = 10;
6050
6078
  var DERIVED = /* @__PURE__ */ new Set(["lookup", "rollup"]);
@@ -6224,6 +6252,8 @@ function compileInstallPlan(written) {
6224
6252
  }
6225
6253
  const needIds = /* @__PURE__ */ new Set([
6226
6254
  ...spec.extras.filter((e) => e.kind === "document" || e.kind === "row_action").map((e) => e.table),
6255
+ // A date reminder names its date Field by id.
6256
+ ...spec.extras.filter((e) => e.kind === "automation" && e.when.field).map((e) => e.when.table),
6227
6257
  ...spec.views.filter((v) => v.anyOf && Object.keys(v.anyOf).length).map((v) => v.table)
6228
6258
  ]);
6229
6259
  const fieldRefs = (token) => {
@@ -6389,7 +6419,13 @@ function compileInstallPlan(written) {
6389
6419
  });
6390
6420
  break;
6391
6421
  case "automation":
6392
- throw new Error(automationHeld(e));
6422
+ steps.push({
6423
+ label: `automations.${e.token}`,
6424
+ door: "automation_declare",
6425
+ args: { p_organization_id: "${org}", p_table_id: tableRef(e.when.table), p_spec: automationSpec(e, fieldRefs(e.when.table)), p_automation_id: null },
6426
+ save: { [`automations.${e.token}`]: ["automation_id"] }
6427
+ });
6428
+ break;
6393
6429
  default:
6394
6430
  throw new Error(`The install plan has no door for a ${e.kind} yet ("${e.token ?? "?"}"), so this template cannot be declared until it does.`);
6395
6431
  }
@@ -7550,6 +7586,36 @@ function switchJob(spec, fixes) {
7550
7586
  fixes.push(`job: ${spec.job} switched to ${to} (linked tables, no realism declarations)`);
7551
7587
  spec.job = to;
7552
7588
  }
7589
+ var ABSOLUTE_SEED = /^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})?)?$/;
7590
+ function fixSeedDates(spec, ctx, fixes) {
7591
+ const today = ctx.today && /^\d{4}-\d{2}-\d{2}$/.test(ctx.today) ? ctx.today : (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
7592
+ const t0 = Date.UTC(+today.slice(0, 4), +today.slice(5, 7) - 1, +today.slice(8, 10));
7593
+ for (const t of spec.tables) {
7594
+ const dates = t.fields.filter((f) => f.parityType === "datetime" && !f.absoluteDates);
7595
+ if (!dates.length || !t.rows?.length) continue;
7596
+ let rewritten = 0;
7597
+ for (const f of dates) {
7598
+ let timed = false;
7599
+ for (const r of t.rows) {
7600
+ const v = r.values?.[f.key];
7601
+ if (typeof v !== "string") continue;
7602
+ const m = ABSOLUTE_SEED.exec(v.trim());
7603
+ if (m) {
7604
+ const days = Math.round((Date.UTC(+m[1], +m[2] - 1, +m[3]) - t0) / 864e5);
7605
+ if (Math.abs(days) > 9999) continue;
7606
+ r.values[f.key] = `@today${days === 0 ? "" : days > 0 ? `+${days}d` : `${days}d`}${m[4] ? ` ${m[4]}:${m[5]}` : ""}`;
7607
+ rewritten++;
7608
+ }
7609
+ if (/ \d{2}:\d{2}$|T\d{2}:\d{2}/.test(String(r.values[f.key]))) timed = true;
7610
+ }
7611
+ if (timed && f.format !== "datetime") {
7612
+ f.format = "datetime";
7613
+ fixes.push(`tables.${t.token}.${f.key}: holds a time of day, so it is a date-and-time column`);
7614
+ }
7615
+ }
7616
+ if (rewritten) fixes.push(`tables.${t.token}.rows: ${rewritten} example date(s) written relative to today`);
7617
+ }
7618
+ }
7553
7619
  function autofixDescribeSpec(input, ctx = {}) {
7554
7620
  const spec = describeSpec(clone2(input));
7555
7621
  const autoFixes = [];
@@ -7572,14 +7638,51 @@ function autofixDescribeSpec(input, ctx = {}) {
7572
7638
  fixRelationships(spec, ctx, autoFixes);
7573
7639
  askRequired(spec, autoFixes);
7574
7640
  switchJob(spec, autoFixes);
7641
+ fixSeedDates(spec, ctx, autoFixes);
7575
7642
  return { spec, autoFixes };
7576
7643
  }
7644
+ var ROW_AT = /^tables(?:\[\d+\])? \((\w+)\)\.rows/;
7645
+ function rowProblemTables(spec, problems) {
7646
+ const out = /* @__PURE__ */ new Set();
7647
+ for (const p of problems) {
7648
+ const m = ROW_AT.exec(p.at);
7649
+ if (m) out.add(m[1]);
7650
+ else if (/^placeholder data "([^"]+)"/.test(p.says)) {
7651
+ const value = /^placeholder data "([^"]+)"/.exec(p.says)[1];
7652
+ for (const t of spec.tables) if (t.rows?.some((r) => JSON.stringify(r.values ?? {}).includes(value))) out.add(t.token);
7653
+ }
7654
+ }
7655
+ return out;
7656
+ }
7577
7657
  function describeCheck(spec, ctx = {}) {
7578
7658
  const fixed = autofixDescribeSpec(spec, ctx);
7579
7659
  const v = validateTemplate(fixed.spec, { profile: "describe" });
7580
7660
  const sameFields = (t) => (ctx.existingTables ?? []).some((e) => e.fields.map((f) => f.key).sort().join("|") === t.fields.map((f) => f.key).sort().join("|"));
7581
7661
  const reused = new Set(fixed.spec.tables.filter(sameFields).map((t) => t.token));
7582
- const problems = v.problems.filter((p) => !(/^This table copies the shared /.test(p.says) && reused.has(/\((\w+)\)/.exec(p.at)?.[1] ?? "")));
7662
+ const notReused = (ps) => ps.filter((p) => !(/^This table copies the shared /.test(p.says) && reused.has(/\((\w+)\)/.exec(p.at)?.[1] ?? "")));
7663
+ let problems = notReused(v.problems);
7664
+ for (let round = 0; round < 3; round++) {
7665
+ const bad = rowProblemTables(fixed.spec, problems);
7666
+ if (!bad.size) break;
7667
+ let grew = true;
7668
+ while (grew) {
7669
+ grew = false;
7670
+ for (const t of fixed.spec.tables) {
7671
+ if (bad.has(t.token) || !t.rows?.length) continue;
7672
+ if (t.fields.some((f) => f.relationTarget && bad.has(f.relationTarget) && t.rows.some((r) => r.values?.[f.key] != null))) {
7673
+ bad.add(t.token);
7674
+ grew = true;
7675
+ }
7676
+ }
7677
+ }
7678
+ for (const t of fixed.spec.tables) {
7679
+ if (!bad.has(t.token) || !t.rows?.length) continue;
7680
+ const why = problems.find((p) => ROW_AT.exec(p.at)?.[1] === t.token)?.says ?? "they did not pass the check";
7681
+ fixed.autoFixes.push(`tables.${t.token}.rows: example rows left out (${why.slice(0, 120)})`);
7682
+ t.rows = [];
7683
+ }
7684
+ problems = notReused(validateTemplate(fixed.spec, { profile: "describe" }).problems);
7685
+ }
7583
7686
  return { ...fixed, problems: problems.map((p) => ({ at: p.at, says: p.says })) };
7584
7687
  }
7585
7688
 
@@ -7808,6 +7911,10 @@ var FORMULA_SHAPE_FACTS = {
7808
7911
  rule: "a formula names every field by its LABEL in braces ({Quantity}), never by key; operators are + - * / % only (no >, <, = or comparisons), functions are IF, ROUND, SUM, DATEDIFF and the other store functions; it never reads a lookup or rollup, nor itself.",
7809
7912
  example: { key: "line_total", label: "Line total", parityType: "formula", formula: "{Quantity} * {Unit price}" }
7810
7913
  };
7914
+ var AUTOMATION_SHAPE_FACTS = {
7915
+ rule: `an automation extra is {kind:"automation", token, name, when:{table, event, field?, daysBefore?}, then, message?, at?}. event is one of: due (a date field is coming up \u2014 field = the key of a datetime field of that table, daysBefore = whole days ahead 0 to 365), overdue (the day after that date field), created (every new row). then says what happens in the business's words; message is the reminder text the person reads (no merge fields); at is the time of day HH:MM (default 09:00). It always tells the person who set it up. Add one only when the sentence asks for a reminder, a heads-up or a follow-up nudge.`,
7916
+ example: { kind: "automation", token: "remind_before_post", name: "Reminder 2 days before a post", when: { table: "post", event: "due", field: "publish_at", daysBefore: 2 }, then: "remind me two days before a post goes out", message: "A post goes out in 2 days \u2014 check the copy and image.", at: "09:00" }
7917
+ };
7811
7918
  var RELATIONSHIP_SHAPE_FACTS = {
7812
7919
  rule: "a relationship's inverseKey is the name the OTHER table sees this link under; it must not be a key that table already has as a field. A parent that lists its children owns ONE multi relation field (multi true); the child's single link to the parent is a second field, and their inverseKeys differ from every field key.",
7813
7920
  example: { fromTable: "request", fromField: "client", toTable: "client", flavor: "referenced", cardinality: "one", onDelete: "restrict", inverseKey: "requests_from" }
@@ -7831,7 +7938,7 @@ function describeVocabulary() {
7831
7938
  template_view_kinds: TEMPLATE_VIEW_KINDS,
7832
7939
  template_audiences: TEMPLATE_AUDIENCES,
7833
7940
  form_audiences: ["public", "member", "staff"],
7834
- extra_kinds_for_describe: ["booking"],
7941
+ extra_kinds_for_describe: ["booking", "automation"],
7835
7942
  booking_window: { weekday: "0 = Sunday \u2026 6 = Saturday", from: "HH:MM", to: "HH:MM later than from" },
7836
7943
  booking_availability: BOOKING_AVAILABILITY_FACTS,
7837
7944
  view_shape: VIEW_SHAPE_FACTS,
@@ -7841,6 +7948,7 @@ function describeVocabulary() {
7841
7948
  field_shape: FIELD_SHAPE_FACTS,
7842
7949
  formula_shape: FORMULA_SHAPE_FACTS,
7843
7950
  relationship_shape: RELATIONSHIP_SHAPE_FACTS,
7951
+ automation_shape: AUTOMATION_SHAPE_FACTS,
7844
7952
  phone_rule: "seed phone numbers are fictional: 555-0100 to 555-0199 (e.g. (813) 555-0142); never a real-looking number, and never a number from a country the person did not name.",
7845
7953
  team_rule: "the team (name, role, work email, mobile, start date) is the shared team_member block; do not declare a table with those fields \u2014 keep a table of your own to just name and role, or point a relation field at team_member.",
7846
7954
  title_field: "a table's titleField is the key of a plain text-like field of that same table (a name, a title) \u2014 never a link to another table, a formula or a timed date"