@ai-matrx/records 0.76.10 → 0.76.12

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog — @ai-matrx/records
2
2
 
3
+ ## 0.76.11
4
+
5
+ Automatic changed-only republish (docs/metadata drift since the last tag — see
6
+ `git diff npm/records/v0.76.10..npm/records/v0.76.11 -- apps/shared/records`).
7
+ No source changes intended and no consumer action required.
8
+
3
9
  ## 0.76.10
4
10
 
5
11
  Automatic changed-only republish (docs/metadata drift since the last tag — see
@@ -104,6 +104,7 @@ __export(templates_exports, {
104
104
  isSelfDeclaredField: () => isSelfDeclaredField,
105
105
  isTimedField: () => isTimedField,
106
106
  lastResortTrim: () => lastResortTrim,
107
+ meaningWords: () => meaningWords,
107
108
  membersAddRowsSteps: () => membersAddRowsSteps,
108
109
  mergeDescribeParts: () => mergeDescribeParts,
109
110
  normalizeForPhones: () => normalizeForPhones,
@@ -133,6 +134,7 @@ __export(templates_exports, {
133
134
  rowsFor: () => rowsFor,
134
135
  ruleBreak: () => ruleBreak,
135
136
  runTemplateDoor: () => runTemplateDoor,
137
+ sameMeaning: () => sameMeaning,
136
138
  selfDeclaredFieldSpec: () => selfDeclaredFieldSpec,
137
139
  sensitivityFloorOf: () => sensitivityFloorOf,
138
140
  shellsPayload: () => shellsPayload,
@@ -4528,8 +4530,8 @@ function validateTemplate(raw, options = {}) {
4528
4530
  for (const k of rule.filled ?? []) if (!filled(val(k))) say(at, `"${row.key}" is ${st}, so "${fieldOf(table, k)?.label ?? k}" is filled; it is empty.`);
4529
4531
  for (const [k, v] of Object.entries(rule.equals ?? {})) {
4530
4532
  const got = val(k) ?? null;
4531
- const same = sameJson(got, v) || typeof v === "number" && parseNumeric(storeText(got) ?? "") !== null && qCmp(parseNumeric(storeText(got) ?? ""), parseNumeric(String(v))) === 0;
4532
- if (!same) say(at, `"${row.key}" is ${st}, so "${fieldOf(table, k)?.label ?? k}" is ${JSON.stringify(v)}; it is ${JSON.stringify(got)}.`);
4533
+ const same2 = sameJson(got, v) || typeof v === "number" && parseNumeric(storeText(got) ?? "") !== null && qCmp(parseNumeric(storeText(got) ?? ""), parseNumeric(String(v))) === 0;
4534
+ if (!same2) say(at, `"${row.key}" is ${st}, so "${fieldOf(table, k)?.label ?? k}" is ${JSON.stringify(v)}; it is ${JSON.stringify(got)}.`);
4533
4535
  }
4534
4536
  }
4535
4537
  };
@@ -5866,7 +5868,7 @@ ${e.body}`.matchAll(MERGE_TOKEN)].map((m) => m[1]));
5866
5868
  }
5867
5869
  }
5868
5870
  const rows = t.rows;
5869
- const same = (x, y) => {
5871
+ const same2 = (x, y) => {
5870
5872
  const a = storeText(x);
5871
5873
  const b = storeText(y);
5872
5874
  if (a === null || b === null) return false;
@@ -5884,7 +5886,7 @@ ${e.body}`.matchAll(MERGE_TOKEN)].map((m) => m[1]));
5884
5886
  };
5885
5887
  for (const sf of t.fields.filter((ff) => !isComputed(ff) && !isRelation(ff) && ff.key !== l.key)) {
5886
5888
  const have = rows.filter((r) => filled(r.values?.[sf.key]));
5887
- if (have.length >= 3 && have.every((r) => same(r.values?.[sf.key], far(r)))) {
5889
+ if (have.length >= 3 && have.every((r) => same2(r.values?.[sf.key], far(r)))) {
5888
5890
  say(`tables (${t.token}).fields (${sf.key})`, `"${sf.label}" repeats "${l.label}" on every row; drop it and use the lookup, which follows the linked record.`);
5889
5891
  }
5890
5892
  }
@@ -6569,6 +6571,33 @@ function describeField(f, table) {
6569
6571
  };
6570
6572
  }
6571
6573
 
6574
+ // src/templates/describe/meaning.ts
6575
+ var FILLER = /* @__PURE__ */ new Set(["of", "the", "a", "an", "for", "to", "in", "on", "and", "or", "your", "their", "is", "are", "was", "when", "what", "which", "with", "my", "our"]);
6576
+ function stemWord(w) {
6577
+ let s = w.toLowerCase().replace(/[^a-z0-9]/g, "");
6578
+ if (s.length > 4 && s.endsWith("ies")) s = s.slice(0, -3) + "y";
6579
+ else if (s.length > 4 && s.endsWith("es") && /(ch|sh|x|ss|s)es$/.test(s)) s = s.slice(0, -2);
6580
+ else if (s.length > 3 && s.endsWith("s") && !s.endsWith("ss")) s = s.slice(0, -1);
6581
+ if (s.length > 6 && s.endsWith("ation")) s = s.slice(0, -5);
6582
+ else if (s.length > 6 && s.endsWith("ion")) s = s.slice(0, -3);
6583
+ if (s.length > 4 && (s.endsWith("y") || s.endsWith("e"))) s = s.slice(0, -1);
6584
+ return s;
6585
+ }
6586
+ function meaningWords(text) {
6587
+ const words = text.replace(/([a-z])([A-Z])/g, "$1 $2").split(/[^A-Za-z0-9]+/).filter(Boolean).map((w) => w.toLowerCase()).filter((w) => !FILLER.has(w)).map(stemWord).filter(Boolean);
6588
+ return [...new Set(words)].sort();
6589
+ }
6590
+ var same = (a, b) => a.length > 0 && a.length === b.length && a.every((w, i) => w === b[i]);
6591
+ function sameMeaning(a, b) {
6592
+ if (a.parityType !== b.parityType) return false;
6593
+ const ka = meaningWords(a.key);
6594
+ const kb = meaningWords(b.key);
6595
+ if (same(ka, kb)) return true;
6596
+ const la = a.label ? meaningWords(a.label) : [];
6597
+ const lb = b.label ? meaningWords(b.label) : [];
6598
+ return same(la, lb);
6599
+ }
6600
+
6572
6601
  // src/templates/describe/merge.ts
6573
6602
  var clone = (v) => v === void 0 ? v : JSON.parse(JSON.stringify(v));
6574
6603
  var labelOfKey = (key) => {
@@ -6601,7 +6630,7 @@ function ensureSkeleton(t, p) {
6601
6630
  t.fields.push({ key: "status", label: "Status", parityType: "select", required: true, choices: [...p.status.choices], default: p.status.default });
6602
6631
  }
6603
6632
  if (p.slot && !field(t, "slot")) t.fields.push({ key: "slot", label: "Visit time", parityType: "datetime", format: "datetime", required: true });
6604
- for (const l of p.links) {
6633
+ for (const l of p.links ?? []) {
6605
6634
  if (field(t, l.key)) continue;
6606
6635
  t.fields.push({ key: l.key, label: l.label, parityType: "relation", relationTarget: l.target, ...l.many ? { multi: true } : { relationMax: 1 } });
6607
6636
  }
@@ -6610,7 +6639,7 @@ function addExistingKeys(t, p, existing) {
6610
6639
  if (!p.reuse) return;
6611
6640
  const e = existing.find((x) => x.id === p.reuse.existing_table_id);
6612
6641
  if (!e) return;
6613
- for (const key of p.reuse.keepKeys) {
6642
+ for (const key of p.reuse.keepKeys ?? []) {
6614
6643
  if (field(t, key)) continue;
6615
6644
  const ef = e.fields.find((f) => f.key === key);
6616
6645
  if (!ef) continue;
@@ -6618,7 +6647,9 @@ function addExistingKeys(t, p, existing) {
6618
6647
  }
6619
6648
  }
6620
6649
  function addRequest(t, r) {
6621
- if (field(t, r.key)) return;
6650
+ if (field(t, r.key)) return void 0;
6651
+ const twin = t.fields.find((f2) => sameMeaning(f2, r));
6652
+ if (twin) return twin.key;
6622
6653
  const f = { key: r.key, label: r.label, parityType: r.parityType };
6623
6654
  if (r.choices?.length) f.choices = [...r.choices];
6624
6655
  if (r.askOfSubject) f.askOfSubject = true;
@@ -6626,6 +6657,7 @@ function addRequest(t, r) {
6626
6657
  if (r.required) f.required = true;
6627
6658
  if (r.help) f.help = r.help;
6628
6659
  t.fields.push(f);
6660
+ return void 0;
6629
6661
  }
6630
6662
  var CHOICE = ["select", "multi_select", "list"];
6631
6663
  var STAGE_KEY = /(^|_)(status|stage)$/;
@@ -6687,7 +6719,7 @@ function sharedBlocksFor(tables) {
6687
6719
  function reusesOf(plan) {
6688
6720
  return plan.tables.filter((t) => t.reuse).map((t) => ({ token: t.token, existing_table_id: t.reuse.existing_table_id }));
6689
6721
  }
6690
- function mergeDescribeParts(plan, tables, doors, views, existingTables = []) {
6722
+ function mergeDescribeParts(plan, tables, doors, views, existingTables = [], notes = []) {
6691
6723
  const out = clone(Array.isArray(tables?.tables) ? tables.tables : []);
6692
6724
  const byToken = new Map(out.map((t) => [t.token, t]));
6693
6725
  for (const p of plan.tables) {
@@ -6700,10 +6732,25 @@ function mergeDescribeParts(plan, tables, doors, views, existingTables = []) {
6700
6732
  ensureSkeleton(t, p);
6701
6733
  addExistingKeys(t, p, existingTables);
6702
6734
  }
6735
+ const repoint = /* @__PURE__ */ new Map();
6703
6736
  for (const r of doors?.fieldRequests ?? []) {
6704
6737
  const t = byToken.get(r.table);
6705
- if (t) addRequest(t, r);
6706
- }
6738
+ if (!t) continue;
6739
+ const twin = addRequest(t, r);
6740
+ if (!twin) continue;
6741
+ if (!repoint.has(r.table)) repoint.set(r.table, /* @__PURE__ */ new Map());
6742
+ repoint.get(r.table).set(r.key, twin);
6743
+ notes.push(`tables.${r.table}.fields.${r.key}: dropped, it means the same as "${twin}" (questions now ask "${twin}")`);
6744
+ }
6745
+ const forms = clone(doors?.forms ?? []);
6746
+ const bookingOut = doors?.booking ? clone(doors.booking) : void 0;
6747
+ const repointQuestions = (table, qs) => {
6748
+ const m = repoint.get(table);
6749
+ if (!m || !Array.isArray(qs)) return;
6750
+ for (const q2 of qs) if (m.has(q2.fieldKey)) q2.fieldKey = m.get(q2.fieldKey);
6751
+ };
6752
+ for (const f of forms) repointQuestions(f.table, f.fields);
6753
+ if (bookingOut) repointQuestions(bookingOut.table, bookingOut.questions);
6707
6754
  const spec = {
6708
6755
  specVersion: 1,
6709
6756
  version: 1,
@@ -6720,9 +6767,9 @@ function mergeDescribeParts(plan, tables, doors, views, existingTables = []) {
6720
6767
  sharedBlocks: sharedBlocksFor(out),
6721
6768
  relationships: clone(tables?.relationships ?? []),
6722
6769
  views: mergeViews(plan, views, byToken),
6723
- forms: clone(doors?.forms ?? []),
6770
+ forms,
6724
6771
  dimensions: [],
6725
- extras: doors?.booking ? [clone(doors.booking)] : []
6772
+ extras: bookingOut ? [bookingOut] : []
6726
6773
  };
6727
6774
  return spec;
6728
6775
  }
@@ -6738,6 +6785,62 @@ var GOOD_ORDER = ["green", "teal", "blue", "violet", "slate", "amber", "orange",
6738
6785
  var BAD_ORDER = ["red", "orange", "gray", "slate", "brown", "pink", "amber", "yellow", "violet", "blue", "teal"];
6739
6786
  var DISTINCT_UP_TO = 7;
6740
6787
  var lower = (s) => s.trim().toLowerCase();
6788
+ function linkedTables(spec, token) {
6789
+ const out = [];
6790
+ const add = (x) => {
6791
+ const t = spec.tables.find((y) => y.token === x);
6792
+ if (t && t.token !== token && !out.includes(t)) out.push(t);
6793
+ };
6794
+ const own = spec.tables.find((t) => t.token === token);
6795
+ for (const f of own?.fields ?? []) if (f.parityType === "relation" && f.relationTarget) add(f.relationTarget);
6796
+ for (const t of spec.tables) if (t.fields.some((f) => f.parityType === "relation" && f.relationTarget === token)) add(t.token);
6797
+ for (const r of spec.relationships ?? []) {
6798
+ if (r.fromTable === token) add(r.toTable);
6799
+ if (r.toTable === token) add(r.fromTable);
6800
+ }
6801
+ return out;
6802
+ }
6803
+ function fixMissingQuestionFields(spec, fixes) {
6804
+ const doors = [];
6805
+ for (const form of spec.forms) doors.push({ where: `forms.${form.token}`, table: form.table, questions: form.fields ?? [], set: (q2) => form.fields = q2 });
6806
+ for (const e of spec.extras ?? []) {
6807
+ if (e.kind !== "booking") continue;
6808
+ const b = e;
6809
+ doors.push({ where: `extras.${b.token}`, table: b.table, questions: b.questions ?? [], set: (q2) => b.questions = q2 });
6810
+ }
6811
+ for (const d of doors) {
6812
+ const t = spec.tables.find((x) => x.token === d.table);
6813
+ if (!t) continue;
6814
+ const kept = [];
6815
+ for (const q2 of d.questions) {
6816
+ if (t.fields.some((f) => f.key === q2.fieldKey)) {
6817
+ kept.push(q2);
6818
+ continue;
6819
+ }
6820
+ const source = linkedTables(spec, t.token).find((o) => o.fields.some((f) => f.key === q2.fieldKey));
6821
+ if (source) {
6822
+ const copy = clone2(source.fields.find((f) => f.key === q2.fieldKey));
6823
+ delete copy.required;
6824
+ delete copy.unique;
6825
+ const isDate2 = copy.parityType === "datetime";
6826
+ if (isDate2 && !copy.absoluteDates && /birth|dob|born/i.test(`${copy.key} ${copy.label}`)) {
6827
+ copy.absoluteDates = true;
6828
+ copy.historicalReason = copy.historicalReason?.trim() || "date of birth";
6829
+ }
6830
+ const hasSchedule = t.fields.some((f) => f.parityType === "datetime" && !f.absoluteDates);
6831
+ t.fields.push(copy);
6832
+ if (isDate2 && !copy.absoluteDates && hasSchedule && !t.before?.length && !t.datesIndependent?.trim()) {
6833
+ t.datesIndependent = `"${copy.label}" is the linked ${source.token}'s own date; it does not depend on the table's other dates.`;
6834
+ }
6835
+ fixes.push(`tables.${t.token}.fields.${q2.fieldKey}: copied from linked table "${source.token}" (${d.where} asks it)`);
6836
+ kept.push(q2);
6837
+ } else if (!q2.required) {
6838
+ fixes.push(`${d.where}.fields.${q2.fieldKey}: question dropped ("${t.token}" declares no such field and no linked table has it)`);
6839
+ } else kept.push(q2);
6840
+ }
6841
+ if (kept.length !== d.questions.length) d.set(kept);
6842
+ }
6843
+ }
6741
6844
  function raiseSensitivity(spec, fixes) {
6742
6845
  for (const t of spec.tables) {
6743
6846
  for (const f of t.fields) {
@@ -6992,6 +7095,7 @@ function switchJob(spec, fixes) {
6992
7095
  function autofixDescribeSpec(input, ctx = {}) {
6993
7096
  const spec = describeSpec(clone2(input));
6994
7097
  const autoFixes = [];
7098
+ fixMissingQuestionFields(spec, autoFixes);
6995
7099
  raiseSensitivity(spec, autoFixes);
6996
7100
  flagAskedOfSubject(spec, ctx, autoFixes);
6997
7101
  fixBooking(spec, autoFixes);