@ai-matrx/records 0.76.8 → 0.76.10

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,8 @@ 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
+ BAD_OUTCOME: () => BAD_OUTCOME,
24
+ BOOKING_AVAILABILITY_FACTS: () => BOOKING_AVAILABILITY_FACTS,
23
25
  CAPABILITY_WORDS: () => CAPABILITY_WORDS,
24
26
  CATALOGUE_QUOTAS: () => CATALOGUE_QUOTAS,
25
27
  CATALOGUE_STATUSES: () => CATALOGUE_STATUSES,
@@ -29,6 +31,8 @@ __export(templates_exports, {
29
31
  DESIGNATED_REAL_NUMBERS: () => DESIGNATED_REAL_NUMBERS,
30
32
  ENTITY_REFERENCE_KINDS: () => ENTITY_REFERENCE_KINDS,
31
33
  FORMULA_FUNCTIONS: () => FORMULA_FUNCTIONS,
34
+ GALLERY_ONLY: () => GALLERY_ONLY,
35
+ GOOD_OUTCOME: () => GOOD_OUTCOME,
32
36
  INDUSTRY_GROUPS: () => INDUSTRY_GROUPS,
33
37
  INSTALL_PLAN_VERSION: () => INSTALL_PLAN_VERSION,
34
38
  MAIN_TABLE_ROWS_MAX: () => MAIN_TABLE_ROWS_MAX,
@@ -56,11 +60,14 @@ __export(templates_exports, {
56
60
  TEMPLATE_VIEW_KINDS: () => TEMPLATE_VIEW_KINDS,
57
61
  TIME_BOUND_STAGE: () => TIME_BOUND_STAGE,
58
62
  TITLE_DISTINCT_SHARE: () => TITLE_DISTINCT_SHARE,
63
+ VIEW_SHAPE_FACTS: () => VIEW_SHAPE_FACTS,
59
64
  agentCopyRequest: () => agentCopyRequest,
60
65
  allTables: () => allTables,
61
66
  anyOfWhere: () => anyOfWhere,
67
+ applyRepairPatches: () => applyRepairPatches,
62
68
  assertTemplate: () => assertTemplate,
63
69
  attachmentProblems: () => attachmentProblems,
70
+ autofixDescribeSpec: () => autofixDescribeSpec,
64
71
  automationHeld: () => automationHeld,
65
72
  blockTable: () => blockTable,
66
73
  bookingArgs: () => bookingArgs,
@@ -77,12 +84,16 @@ __export(templates_exports, {
77
84
  deferredRelationFields: () => deferredRelationFields,
78
85
  deferredRelations: () => deferredRelations,
79
86
  derivedFieldSpec: () => derivedFieldSpec,
87
+ describeCheck: () => describeCheck,
88
+ describeSpec: () => describeSpec,
89
+ describeVocabulary: () => describeVocabulary,
80
90
  digestSpec: () => digestSpec,
81
91
  documentBody: () => documentBody,
82
92
  domainsIn: () => domainsIn,
83
93
  drillDownDashboard: () => drillDownDashboard,
84
94
  evalFormula: () => evalFormula,
85
95
  extrasProblems: () => extrasProblems,
96
+ fallbackView: () => fallbackView,
86
97
  footprintOf: () => footprintOf,
87
98
  foreignTextProblems: () => foreignTextProblems,
88
99
  formArgs: () => formArgs,
@@ -92,13 +103,18 @@ __export(templates_exports, {
92
103
  foundationTokens: () => foundationTokens,
93
104
  isSelfDeclaredField: () => isSelfDeclaredField,
94
105
  isTimedField: () => isTimedField,
106
+ lastResortTrim: () => lastResortTrim,
95
107
  membersAddRowsSteps: () => membersAddRowsSteps,
108
+ mergeDescribeParts: () => mergeDescribeParts,
96
109
  normalizeForPhones: () => normalizeForPhones,
97
110
  notificationParts: () => notificationParts,
98
111
  numberText: () => numberText,
99
112
  numberedStreet: () => numberedStreet,
113
+ packageDescribeResult: () => packageDescribeResult,
114
+ parseAt: () => parseAt,
100
115
  parseFormula: () => parseFormula,
101
116
  parseNumeric: () => parseNumeric,
117
+ pathOfAt: () => pathOfAt,
102
118
  peopleOf: () => peopleOf,
103
119
  phonesOutsideFictionalRange: () => phonesOutsideFictionalRange,
104
120
  planTables: () => planTables,
@@ -106,9 +122,12 @@ __export(templates_exports, {
106
122
  relationFieldSpec: () => relationFieldSpec,
107
123
  relationPatches: () => relationPatches,
108
124
  relativeHasTime: () => relativeHasTime,
125
+ repairSlices: () => repairSlices,
109
126
  requiredCircleProblem: () => requiredCircleProblem,
110
127
  resolveDateToken: () => resolveDateToken,
128
+ resolvePath: () => resolvePath,
111
129
  resolveRelativeDates: () => resolveRelativeDates,
130
+ reusesOf: () => reusesOf,
112
131
  rowActionList: () => rowActionList,
113
132
  rowLevels: () => rowLevels,
114
133
  rowsFor: () => rowsFor,
@@ -514,9 +533,9 @@ function membersAddRowsSteps(spec, tableRef) {
514
533
  }));
515
534
  }
516
535
  function confidentialRequests(spec, made) {
517
- const idOf = (token) => made.find((m) => m.kind === "table" && m.ref === `tables.${token}`)?.id ?? null;
536
+ const idOf2 = (token) => made.find((m) => m.kind === "table" && m.ref === `tables.${token}`)?.id ?? null;
518
537
  return (spec.confidential ?? []).map((c) => {
519
- const tableId = idOf(c.table);
538
+ const tableId = idOf2(c.table);
520
539
  if (!tableId) throw new Error(`The install made no "${c.table}" table, so it cannot be made Confidential.`);
521
540
  const readers = c.readers.map((r) => ({ field: r.field, level: r.level, ...r.when ? { when: whenRule(c.table, r.when, made) } : {} }));
522
541
  return {
@@ -1622,6 +1641,7 @@ var TABLE_TYPES = [
1622
1641
  var RELATION_FLAVORS = ["owned", "referenced"];
1623
1642
  var ON_DELETE = ["cascade", "set_null", "restrict"];
1624
1643
  var RELATION_CARDINALITIES = ["one", "many"];
1644
+ var FIELD_SOURCES = ["manual", "formula", "agent", "synced"];
1625
1645
  var FIELD_SENSITIVITIES = ["public", "internal", "confidential", "restricted"];
1626
1646
  var CONTEXT_POLICIES = ["include", "summarize", "exclude", "on_request"];
1627
1647
  var TABLE_DISPLAYS = ["list", "grid", "board", "calendar", "timeline"];
@@ -1968,39 +1988,39 @@ function validateUseCase(useCase) {
1968
1988
  `The title field "${table.titleField}" is not one of this table's fields \u2014 without it a record cannot be a chip.`
1969
1989
  );
1970
1990
  }
1971
- table.fields.forEach((field, fi) => {
1972
- const fat = `${at}.fields[${fi}] (${field.key})`;
1973
- if (!/^[a-z][a-z0-9_]*$/.test(field.key ?? "")) {
1974
- say(`${fat}.key`, `"${field.key}" should be snake_case and start with a letter.`);
1991
+ table.fields.forEach((field2, fi) => {
1992
+ const fat = `${at}.fields[${fi}] (${field2.key})`;
1993
+ if (!/^[a-z][a-z0-9_]*$/.test(field2.key ?? "")) {
1994
+ say(`${fat}.key`, `"${field2.key}" should be snake_case and start with a letter.`);
1975
1995
  }
1976
- if (!RESOLVABLE_FIELD_TYPE_WORDS.includes(field.parityType)) {
1996
+ if (!RESOLVABLE_FIELD_TYPE_WORDS.includes(field2.parityType)) {
1977
1997
  say(
1978
1998
  `${fat}.parityType`,
1979
- `The store does not resolve the type word "${field.parityType}". Use one of: ${RESOLVABLE_FIELD_TYPE_WORDS.join(", ")}.`
1999
+ `The store does not resolve the type word "${field2.parityType}". Use one of: ${RESOLVABLE_FIELD_TYPE_WORDS.join(", ")}.`
1980
2000
  );
1981
2001
  }
1982
- if (!FIELD_SENSITIVITIES.includes(field.sensitivity)) {
2002
+ if (!FIELD_SENSITIVITIES.includes(field2.sensitivity)) {
1983
2003
  say(
1984
2004
  `${fat}.sensitivity`,
1985
2005
  `The store refuses a field that does not say how sensitive its values are. Use one of: ${FIELD_SENSITIVITIES.join(", ")}.`
1986
2006
  );
1987
2007
  }
1988
- if (!CONTEXT_POLICIES.includes(field.contextPolicy)) {
2008
+ if (!CONTEXT_POLICIES.includes(field2.contextPolicy)) {
1989
2009
  say(
1990
2010
  `${fat}.contextPolicy`,
1991
2011
  `The store refuses a field that does not say whether an agent may see its values. Use one of: ${CONTEXT_POLICIES.join(", ")}.`
1992
2012
  );
1993
2013
  }
1994
- if (CHOICE_TYPES.has(field.parityType) && !field.choices?.length) {
2014
+ if (CHOICE_TYPES.has(field2.parityType) && !field2.choices?.length) {
1995
2015
  say(
1996
2016
  `${fat}.choices`,
1997
2017
  "A choice field has to list the business's real words, in the order they really happen \u2014 'Scheduled, Confirmed, Seen, No-show'."
1998
2018
  );
1999
2019
  }
2000
- if (RELATION_TYPES.has(field.parityType) && field.relationTarget && !tablesByToken.has(field.relationTarget)) {
2001
- say(`${fat}.relationTarget`, `It points at the table "${field.relationTarget}", which this use case does not declare.`);
2020
+ if (RELATION_TYPES.has(field2.parityType) && field2.relationTarget && !tablesByToken.has(field2.relationTarget)) {
2021
+ say(`${fat}.relationTarget`, `It points at the table "${field2.relationTarget}", which this use case does not declare.`);
2002
2022
  }
2003
- if (field.parityType === "currency" && !field.unit) {
2023
+ if (field2.parityType === "currency" && !field2.unit) {
2004
2024
  say(`${fat}.unit`, "A currency field carries its currency as the unit \u2014 the unit changes the meaning.");
2005
2025
  }
2006
2026
  });
@@ -2015,16 +2035,16 @@ function validateUseCase(useCase) {
2015
2035
  say(`${rat}.values.${valueKey}`, `This row sets "${valueKey}", which the table does not declare as a field.`);
2016
2036
  }
2017
2037
  }
2018
- for (const field of table.fields) {
2019
- if (field.required && (row.values?.[field.key] === void 0 || row.values?.[field.key] === null)) {
2020
- say(`${rat}.values.${field.key}`, `"${field.label}" is required, and this row leaves it empty.`);
2038
+ for (const field2 of table.fields) {
2039
+ if (field2.required && (row.values?.[field2.key] === void 0 || row.values?.[field2.key] === null)) {
2040
+ say(`${rat}.values.${field2.key}`, `"${field2.label}" is required, and this row leaves it empty.`);
2021
2041
  }
2022
- const value = row.values?.[field.key];
2023
- if (value !== void 0 && field.choices?.length && !field.multi) {
2024
- if (typeof value === "string" && !field.choices.includes(value)) {
2042
+ const value = row.values?.[field2.key];
2043
+ if (value !== void 0 && field2.choices?.length && !field2.multi) {
2044
+ if (typeof value === "string" && !field2.choices.includes(value)) {
2025
2045
  say(
2026
- `${rat}.values.${field.key}`,
2027
- `"${value}" is not one of "${field.label}"'s choices (${field.choices.join(", ")}).`
2046
+ `${rat}.values.${field2.key}`,
2047
+ `"${value}" is not one of "${field2.label}"'s choices (${field2.choices.join(", ")}).`
2028
2048
  );
2029
2049
  }
2030
2050
  }
@@ -2048,18 +2068,18 @@ function validateUseCase(useCase) {
2048
2068
  if (!tablesByToken.has(rel.toTable)) {
2049
2069
  say(`${at}.toTable`, `There is no table "${rel.toTable}" in this use case.`);
2050
2070
  }
2051
- const field = resolveField(rel.fromTable, rel.fromField);
2052
- if (!field) {
2071
+ const field2 = resolveField(rel.fromTable, rel.fromField);
2072
+ if (!field2) {
2053
2073
  say(`${at}.fromField`, `The table "${rel.fromTable}" does not declare a field "${rel.fromField}".`);
2054
- } else if (!RELATION_TYPES.has(field.parityType)) {
2074
+ } else if (!RELATION_TYPES.has(field2.parityType)) {
2055
2075
  say(
2056
2076
  `${at}.fromField`,
2057
- `"${rel.fromField}" is a ${field.parityType} field, so it cannot hold a relation. Declare it as a relation.`
2077
+ `"${rel.fromField}" is a ${field2.parityType} field, so it cannot hold a relation. Declare it as a relation.`
2058
2078
  );
2059
- } else if (field.relationTarget && field.relationTarget !== rel.toTable) {
2079
+ } else if (field2.relationTarget && field2.relationTarget !== rel.toTable) {
2060
2080
  say(
2061
2081
  `${at}.fromField`,
2062
- `The field points at "${field.relationTarget}" but the relationship says "${rel.toTable}". They have to agree.`
2082
+ `The field points at "${field2.relationTarget}" but the relationship says "${rel.toTable}". They have to agree.`
2063
2083
  );
2064
2084
  }
2065
2085
  if (!RELATION_FLAVORS.includes(rel.flavor)) {
@@ -2217,12 +2237,12 @@ var ACTOR = "user";
2217
2237
  var COMPUTED = /* @__PURE__ */ new Set(["lookup", "rollup", "formula"]);
2218
2238
  var isTimedField = (f) => f.parityType === "datetime" && f.format === "datetime";
2219
2239
  var isSelfDeclaredField = (f) => f.parityType !== "relation" && !COMPUTED.has(f.parityType) && (isTimedField(f) || f.default !== void 0 || f.parityType === "entity_reference");
2220
- function choiceDisplayFormat(field) {
2221
- if (!field.choices?.length || !field.choiceColors) return void 0;
2240
+ function choiceDisplayFormat(field2) {
2241
+ if (!field2.choices?.length || !field2.choiceColors) return void 0;
2222
2242
  return {
2223
- id: field.parityType === "multi_select" ? "multi_choice" : "choice",
2243
+ id: field2.parityType === "multi_select" ? "multi_choice" : "choice",
2224
2244
  options: {
2225
- choices: field.choices.filter((c) => field.choiceColors?.[c]).map((c) => ({ value: c, color: field.choiceColors[c] }))
2245
+ choices: field2.choices.filter((c) => field2.choiceColors?.[c]).map((c) => ({ value: c, color: field2.choiceColors[c] }))
2226
2246
  }
2227
2247
  };
2228
2248
  }
@@ -2290,49 +2310,49 @@ function shellsPayload(spec, tables, deferred = deferredRelations(tables)) {
2290
2310
  })
2291
2311
  };
2292
2312
  }
2293
- function timedFieldSpec(field) {
2313
+ function timedFieldSpec(field2) {
2294
2314
  return {
2295
- key: field.key,
2296
- label: field.label,
2315
+ key: field2.key,
2316
+ label: field2.label,
2297
2317
  type: "datetime",
2298
2318
  kind: "datetime",
2299
- ...field.required ? { required: true } : {},
2300
- ...field.rules ? { rules: field.rules } : {},
2301
- sensitivity: field.sensitivity,
2302
- context_policy: field.contextPolicy
2319
+ ...field2.required ? { required: true } : {},
2320
+ ...field2.rules ? { rules: field2.rules } : {},
2321
+ sensitivity: field2.sensitivity,
2322
+ context_policy: field2.contextPolicy
2303
2323
  };
2304
2324
  }
2305
- function selfDeclaredFieldSpec(field) {
2306
- if (isTimedField(field)) return { ...timedFieldSpec(field), ...field.default !== void 0 ? { default: field.default } : {} };
2307
- const allowed = field.parityType === "entity_reference" ? { allowed_types: field.allowedTypes ?? [] } : {};
2308
- const displayFormat = choiceDisplayFormat(field);
2325
+ function selfDeclaredFieldSpec(field2) {
2326
+ if (isTimedField(field2)) return { ...timedFieldSpec(field2), ...field2.default !== void 0 ? { default: field2.default } : {} };
2327
+ const allowed = field2.parityType === "entity_reference" ? { allowed_types: field2.allowedTypes ?? [] } : {};
2328
+ const displayFormat = choiceDisplayFormat(field2);
2309
2329
  return {
2310
- key: field.key,
2311
- label: field.label,
2312
- type: field.parityType,
2313
- ...field.multi ? { multi: true } : {},
2314
- ...field.required ? { required: true } : {},
2315
- ...field.unit ? { unit: field.unit } : {},
2316
- ...field.rules ? { rules: field.rules } : {},
2317
- ...field.choices ? { options: field.choices } : {},
2330
+ key: field2.key,
2331
+ label: field2.label,
2332
+ type: field2.parityType,
2333
+ ...field2.multi ? { multi: true } : {},
2334
+ ...field2.required ? { required: true } : {},
2335
+ ...field2.unit ? { unit: field2.unit } : {},
2336
+ ...field2.rules ? { rules: field2.rules } : {},
2337
+ ...field2.choices ? { options: field2.choices } : {},
2318
2338
  ...displayFormat ? { display_format: displayFormat } : {},
2319
- ...field.default !== void 0 ? { default: field.default } : {},
2339
+ ...field2.default !== void 0 ? { default: field2.default } : {},
2320
2340
  ...allowed,
2321
- sensitivity: field.sensitivity,
2322
- context_policy: field.contextPolicy
2341
+ sensitivity: field2.sensitivity,
2342
+ context_policy: field2.contextPolicy
2323
2343
  };
2324
2344
  }
2325
- function relationFieldSpec(field, targetTableId) {
2345
+ function relationFieldSpec(field2, targetTableId) {
2326
2346
  return {
2327
- key: field.key,
2328
- label: field.label,
2347
+ key: field2.key,
2348
+ label: field2.label,
2329
2349
  type: "relation",
2330
2350
  relation_target: targetTableId,
2331
- ...field.multi ? { multi: true } : {},
2332
- ...field.relationMax ? { relation_max: field.relationMax } : {},
2333
- ...field.required ? { required: true } : {},
2334
- sensitivity: field.sensitivity,
2335
- context_policy: field.contextPolicy
2351
+ ...field2.multi ? { multi: true } : {},
2352
+ ...field2.relationMax ? { relation_max: field2.relationMax } : {},
2353
+ ...field2.required ? { required: true } : {},
2354
+ sensitivity: field2.sensitivity,
2355
+ context_policy: field2.contextPolicy
2336
2356
  };
2337
2357
  }
2338
2358
  function rowLevels(tables) {
@@ -2392,12 +2412,12 @@ function relationPatches(table, ids, deferred) {
2392
2412
  for (const [key, target] of rel) {
2393
2413
  const v = r.values?.[key];
2394
2414
  if (v === null || v === void 0) continue;
2395
- const idOf = (k) => {
2415
+ const idOf2 = (k) => {
2396
2416
  const id = ids.get(`${target}.${String(k)}`);
2397
2417
  if (!id) throw new Error(`A row points at "${String(k)}" in "${target}", which the template does not have.`);
2398
2418
  return id;
2399
2419
  };
2400
- patch[key] = Array.isArray(v) ? v.map(idOf) : idOf(v);
2420
+ patch[key] = Array.isArray(v) ? v.map(idOf2) : idOf2(v);
2401
2421
  }
2402
2422
  if (Object.keys(patch).length) out.push({ op: "update", record_id: ids.get(`${table.token}.${r.key}`), patch: { ...patch, _actor: ACTOR } });
2403
2423
  }
@@ -2406,7 +2426,7 @@ function relationPatches(table, ids, deferred) {
2406
2426
  function rowsFor(table, ids, deferred) {
2407
2427
  const relation = new Map(table.fields.filter((f) => f.parityType === "relation").map((f) => [f.key, f]));
2408
2428
  const derived = new Set(table.fields.filter((f) => COMPUTED.has(f.parityType)).map((f) => f.key));
2409
- const idOf = (target, key) => {
2429
+ const idOf2 = (target, key) => {
2410
2430
  const id = ids.get(`${target}.${String(key)}`);
2411
2431
  if (!id) throw new Error(`A row points at "${String(key)}" in "${target}", which the template does not have.`);
2412
2432
  return id;
@@ -2420,7 +2440,7 @@ function rowsFor(table, ids, deferred) {
2420
2440
  values[key] = value;
2421
2441
  continue;
2422
2442
  }
2423
- values[key] = Array.isArray(value) ? value.map((v) => idOf(rel.relationTarget, v)) : idOf(rel.relationTarget, value);
2443
+ values[key] = Array.isArray(value) ? value.map((v) => idOf2(rel.relationTarget, v)) : idOf2(rel.relationTarget, value);
2424
2444
  }
2425
2445
  if (typeof r.icon === "string" && r.icon.trim()) values["icon"] = r.icon.trim();
2426
2446
  values["_actor"] = ACTOR;
@@ -2428,17 +2448,17 @@ function rowsFor(table, ids, deferred) {
2428
2448
  });
2429
2449
  return { rows, ids: table.rows.map((r) => ids.get(`${table.token}.${r.key}`)) };
2430
2450
  }
2431
- function derivedFieldSpec(field) {
2451
+ function derivedFieldSpec(field2) {
2432
2452
  const base = {
2433
- key: field.key,
2434
- label: field.label,
2435
- type: field.parityType,
2436
- via: field.via,
2437
- sensitivity: field.sensitivity,
2438
- context_policy: field.contextPolicy,
2439
- ...field.unit ? { unit: field.unit } : {}
2453
+ key: field2.key,
2454
+ label: field2.label,
2455
+ type: field2.parityType,
2456
+ via: field2.via,
2457
+ sensitivity: field2.sensitivity,
2458
+ context_policy: field2.contextPolicy,
2459
+ ...field2.unit ? { unit: field2.unit } : {}
2440
2460
  };
2441
- return field.parityType === "rollup" ? { ...base, agg: field.agg, of: field.pick, ...field.filter ? { filter: field.filter } : {} } : { ...base, pick: field.pick };
2461
+ return field2.parityType === "rollup" ? { ...base, agg: field2.agg, of: field2.pick, ...field2.filter ? { filter: field2.filter } : {} } : { ...base, pick: field2.pick };
2442
2462
  }
2443
2463
  function viewSpec(view, fieldIds = {}) {
2444
2464
  const definition = { layout: view.kind };
@@ -2484,15 +2504,15 @@ function anyOfWhere(anyOf, fieldIds) {
2484
2504
  })
2485
2505
  };
2486
2506
  }
2487
- function formulaFieldSpec(field) {
2507
+ function formulaFieldSpec(field2) {
2488
2508
  return {
2489
- key: field.key,
2490
- label: field.label,
2509
+ key: field2.key,
2510
+ label: field2.label,
2491
2511
  type: "formula",
2492
- formula_text: field.formula,
2493
- ...field.unit ? { unit: field.unit } : {},
2494
- sensitivity: field.sensitivity,
2495
- context_policy: field.contextPolicy
2512
+ formula_text: field2.formula,
2513
+ ...field2.unit ? { unit: field2.unit } : {},
2514
+ sensitivity: field2.sensitivity,
2515
+ context_policy: field2.contextPolicy
2496
2516
  };
2497
2517
  }
2498
2518
  function formulaOrder(table) {
@@ -3110,23 +3130,23 @@ var BUCKETS = ["year", "quarter", "month", "week", "day"];
3110
3130
  var AGG_OPERATIONS = ["count", "sum", "avg", "min", "max", "median", "filled", "empty", "unique"];
3111
3131
  var MEASURE_KEY = /^[a-zA-Z_][a-zA-Z0-9_]{0,62}$/;
3112
3132
  var STORE_LIST_RELATION_MAX = 25;
3113
- function ruleBreak(field, v) {
3133
+ function ruleBreak(field2, v) {
3114
3134
  const t = storeText(v) ?? "";
3115
3135
  if (t === "") return null;
3116
- if (field.choices?.length && !field.multi && typeof v === "string" && !field.choices.includes(v)) return `"${v}" is not one of "${field.label}"'s choices.`;
3117
- for (const rule of field.rules ?? []) {
3136
+ if (field2.choices?.length && !field2.multi && typeof v === "string" && !field2.choices.includes(v)) return `"${v}" is not one of "${field2.label}"'s choices.`;
3137
+ for (const rule of field2.rules ?? []) {
3118
3138
  if (rule.kind === "min" || rule.kind === "max") {
3119
3139
  const n = parseNumeric(t);
3120
3140
  const bound = parseNumeric(String(rule.value));
3121
- if (n && bound && (rule.kind === "min" ? qCmp(n, bound) < 0 : qCmp(n, bound) > 0)) return `${t} breaks "${field.label}"'s rule: at ${rule.kind === "min" ? "least" : "most"} ${rule.value}.`;
3141
+ if (n && bound && (rule.kind === "min" ? qCmp(n, bound) < 0 : qCmp(n, bound) > 0)) return `${t} breaks "${field2.label}"'s rule: at ${rule.kind === "min" ? "least" : "most"} ${rule.value}.`;
3122
3142
  } else if (rule.kind === "length") {
3123
- if (rule.value !== void 0 && t.length > Number(rule.value)) return `"${t}" is longer than "${field.label}" allows (${rule.value}).`;
3124
- if (rule.min !== void 0 && t.length < rule.min) return `"${t}" is shorter than "${field.label}" allows (${rule.min}).`;
3143
+ if (rule.value !== void 0 && t.length > Number(rule.value)) return `"${t}" is longer than "${field2.label}" allows (${rule.value}).`;
3144
+ if (rule.min !== void 0 && t.length < rule.min) return `"${t}" is shorter than "${field2.label}" allows (${rule.min}).`;
3125
3145
  } else if (rule.kind === "pattern" && rule.value !== void 0) {
3126
3146
  try {
3127
- if (!new RegExp(String(rule.value)).test(t)) return `"${t}" does not match "${field.label}"'s own pattern.`;
3147
+ if (!new RegExp(String(rule.value)).test(t)) return `"${t}" does not match "${field2.label}"'s own pattern.`;
3128
3148
  } catch {
3129
- return `"${field.label}"'s pattern is not a pattern.`;
3149
+ return `"${field2.label}"'s pattern is not a pattern.`;
3130
3150
  }
3131
3151
  }
3132
3152
  }
@@ -3163,14 +3183,14 @@ function deriveSingulars(spec) {
3163
3183
  if (form) out.form = form;
3164
3184
  const stages = spec.extras?.find((e) => e.kind === "stage_rules");
3165
3185
  if (stages?.kind === "stage_rules") {
3166
- const field = allTables(spec).find((t) => t.token === stages.table)?.fields.find((f) => f.key === stages.stageField);
3186
+ const field2 = allTables(spec).find((t) => t.token === stages.table)?.fields.find((f) => f.key === stages.stageField);
3167
3187
  out.pipeline = {
3168
3188
  token: stages.token,
3169
3189
  name: stages.name,
3170
3190
  describes: stages.movesMessage ?? stages.name,
3171
3191
  table: stages.table,
3172
3192
  stageField: stages.stageField,
3173
- stages: (field?.choices ?? Object.keys(stages.stages ?? {})).map((key) => ({ key, label: key, describes: stages.stages?.[key] ?? "" }))
3193
+ stages: (field2?.choices ?? Object.keys(stages.stages ?? {})).map((key) => ({ key, label: key, describes: stages.stages?.[key] ?? "" }))
3174
3194
  };
3175
3195
  }
3176
3196
  const q6 = spec.agent?.question;
@@ -3289,12 +3309,12 @@ function capabilitiesUsed(spec) {
3289
3309
  }
3290
3310
  if (spec.teaches === "gallery") used.add("seed.attachments");
3291
3311
  for (const table of allTables(spec)) {
3292
- for (const field of table.fields ?? []) {
3293
- const word = `field.${field.parityType}`;
3312
+ for (const field2 of table.fields ?? []) {
3313
+ const word = `field.${field2.parityType}`;
3294
3314
  if (CAPABILITY_WORDS.includes(word)) used.add(word);
3295
- if (field.choiceColors && Object.keys(field.choiceColors).length) used.add("choice.colors");
3296
- if (field.format === "rating") used.add("field.rating");
3297
- if (field.format === "signature") used.add("field.signature");
3315
+ if (field2.choiceColors && Object.keys(field2.choiceColors).length) used.add("choice.colors");
3316
+ if (field2.format === "rating") used.add("field.rating");
3317
+ if (field2.format === "signature") used.add("field.signature");
3298
3318
  }
3299
3319
  }
3300
3320
  if (spec.forms?.length) {
@@ -3505,9 +3525,9 @@ function foreignTextProblems(spec, library) {
3505
3525
  const all = hay.map(([, v]) => v).join("\n");
3506
3526
  const esc = (w) => w.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3507
3527
  const find = (word) => {
3508
- const lower = word.toLowerCase();
3509
- if (!all.includes(lower)) return void 0;
3510
- const re = new RegExp(`(^|[^a-z0-9])${esc(lower)}($|[^a-z0-9])`);
3528
+ const lower2 = word.toLowerCase();
3529
+ if (!all.includes(lower2)) return void 0;
3530
+ const re = new RegExp(`(^|[^a-z0-9])${esc(lower2)}($|[^a-z0-9])`);
3511
3531
  return hay.find(([, v]) => re.test(v));
3512
3532
  };
3513
3533
  const mine = proseOf(spec).map(([at, w]) => {
@@ -3666,6 +3686,10 @@ function validateTemplate(raw, options = {}) {
3666
3686
  }
3667
3687
  for (const table of tablesRaw) {
3668
3688
  const tf = table.fields.find((f) => f.key === table.titleField);
3689
+ if (!tf) say(`tables (${table.token}).titleField`, `The title field "${table.titleField}" is not one of this table's fields; name a plain text-like field of "${table.token}" (a name, a title) \u2014 the install refuses anything else.`);
3690
+ else if (isRelation(tf) || ["lookup", "rollup", "formula"].includes(tf.parityType) || isTimedField(tf) || tf.parityType === "entity_reference") {
3691
+ say(`tables (${table.token}).titleField`, `The title field "${tf.key}" is a ${isRelation(tf) ? "link to another table" : tf.parityType} column, which the store adds after the table is made; the title has to be a plain text-like field made with the table (a name or title). Add one, or point titleField at it.`);
3692
+ }
3669
3693
  if (tf && tf.default !== void 0) say(`tables (${table.token}).fields (${tf.key})`, `"${tf.label}" names each ${table.labelSingular.toLowerCase()}, so it takes no default; ask it on the form instead.`);
3670
3694
  }
3671
3695
  for (const table of tablesRaw) {
@@ -3854,49 +3878,49 @@ function validateTemplate(raw, options = {}) {
3854
3878
  let hasLookupOrRollup = false;
3855
3879
  tables.forEach((table) => {
3856
3880
  const tat = `tables (${table.token})`;
3857
- table.fields.forEach((field) => {
3858
- const at = `${tat}.fields (${field.key})`;
3859
- if (isChoice(field) && field.choices?.length) {
3860
- const colors = field.choices.map((c) => field.choiceColors?.[c]);
3861
- field.choices.forEach((c, i) => {
3881
+ table.fields.forEach((field2) => {
3882
+ const at = `${tat}.fields (${field2.key})`;
3883
+ if (isChoice(field2) && field2.choices?.length) {
3884
+ const colors = field2.choices.map((c) => field2.choiceColors?.[c]);
3885
+ field2.choices.forEach((c, i) => {
3862
3886
  const color = colors[i];
3863
3887
  if (!color) say(`${at}.choiceColors`, `The choice "${c}" wears no colour. Give every choice one of: ${CHOICE_COLORS.join(", ")}.`);
3864
3888
  else if (!CHOICE_COLORS.includes(color)) say(`${at}.choiceColors`, `"${color}" is not a colour the grid can paint.`);
3865
3889
  });
3866
- for (const k of Object.keys(field.choiceColors ?? {})) if (!field.choices.includes(k)) say(`${at}.choiceColors`, `It colours "${k}", which is not one of the field's choices.`);
3890
+ for (const k of Object.keys(field2.choiceColors ?? {})) if (!field2.choices.includes(k)) say(`${at}.choiceColors`, `It colours "${k}", which is not one of the field's choices.`);
3867
3891
  const distinct = new Set(colors.filter(Boolean)).size;
3868
- const need = Math.min(field.choices.length, CHOICE_COLORS_DISTINCT_UP_TO);
3869
- if (distinct < need) say(`${at}.choiceColors`, `${field.choices.length} choices wear only ${distinct} distinct colour(s); each choice needs its own (${need}), so the board reads at a glance.`);
3892
+ const need = Math.min(field2.choices.length, CHOICE_COLORS_DISTINCT_UP_TO);
3893
+ if (distinct < need) say(`${at}.choiceColors`, `${field2.choices.length} choices wear only ${distinct} distinct colour(s); each choice needs its own (${need}), so the board reads at a glance.`);
3870
3894
  }
3871
- if (isNumber(field) && !isComputed(field)) {
3872
- const kinds2 = new Set((field.rules ?? []).map((r) => r.kind));
3873
- if (!kinds2.has("min") || !kinds2.has("max")) say(`${at}.rules`, `"${field.label}" is measured, so it declares the least and the most a real value can be (min and max rules).`);
3874
- const seeds = table.rows.map((r) => parseNumeric(storeText(r.values?.[field.key]) ?? "")).filter((v) => !!v).map((v) => Number(v.n) / Number(v.d));
3895
+ if (isNumber(field2) && !isComputed(field2)) {
3896
+ const kinds2 = new Set((field2.rules ?? []).map((r) => r.kind));
3897
+ if (!kinds2.has("min") || !kinds2.has("max")) say(`${at}.rules`, `"${field2.label}" is measured, so it declares the least and the most a real value can be (min and max rules).`);
3898
+ const seeds = table.rows.map((r) => parseNumeric(storeText(r.values?.[field2.key]) ?? "")).filter((v) => !!v).map((v) => Number(v.n) / Number(v.d));
3875
3899
  if (seeds.length) {
3876
3900
  const hi = Math.max(...seeds);
3877
3901
  const lo = Math.min(...seeds);
3878
- if (new Set(seeds).size < 2) say(`${at}`, `Every seed row holds the same ${field.label}; real records vary.`);
3879
- for (const rule of field.rules ?? []) {
3902
+ if (new Set(seeds).size < 2) say(`${at}`, `Every seed row holds the same ${field2.label}; real records vary.`);
3903
+ for (const rule of field2.rules ?? []) {
3880
3904
  const bound = Number(rule.value);
3881
- if (rule.kind === "max" && hi > 0 && bound > 100 * hi) say(`${at}.rules`, `The most "${field.label}" allows (${rule.value}) is over 100\xD7 the largest real value (${hi}); write the real ceiling.`);
3882
- if (rule.kind === "min" && bound < -1e3 * Math.abs(lo)) say(`${at}.rules`, `The least "${field.label}" allows (${rule.value}) is far below any real value (${lo}); write the real floor.`);
3883
- if (rule.kind === "min" && bound < 0 && lo >= 0) say(`${at}.rules`, `"${field.label}" allows negative values, and no real one is negative; the floor is 0 or above.`);
3905
+ 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.`);
3906
+ 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.`);
3907
+ 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.`);
3884
3908
  }
3885
3909
  }
3886
3910
  }
3887
- if (field.parityType === "lookup" || field.parityType === "rollup") {
3888
- const via = field.via ? fieldOf(table.token, field.via) : void 0;
3911
+ if (field2.parityType === "lookup" || field2.parityType === "rollup") {
3912
+ const via = field2.via ? fieldOf(table.token, field2.via) : void 0;
3889
3913
  if (!via || !isRelation(via) || !via.relationTarget) {
3890
- say(`${at}.via`, `A ${field.parityType} reads THROUGH a relation field of this table; "${field.via ?? "(none)"}" is not one.`);
3914
+ say(`${at}.via`, `A ${field2.parityType} reads THROUGH a relation field of this table; "${field2.via ?? "(none)"}" is not one.`);
3891
3915
  } else {
3892
3916
  hasLookupOrRollup = true;
3893
- if (!field.pick || !fieldOf(via.relationTarget, field.pick)) say(`${at}.pick`, `It reads "${field.pick ?? "(nothing)"}" on "${via.relationTarget}", which that table does not declare.`);
3894
- if (field.parityType === "rollup" && !field.agg) say(`${at}.agg`, "A rollup says how it adds up the far side.");
3895
- if (field.parityType === "rollup" && !via.multi && (via.relationMax ?? 1) <= 1) say(`${at}.via`, `"${field.label}" adds up "${via.label}", which links one record. Make "${via.label}" a list (multi), or read its one value with a lookup.`);
3917
+ if (!field2.pick || !fieldOf(via.relationTarget, field2.pick)) say(`${at}.pick`, `It reads "${field2.pick ?? "(nothing)"}" on "${via.relationTarget}", which that table does not declare.`);
3918
+ if (field2.parityType === "rollup" && !field2.agg) say(`${at}.agg`, "A rollup says how it adds up the far side.");
3919
+ if (field2.parityType === "rollup" && !via.multi && (via.relationMax ?? 1) <= 1) say(`${at}.via`, `"${field2.label}" adds up "${via.label}", which links one record. Make "${via.label}" a list (multi), or read its one value with a lookup.`);
3896
3920
  }
3897
3921
  }
3898
- if (field.parityType === "entity_reference" && !field.allowedTypes?.length) say(`${at}.allowedTypes`, "A column that points at the platform names the kinds it may point at.");
3899
- if (isRelation(field) && field.relationTarget && !byToken.has(field.relationTarget)) say(`${at}.relationTarget`, `It points at "${field.relationTarget}", which is neither a table nor a shared block of this template.`);
3922
+ if (field2.parityType === "entity_reference" && !field2.allowedTypes?.length) say(`${at}.allowedTypes`, "A column that points at the platform names the kinds it may point at.");
3923
+ if (isRelation(field2) && field2.relationTarget && !byToken.has(field2.relationTarget)) say(`${at}.relationTarget`, `It points at "${field2.relationTarget}", which is neither a table nor a shared block of this template.`);
3900
3924
  });
3901
3925
  const seen = /* @__PURE__ */ new Map();
3902
3926
  const titles = /* @__PURE__ */ new Set();
@@ -3906,47 +3930,47 @@ function validateTemplate(raw, options = {}) {
3906
3930
  const twin = seen.get(sig);
3907
3931
  if (twin) say(rat, `This row is identical to "${twin}" apart from its key. Every seed row is a different real record.`);
3908
3932
  seen.set(sig, row.key);
3909
- for (const field of table.fields) {
3910
- if (!isRelation(field) || !field.relationTarget || field.parityType === "attachment") continue;
3911
- const target = byToken.get(field.relationTarget);
3912
- const v = row.values?.[field.key];
3933
+ for (const field2 of table.fields) {
3934
+ if (!isRelation(field2) || !field2.relationTarget || field2.parityType === "attachment") continue;
3935
+ const target = byToken.get(field2.relationTarget);
3936
+ const v = row.values?.[field2.key];
3913
3937
  if (!target || v === void 0 || v === null) continue;
3914
3938
  for (const ref2 of Array.isArray(v) ? v : [v]) {
3915
- if (typeof ref2 === "string" && !target.rows.some((r) => r.key === ref2)) say(`${rat}.values.${field.key}`, `It points at the row "${ref2}", which does not exist in "${field.relationTarget}".`);
3939
+ if (typeof ref2 === "string" && !target.rows.some((r) => r.key === ref2)) say(`${rat}.values.${field2.key}`, `It points at the row "${ref2}", which does not exist in "${field2.relationTarget}".`);
3916
3940
  }
3917
3941
  }
3918
3942
  const title = storeText(row.values?.[table.titleField]);
3919
3943
  if (title) titles.add(title.trim().toLowerCase());
3920
- for (const field of table.fields) {
3921
- const v = row.values?.[field.key];
3944
+ for (const field2 of table.fields) {
3945
+ const v = row.values?.[field2.key];
3922
3946
  if (v === void 0 || v === null || v === "") continue;
3923
- for (const rule of field.rules ?? []) {
3947
+ for (const rule of field2.rules ?? []) {
3924
3948
  const t = storeText(v) ?? "";
3925
3949
  if (rule.kind === "min" || rule.kind === "max") {
3926
3950
  const n = parseNumeric(t);
3927
3951
  const bound = parseNumeric(String(rule.value));
3928
3952
  if (n && bound && (rule.kind === "min" ? qCmp(n, bound) < 0 : qCmp(n, bound) > 0)) {
3929
- say(`${rat}.values.${field.key}`, `${t} breaks its own rule: "${field.label}" is at ${rule.kind === "min" ? "least" : "most"} ${rule.value}.`);
3953
+ say(`${rat}.values.${field2.key}`, `${t} breaks its own rule: "${field2.label}" is at ${rule.kind === "min" ? "least" : "most"} ${rule.value}.`);
3930
3954
  }
3931
3955
  } else if (rule.kind === "length") {
3932
- if (rule.value !== void 0 && t.length > Number(rule.value)) say(`${rat}.values.${field.key}`, `"${t}" is longer than "${field.label}" allows (${rule.value}).`);
3933
- if (rule.min !== void 0 && t.length < rule.min) say(`${rat}.values.${field.key}`, `"${t}" is shorter than "${field.label}" allows (${rule.min}).`);
3956
+ if (rule.value !== void 0 && t.length > Number(rule.value)) say(`${rat}.values.${field2.key}`, `"${t}" is longer than "${field2.label}" allows (${rule.value}).`);
3957
+ if (rule.min !== void 0 && t.length < rule.min) say(`${rat}.values.${field2.key}`, `"${t}" is shorter than "${field2.label}" allows (${rule.min}).`);
3934
3958
  } else if (rule.kind === "pattern" && rule.value !== void 0) {
3935
3959
  let re = null;
3936
3960
  try {
3937
3961
  re = new RegExp(String(rule.value));
3938
3962
  } catch {
3939
- say(`${tat}.fields (${field.key}).rules`, `The pattern "${rule.value}" is not a pattern.`);
3963
+ say(`${tat}.fields (${field2.key}).rules`, `The pattern "${rule.value}" is not a pattern.`);
3940
3964
  }
3941
- if (re && !re.test(t)) say(`${rat}.values.${field.key}`, `"${t}" does not match "${field.label}"'s own pattern.`);
3965
+ if (re && !re.test(t)) say(`${rat}.values.${field2.key}`, `"${t}" does not match "${field2.label}"'s own pattern.`);
3942
3966
  }
3943
3967
  }
3944
3968
  }
3945
3969
  });
3946
- for (const field of table.fields) {
3947
- if (!(field.rules ?? []).some((r) => r.kind === "unique")) continue;
3948
- const vals = table.rows.map((r) => storeText(r.values?.[field.key])).filter((v) => !!v).map((v) => v.trim().toLowerCase());
3949
- if (new Set(vals).size !== vals.length) say(`${tat}.fields (${field.key})`, `"${field.label}" is unique, and two seed rows share a value.`);
3970
+ for (const field2 of table.fields) {
3971
+ if (!(field2.rules ?? []).some((r) => r.kind === "unique")) continue;
3972
+ const vals = table.rows.map((r) => storeText(r.values?.[field2.key])).filter((v) => !!v).map((v) => v.trim().toLowerCase());
3973
+ if (new Set(vals).size !== vals.length) say(`${tat}.fields (${field2.key})`, `"${field2.label}" is unique, and two seed rows share a value.`);
3950
3974
  }
3951
3975
  if (table.rows.length >= 2 && titles.size < Math.ceil(table.rows.length * TITLE_DISTINCT_SHARE)) {
3952
3976
  say(`${tat}.rows`, `Only ${titles.size} of ${table.rows.length} rows have a title of their own; a list of look-alike names reads as generated.`);
@@ -5175,8 +5199,8 @@ function validateTemplate(raw, options = {}) {
5175
5199
  }
5176
5200
  const currencies = new Set(fields.filter((f) => f.parityType === "currency").map((f) => (f.unit ?? "").toUpperCase()));
5177
5201
  if (currencies.size > 1) say("tables", `Money is kept in ${[...currencies].join(" and ")}; one business keeps its books in one currency.`);
5178
- const GOOD = /^(completed?|done|paid|approved|won|closed won|booked|confirmed|delivered|resolved|published|signed|passed|hired|accepted)$/i;
5179
- const BAD = /^(cancell?ed|skipped|lost|closed lost|denied|rejected|no[- ]show|failed|overdue|declined|void(ed)?|refunded|churned|expired|blocked)$/i;
5202
+ const GOOD = GOOD_OUTCOME;
5203
+ const BAD = BAD_OUTCOME;
5180
5204
  for (const t of own) {
5181
5205
  for (const f of t.fields) {
5182
5206
  for (const [choice, color] of Object.entries(f.choiceColors ?? {})) {
@@ -5353,14 +5377,25 @@ function validateTemplate(raw, options = {}) {
5353
5377
  if (columns && !(status2 && /writes "booked" and "cancelled"/.test(columns))) say(`${at}.table`, columns);
5354
5378
  for (const qq of e.questions ?? []) if (["slot", "status", "booked_with"].includes(qq.fieldKey)) say(`${at}.questions`, `A booking page never asks for "${qq.fieldKey}": the store fills it in from the slot that was held.`);
5355
5379
  const a = e.availability ?? {};
5356
- const rng = (v, lo, hi, what) => {
5357
- if (v !== void 0 && (v < lo || v > hi)) say(`${at}.availability`, `${what} is between ${lo} and ${hi}; this says ${v}.`);
5380
+ const wholeNumber = (key, what) => {
5381
+ const v = a[key];
5382
+ if (v === void 0 || typeof v === "number" && Number.isInteger(v)) return true;
5383
+ say(`${at}.availability.${key}`, key === "days" ? `${what} is ONE whole number such as 21 (how many days ahead a person may book); the weekdays belong in windows, not here.` : `${what} is one whole number; this says ${JSON.stringify(v)}.`);
5384
+ return false;
5385
+ };
5386
+ const rng = (v, lo, hi, what, key) => {
5387
+ if (v !== void 0 && wholeNumber(key, what) && (v < lo || v > hi)) say(`${at}.availability`, `${what} is between ${lo} and ${hi}; this says ${v}.`);
5358
5388
  };
5359
- rng(a.slotMinutes, 5, 480, "Slot length (minutes)");
5360
- rng(a.bufferMinutes, 0, 480, "The buffer (minutes)");
5361
- rng(a.leadMinutes, 0, 43200, "Lead time (minutes)");
5362
- rng(a.maxPerDay, 1, 100, "Appointments a day");
5363
- rng(a.days, 1, 90, "Days ahead");
5389
+ (a.windows ?? []).forEach((w, wi) => {
5390
+ const ww = w;
5391
+ if (!Number.isInteger(ww.weekday)) say(`${at}.availability.windows[${wi}]`, `A window's weekday is one whole number, 0 (Sunday) to 6 (Saturday); this says ${JSON.stringify(ww.weekday)}.`);
5392
+ for (const k of ["from", "to"]) if (typeof ww[k] !== "string" || !/^\d{2}:\d{2}$/.test(ww[k])) say(`${at}.availability.windows[${wi}]`, `A window's "${k}" is a time of day written "09:00"; this says ${JSON.stringify(ww[k])}.`);
5393
+ });
5394
+ rng(a.slotMinutes, 5, 480, "Slot length (minutes)", "slotMinutes");
5395
+ rng(a.bufferMinutes, 0, 480, "The buffer (minutes)", "bufferMinutes");
5396
+ rng(a.leadMinutes, 0, 43200, "Lead time (minutes)", "leadMinutes");
5397
+ rng(a.maxPerDay, 1, 100, "Appointments a day", "maxPerDay");
5398
+ rng(a.days, 1, 90, "Days ahead", "days");
5364
5399
  if (a.timezone) {
5365
5400
  try {
5366
5401
  new Intl.DateTimeFormat("en-US", { timeZone: a.timezone });
@@ -6016,6 +6051,8 @@ var RESTRICTED_KEY = /(^|_)(member_id|ssn|social_security|dob|date_of_birth|birt
6016
6051
  var RESTRICTED_LABEL = /\b(bank|routing number|iban|card on file|credit card|card number|cvv|medical conditions?|medications?|diagnos[ie]s|case notes|legal notes|privileged)\b/;
6017
6052
  var PERSONAL_ONLY = /(^|_)(dob|date_of_birth|birth_date|allerg(y|ies)|diagnos(is|es)|medical_conditions?|conditions|medications?|current_medications|clinical_notes?|visit_notes?|progress_notes?|soap_notes?|member_id|ssn|social_security)($|_)/;
6018
6053
  var holdsPeople = (t) => t.subject ? t.subject === "person" : t.fields.some((f) => /name/.test(f.key)) && t.fields.some((f) => f.parityType === "phone" || f.parityType === "email");
6054
+ var GOOD_OUTCOME = /^(completed?|done|paid|approved|won|closed won|booked|confirmed|delivered|resolved|published|signed|passed|hired|accepted)$/i;
6055
+ var BAD_OUTCOME = /^(cancell?ed|skipped|lost|closed lost|denied|rejected|no[- ]show|failed|overdue|declined|void(ed)?|refunded|churned|expired|blocked)$/i;
6019
6056
  function sensitivityFloorOf(table, f) {
6020
6057
  const text = `${f.key} ${norm(f.label)}`;
6021
6058
  const billingTable = /(invoice|bill|statement|receipt|payment|charge|fee|order|account)/i.test(`${table.token} ${table.labelSingular ?? ""}`);
@@ -6493,4 +6530,714 @@ async function runTemplateDoor(source, door, organizationId, id, options = {}) {
6493
6530
  function planTables(plan) {
6494
6531
  return plan.steps.flatMap((s) => (s.made ?? []).filter((m) => m.kind === "table").map((m) => m.ref));
6495
6532
  }
6533
+
6534
+ // src/templates/describe/shape.ts
6535
+ var GALLERY_ONLY = ["catalogueId", "strengths", "provenance", "walk", "agent", "requires", "persona", "extraAgents", "workflows", "guide", "__kind"];
6536
+ function describeSpec(template) {
6537
+ const s = { ...template };
6538
+ for (const k of GALLERY_ONLY) delete s[k];
6539
+ s.specVersion = 1;
6540
+ if (typeof s.version !== "number") s.version = 1;
6541
+ if (typeof s.audience !== "string") s.audience = "organization";
6542
+ if (typeof s.cleanupTag !== "string" && typeof s.id === "string") s.cleanupTag = `describe:${s.id}`;
6543
+ for (const k of ["relationships", "sharedBlocks", "views", "forms", "dimensions", "extras"]) if (!Array.isArray(s[k])) s[k] = [];
6544
+ if (Array.isArray(s.tables)) s.tables = s.tables.map(describeTable);
6545
+ return s;
6546
+ }
6547
+ var TABLE_DEFAULTS = { type: "entity", display: "grid", weight: "light", ordered: false };
6548
+ var CONTACT_KINDS = /* @__PURE__ */ new Set(["phone", "email"]);
6549
+ function describeTable(t) {
6550
+ const own = Array.isArray(t.fields) ? t.fields : null;
6551
+ const shape = { token: String(t.token ?? ""), labelSingular: String(t.labelSingular ?? ""), subject: t.subject, fields: own ?? [] };
6552
+ const fields = own ? own.map((f) => describeField(f, shape)) : t.fields;
6553
+ return {
6554
+ ...TABLE_DEFAULTS,
6555
+ ...t,
6556
+ ...typeof t.labelPlural !== "string" && typeof t.name === "string" ? { labelPlural: t.name } : {},
6557
+ fields,
6558
+ rows: Array.isArray(t.rows) ? t.rows : []
6559
+ };
6560
+ }
6561
+ function describeField(f, table) {
6562
+ const contact = CONTACT_KINDS.has(String(f.parityType ?? ""));
6563
+ const floor = sensitivityFloorOf(table, { key: String(f.key ?? ""), label: String(f.label ?? ""), parityType: String(f.parityType ?? "") });
6564
+ return {
6565
+ ...contact ? { format: f.parityType } : {},
6566
+ sensitivity: floor ?? "internal",
6567
+ contextPolicy: floor ? "exclude" : "include",
6568
+ ...f
6569
+ };
6570
+ }
6571
+
6572
+ // src/templates/describe/merge.ts
6573
+ var clone = (v) => v === void 0 ? v : JSON.parse(JSON.stringify(v));
6574
+ var labelOfKey = (key) => {
6575
+ const w = key.replace(/_/g, " ").trim();
6576
+ return w ? w[0].toUpperCase() + w.slice(1) : key;
6577
+ };
6578
+ var field = (t, key) => t.fields.find((f) => f.key === key);
6579
+ function skeletonTable(p) {
6580
+ return {
6581
+ token: p.token,
6582
+ name: p.name,
6583
+ labelSingular: p.labelSingular,
6584
+ labelPlural: p.name,
6585
+ subject: p.subject,
6586
+ titleField: p.titleField.key,
6587
+ describes: p.purpose.slice(0, 140),
6588
+ fields: [],
6589
+ rows: []
6590
+ };
6591
+ }
6592
+ function ensureSkeleton(t, p) {
6593
+ if (typeof t.name !== "string") t.name = p.name;
6594
+ if (typeof t.labelSingular !== "string") t.labelSingular = p.labelSingular;
6595
+ if (typeof t.subject !== "string") t.subject = p.subject;
6596
+ if (typeof t.describes !== "string" || !t.describes.trim()) t.describes = p.purpose.slice(0, 140);
6597
+ if (!Array.isArray(t.fields)) t.fields = [];
6598
+ if (typeof t.titleField !== "string" || !t.titleField) t.titleField = p.titleField.key;
6599
+ if (!field(t, p.titleField.key)) t.fields.unshift({ key: p.titleField.key, label: p.titleField.label, parityType: "text", required: true });
6600
+ if (p.status && !field(t, "status")) {
6601
+ t.fields.push({ key: "status", label: "Status", parityType: "select", required: true, choices: [...p.status.choices], default: p.status.default });
6602
+ }
6603
+ 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) {
6605
+ if (field(t, l.key)) continue;
6606
+ t.fields.push({ key: l.key, label: l.label, parityType: "relation", relationTarget: l.target, ...l.many ? { multi: true } : { relationMax: 1 } });
6607
+ }
6608
+ }
6609
+ function addExistingKeys(t, p, existing) {
6610
+ if (!p.reuse) return;
6611
+ const e = existing.find((x) => x.id === p.reuse.existing_table_id);
6612
+ if (!e) return;
6613
+ for (const key of p.reuse.keepKeys) {
6614
+ if (field(t, key)) continue;
6615
+ const ef = e.fields.find((f) => f.key === key);
6616
+ if (!ef) continue;
6617
+ t.fields.push({ key, label: ef.label, parityType: ef.kind, ...ef.choices?.length ? { choices: [...ef.choices] } : {} });
6618
+ }
6619
+ }
6620
+ function addRequest(t, r) {
6621
+ if (field(t, r.key)) return;
6622
+ const f = { key: r.key, label: r.label, parityType: r.parityType };
6623
+ if (r.choices?.length) f.choices = [...r.choices];
6624
+ if (r.askOfSubject) f.askOfSubject = true;
6625
+ if (r.askOfGuardian) f.askOfGuardian = true;
6626
+ if (r.required) f.required = true;
6627
+ if (r.help) f.help = r.help;
6628
+ t.fields.push(f);
6629
+ }
6630
+ var CHOICE = ["select", "multi_select", "list"];
6631
+ var STAGE_KEY = /(^|_)(status|stage)$/;
6632
+ function viewHolds(v, tables) {
6633
+ const t = tables.get(v.table);
6634
+ if (!t) return false;
6635
+ const has = (k) => typeof k === "string" && !!field(t, k);
6636
+ const x = v;
6637
+ const kindOf = (k) => t.fields.find((f) => f.key === k)?.parityType;
6638
+ if (v.kind === "kanban" && !(has(x.groupBy) && CHOICE.includes(kindOf(x.groupBy)))) return false;
6639
+ if (v.kind === "calendar" && !(has(x.dateField) && kindOf(x.dateField) === "datetime")) return false;
6640
+ if (v.kind === "timeline" && !(has(x.startField) && has(x.endField))) return false;
6641
+ if (v.kind === "gallery" && !has(x.coverField)) return false;
6642
+ if (x.groupBy !== void 0 && !has(x.groupBy)) return false;
6643
+ for (const k of Object.keys(x.filters ?? {})) if (!has(k)) return false;
6644
+ for (const k of Object.keys(x.anyOf ?? {})) if (!has(k)) return false;
6645
+ for (const s of x.sorts ?? []) if (!has(s.field)) return false;
6646
+ return typeof v.token === "string" && !!v.token && typeof v.name === "string" && !!v.name;
6647
+ }
6648
+ function fallbackView(t) {
6649
+ const plural = typeof t.name === "string" ? t.name : labelOfKey(t.token);
6650
+ const stage = t.fields.find((f) => STAGE_KEY.test(f.key) && CHOICE.includes(f.parityType));
6651
+ if (stage) return { token: `${t.token}_board`, name: `${plural} board`, table: t.token, kind: "kanban", groupBy: stage.key, isDefault: true };
6652
+ if (t.fields.some((f) => f.key === "slot" && f.parityType === "datetime")) {
6653
+ return { token: `${t.token}_calendar`, name: `${plural} calendar`, table: t.token, kind: "calendar", dateField: "slot", isDefault: true };
6654
+ }
6655
+ return { token: `${t.token}_list`, name: `All ${plural.toLowerCase()}`, table: t.token, kind: "grid", isDefault: true, sorts: [{ field: String(t.titleField), direction: "asc" }] };
6656
+ }
6657
+ function mergeViews(plan, part, tables) {
6658
+ const given = Array.isArray(part?.views) ? part.views : [];
6659
+ const seen = /* @__PURE__ */ new Set();
6660
+ const kept = [];
6661
+ for (const v of given) {
6662
+ if (!v || !viewHolds(v, tables) || seen.has(v.token)) continue;
6663
+ seen.add(v.token);
6664
+ kept.push(clone(v));
6665
+ }
6666
+ const defaulted = /* @__PURE__ */ new Set();
6667
+ for (const v of kept) {
6668
+ if (!v.isDefault) continue;
6669
+ if (defaulted.has(v.table)) delete v.isDefault;
6670
+ else defaulted.add(v.table);
6671
+ }
6672
+ if (kept.length) return kept;
6673
+ const wanted = [...new Set(plan.views.map((v) => v.table))].filter((tok) => tables.has(tok));
6674
+ const targets = wanted.length ? wanted : [...tables.keys()].slice(0, 1);
6675
+ return targets.map((tok) => fallbackView(tables.get(tok)));
6676
+ }
6677
+ function sharedBlocksFor(tables) {
6678
+ const own = new Set(tables.map((t) => t.token));
6679
+ const out = [];
6680
+ for (const id of SHARED_BLOCK_IDS) {
6681
+ const token = SHARED_BLOCKS[id].table;
6682
+ if (own.has(token)) continue;
6683
+ if (tables.some((t) => t.fields.some((f) => f.parityType === "relation" && f.relationTarget === token))) out.push({ block: id, rows: [] });
6684
+ }
6685
+ return out;
6686
+ }
6687
+ function reusesOf(plan) {
6688
+ return plan.tables.filter((t) => t.reuse).map((t) => ({ token: t.token, existing_table_id: t.reuse.existing_table_id }));
6689
+ }
6690
+ function mergeDescribeParts(plan, tables, doors, views, existingTables = []) {
6691
+ const out = clone(Array.isArray(tables?.tables) ? tables.tables : []);
6692
+ const byToken = new Map(out.map((t) => [t.token, t]));
6693
+ for (const p of plan.tables) {
6694
+ let t = byToken.get(p.token);
6695
+ if (!t) {
6696
+ t = skeletonTable(p);
6697
+ out.push(t);
6698
+ byToken.set(p.token, t);
6699
+ }
6700
+ ensureSkeleton(t, p);
6701
+ addExistingKeys(t, p, existingTables);
6702
+ }
6703
+ for (const r of doors?.fieldRequests ?? []) {
6704
+ const t = byToken.get(r.table);
6705
+ if (t) addRequest(t, r);
6706
+ }
6707
+ const spec = {
6708
+ specVersion: 1,
6709
+ version: 1,
6710
+ id: plan.id,
6711
+ useCase: plan.useCase,
6712
+ industry: plan.industry,
6713
+ vertical: plan.vertical,
6714
+ job: plan.job,
6715
+ teaches: plan.teaches,
6716
+ audience: plan.audience,
6717
+ cleanupTag: `describe:${plan.id}`,
6718
+ business: clone(plan.business),
6719
+ tables: out,
6720
+ sharedBlocks: sharedBlocksFor(out),
6721
+ relationships: clone(tables?.relationships ?? []),
6722
+ views: mergeViews(plan, views, byToken),
6723
+ forms: clone(doors?.forms ?? []),
6724
+ dimensions: [],
6725
+ extras: doors?.booking ? [clone(doors.booking)] : []
6726
+ };
6727
+ return spec;
6728
+ }
6729
+
6730
+ // src/templates/describe/autofix.ts
6731
+ var clone2 = (v) => JSON.parse(JSON.stringify(v));
6732
+ var SENSITIVITY_RANK2 = ["public", "internal", "confidential", "restricted"];
6733
+ var isChoice2 = (f) => ["select", "multi_select", "list"].includes(f.parityType);
6734
+ var isComputed2 = (f) => ["formula", "lookup", "rollup", "autonumber", "created_time", "modified_time"].includes(f.parityType) || !!f.source && f.source !== "manual";
6735
+ var MINOR_VERTICAL = /pediatric|paediatric|youth|child|kids?\b|school|daycare|day care|junior|teen|infant|nursery|camp\b/i;
6736
+ var NEUTRAL_ORDER = ["amber", "blue", "violet", "teal", "slate", "orange", "yellow", "pink", "brown", "gray", "green", "red"];
6737
+ var GOOD_ORDER = ["green", "teal", "blue", "violet", "slate", "amber", "orange", "yellow", "pink", "brown", "gray"];
6738
+ var BAD_ORDER = ["red", "orange", "gray", "slate", "brown", "pink", "amber", "yellow", "violet", "blue", "teal"];
6739
+ var DISTINCT_UP_TO = 7;
6740
+ var lower = (s) => s.trim().toLowerCase();
6741
+ function raiseSensitivity(spec, fixes) {
6742
+ for (const t of spec.tables) {
6743
+ for (const f of t.fields) {
6744
+ const at = `tables.${t.token}.fields.${f.key}`;
6745
+ const floor = sensitivityFloorOf(t, f);
6746
+ const rank = (s) => SENSITIVITY_RANK2.indexOf(s);
6747
+ if (floor && rank(f.sensitivity) < rank(floor)) {
6748
+ fixes.push(`${at}: sensitivity ${f.sensitivity} raised to ${floor}`);
6749
+ f.sensitivity = floor;
6750
+ }
6751
+ if (floor === "restricted" && f.contextPolicy !== "exclude") {
6752
+ fixes.push(`${at}: contextPolicy ${f.contextPolicy} set to exclude (restricted)`);
6753
+ f.contextPolicy = "exclude";
6754
+ }
6755
+ if (/note/.test(`${f.key} ${lower(f.label)}`) && f.sensitivity === "public") {
6756
+ fixes.push(`${at}: notes are never public; raised to internal`);
6757
+ f.sensitivity = "internal";
6758
+ }
6759
+ }
6760
+ }
6761
+ }
6762
+ function flagAskedOfSubject(spec, ctx, fixes) {
6763
+ const minor = MINOR_VERTICAL.test(`${spec.vertical ?? ""} ${ctx.plan?.vertical ?? ""}`);
6764
+ const byToken = new Map(spec.tables.map((t) => [t.token, t]));
6765
+ for (const form of spec.forms) {
6766
+ if (form.audience !== "public") continue;
6767
+ const t = byToken.get(form.table);
6768
+ if (!t) continue;
6769
+ for (const q2 of form.fields ?? []) {
6770
+ const f = t.fields.find((x) => x.key === q2.fieldKey);
6771
+ if (!f) continue;
6772
+ if (f.sensitivity !== "confidential" && f.sensitivity !== "restricted") continue;
6773
+ if (f.askOfSubject || f.askOfGuardian) continue;
6774
+ if (minor) f.askOfGuardian = true;
6775
+ else f.askOfSubject = true;
6776
+ fixes.push(`tables.${t.token}.fields.${f.key}: ${minor ? "askOfGuardian" : "askOfSubject"} set (asked on the public form "${form.token}")`);
6777
+ }
6778
+ }
6779
+ }
6780
+ var DOOR_KEYS = ["slot", "status", "booked_with"];
6781
+ function colorOf(name) {
6782
+ return /^booked$/i.test(name) ? "blue" : "slate";
6783
+ }
6784
+ function fixBooking(spec, fixes) {
6785
+ for (const e of spec.extras) {
6786
+ if (e.kind !== "booking") continue;
6787
+ const b = e;
6788
+ const t = spec.tables.find((x) => x.token === b.table);
6789
+ if (t) {
6790
+ const at = `tables.${t.token}.fields`;
6791
+ const slot = t.fields.find((f) => f.key === "slot");
6792
+ if (!slot) {
6793
+ t.fields.push({ key: "slot", label: "Visit time", parityType: "datetime", format: "datetime", required: true, sensitivity: "internal", contextPolicy: "include" });
6794
+ fixes.push(`${at}.slot: added the required datetime the booking page writes`);
6795
+ } else {
6796
+ if (slot.parityType !== "datetime") {
6797
+ slot.parityType = "datetime";
6798
+ fixes.push(`${at}.slot: made a datetime`);
6799
+ }
6800
+ if (!slot.required) {
6801
+ slot.required = true;
6802
+ fixes.push(`${at}.slot: made required`);
6803
+ }
6804
+ if (slot.format !== "datetime") slot.format = "datetime";
6805
+ }
6806
+ let status = t.fields.find((f) => f.key === "status");
6807
+ if (!status) {
6808
+ status = { key: "status", label: "Status", parityType: "select", required: true, choices: ["Booked", "Cancelled"], default: "Booked", choiceColors: { Booked: "blue", Cancelled: "slate" }, sensitivity: "internal", contextPolicy: "include" };
6809
+ t.fields.push(status);
6810
+ fixes.push(`${at}.status: added a Booked / Cancelled choice field`);
6811
+ } else {
6812
+ if (!isChoice2(status)) {
6813
+ status.parityType = "select";
6814
+ status.choices = status.choices ?? [];
6815
+ fixes.push(`${at}.status: made a choice field`);
6816
+ }
6817
+ const have = new Set((status.choices ?? []).map(lower));
6818
+ for (const w of ["Booked", "Cancelled"]) {
6819
+ if (have.has(lower(w))) continue;
6820
+ status.choices = [...status.choices ?? [], w];
6821
+ status.choiceColors = { ...status.choiceColors ?? {}, [w]: colorOf(w) };
6822
+ fixes.push(`${at}.status: added the choice "${w}" the booking page writes`);
6823
+ }
6824
+ }
6825
+ }
6826
+ const before = b.questions?.length ?? 0;
6827
+ b.questions = (b.questions ?? []).filter((q2) => !DOOR_KEYS.includes(q2.fieldKey));
6828
+ if (b.questions.length !== before) fixes.push(`extras.${b.token}.questions: dropped ${before - b.questions.length} question(s) on slot/status/booked_with`);
6829
+ }
6830
+ }
6831
+ var DEFAULT_DAYS_AHEAD = 28;
6832
+ var AVAILABILITY_NUMBERS = ["slotMinutes", "bufferMinutes", "leadMinutes", "maxPerDay"];
6833
+ function fixAvailability(spec, fixes) {
6834
+ for (const e of spec.extras) {
6835
+ if (e.kind !== "booking") continue;
6836
+ const b = e;
6837
+ const a = b.availability;
6838
+ if (!a) continue;
6839
+ const at = `extras.${b.token}.availability`;
6840
+ if (Array.isArray(a.days)) {
6841
+ const weekdays = [...new Set(a.days.filter((d) => Number.isInteger(d) && d >= 0 && d <= 6))];
6842
+ a.days = DEFAULT_DAYS_AHEAD;
6843
+ fixes.push(`${at}.days: a weekday list is not "days ahead"; set to ${DEFAULT_DAYS_AHEAD} (weekdays belong in windows)`);
6844
+ const windows = Array.isArray(a.windows) ? a.windows : [];
6845
+ const model = windows[0];
6846
+ if (model && /^\d{2}:\d{2}$/.test(String(model.from))) {
6847
+ for (const d of weekdays.sort((x, y) => x - y)) {
6848
+ if (windows.some((w) => w.weekday === d)) continue;
6849
+ windows.push({ weekday: d, from: model.from, to: model.to });
6850
+ fixes.push(`${at}.windows: added a ${model.from}\u2013${model.to} window on weekday ${d} (it was listed in days)`);
6851
+ }
6852
+ a.windows = windows;
6853
+ }
6854
+ } else if (a.days !== void 0) {
6855
+ const n = typeof a.days === "string" && a.days.trim() !== "" ? Number(a.days) : a.days;
6856
+ if (typeof n === "number" && Number.isFinite(n) && n !== a.days) {
6857
+ a.days = Math.round(n);
6858
+ fixes.push(`${at}.days: written as one whole number (${a.days})`);
6859
+ } else if (typeof n === "number" && Number.isFinite(n) && !Number.isInteger(n)) {
6860
+ a.days = Math.round(n);
6861
+ fixes.push(`${at}.days: rounded to ${a.days}`);
6862
+ }
6863
+ }
6864
+ for (const k of AVAILABILITY_NUMBERS) {
6865
+ const v = a[k];
6866
+ if (v === void 0) continue;
6867
+ const n = typeof v === "string" && v.trim() !== "" ? Number(v) : v;
6868
+ if (typeof n === "number" && Number.isFinite(n) && (!Number.isInteger(n) || n !== v)) {
6869
+ a[k] = Math.round(n);
6870
+ fixes.push(`${at}.${k}: written as one whole number (${a[k]})`);
6871
+ }
6872
+ }
6873
+ for (const w of Array.isArray(a.windows) ? a.windows : []) {
6874
+ if (typeof w.weekday === "string" && /^\d$/.test(w.weekday)) {
6875
+ w.weekday = Number(w.weekday);
6876
+ fixes.push(`${at}.windows: a weekday written as a whole number`);
6877
+ }
6878
+ }
6879
+ }
6880
+ }
6881
+ function fixViewTokens(spec, fixes) {
6882
+ const taken = /* @__PURE__ */ new Set();
6883
+ const claim = (t) => typeof t === "string" && t.trim() !== "" && taken.add(t);
6884
+ for (const v of spec.views) claim(v.token);
6885
+ for (const f of spec.forms) claim(f.token);
6886
+ for (const e of spec.extras) claim(e.token);
6887
+ const snake = (x) => x.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "view";
6888
+ spec.views.forEach((v, i) => {
6889
+ if (typeof v.token === "string" && v.token.trim() !== "") return;
6890
+ const base = `${snake(String(v.table ?? "table"))}_${snake(String(v.kind ?? "view"))}`;
6891
+ let token = base;
6892
+ for (let n = 2; taken.has(token); n++) token = `${base}_${n}`;
6893
+ taken.add(token);
6894
+ v.token = token;
6895
+ fixes.push(`views[${i}]: had no token; named "${token}" (table and kind)`);
6896
+ });
6897
+ }
6898
+ function fixColours(spec, fixes) {
6899
+ for (const t of spec.tables) {
6900
+ for (const f of t.fields) {
6901
+ if (!isChoice2(f) || !f.choices?.length) continue;
6902
+ const choices = f.choices;
6903
+ const palette = CHOICE_COLORS;
6904
+ const colors = {};
6905
+ for (const [k, v] of Object.entries(f.choiceColors ?? {})) if (choices.includes(k) && palette.includes(v)) colors[k] = v;
6906
+ const orderOf = (c) => GOOD_OUTCOME.test(c) ? GOOD_ORDER : BAD_OUTCOME.test(c) ? BAD_ORDER : NEUTRAL_ORDER;
6907
+ const holder = (color, except) => choices.find((c) => c !== except && colors[c] === color);
6908
+ const unused = (order) => order.find((c) => !choices.some((x) => colors[x] === c));
6909
+ const need = Math.min(choices.length, DISTINCT_UP_TO);
6910
+ const seen = /* @__PURE__ */ new Set();
6911
+ for (const c of choices) {
6912
+ const col = colors[c];
6913
+ if (col && seen.has(col) && new Set(Object.values(colors)).size < need) {
6914
+ const next = unused(orderOf(c));
6915
+ if (next) colors[c] = next;
6916
+ }
6917
+ if (colors[c]) seen.add(colors[c]);
6918
+ }
6919
+ for (const c of choices) {
6920
+ if (colors[c]) continue;
6921
+ colors[c] = unused(orderOf(c)) ?? orderOf(c)[choices.indexOf(c) % orderOf(c).length];
6922
+ }
6923
+ for (const c of choices) {
6924
+ const wrong = GOOD_OUTCOME.test(c) ? "red" : BAD_OUTCOME.test(c) ? "green" : null;
6925
+ if (!wrong || colors[c] !== wrong) continue;
6926
+ const want = GOOD_OUTCOME.test(c) ? "green" : "red";
6927
+ const other = holder(want, c);
6928
+ if (!other) colors[c] = want;
6929
+ else if (!(want === "red" ? GOOD_OUTCOME.test(other) : BAD_OUTCOME.test(other))) {
6930
+ colors[other] = wrong;
6931
+ colors[c] = want;
6932
+ } else colors[c] = unused(orderOf(c).filter((x) => x !== wrong)) ?? colors[c];
6933
+ }
6934
+ const ordered = {};
6935
+ for (const c of choices) ordered[c] = colors[c];
6936
+ const was = f.choiceColors ?? {};
6937
+ if (Object.keys(was).length !== choices.length || choices.some((c) => was[c] !== ordered[c])) {
6938
+ f.choiceColors = ordered;
6939
+ fixes.push(`tables.${t.token}.fields.${f.key}: choice colours filled and checked (good never red, bad never green)`);
6940
+ }
6941
+ }
6942
+ }
6943
+ }
6944
+ function fixRelationships(spec, ctx, fixes) {
6945
+ const everyTable = allTables(spec);
6946
+ const byToken = new Map(everyTable.map((t) => [t.token, t]));
6947
+ const rels = spec.relationships ?? [];
6948
+ for (const t of spec.tables) {
6949
+ for (const f of t.fields) {
6950
+ if (f.parityType !== "relation" || !f.relationTarget) continue;
6951
+ if (rels.some((r) => r.fromTable === t.token && r.fromField === f.key)) continue;
6952
+ const target = byToken.get(f.relationTarget);
6953
+ const toName = lower(target?.labelSingular ?? f.relationTarget.replace(/_/g, " "));
6954
+ const fromName = lower(t.labelSingular ?? t.token.replace(/_/g, " "));
6955
+ const many = !!f.multi || f.relationMax !== void 0 && f.relationMax !== 1;
6956
+ const planned = ctx.plan?.tables.find((p) => p.token === t.token && p.links.some((l) => l.key === f.key))?.purpose?.trim();
6957
+ const describes = planned && planned.length >= 15 && planned.length <= 140 ? planned : many ? `A ${fromName} lists its ${toName} records.` : `A ${fromName} belongs to one ${toName}.`;
6958
+ const taken = /* @__PURE__ */ new Set([...rels.map((r) => r.inverseKey), ...(target?.fields ?? []).map((x) => x.key)]);
6959
+ let inverseKey = `${t.token}s`;
6960
+ if (taken.has(inverseKey)) inverseKey = `${t.token}_${f.key}s`;
6961
+ for (let i = 2; taken.has(inverseKey); i++) inverseKey = `${t.token}_${f.key}s_${i}`;
6962
+ const toPerson = target?.subject === "person" && f.relationTarget !== "team_member";
6963
+ rels.push({ describes, fromTable: t.token, fromField: f.key, toTable: f.relationTarget, flavor: "referenced", cardinality: many ? "many" : "one", onDelete: toPerson ? "restrict" : "set_null", inverseKey });
6964
+ fixes.push(`relationships: stated ${t.token}.${f.key} \u2192 ${f.relationTarget}`);
6965
+ }
6966
+ }
6967
+ spec.relationships = rels;
6968
+ }
6969
+ function askRequired(spec, fixes) {
6970
+ for (const form of spec.forms) {
6971
+ const t = spec.tables.find((x) => x.token === form.table);
6972
+ if (!t) continue;
6973
+ const asked = new Set((form.fields ?? []).map((q2) => q2.fieldKey));
6974
+ for (const f of t.fields) {
6975
+ if (!f.required || isComputed2(f) || f.default !== void 0 || asked.has(f.key)) continue;
6976
+ form.fields = [...form.fields ?? [], { fieldKey: f.key, ask: `What is the ${lower(f.label)}?`, required: true }];
6977
+ fixes.push(`forms.${form.token}.fields.${f.key}: asked (a required field no question covered); reword in repair`);
6978
+ }
6979
+ }
6980
+ }
6981
+ var REALISM = ["childDates", "withinParent", "statusAgrees", "countMatches", "datePairs"];
6982
+ function switchJob(spec, fixes) {
6983
+ if (spec.job !== "crm_pipeline" && spec.job !== "billing") return;
6984
+ const linked = (spec.relationships ?? []).length > 0 || spec.tables.some((t) => t.fields.some((f) => f.parityType === "relation" && f.relationTarget && spec.tables.some((x) => x.token === f.relationTarget)));
6985
+ if (!linked) return;
6986
+ const declared = spec.tables.some((t) => REALISM.some((k) => Array.isArray(t[k]) && t[k].length > 0));
6987
+ if (declared) return;
6988
+ const to = spec.job === "crm_pipeline" ? "work_orders" : "intake";
6989
+ fixes.push(`job: ${spec.job} switched to ${to} (linked tables, no realism declarations)`);
6990
+ spec.job = to;
6991
+ }
6992
+ function autofixDescribeSpec(input, ctx = {}) {
6993
+ const spec = describeSpec(clone2(input));
6994
+ const autoFixes = [];
6995
+ raiseSensitivity(spec, autoFixes);
6996
+ flagAskedOfSubject(spec, ctx, autoFixes);
6997
+ fixBooking(spec, autoFixes);
6998
+ fixAvailability(spec, autoFixes);
6999
+ fixViewTokens(spec, autoFixes);
7000
+ fixColours(spec, autoFixes);
7001
+ fixRelationships(spec, ctx, autoFixes);
7002
+ askRequired(spec, autoFixes);
7003
+ switchJob(spec, autoFixes);
7004
+ return { spec, autoFixes };
7005
+ }
7006
+ function describeCheck(spec, ctx = {}) {
7007
+ const fixed = autofixDescribeSpec(spec, ctx);
7008
+ const v = validateTemplate(fixed.spec, { profile: "describe" });
7009
+ return { ...fixed, problems: v.problems.map((p) => ({ at: p.at, says: p.says })) };
7010
+ }
7011
+
7012
+ // src/templates/describe/repair.ts
7013
+ var clone3 = (v) => JSON.parse(JSON.stringify(v));
7014
+ function parseAt(at) {
7015
+ const out = [];
7016
+ const re = /([A-Za-z_][A-Za-z0-9_]*)(?:\[(\d+)\])?(?: \(([^)]*)\))?/g;
7017
+ for (let m = re.exec(at); m; m = re.exec(at)) {
7018
+ const named = m[3] !== void 0 && m[3] !== "" && m[3] !== "undefined" && m[3] !== "null";
7019
+ out.push({ root: m[1], ...named ? { id: m[3] } : m[2] !== void 0 ? { id: `#${m[2]}` } : {} });
7020
+ }
7021
+ return out;
7022
+ }
7023
+ function pathOfAt(at) {
7024
+ const segs = parseAt(at);
7025
+ const [a, b] = segs;
7026
+ if (!a) return "spec";
7027
+ if ((a.root === "tables" || a.root === "forms") && a.id !== void 0) {
7028
+ if (b && (b.root === "fields" || b.root === "questions") && b.id) return `${a.root}.${a.id}.fields.${b.id}`;
7029
+ return `${a.root}.${a.id}`;
7030
+ }
7031
+ if (a.root === "extras" && a.id) {
7032
+ if (b && b.root === "questions" && b.id) return `extras.${a.id}.questions.${b.id}`;
7033
+ return b && b.root === "availability" ? `extras.${a.id}.availability` : `extras.${a.id}`;
7034
+ }
7035
+ if (a.root === "views" && a.id) return `views.${a.id}`;
7036
+ if (a.root === "business") return "business";
7037
+ return "spec";
7038
+ }
7039
+ var idOf = (o) => typeof o.key === "string" ? o.key : typeof o.fieldKey === "string" ? o.fieldKey : typeof o.token === "string" ? o.token : void 0;
7040
+ function findIn(list, id) {
7041
+ if (!Array.isArray(list)) return void 0;
7042
+ if (/^#\d+$/.test(id)) return list[Number(id.slice(1))];
7043
+ return list.find((x) => x && idOf(x) === id);
7044
+ }
7045
+ function resolvePath(spec, path) {
7046
+ const s = spec;
7047
+ const parts = path.split(".");
7048
+ const [root, id, sub, subId] = parts;
7049
+ if (root === "spec" || !root) return s;
7050
+ if (root === "business") return s.business;
7051
+ if (root === "relationships") {
7052
+ const rels = s.relationships ?? [];
7053
+ return rels.find((r) => r.fromTable === id && (parts[2] === void 0 || r.fromField === parts[2]));
7054
+ }
7055
+ let item;
7056
+ if (root === "extras") item = findIn(s.extras, id ?? "") ?? (s.extras ?? []).find((e) => e.kind === id);
7057
+ else if (root === "tables" || root === "forms" || root === "views") item = findIn(s[root], id ?? "");
7058
+ if (!item) return void 0;
7059
+ if (!sub) return item;
7060
+ if (sub === "availability") return item.availability;
7061
+ if ((sub === "fields" || sub === "questions") && subId) return findIn(item[sub], subId);
7062
+ return void 0;
7063
+ }
7064
+ function specBrief(spec, problems) {
7065
+ const s = spec;
7066
+ const brief = { job: spec.job, industry: spec.industry, vertical: spec.vertical, business: spec.business, tables: spec.tables.map((t) => t.token), forms: spec.forms.map((f) => f.token), views: spec.views.map((v) => v.token) };
7067
+ for (const p of problems) {
7068
+ const root = parseAt(p.at)[0]?.root;
7069
+ if (root && root !== "spec" && root in s && !(root in brief)) brief[root] = clone3(s[root]);
7070
+ }
7071
+ return brief;
7072
+ }
7073
+ function tableBrief(t) {
7074
+ const { fields, rows: _rows, ...own } = t;
7075
+ return { ...own, fields: fields.map((f) => ({ key: f.key, label: f.label, parityType: f.parityType, ...f.choices ? { choices: f.choices } : {}, ...f.relationTarget ? { relationTarget: f.relationTarget } : {}, ...f.required ? { required: true } : {} })) };
7076
+ }
7077
+ function repairSlices(spec, problems) {
7078
+ const byPath = /* @__PURE__ */ new Map();
7079
+ for (const p of problems) {
7080
+ const first = parseAt(p.at)[0];
7081
+ const rel = first?.root === "relationships" && first.id !== void 0 && /^#\d+$/.test(first.id) ? spec.relationships?.[Number(first.id.slice(1))] : void 0;
7082
+ const path = rel ? `relationships.${rel.fromTable}.${rel.fromField}` : pathOfAt(p.at);
7083
+ let slice = byPath.get(path);
7084
+ if (!slice) {
7085
+ const found = resolvePath(spec, path);
7086
+ const isTable = path.startsWith("tables.") && path.split(".").length === 2;
7087
+ const value = path === "spec" ? specBrief(spec, problems.filter((q2) => pathOfAt(q2.at) === "spec")) : isTable && found ? tableBrief(found) : found ? clone3(found) : null;
7088
+ slice = { path, slice: value, problems: [] };
7089
+ byPath.set(path, slice);
7090
+ }
7091
+ slice.problems.push({ at: p.at, says: p.says });
7092
+ }
7093
+ return [...byPath.values()];
7094
+ }
7095
+ var IMMUTABLE = /* @__PURE__ */ new Set(["key", "token", "fieldKey"]);
7096
+ function applyRepairPatches(input, patches) {
7097
+ const spec = clone3(input);
7098
+ const applied = [];
7099
+ const skipped = [];
7100
+ for (const patch of patches ?? []) {
7101
+ const path = String(patch?.path ?? "");
7102
+ const set = patch?.set;
7103
+ if (!path || !set || typeof set !== "object" || Array.isArray(set)) {
7104
+ skipped.push({ path, why: "a patch needs a path and a set object" });
7105
+ continue;
7106
+ }
7107
+ let target = resolvePath(spec, path);
7108
+ if (!target) {
7109
+ const parts = path.split(".");
7110
+ const parent = resolvePath(spec, parts.slice(0, 2).join("."));
7111
+ const [root, , sub, subId] = parts;
7112
+ if (parent && root === "tables" && sub === "fields" && subId && parts.length === 4 && typeof set.label === "string" && typeof set.parityType === "string") {
7113
+ const f = { key: subId, sensitivity: "internal", contextPolicy: "include", ...set };
7114
+ parent.fields.push(f);
7115
+ target = f;
7116
+ applied.push(path);
7117
+ continue;
7118
+ }
7119
+ if (parent && root === "forms" && sub === "fields" && subId && parts.length === 4 && typeof set.ask === "string") {
7120
+ (parent.fields ??= []).push({ fieldKey: subId, ...set });
7121
+ applied.push(path);
7122
+ continue;
7123
+ }
7124
+ skipped.push({ path, why: "no such piece in the spec" });
7125
+ continue;
7126
+ }
7127
+ for (const [k, v] of Object.entries(set)) {
7128
+ if (IMMUTABLE.has(k) && v !== target[k]) {
7129
+ skipped.push({ path: `${path}.${k}`, why: `${k} is never renamed by a patch` });
7130
+ continue;
7131
+ }
7132
+ if (v === null) delete target[k];
7133
+ else target[k] = v;
7134
+ }
7135
+ applied.push(path);
7136
+ }
7137
+ return { spec, applied, skipped };
7138
+ }
7139
+ function lastResortTrim(input, problems) {
7140
+ const spec = clone3(input);
7141
+ const notes = [];
7142
+ const droppedViews = /* @__PURE__ */ new Set();
7143
+ const views = spec.views;
7144
+ for (const p of problems) {
7145
+ const segs = parseAt(p.at);
7146
+ const [a, b] = segs;
7147
+ const last = segs[segs.length - 1];
7148
+ if (!a || !a.id) continue;
7149
+ if (a.root === "views") {
7150
+ const v = findIn(views, a.id);
7151
+ if (v && views.length - droppedViews.size > 1 && !droppedViews.has(v)) {
7152
+ droppedViews.add(v);
7153
+ notes.push(`Left out the view "${v.name}": it did not pass the store's check.`);
7154
+ }
7155
+ continue;
7156
+ }
7157
+ if (last?.root === "help") {
7158
+ const holder = a.root === "tables" ? resolvePath(spec, `tables.${a.id}.fields.${b?.id ?? ""}`) : a.root === "forms" ? resolvePath(spec, `forms.${a.id}.fields.${b?.id ?? ""}`) : a.root === "extras" ? resolvePath(spec, `extras.${a.id}.questions.${b?.id ?? ""}`) : void 0;
7159
+ if (holder && "help" in holder) {
7160
+ delete holder.help;
7161
+ notes.push(`Left out the help text on "${b?.id}": it did not pass the store's check.`);
7162
+ }
7163
+ continue;
7164
+ }
7165
+ if (last?.root === "ask" && b?.id) {
7166
+ const holder = a.root === "forms" ? findIn(spec.forms, a.id) : a.root === "extras" ? findIn(spec.extras, a.id) : void 0;
7167
+ const list = (a.root === "forms" ? holder?.fields : holder?.questions) ?? null;
7168
+ const table = holder?.table;
7169
+ const field2 = spec.tables.find((t) => t.token === table)?.fields.find((f) => f.key === b.id);
7170
+ const q2 = list?.find((x) => x.fieldKey === b.id);
7171
+ if (list && q2 && !q2.required && !field2?.required) {
7172
+ list.splice(list.indexOf(q2), 1);
7173
+ notes.push(`Left out the question on "${b.id}": it did not pass the store's check.`);
7174
+ }
7175
+ }
7176
+ }
7177
+ if (droppedViews.size) spec.views = spec.views.filter((x) => !droppedViews.has(x));
7178
+ return { spec, notes };
7179
+ }
7180
+
7181
+ // src/templates/describe/package.ts
7182
+ var WORTH_SAYING = /^job:|: added |: asked |: dropped |dropped \d+ question/;
7183
+ var NOTES_MAX = 7;
7184
+ function packageDescribeResult(spec, plan, autoFixes, reuses, extraNotes = []) {
7185
+ const tokens = new Set(spec.tables.map((t) => t.token));
7186
+ const left = plan?.leaveOut?.length ? [`Left out: ${plan.leaveOut.join("; ")}.`] : [];
7187
+ const said = autoFixes.filter((f) => WORTH_SAYING.test(f)).map((f) => f.startsWith("job:") ? `Used a simpler setup type: ${f.slice(5)}.` : f);
7188
+ const notes = [...plan?.assumptions ?? [], ...left, ...said, ...extraNotes].map((n) => n.trim()).filter(Boolean).slice(0, NOTES_MAX);
7189
+ return {
7190
+ __kind: "describe_template_result",
7191
+ template: JSON.parse(JSON.stringify(spec)),
7192
+ notes,
7193
+ reuses: (reuses ?? []).filter((r) => r && tokens.has(r.token))
7194
+ };
7195
+ }
7196
+
7197
+ // src/templates/describe/vocabulary.ts
7198
+ var BOOKING_AVAILABILITY_FACTS = {
7199
+ timezone: "text \u2014 an IANA time zone such as America/Los_Angeles; defaults to the business's",
7200
+ slotMinutes: "one whole number 5 to 480 \u2014 how long one appointment is, in minutes",
7201
+ bufferMinutes: "one whole number 0 to 480 \u2014 rest between appointments, in minutes",
7202
+ leadMinutes: "one whole number 0 to 43200 \u2014 how much notice a person must give, in minutes",
7203
+ maxPerDay: "one whole number 1 to 100 \u2014 appointments accepted in one day",
7204
+ days: "how many days ahead a person may book \u2014 ONE whole number such as 21 or 28 (1 to 90); weekdays belong in windows, never here",
7205
+ windows: "list of {weekday, from, to}: weekday one whole number 0 (Sunday) to 6 (Saturday), from and to written HH:MM with to later than from; one entry per weekday offered"
7206
+ };
7207
+ var VIEW_SHAPE_FACTS = {
7208
+ token: "snake_case, unique among every view, form and extra \u2014 always written",
7209
+ name: 'what a person clicks, such as "Intake board"',
7210
+ table: "the token of one of the template's tables",
7211
+ kind: TEMPLATE_VIEW_KINDS,
7212
+ groupBy: "kanban only, required: the key of a select field whose choices become the columns",
7213
+ dateField: "calendar only, required: the key of a datetime field",
7214
+ startField_endField: "timeline only, required: keys of two datetime fields",
7215
+ coverField: "gallery only, required: the key of a file field"
7216
+ };
7217
+ function describeVocabulary() {
7218
+ return {
7219
+ field_kinds: RESOLVABLE_FIELD_TYPE_WORDS,
7220
+ table_types: TABLE_TYPES,
7221
+ table_displays: TABLE_DISPLAYS,
7222
+ storage_modes: STORAGE_MODES,
7223
+ field_sensitivities: FIELD_SENSITIVITIES,
7224
+ context_policies: CONTEXT_POLICIES,
7225
+ field_sources: FIELD_SOURCES,
7226
+ relation_flavors: RELATION_FLAVORS,
7227
+ relation_cardinalities: RELATION_CARDINALITIES,
7228
+ on_delete: ON_DELETE,
7229
+ industry_groups: INDUSTRY_GROUPS,
7230
+ template_jobs: TEMPLATE_JOBS,
7231
+ teaches_features: TEACHES_FEATURES,
7232
+ choice_colors: CHOICE_COLORS,
7233
+ template_view_kinds: TEMPLATE_VIEW_KINDS,
7234
+ template_audiences: TEMPLATE_AUDIENCES,
7235
+ form_audiences: ["public", "member", "staff"],
7236
+ extra_kinds_for_describe: ["booking"],
7237
+ booking_window: { weekday: "0 = Sunday \u2026 6 = Saturday", from: "HH:MM", to: "HH:MM later than from" },
7238
+ booking_availability: BOOKING_AVAILABILITY_FACTS,
7239
+ view_shape: VIEW_SHAPE_FACTS,
7240
+ 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"
7241
+ };
7242
+ }
6496
7243
  //# sourceMappingURL=index.cjs.map