@ai-matrx/records 0.76.16 → 0.76.19

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 (40) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/app-table/index.cjs.map +1 -1
  3. package/dist/app-table/index.d.cts +3 -3
  4. package/dist/app-table/index.d.ts +3 -3
  5. package/dist/app-table/index.js.map +1 -1
  6. package/dist/{client-kVZ54twD.d.ts → client-C8G7zUMQ.d.ts} +1 -1
  7. package/dist/{client-PaKP4Cnd.d.cts → client-EaE4ta6p.d.cts} +1 -1
  8. package/dist/core/index.cjs.map +1 -1
  9. package/dist/core/index.d.cts +3 -3
  10. package/dist/core/index.d.ts +3 -3
  11. package/dist/core/index.js.map +1 -1
  12. package/dist/data/index.cjs.map +1 -1
  13. package/dist/data/index.d.cts +2 -2
  14. package/dist/data/index.d.ts +2 -2
  15. package/dist/data/index.js.map +1 -1
  16. package/dist/{declareTable-4MK51nXs.d.ts → declareTable-3yW8GrJE.d.ts} +1 -1
  17. package/dist/{declareTable-Bv4V8Bjm.d.cts → declareTable-aoFu32Ca.d.cts} +1 -1
  18. package/dist/index.cjs.map +1 -1
  19. package/dist/index.d.cts +1 -1
  20. package/dist/index.d.ts +1 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/memory/index.cjs.map +1 -1
  23. package/dist/memory/index.d.cts +2 -2
  24. package/dist/memory/index.d.ts +2 -2
  25. package/dist/memory/index.js.map +1 -1
  26. package/dist/react/index.cjs.map +1 -1
  27. package/dist/react/index.d.cts +7 -1
  28. package/dist/react/index.d.ts +7 -1
  29. package/dist/react/index.js.map +1 -1
  30. package/dist/{subscription-D8OHY3kw.d.ts → subscription-DxtZgyFR.d.ts} +7 -1
  31. package/dist/{subscription-BJgDMerQ.d.cts → subscription-vpnl6GxJ.d.cts} +7 -1
  32. package/dist/templates/index.cjs +17 -6
  33. package/dist/templates/index.cjs.map +1 -1
  34. package/dist/templates/index.d.cts +5 -5
  35. package/dist/templates/index.d.ts +5 -5
  36. package/dist/templates/index.js +17 -6
  37. package/dist/templates/index.js.map +1 -1
  38. package/dist/{types-Mx-Wzm_d.d.cts → types-X9P-cQd0.d.cts} +1 -1
  39. package/dist/{types-C6gdqgsI.d.ts → types-y8GX3-QC.d.ts} +1 -1
  40. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
