@ai-matrx/records 0.76.8 → 0.76.11

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