- import { A as AgentSnapshotRow, T as TemplateSpec, a as TemplateTable, S as SharedBlockId, C as CapabilityWord, b as TemplateField, c as TemplateAgent, d as TemplateAutomation, e as TemplateBooking, f as TemplateChecklist, g as TemplateDashboard, h as TemplateDigest, i as TemplateDocument, j as TemplateDrillDown, k as TemplateDimensions, l as TemplateForm, m as TemplateNotification, n as TemplatePortal, o as TemplateRowAction, p as TemplateStageRules, q as TemplateView, r as TemplateMergeBinding, s as TemplateTryIt, t as TemplateWorkflow, u as TemplateFootprint, I as IndustryGroup, v as TemplateJob, w as TeachesFeature, x as TemplateViewKind, y as TemplateBookingWindow } from '../types-Mx-Wzm_d.cjs';
2
- export { z as CAPABILITY_WORDS, B as CATALOGUE_QUOTAS, D as CATALOGUE_STATUSES, E as CHOICE_COLORS, F as CHOICE_COLORS_DISTINCT_UP_TO, G as CatalogueRow, H as CatalogueStatus, J as ChoiceColor, K as ConfidentialLevel, L as ConfidentialRequest, M as DEFAULT_TEMPLATE_AGENT, N as ENTITY_REFERENCE_KINDS, O as INDUSTRY_GROUPS, R as RELATIVE_DATE, P as RetiredId, Q as SHARED_BLOCKS, U as SHARED_BLOCK_IDS, V as STRENGTHS, W as SharedBlock, X as SharedBlockField, Y as SharedBlockUse, Z as Strength, _ as TABLE_KERNEL_ID, $ as TEACHES_FEATURES, a0 as TEMPLATE_AUDIENCES, a1 as TEMPLATE_EXTRA_KINDS, a2 as TEMPLATE_JOBS, a3 as TEMPLATE_REFERENCE_DATE, a4 as TEMPLATE_SPEC_VERSION, a5 as TEMPLATE_VIEW_KINDS, a6 as TemplateAgentQuestion, a7 as TemplateAgentVariable, a8 as TemplateAudience, a9 as TemplateCalendarView, aa as TemplateChecklistStep, ab as TemplateConfidential, ac as TemplateConfidentialReader, ad as TemplateDashboardBlock, ae as TemplateDimensionOverride, af as TemplateDrillPath, ag as TemplateExtra, ah as TemplateExtraAgent, ai as TemplateExtraAgentVariable, aj as TemplateExtraKind, ak as TemplateFormField, al as TemplateFoundation, am as TemplateGalleryView, an as TemplateGridView, ao as TemplateGuideStep, ap as TemplateKanbanView, aq as TemplateMeasureOverride, ar as TemplatePortalTable, as as TemplateProvenance, at as TemplateSource, au as TemplateTableSettings, av as TemplateTimelineView, aw as TemplateViewSort, ax as UseCaseSingulars, ay as WalkAction, az as WalkAssertion, aA as WalkImport, aB as WalkMove, aC as WalkStep, aD as confidentialPending, aE as confidentialRequests, aF as footprintOf, aG as membersAddRowsSteps, aH as tableSettingsProblems, aI as whenRule } from '../types-Mx-Wzm_d.cjs';
3
- import { g as AggregateMeasure, p as AggregateFilter } from '../subscription-BJgDMerQ.cjs';
1
+ import { A as AgentSnapshotRow, T as TemplateSpec, a as TemplateTable, S as SharedBlockId, C as CapabilityWord, b as TemplateField, c as TemplateAgent, d as TemplateAutomation, e as TemplateBooking, f as TemplateChecklist, g as TemplateDashboard, h as TemplateDigest, i as TemplateDocument, j as TemplateDrillDown, k as TemplateDimensions, l as TemplateForm, m as TemplateNotification, n as TemplatePortal, o as TemplateRowAction, p as TemplateStageRules, q as TemplateView, r as TemplateMergeBinding, s as TemplateTryIt, t as TemplateWorkflow, u as TemplateFootprint, I as IndustryGroup, v as TemplateJob, w as TeachesFeature, x as TemplateViewKind, y as TemplateBookingWindow } from '../types-X9P-cQd0.cjs';
2
+ export { z as CAPABILITY_WORDS, B as CATALOGUE_QUOTAS, D as CATALOGUE_STATUSES, E as CHOICE_COLORS, F as CHOICE_COLORS_DISTINCT_UP_TO, G as CatalogueRow, H as CatalogueStatus, J as ChoiceColor, K as ConfidentialLevel, L as ConfidentialRequest, M as DEFAULT_TEMPLATE_AGENT, N as ENTITY_REFERENCE_KINDS, O as INDUSTRY_GROUPS, R as RELATIVE_DATE, P as RetiredId, Q as SHARED_BLOCKS, U as SHARED_BLOCK_IDS, V as STRENGTHS, W as SharedBlock, X as SharedBlockField, Y as SharedBlockUse, Z as Strength, _ as TABLE_KERNEL_ID, $ as TEACHES_FEATURES, a0 as TEMPLATE_AUDIENCES, a1 as TEMPLATE_EXTRA_KINDS, a2 as TEMPLATE_JOBS, a3 as TEMPLATE_REFERENCE_DATE, a4 as TEMPLATE_SPEC_VERSION, a5 as TEMPLATE_VIEW_KINDS, a6 as TemplateAgentQuestion, a7 as TemplateAgentVariable, a8 as TemplateAudience, a9 as TemplateCalendarView, aa as TemplateChecklistStep, ab as TemplateConfidential, ac as TemplateConfidentialReader, ad as TemplateDashboardBlock, ae as TemplateDimensionOverride, af as TemplateDrillPath, ag as TemplateExtra, ah as TemplateExtraAgent, ai as TemplateExtraAgentVariable, aj as TemplateExtraKind, ak as TemplateFormField, al as TemplateFoundation, am as TemplateGalleryView, an as TemplateGridView, ao as TemplateGuideStep, ap as TemplateKanbanView, aq as TemplateMeasureOverride, ar as TemplatePortalTable, as as TemplateProvenance, at as TemplateSource, au as TemplateTableSettings, av as TemplateTimelineView, aw as TemplateViewSort, ax as UseCaseSingulars, ay as WalkAction, az as WalkAssertion, aA as WalkImport, aB as WalkMove, aC as WalkStep, aD as confidentialPending, aE as confidentialRequests, aF as footprintOf, aG as membersAddRowsSteps, aH as tableSettingsProblems, aI as whenRule } from '../types-X9P-cQd0.cjs';
3
+ import { g as AggregateMeasure, p as AggregateFilter } from '../subscription-vpnl6GxJ.cjs';
4
4
  import { U as UseCase, o as UseCaseRelationship } from '../types-DWoJFBbr.cjs';
5
5
  import { U as UseCaseProblem } from '../validate-CEE0ts9g.cjs';
6
6
  export { R as RESOLVABLE_FIELD_TYPE_WORDS } from '../validate-CEE0ts9g.cjs';
@@ -49,9 +49,9 @@ type AggregateOutcome = {
49
49
  };
50
50
  /**
51
51
  * custom.record_aggregate for ONE measure over seed rows (live, non-archived), as the store
52
- * computes it: count(*); sum/avg/min/max/median over `nullif(text,'')::numeric` (a text that
52
+ * computes it: count(*); sum/avg/min/max/median/range over `nullif(text,'')::numeric` (a text that
53
53
  * is not a number is a refusal; no values at all is NULL, not 0); filled/empty/unique by
54
- * `nullif(text,'')`. Exact: an average that does not terminate is reported as inexact.
54
+ * `nullif(text,'')`; percent_empty/percent_filled as 100 x share of the rows (NULL over none). Exact: an average that does not terminate is reported as inexact.
55
55
  */
56
56
  /** custom.formula_node_kinds(), the functions a formula may call (read on the clone 2026-10-03). */
57
57
  declare const FORMULA_FUNCTIONS: Set<string>;
@@ -1,6 +1,6 @@
1
- import { A as AgentSnapshotRow, T as TemplateSpec, a as TemplateTable, S as SharedBlockId, C as CapabilityWord, b as TemplateField, c as TemplateAgent, d as TemplateAutomation, e as TemplateBooking, f as TemplateChecklist, g as TemplateDashboard, h as TemplateDigest, i as TemplateDocument, j as TemplateDrillDown, k as TemplateDimensions, l as TemplateForm, m as TemplateNotification, n as TemplatePortal, o as TemplateRowAction, p as TemplateStageRules, q as TemplateView, r as TemplateMergeBinding, s as TemplateTryIt, t as TemplateWorkflow, u as TemplateFootprint, I as IndustryGroup, v as TemplateJob, w as TeachesFeature, x as TemplateViewKind, y as TemplateBookingWindow } from '../types-C6gdqgsI.js';
2
- export { z as CAPABILITY_WORDS, B as CATALOGUE_QUOTAS, D as CATALOGUE_STATUSES, E as CHOICE_COLORS, F as CHOICE_COLORS_DISTINCT_UP_TO, G as CatalogueRow, H as CatalogueStatus, J as ChoiceColor, K as ConfidentialLevel, L as ConfidentialRequest, M as DEFAULT_TEMPLATE_AGENT, N as ENTITY_REFERENCE_KINDS, O as INDUSTRY_GROUPS, R as RELATIVE_DATE, P as RetiredId, Q as SHARED_BLOCKS, U as SHARED_BLOCK_IDS, V as STRENGTHS, W as SharedBlock, X as SharedBlockField, Y as SharedBlockUse, Z as Strength, _ as TABLE_KERNEL_ID, $ as TEACHES_FEATURES, a0 as TEMPLATE_AUDIENCES, a1 as TEMPLATE_EXTRA_KINDS, a2 as TEMPLATE_JOBS, a3 as TEMPLATE_REFERENCE_DATE, a4 as TEMPLATE_SPEC_VERSION, a5 as TEMPLATE_VIEW_KINDS, a6 as TemplateAgentQuestion, a7 as TemplateAgentVariable, a8 as TemplateAudience, a9 as TemplateCalendarView, aa as TemplateChecklistStep, ab as TemplateConfidential, ac as TemplateConfidentialReader, ad as TemplateDashboardBlock, ae as TemplateDimensionOverride, af as TemplateDrillPath, ag as TemplateExtra, ah as TemplateExtraAgent, ai as TemplateExtraAgentVariable, aj as TemplateExtraKind, ak as TemplateFormField, al as TemplateFoundation, am as TemplateGalleryView, an as TemplateGridView, ao as TemplateGuideStep, ap as TemplateKanbanView, aq as TemplateMeasureOverride, ar as TemplatePortalTable, as as TemplateProvenance, at as TemplateSource, au as TemplateTableSettings, av as TemplateTimelineView, aw as TemplateViewSort, ax as UseCaseSingulars, ay as WalkAction, az as WalkAssertion, aA as WalkImport, aB as WalkMove, aC as WalkStep, aD as confidentialPending, aE as confidentialRequests, aF as footprintOf, aG as membersAddRowsSteps, aH as tableSettingsProblems, aI as whenRule } from '../types-C6gdqgsI.js';
3
- import { g as AggregateMeasure, p as AggregateFilter } from '../subscription-D8OHY3kw.js';
1
+ import { A as AgentSnapshotRow, T as TemplateSpec, a as TemplateTable, S as SharedBlockId, C as CapabilityWord, b as TemplateField, c as TemplateAgent, d as TemplateAutomation, e as TemplateBooking, f as TemplateChecklist, g as TemplateDashboard, h as TemplateDigest, i as TemplateDocument, j as TemplateDrillDown, k as TemplateDimensions, l as TemplateForm, m as TemplateNotification, n as TemplatePortal, o as TemplateRowAction, p as TemplateStageRules, q as TemplateView, r as TemplateMergeBinding, s as TemplateTryIt, t as TemplateWorkflow, u as TemplateFootprint, I as IndustryGroup, v as TemplateJob, w as TeachesFeature, x as TemplateViewKind, y as TemplateBookingWindow } from '../types-y8GX3-QC.js';
2
+ export { z as CAPABILITY_WORDS, B as CATALOGUE_QUOTAS, D as CATALOGUE_STATUSES, E as CHOICE_COLORS, F as CHOICE_COLORS_DISTINCT_UP_TO, G as CatalogueRow, H as CatalogueStatus, J as ChoiceColor, K as ConfidentialLevel, L as ConfidentialRequest, M as DEFAULT_TEMPLATE_AGENT, N as ENTITY_REFERENCE_KINDS, O as INDUSTRY_GROUPS, R as RELATIVE_DATE, P as RetiredId, Q as SHARED_BLOCKS, U as SHARED_BLOCK_IDS, V as STRENGTHS, W as SharedBlock, X as SharedBlockField, Y as SharedBlockUse, Z as Strength, _ as TABLE_KERNEL_ID, $ as TEACHES_FEATURES, a0 as TEMPLATE_AUDIENCES, a1 as TEMPLATE_EXTRA_KINDS, a2 as TEMPLATE_JOBS, a3 as TEMPLATE_REFERENCE_DATE, a4 as TEMPLATE_SPEC_VERSION, a5 as TEMPLATE_VIEW_KINDS, a6 as TemplateAgentQuestion, a7 as TemplateAgentVariable, a8 as TemplateAudience, a9 as TemplateCalendarView, aa as TemplateChecklistStep, ab as TemplateConfidential, ac as TemplateConfidentialReader, ad as TemplateDashboardBlock, ae as TemplateDimensionOverride, af as TemplateDrillPath, ag as TemplateExtra, ah as TemplateExtraAgent, ai as TemplateExtraAgentVariable, aj as TemplateExtraKind, ak as TemplateFormField, al as TemplateFoundation, am as TemplateGalleryView, an as TemplateGridView, ao as TemplateGuideStep, ap as TemplateKanbanView, aq as TemplateMeasureOverride, ar as TemplatePortalTable, as as TemplateProvenance, at as TemplateSource, au as TemplateTableSettings, av as TemplateTimelineView, aw as TemplateViewSort, ax as UseCaseSingulars, ay as WalkAction, az as WalkAssertion, aA as WalkImport, aB as WalkMove, aC as WalkStep, aD as confidentialPending, aE as confidentialRequests, aF as footprintOf, aG as membersAddRowsSteps, aH as tableSettingsProblems, aI as whenRule } from '../types-y8GX3-QC.js';
3
+ import { g as AggregateMeasure, p as AggregateFilter } from '../subscription-DxtZgyFR.js';
4
4
  import { U as UseCase, o as UseCaseRelationship } from '../types-DqCWKOYh.js';
5
5
  import { U as UseCaseProblem } from '../validate-d9QMYjBm.js';
6
6
  export { R as RESOLVABLE_FIELD_TYPE_WORDS } from '../validate-d9QMYjBm.js';
@@ -49,9 +49,9 @@ type AggregateOutcome = {
49
49
  };
50
50
  /**
51
51
  * custom.record_aggregate for ONE measure over seed rows (live, non-archived), as the store
52
- * computes it: count(*); sum/avg/min/max/median over `nullif(text,'')::numeric` (a text that
52
+ * computes it: count(*); sum/avg/min/max/median/range over `nullif(text,'')::numeric` (a text that
53
53
  * is not a number is a refusal; no values at all is NULL, not 0); filled/empty/unique by
54
- * `nullif(text,'')`. Exact: an average that does not terminate is reported as inexact.
54
+ * `nullif(text,'')`; percent_empty/percent_filled as 100 x share of the rows (NULL over none). Exact: an average that does not terminate is reported as inexact.
55
55
  */
56
56
  /** custom.formula_node_kinds(), the functions a formula may call (read on the clone 2026-10-03). */
57
57
  declare const FORMULA_FUNCTIONS: Set<string>;
@@ -2915,6 +2915,12 @@ function storeAggregate(rows, measure, filter, timeZone) {
2915
2915
  if (op === "filled") return { ok: true, value: present.length, exact: true };
2916
2916
  if (op === "empty") return { ok: true, value: live.length - present.length, exact: true };
2917
2917
  if (op === "unique") return { ok: true, value: new Set(present).size, exact: true };
2918
+ if (op === "percent_empty" || op === "percent_filled") {
2919
+ if (!live.length) return { ok: true, value: null, exact: true };
2920
+ const share = q(100n * BigInt(op === "percent_filled" ? present.length : live.length - present.length), BigInt(live.length));
2921
+ const pct = qToNumber(share);
2922
+ return pct === null ? { ok: true, value: Number(share.n) / Number(share.d), exact: false } : { ok: true, value: pct, exact: true };
2923
+ }
2918
2924
  const nums = [];
2919
2925
  for (const t of present) {
2920
2926
  const v = parseNumeric(t);
@@ -2928,6 +2934,10 @@ function storeAggregate(rows, measure, filter, timeZone) {
2928
2934
  if (op === "avg") result = q(result.n, result.d * BigInt(nums.length));
2929
2935
  } else if (op === "min" || op === "max") {
2930
2936
  result = nums.reduce((a, b) => (op === "min" ? qCmp(b, a) < 0 : qCmp(b, a) > 0) ? b : a);
2937
+ } else if (op === "range") {
2938
+ const lo = nums.reduce((a, b) => qCmp(b, a) < 0 ? b : a);
2939
+ const hi = nums.reduce((a, b) => qCmp(b, a) > 0 ? b : a);
2940
+ result = qAdd(hi, q(-lo.n, lo.d));
2931
2941
  } else if (op === "median") {
2932
2942
  const s = [...nums].sort(qCmp);
2933
2943
  const mid = Math.floor(s.length / 2);
@@ -2964,7 +2974,7 @@ var TIME_BOUND_STAGE = /\b(?:no[- ]?show|completed?|done|seen|checked[- ]?in|arr
2964
2974
  var TERMINAL_STAGE = /^(?:cancell?ed|no[- ]?show|lost|closed[- ]lost|declined|closed|withdrawn|fell off|rejected|void(?:ed)?|skipped|missed)$/i;
2965
2975
  var CHART_BLOCK_KINDS = /* @__PURE__ */ new Set(["bar", "column", "line", "donut"]);
2966
2976
  var BUCKETS = ["year", "quarter", "month", "week", "day"];
2967
- var AGG_OPERATIONS = ["count", "sum", "avg", "min", "max", "median", "filled", "empty", "unique"];
2977
+ var AGG_OPERATIONS = ["count", "sum", "avg", "min", "max", "median", "filled", "empty", "unique", "percent_empty", "percent_filled", "range"];
2968
2978
  var MEASURE_KEY = /^[a-zA-Z_][a-zA-Z0-9_]{0,62}$/;
2969
2979
  var STORE_LIST_RELATION_MAX = 25;
2970
2980
  function ruleBreak(field2, v) {
@@ -3979,7 +3989,7 @@ function validateTemplate(raw, options = {}) {
3979
3989
  if (!m.op || !AGG_OPERATIONS.includes(m.op) || m.op === "count") say(mat, `The measure "${m.key}" needs one of: ${AGG_OPERATIONS.filter((o) => o !== "count").join(", ")}.`);
3980
3990
  const of = m.of ? fieldOf(dim.table, m.of) : void 0;
3981
3991
  if (!of) say(mat, `The measure "${m.key}" reads "${m.of ?? "(nothing)"}", which is not a column of "${dim.table}".`);
3982
- else if (m.op && ["sum", "avg", "min", "max", "median"].includes(m.op) && !(isNumber(of) && !isComputed(of))) say(mat, isComputed(of) ? `"${m.of}" is worked out, and a dimension totals only a number that is typed in; total the stored columns it is made from.` : `"${m.of}" is not a number, so it cannot be added up or averaged.`);
3992
+ else if (m.op && ["sum", "avg", "min", "max", "median", "range"].includes(m.op) && !(isNumber(of) && !isComputed(of))) say(mat, isComputed(of) ? `"${m.of}" is worked out, and a dimension totals only a number that is typed in; total the stored columns it is made from.` : `"${m.of}" is not a number, so it cannot be added up or averaged.`);
3983
3993
  if (newMeasures.has(m.key)) say(mat, `Two measures are both called "${m.key}".`);
3984
3994
  newMeasures.add(m.key);
3985
3995
  });
@@ -4072,7 +4082,7 @@ function validateTemplate(raw, options = {}) {
4072
4082
  let honest = true;
4073
4083
  for (const m of b.measures ?? []) {
4074
4084
  const f = m.key ? fieldOf(table, m.key) : void 0;
4075
- if (f && ["sum", "avg", "min", "max", "median"].includes(m.op) && !isNumberish(table, f)) {
4085
+ if (f && (["sum", "avg", "min", "max", "median"].includes(m.op) || m.op === "range" && !isDate(f)) && !isNumberish(table, f)) {
4076
4086
  honest = false;
4077
4087
  say(`${at}.blocks[${bi}]`, `"${b.title}" takes the ${m.op} of "${f.label}", which is not a number.`);
4078
4088
  }
@@ -4401,6 +4411,7 @@ function validateTemplate(raw, options = {}) {
4401
4411
  }
4402
4412
  }
4403
4413
  const refWeek = [0, 1, 2, 3, 4, 5, 6].map((i) => shiftDays(TEMPLATE_REFERENCE_DATE, i));
4414
+ const KEY_PART = "";
4404
4415
  const crossBreaks = () => {
4405
4416
  const out = /* @__PURE__ */ new Map();
4406
4417
  const live = (token) => (state.get(token) ?? []).filter((r) => !r.archived);
@@ -4446,7 +4457,7 @@ function validateTemplate(raw, options = {}) {
4446
4457
  const linked = Array.isArray(ref2) ? ref2.includes(r.key) : ref2 === r.key;
4447
4458
  return linked && (!need.field || (need.in ?? []).includes(storeText(c.values?.[need.field]) ?? ""));
4448
4459
  });
4449
- if (kids.length < (need.min ?? 1)) out.set(`rc|${t.token}|${r.key}|${st}|${kids.length}`, `"${name(t.token, r)}" is ${st}, which needs ${need.min ?? 1} linked ${byToken.get(need.table)?.labelSingular.toLowerCase() ?? need.table}${need.field ? ` in ${(need.in ?? []).join("/")}` : ""}; it has ${kids.length}.`);
4460
+ if (kids.length < (need.min ?? 1)) out.set(["rc", t.token, r.key, st, kids.length].join(KEY_PART), `"${name(t.token, r)}" is ${st}, which needs ${need.min ?? 1} linked ${byToken.get(need.table)?.labelSingular.toLowerCase() ?? need.table}${need.field ? ` in ${(need.in ?? []).join("/")}` : ""}; it has ${kids.length}.`);
4450
4461
  }
4451
4462
  for (const f of t.fields.filter((x) => x.eligible)) {
4452
4463
  const el = f.eligible;
@@ -4460,7 +4471,7 @@ function validateTemplate(raw, options = {}) {
4460
4471
  if (!ok) {
4461
4472
  const target = fieldOf(t.token, f.key)?.relationTarget ?? "";
4462
4473
  const prow = live(target).find((x) => x.key === who);
4463
- out.set(`el|${t.token}|${r.key}|${f.key}|${String(who)}|${on}`, `"${name(t.token, r)}" (${on}) has ${f.label.toLowerCase()} "${prow ? name(target, prow) : String(who)}", who holds no ${byToken.get(el.table)?.labelSingular.toLowerCase() ?? el.table}${el.where ? ` (${Object.values(el.where).join(", ")})` : ""} valid on that day.`);
4474
+ out.set(["el", t.token, r.key, f.key, String(who), on].join(KEY_PART), `"${name(t.token, r)}" (${on}) has ${f.label.toLowerCase()} "${prow ? name(target, prow) : String(who)}", who holds no ${byToken.get(el.table)?.labelSingular.toLowerCase() ?? el.table}${el.where ? ` (${Object.values(el.where).join(", ")})` : ""} valid on that day.`);
4464
4475
  }
4465
4476
  }
4466
4477
  }
@@ -4470,7 +4481,7 @@ function validateTemplate(raw, options = {}) {
4470
4481
  };
4471
4482
  const crossSeen = new Set(crossBreaks().keys());
4472
4483
  for (const [k, msg] of crossBreaks()) {
4473
- const [kind, table, row] = k.split("|");
4484
+ const [kind, table, row] = k.split(KEY_PART);
4474
4485
  if (kind === "rc" || kind === "el") say(`tables (${table}).rows (${row})`, msg);
4475
4486
  }
4476
4487
  const moveOne = (a, at, rawSet) => {