@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.
@@ -371,9 +371,9 @@ function membersAddRowsSteps(spec, tableRef) {
371
371
  }));
372
372
  }
373
373
  function confidentialRequests(spec, made) {
374
- const idOf = (token) => made.find((m) => m.kind === "table" && m.ref === `tables.${token}`)?.id ?? null;
374
+ const idOf2 = (token) => made.find((m) => m.kind === "table" && m.ref === `tables.${token}`)?.id ?? null;
375
375
  return (spec.confidential ?? []).map((c) => {
376
- const tableId = idOf(c.table);
376
+ const tableId = idOf2(c.table);
377
377
  if (!tableId) throw new Error(`The install made no "${c.table}" table, so it cannot be made Confidential.`);
378
378
  const readers = c.readers.map((r) => ({ field: r.field, level: r.level, ...r.when ? { when: whenRule(c.table, r.when, made) } : {} }));
379
379
  return {
@@ -1479,6 +1479,7 @@ var TABLE_TYPES = [
1479
1479
  var RELATION_FLAVORS = ["owned", "referenced"];
1480
1480
  var ON_DELETE = ["cascade", "set_null", "restrict"];
1481
1481
  var RELATION_CARDINALITIES = ["one", "many"];
1482
+ var FIELD_SOURCES = ["manual", "formula", "agent", "synced"];
1482
1483
  var FIELD_SENSITIVITIES = ["public", "internal", "confidential", "restricted"];
1483
1484
  var CONTEXT_POLICIES = ["include", "summarize", "exclude", "on_request"];
1484
1485
  var TABLE_DISPLAYS = ["list", "grid", "board", "calendar", "timeline"];
@@ -1825,39 +1826,39 @@ function validateUseCase(useCase) {
1825
1826
  `The title field "${table.titleField}" is not one of this table's fields \u2014 without it a record cannot be a chip.`
1826
1827
  );
1827
1828
  }
1828
- table.fields.forEach((field, fi) => {
1829
- const fat = `${at}.fields[${fi}] (${field.key})`;
1830
- if (!/^[a-z][a-z0-9_]*$/.test(field.key ?? "")) {
1831
- say(`${fat}.key`, `"${field.key}" should be snake_case and start with a letter.`);
1829
+ table.fields.forEach((field2, fi) => {
1830
+ const fat = `${at}.fields[${fi}] (${field2.key})`;
1831
+ if (!/^[a-z][a-z0-9_]*$/.test(field2.key ?? "")) {
1832
+ say(`${fat}.key`, `"${field2.key}" should be snake_case and start with a letter.`);
1832
1833
  }
1833
- if (!RESOLVABLE_FIELD_TYPE_WORDS.includes(field.parityType)) {
1834
+ if (!RESOLVABLE_FIELD_TYPE_WORDS.includes(field2.parityType)) {
1834
1835
  say(
1835
1836
  `${fat}.parityType`,
1836
- `The store does not resolve the type word "${field.parityType}". Use one of: ${RESOLVABLE_FIELD_TYPE_WORDS.join(", ")}.`
1837
+ `The store does not resolve the type word "${field2.parityType}". Use one of: ${RESOLVABLE_FIELD_TYPE_WORDS.join(", ")}.`
1837
1838
  );
1838
1839
  }
1839
- if (!FIELD_SENSITIVITIES.includes(field.sensitivity)) {
1840
+ if (!FIELD_SENSITIVITIES.includes(field2.sensitivity)) {
1840
1841
  say(
1841
1842
  `${fat}.sensitivity`,
1842
1843
  `The store refuses a field that does not say how sensitive its values are. Use one of: ${FIELD_SENSITIVITIES.join(", ")}.`
1843
1844
  );
1844
1845
  }
1845
- if (!CONTEXT_POLICIES.includes(field.contextPolicy)) {
1846
+ if (!CONTEXT_POLICIES.includes(field2.contextPolicy)) {
1846
1847
  say(
1847
1848
  `${fat}.contextPolicy`,
1848
1849
  `The store refuses a field that does not say whether an agent may see its values. Use one of: ${CONTEXT_POLICIES.join(", ")}.`
1849
1850
  );
1850
1851
  }
1851
- if (CHOICE_TYPES.has(field.parityType) && !field.choices?.length) {
1852
+ if (CHOICE_TYPES.has(field2.parityType) && !field2.choices?.length) {
1852
1853
  say(
1853
1854
  `${fat}.choices`,
1854
1855
  "A choice field has to list the business's real words, in the order they really happen \u2014 'Scheduled, Confirmed, Seen, No-show'."
1855
1856
  );
1856
1857
  }
1857
- if (RELATION_TYPES.has(field.parityType) && field.relationTarget && !tablesByToken.has(field.relationTarget)) {
1858
- say(`${fat}.relationTarget`, `It points at the table "${field.relationTarget}", which this use case does not declare.`);
1858
+ if (RELATION_TYPES.has(field2.parityType) && field2.relationTarget && !tablesByToken.has(field2.relationTarget)) {
1859
+ say(`${fat}.relationTarget`, `It points at the table "${field2.relationTarget}", which this use case does not declare.`);
1859
1860
  }
1860
- if (field.parityType === "currency" && !field.unit) {
1861
+ if (field2.parityType === "currency" && !field2.unit) {
1861
1862
  say(`${fat}.unit`, "A currency field carries its currency as the unit \u2014 the unit changes the meaning.");
1862
1863
  }
1863
1864
  });
@@ -1872,16 +1873,16 @@ function validateUseCase(useCase) {
1872
1873
  say(`${rat}.values.${valueKey}`, `This row sets "${valueKey}", which the table does not declare as a field.`);
1873
1874
  }
1874
1875
  }
1875
- for (const field of table.fields) {
1876
- if (field.required && (row.values?.[field.key] === void 0 || row.values?.[field.key] === null)) {
1877
- say(`${rat}.values.${field.key}`, `"${field.label}" is required, and this row leaves it empty.`);
1876
+ for (const field2 of table.fields) {
1877
+ if (field2.required && (row.values?.[field2.key] === void 0 || row.values?.[field2.key] === null)) {
1878
+ say(`${rat}.values.${field2.key}`, `"${field2.label}" is required, and this row leaves it empty.`);
1878
1879
  }
1879
- const value = row.values?.[field.key];
1880
- if (value !== void 0 && field.choices?.length && !field.multi) {
1881
- if (typeof value === "string" && !field.choices.includes(value)) {
1880
+ const value = row.values?.[field2.key];
1881
+ if (value !== void 0 && field2.choices?.length && !field2.multi) {
1882
+ if (typeof value === "string" && !field2.choices.includes(value)) {
1882
1883
  say(
1883
- `${rat}.values.${field.key}`,
1884
- `"${value}" is not one of "${field.label}"'s choices (${field.choices.join(", ")}).`
1884
+ `${rat}.values.${field2.key}`,
1885
+ `"${value}" is not one of "${field2.label}"'s choices (${field2.choices.join(", ")}).`
1885
1886
  );
1886
1887
  }
1887
1888
  }
@@ -1905,18 +1906,18 @@ function validateUseCase(useCase) {
1905
1906
  if (!tablesByToken.has(rel.toTable)) {
1906
1907
  say(`${at}.toTable`, `There is no table "${rel.toTable}" in this use case.`);
1907
1908
  }
1908
- const field = resolveField(rel.fromTable, rel.fromField);
1909
- if (!field) {
1909
+ const field2 = resolveField(rel.fromTable, rel.fromField);
1910
+ if (!field2) {
1910
1911
  say(`${at}.fromField`, `The table "${rel.fromTable}" does not declare a field "${rel.fromField}".`);
1911
- } else if (!RELATION_TYPES.has(field.parityType)) {
1912
+ } else if (!RELATION_TYPES.has(field2.parityType)) {
1912
1913
  say(
1913
1914
  `${at}.fromField`,
1914
- `"${rel.fromField}" is a ${field.parityType} field, so it cannot hold a relation. Declare it as a relation.`
1915
+ `"${rel.fromField}" is a ${field2.parityType} field, so it cannot hold a relation. Declare it as a relation.`
1915
1916
  );
1916
- } else if (field.relationTarget && field.relationTarget !== rel.toTable) {
1917
+ } else if (field2.relationTarget && field2.relationTarget !== rel.toTable) {
1917
1918
  say(
1918
1919
  `${at}.fromField`,
1919
- `The field points at "${field.relationTarget}" but the relationship says "${rel.toTable}". They have to agree.`
1920
+ `The field points at "${field2.relationTarget}" but the relationship says "${rel.toTable}". They have to agree.`
1920
1921
  );
1921
1922
  }
1922
1923
  if (!RELATION_FLAVORS.includes(rel.flavor)) {
@@ -2074,12 +2075,12 @@ var ACTOR = "user";
2074
2075
  var COMPUTED = /* @__PURE__ */ new Set(["lookup", "rollup", "formula"]);
2075
2076
  var isTimedField = (f) => f.parityType === "datetime" && f.format === "datetime";
2076
2077
  var isSelfDeclaredField = (f) => f.parityType !== "relation" && !COMPUTED.has(f.parityType) && (isTimedField(f) || f.default !== void 0 || f.parityType === "entity_reference");
2077
- function choiceDisplayFormat(field) {
2078
- if (!field.choices?.length || !field.choiceColors) return void 0;
2078
+ function choiceDisplayFormat(field2) {
2079
+ if (!field2.choices?.length || !field2.choiceColors) return void 0;
2079
2080
  return {
2080
- id: field.parityType === "multi_select" ? "multi_choice" : "choice",
2081
+ id: field2.parityType === "multi_select" ? "multi_choice" : "choice",
2081
2082
  options: {
2082
- choices: field.choices.filter((c) => field.choiceColors?.[c]).map((c) => ({ value: c, color: field.choiceColors[c] }))
2083
+ choices: field2.choices.filter((c) => field2.choiceColors?.[c]).map((c) => ({ value: c, color: field2.choiceColors[c] }))
2083
2084
  }
2084
2085
  };
2085
2086
  }
@@ -2147,49 +2148,49 @@ function shellsPayload(spec, tables, deferred = deferredRelations(tables)) {
2147
2148
  })
2148
2149
  };
2149
2150
  }
2150
- function timedFieldSpec(field) {
2151
+ function timedFieldSpec(field2) {
2151
2152
  return {
2152
- key: field.key,
2153
- label: field.label,
2153
+ key: field2.key,
2154
+ label: field2.label,
2154
2155
  type: "datetime",
2155
2156
  kind: "datetime",
2156
- ...field.required ? { required: true } : {},
2157
- ...field.rules ? { rules: field.rules } : {},
2158
- sensitivity: field.sensitivity,
2159
- context_policy: field.contextPolicy
2157
+ ...field2.required ? { required: true } : {},
2158
+ ...field2.rules ? { rules: field2.rules } : {},
2159
+ sensitivity: field2.sensitivity,
2160
+ context_policy: field2.contextPolicy
2160
2161
  };
2161
2162
  }
2162
- function selfDeclaredFieldSpec(field) {
2163
- if (isTimedField(field)) return { ...timedFieldSpec(field), ...field.default !== void 0 ? { default: field.default } : {} };
2164
- const allowed = field.parityType === "entity_reference" ? { allowed_types: field.allowedTypes ?? [] } : {};
2165
- const displayFormat = choiceDisplayFormat(field);
2163
+ function selfDeclaredFieldSpec(field2) {
2164
+ if (isTimedField(field2)) return { ...timedFieldSpec(field2), ...field2.default !== void 0 ? { default: field2.default } : {} };
2165
+ const allowed = field2.parityType === "entity_reference" ? { allowed_types: field2.allowedTypes ?? [] } : {};
2166
+ const displayFormat = choiceDisplayFormat(field2);
2166
2167
  return {
2167
- key: field.key,
2168
- label: field.label,
2169
- type: field.parityType,
2170
- ...field.multi ? { multi: true } : {},
2171
- ...field.required ? { required: true } : {},
2172
- ...field.unit ? { unit: field.unit } : {},
2173
- ...field.rules ? { rules: field.rules } : {},
2174
- ...field.choices ? { options: field.choices } : {},
2168
+ key: field2.key,
2169
+ label: field2.label,
2170
+ type: field2.parityType,
2171
+ ...field2.multi ? { multi: true } : {},
2172
+ ...field2.required ? { required: true } : {},
2173
+ ...field2.unit ? { unit: field2.unit } : {},
2174
+ ...field2.rules ? { rules: field2.rules } : {},
2175
+ ...field2.choices ? { options: field2.choices } : {},
2175
2176
  ...displayFormat ? { display_format: displayFormat } : {},
2176
- ...field.default !== void 0 ? { default: field.default } : {},
2177
+ ...field2.default !== void 0 ? { default: field2.default } : {},
2177
2178
  ...allowed,
2178
- sensitivity: field.sensitivity,
2179
- context_policy: field.contextPolicy
2179
+ sensitivity: field2.sensitivity,
2180
+ context_policy: field2.contextPolicy
2180
2181
  };
2181
2182
  }
2182
- function relationFieldSpec(field, targetTableId) {
2183
+ function relationFieldSpec(field2, targetTableId) {
2183
2184
  return {
2184
- key: field.key,
2185
- label: field.label,
2185
+ key: field2.key,
2186
+ label: field2.label,
2186
2187
  type: "relation",
2187
2188
  relation_target: targetTableId,
2188
- ...field.multi ? { multi: true } : {},
2189
- ...field.relationMax ? { relation_max: field.relationMax } : {},
2190
- ...field.required ? { required: true } : {},
2191
- sensitivity: field.sensitivity,
2192
- context_policy: field.contextPolicy
2189
+ ...field2.multi ? { multi: true } : {},
2190
+ ...field2.relationMax ? { relation_max: field2.relationMax } : {},
2191
+ ...field2.required ? { required: true } : {},
2192
+ sensitivity: field2.sensitivity,
2193
+ context_policy: field2.contextPolicy
2193
2194
  };
2194
2195
  }
2195
2196
  function rowLevels(tables) {
@@ -2249,12 +2250,12 @@ function relationPatches(table, ids, deferred) {
2249
2250
  for (const [key, target] of rel) {
2250
2251
  const v = r.values?.[key];
2251
2252
  if (v === null || v === void 0) continue;
2252
- const idOf = (k) => {
2253
+ const idOf2 = (k) => {
2253
2254
  const id = ids.get(`${target}.${String(k)}`);
2254
2255
  if (!id) throw new Error(`A row points at "${String(k)}" in "${target}", which the template does not have.`);
2255
2256
  return id;
2256
2257
  };
2257
- patch[key] = Array.isArray(v) ? v.map(idOf) : idOf(v);
2258
+ patch[key] = Array.isArray(v) ? v.map(idOf2) : idOf2(v);
2258
2259
  }
2259
2260
  if (Object.keys(patch).length) out.push({ op: "update", record_id: ids.get(`${table.token}.${r.key}`), patch: { ...patch, _actor: ACTOR } });
2260
2261
  }
@@ -2263,7 +2264,7 @@ function relationPatches(table, ids, deferred) {
2263
2264
  function rowsFor(table, ids, deferred) {
2264
2265
  const relation = new Map(table.fields.filter((f) => f.parityType === "relation").map((f) => [f.key, f]));
2265
2266
  const derived = new Set(table.fields.filter((f) => COMPUTED.has(f.parityType)).map((f) => f.key));
2266
- const idOf = (target, key) => {
2267
+ const idOf2 = (target, key) => {
2267
2268
  const id = ids.get(`${target}.${String(key)}`);
2268
2269
  if (!id) throw new Error(`A row points at "${String(key)}" in "${target}", which the template does not have.`);
2269
2270
  return id;
@@ -2277,7 +2278,7 @@ function rowsFor(table, ids, deferred) {
2277
2278
  values[key] = value;
2278
2279
  continue;
2279
2280
  }
2280
- values[key] = Array.isArray(value) ? value.map((v) => idOf(rel.relationTarget, v)) : idOf(rel.relationTarget, value);
2281
+ values[key] = Array.isArray(value) ? value.map((v) => idOf2(rel.relationTarget, v)) : idOf2(rel.relationTarget, value);
2281
2282
  }
2282
2283
  if (typeof r.icon === "string" && r.icon.trim()) values["icon"] = r.icon.trim();
2283
2284
  values["_actor"] = ACTOR;
@@ -2285,17 +2286,17 @@ function rowsFor(table, ids, deferred) {
2285
2286
  });
2286
2287
  return { rows, ids: table.rows.map((r) => ids.get(`${table.token}.${r.key}`)) };
2287
2288
  }
2288
- function derivedFieldSpec(field) {
2289
+ function derivedFieldSpec(field2) {
2289
2290
  const base = {
2290
- key: field.key,
2291
- label: field.label,
2292
- type: field.parityType,
2293
- via: field.via,
2294
- sensitivity: field.sensitivity,
2295
- context_policy: field.contextPolicy,
2296
- ...field.unit ? { unit: field.unit } : {}
2291
+ key: field2.key,
2292
+ label: field2.label,
2293
+ type: field2.parityType,
2294
+ via: field2.via,
2295
+ sensitivity: field2.sensitivity,
2296
+ context_policy: field2.contextPolicy,
2297
+ ...field2.unit ? { unit: field2.unit } : {}
2297
2298
  };
2298
- return field.parityType === "rollup" ? { ...base, agg: field.agg, of: field.pick, ...field.filter ? { filter: field.filter } : {} } : { ...base, pick: field.pick };
2299
+ return field2.parityType === "rollup" ? { ...base, agg: field2.agg, of: field2.pick, ...field2.filter ? { filter: field2.filter } : {} } : { ...base, pick: field2.pick };
2299
2300
  }
2300
2301
  function viewSpec(view, fieldIds = {}) {
2301
2302
  const definition = { layout: view.kind };
@@ -2341,15 +2342,15 @@ function anyOfWhere(anyOf, fieldIds) {
2341
2342
  })
2342
2343
  };
2343
2344
  }
2344
- function formulaFieldSpec(field) {
2345
+ function formulaFieldSpec(field2) {
2345
2346
  return {
2346
- key: field.key,
2347
- label: field.label,
2347
+ key: field2.key,
2348
+ label: field2.label,
2348
2349
  type: "formula",
2349
- formula_text: field.formula,
2350
- ...field.unit ? { unit: field.unit } : {},
2351
- sensitivity: field.sensitivity,
2352
- context_policy: field.contextPolicy
2350
+ formula_text: field2.formula,
2351
+ ...field2.unit ? { unit: field2.unit } : {},
2352
+ sensitivity: field2.sensitivity,
2353
+ context_policy: field2.contextPolicy
2353
2354
  };
2354
2355
  }
2355
2356
  function formulaOrder(table) {
@@ -2967,23 +2968,23 @@ var BUCKETS = ["year", "quarter", "month", "week", "day"];
2967
2968
  var AGG_OPERATIONS = ["count", "sum", "avg", "min", "max", "median", "filled", "empty", "unique"];
2968
2969
  var MEASURE_KEY = /^[a-zA-Z_][a-zA-Z0-9_]{0,62}$/;
2969
2970
  var STORE_LIST_RELATION_MAX = 25;
2970
- function ruleBreak(field, v) {
2971
+ function ruleBreak(field2, v) {
2971
2972
  const t = storeText(v) ?? "";
2972
2973
  if (t === "") return null;
2973
- if (field.choices?.length && !field.multi && typeof v === "string" && !field.choices.includes(v)) return `"${v}" is not one of "${field.label}"'s choices.`;
2974
- for (const rule of field.rules ?? []) {
2974
+ if (field2.choices?.length && !field2.multi && typeof v === "string" && !field2.choices.includes(v)) return `"${v}" is not one of "${field2.label}"'s choices.`;
2975
+ for (const rule of field2.rules ?? []) {
2975
2976
  if (rule.kind === "min" || rule.kind === "max") {
2976
2977
  const n = parseNumeric(t);
2977
2978
  const bound = parseNumeric(String(rule.value));
2978
- 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}.`;
2979
+ 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}.`;
2979
2980
  } else if (rule.kind === "length") {
2980
- if (rule.value !== void 0 && t.length > Number(rule.value)) return `"${t}" is longer than "${field.label}" allows (${rule.value}).`;
2981
- if (rule.min !== void 0 && t.length < rule.min) return `"${t}" is shorter than "${field.label}" allows (${rule.min}).`;
2981
+ if (rule.value !== void 0 && t.length > Number(rule.value)) return `"${t}" is longer than "${field2.label}" allows (${rule.value}).`;
2982
+ if (rule.min !== void 0 && t.length < rule.min) return `"${t}" is shorter than "${field2.label}" allows (${rule.min}).`;
2982
2983
  } else if (rule.kind === "pattern" && rule.value !== void 0) {
2983
2984
  try {
2984
- if (!new RegExp(String(rule.value)).test(t)) return `"${t}" does not match "${field.label}"'s own pattern.`;
2985
+ if (!new RegExp(String(rule.value)).test(t)) return `"${t}" does not match "${field2.label}"'s own pattern.`;
2985
2986
  } catch {
2986
- return `"${field.label}"'s pattern is not a pattern.`;
2987
+ return `"${field2.label}"'s pattern is not a pattern.`;
2987
2988
  }
2988
2989
  }
2989
2990
  }
@@ -3020,14 +3021,14 @@ function deriveSingulars(spec) {
3020
3021
  if (form) out.form = form;
3021
3022
  const stages = spec.extras?.find((e) => e.kind === "stage_rules");
3022
3023
  if (stages?.kind === "stage_rules") {
3023
- const field = allTables(spec).find((t) => t.token === stages.table)?.fields.find((f) => f.key === stages.stageField);
3024
+ const field2 = allTables(spec).find((t) => t.token === stages.table)?.fields.find((f) => f.key === stages.stageField);
3024
3025
  out.pipeline = {
3025
3026
  token: stages.token,
3026
3027
  name: stages.name,
3027
3028
  describes: stages.movesMessage ?? stages.name,
3028
3029
  table: stages.table,
3029
3030
  stageField: stages.stageField,
3030
- stages: (field?.choices ?? Object.keys(stages.stages ?? {})).map((key) => ({ key, label: key, describes: stages.stages?.[key] ?? "" }))
3031
+ stages: (field2?.choices ?? Object.keys(stages.stages ?? {})).map((key) => ({ key, label: key, describes: stages.stages?.[key] ?? "" }))
3031
3032
  };
3032
3033
  }
3033
3034
  const q6 = spec.agent?.question;
@@ -3146,12 +3147,12 @@ function capabilitiesUsed(spec) {
3146
3147
  }
3147
3148
  if (spec.teaches === "gallery") used.add("seed.attachments");
3148
3149
  for (const table of allTables(spec)) {
3149
- for (const field of table.fields ?? []) {
3150
- const word = `field.${field.parityType}`;
3150
+ for (const field2 of table.fields ?? []) {
3151
+ const word = `field.${field2.parityType}`;
3151
3152
  if (CAPABILITY_WORDS.includes(word)) used.add(word);
3152
- if (field.choiceColors && Object.keys(field.choiceColors).length) used.add("choice.colors");
3153
- if (field.format === "rating") used.add("field.rating");
3154
- if (field.format === "signature") used.add("field.signature");
3153
+ if (field2.choiceColors && Object.keys(field2.choiceColors).length) used.add("choice.colors");
3154
+ if (field2.format === "rating") used.add("field.rating");
3155
+ if (field2.format === "signature") used.add("field.signature");
3155
3156
  }
3156
3157
  }
3157
3158
  if (spec.forms?.length) {
@@ -3362,9 +3363,9 @@ function foreignTextProblems(spec, library) {
3362
3363
  const all = hay.map(([, v]) => v).join("\n");
3363
3364
  const esc = (w) => w.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3364
3365
  const find = (word) => {
3365
- const lower = word.toLowerCase();
3366
- if (!all.includes(lower)) return void 0;
3367
- const re = new RegExp(`(^|[^a-z0-9])${esc(lower)}($|[^a-z0-9])`);
3366
+ const lower2 = word.toLowerCase();
3367
+ if (!all.includes(lower2)) return void 0;
3368
+ const re = new RegExp(`(^|[^a-z0-9])${esc(lower2)}($|[^a-z0-9])`);
3368
3369
  return hay.find(([, v]) => re.test(v));
3369
3370
  };
3370
3371
  const mine = proseOf(spec).map(([at, w]) => {
@@ -3523,6 +3524,10 @@ function validateTemplate(raw, options = {}) {
3523
3524
  }
3524
3525
  for (const table of tablesRaw) {
3525
3526
  const tf = table.fields.find((f) => f.key === table.titleField);
3527
+ 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.`);
3528
+ else if (isRelation(tf) || ["lookup", "rollup", "formula"].includes(tf.parityType) || isTimedField(tf) || tf.parityType === "entity_reference") {
3529
+ 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.`);
3530
+ }
3526
3531
  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.`);
3527
3532
  }
3528
3533
  for (const table of tablesRaw) {
@@ -3711,49 +3716,49 @@ function validateTemplate(raw, options = {}) {
3711
3716
  let hasLookupOrRollup = false;
3712
3717
  tables.forEach((table) => {
3713
3718
  const tat = `tables (${table.token})`;
3714
- table.fields.forEach((field) => {
3715
- const at = `${tat}.fields (${field.key})`;
3716
- if (isChoice(field) && field.choices?.length) {
3717
- const colors = field.choices.map((c) => field.choiceColors?.[c]);
3718
- field.choices.forEach((c, i) => {
3719
+ table.fields.forEach((field2) => {
3720
+ const at = `${tat}.fields (${field2.key})`;
3721
+ if (isChoice(field2) && field2.choices?.length) {
3722
+ const colors = field2.choices.map((c) => field2.choiceColors?.[c]);
3723
+ field2.choices.forEach((c, i) => {
3719
3724
  const color = colors[i];
3720
3725
  if (!color) say(`${at}.choiceColors`, `The choice "${c}" wears no colour. Give every choice one of: ${CHOICE_COLORS.join(", ")}.`);
3721
3726
  else if (!CHOICE_COLORS.includes(color)) say(`${at}.choiceColors`, `"${color}" is not a colour the grid can paint.`);
3722
3727
  });
3723
- 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.`);
3728
+ 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.`);
3724
3729
  const distinct = new Set(colors.filter(Boolean)).size;
3725
- const need = Math.min(field.choices.length, CHOICE_COLORS_DISTINCT_UP_TO);
3726
- 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.`);
3730
+ const need = Math.min(field2.choices.length, CHOICE_COLORS_DISTINCT_UP_TO);
3731
+ 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.`);
3727
3732
  }
3728
- if (isNumber(field) && !isComputed(field)) {
3729
- const kinds2 = new Set((field.rules ?? []).map((r) => r.kind));
3730
- 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).`);
3731
- const seeds = table.rows.map((r) => parseNumeric(storeText(r.values?.[field.key]) ?? "")).filter((v) => !!v).map((v) => Number(v.n) / Number(v.d));
3733
+ if (isNumber(field2) && !isComputed(field2)) {
3734
+ const kinds2 = new Set((field2.rules ?? []).map((r) => r.kind));
3735
+ 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).`);
3736
+ const seeds = table.rows.map((r) => parseNumeric(storeText(r.values?.[field2.key]) ?? "")).filter((v) => !!v).map((v) => Number(v.n) / Number(v.d));
3732
3737
  if (seeds.length) {
3733
3738
  const hi = Math.max(...seeds);
3734
3739
  const lo = Math.min(...seeds);
3735
- if (new Set(seeds).size < 2) say(`${at}`, `Every seed row holds the same ${field.label}; real records vary.`);
3736
- for (const rule of field.rules ?? []) {
3740
+ if (new Set(seeds).size < 2) say(`${at}`, `Every seed row holds the same ${field2.label}; real records vary.`);
3741
+ for (const rule of field2.rules ?? []) {
3737
3742
  const bound = Number(rule.value);
3738
- 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.`);
3739
- 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.`);
3740
- 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.`);
3743
+ 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.`);
3744
+ 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.`);
3745
+ 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.`);
3741
3746
  }
3742
3747
  }
3743
3748
  }
3744
- if (field.parityType === "lookup" || field.parityType === "rollup") {
3745
- const via = field.via ? fieldOf(table.token, field.via) : void 0;
3749
+ if (field2.parityType === "lookup" || field2.parityType === "rollup") {
3750
+ const via = field2.via ? fieldOf(table.token, field2.via) : void 0;
3746
3751
  if (!via || !isRelation(via) || !via.relationTarget) {
3747
- say(`${at}.via`, `A ${field.parityType} reads THROUGH a relation field of this table; "${field.via ?? "(none)"}" is not one.`);
3752
+ say(`${at}.via`, `A ${field2.parityType} reads THROUGH a relation field of this table; "${field2.via ?? "(none)"}" is not one.`);
3748
3753
  } else {
3749
3754
  hasLookupOrRollup = true;
3750
- 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.`);
3751
- if (field.parityType === "rollup" && !field.agg) say(`${at}.agg`, "A rollup says how it adds up the far side.");
3752
- 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.`);
3755
+ 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.`);
3756
+ if (field2.parityType === "rollup" && !field2.agg) say(`${at}.agg`, "A rollup says how it adds up the far side.");
3757
+ 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.`);
3753
3758
  }
3754
3759
  }
3755
- 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.");
3756
- 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.`);
3760
+ 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.");
3761
+ 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.`);
3757
3762
  });
3758
3763
  const seen = /* @__PURE__ */ new Map();
3759
3764
  const titles = /* @__PURE__ */ new Set();
@@ -3763,47 +3768,47 @@ function validateTemplate(raw, options = {}) {
3763
3768
  const twin = seen.get(sig);
3764
3769
  if (twin) say(rat, `This row is identical to "${twin}" apart from its key. Every seed row is a different real record.`);
3765
3770
  seen.set(sig, row.key);
3766
- for (const field of table.fields) {
3767
- if (!isRelation(field) || !field.relationTarget || field.parityType === "attachment") continue;
3768
- const target = byToken.get(field.relationTarget);
3769
- const v = row.values?.[field.key];
3771
+ for (const field2 of table.fields) {
3772
+ if (!isRelation(field2) || !field2.relationTarget || field2.parityType === "attachment") continue;
3773
+ const target = byToken.get(field2.relationTarget);
3774
+ const v = row.values?.[field2.key];
3770
3775
  if (!target || v === void 0 || v === null) continue;
3771
3776
  for (const ref2 of Array.isArray(v) ? v : [v]) {
3772
- 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}".`);
3777
+ 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}".`);
3773
3778
  }
3774
3779
  }
3775
3780
  const title = storeText(row.values?.[table.titleField]);
3776
3781
  if (title) titles.add(title.trim().toLowerCase());
3777
- for (const field of table.fields) {
3778
- const v = row.values?.[field.key];
3782
+ for (const field2 of table.fields) {
3783
+ const v = row.values?.[field2.key];
3779
3784
  if (v === void 0 || v === null || v === "") continue;
3780
- for (const rule of field.rules ?? []) {
3785
+ for (const rule of field2.rules ?? []) {
3781
3786
  const t = storeText(v) ?? "";
3782
3787
  if (rule.kind === "min" || rule.kind === "max") {
3783
3788
  const n = parseNumeric(t);
3784
3789
  const bound = parseNumeric(String(rule.value));
3785
3790
  if (n && bound && (rule.kind === "min" ? qCmp(n, bound) < 0 : qCmp(n, bound) > 0)) {
3786
- say(`${rat}.values.${field.key}`, `${t} breaks its own rule: "${field.label}" is at ${rule.kind === "min" ? "least" : "most"} ${rule.value}.`);
3791
+ say(`${rat}.values.${field2.key}`, `${t} breaks its own rule: "${field2.label}" is at ${rule.kind === "min" ? "least" : "most"} ${rule.value}.`);
3787
3792
  }
3788
3793
  } else if (rule.kind === "length") {
3789
- if (rule.value !== void 0 && t.length > Number(rule.value)) say(`${rat}.values.${field.key}`, `"${t}" is longer than "${field.label}" allows (${rule.value}).`);
3790
- if (rule.min !== void 0 && t.length < rule.min) say(`${rat}.values.${field.key}`, `"${t}" is shorter than "${field.label}" allows (${rule.min}).`);
3794
+ if (rule.value !== void 0 && t.length > Number(rule.value)) say(`${rat}.values.${field2.key}`, `"${t}" is longer than "${field2.label}" allows (${rule.value}).`);
3795
+ if (rule.min !== void 0 && t.length < rule.min) say(`${rat}.values.${field2.key}`, `"${t}" is shorter than "${field2.label}" allows (${rule.min}).`);
3791
3796
  } else if (rule.kind === "pattern" && rule.value !== void 0) {
3792
3797
  let re = null;
3793
3798
  try {
3794
3799
  re = new RegExp(String(rule.value));
3795
3800
  } catch {
3796
- say(`${tat}.fields (${field.key}).rules`, `The pattern "${rule.value}" is not a pattern.`);
3801
+ say(`${tat}.fields (${field2.key}).rules`, `The pattern "${rule.value}" is not a pattern.`);
3797
3802
  }
3798
- if (re && !re.test(t)) say(`${rat}.values.${field.key}`, `"${t}" does not match "${field.label}"'s own pattern.`);
3803
+ if (re && !re.test(t)) say(`${rat}.values.${field2.key}`, `"${t}" does not match "${field2.label}"'s own pattern.`);
3799
3804
  }
3800
3805
  }
3801
3806
  }
3802
3807
  });
3803
- for (const field of table.fields) {
3804
- if (!(field.rules ?? []).some((r) => r.kind === "unique")) continue;
3805
- const vals = table.rows.map((r) => storeText(r.values?.[field.key])).filter((v) => !!v).map((v) => v.trim().toLowerCase());
3806
- if (new Set(vals).size !== vals.length) say(`${tat}.fields (${field.key})`, `"${field.label}" is unique, and two seed rows share a value.`);
3808
+ for (const field2 of table.fields) {
3809
+ if (!(field2.rules ?? []).some((r) => r.kind === "unique")) continue;
3810
+ const vals = table.rows.map((r) => storeText(r.values?.[field2.key])).filter((v) => !!v).map((v) => v.trim().toLowerCase());
3811
+ if (new Set(vals).size !== vals.length) say(`${tat}.fields (${field2.key})`, `"${field2.label}" is unique, and two seed rows share a value.`);
3807
3812
  }
3808
3813
  if (table.rows.length >= 2 && titles.size < Math.ceil(table.rows.length * TITLE_DISTINCT_SHARE)) {
3809
3814
  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.`);
@@ -5032,8 +5037,8 @@ function validateTemplate(raw, options = {}) {
5032
5037
  }
5033
5038
  const currencies = new Set(fields.filter((f) => f.parityType === "currency").map((f) => (f.unit ?? "").toUpperCase()));
5034
5039
  if (currencies.size > 1) say("tables", `Money is kept in ${[...currencies].join(" and ")}; one business keeps its books in one currency.`);
5035
- const GOOD = /^(completed?|done|paid|approved|won|closed won|booked|confirmed|delivered|resolved|published|signed|passed|hired|accepted)$/i;
5036
- const BAD = /^(cancell?ed|skipped|lost|closed lost|denied|rejected|no[- ]show|failed|overdue|declined|void(ed)?|refunded|churned|expired|blocked)$/i;
5040
+ const GOOD = GOOD_OUTCOME;
5041
+ const BAD = BAD_OUTCOME;
5037
5042
  for (const t of own) {
5038
5043
  for (const f of t.fields) {
5039
5044
  for (const [choice, color] of Object.entries(f.choiceColors ?? {})) {
@@ -5210,14 +5215,25 @@ function validateTemplate(raw, options = {}) {
5210
5215
  if (columns && !(status2 && /writes "booked" and "cancelled"/.test(columns))) say(`${at}.table`, columns);
5211
5216
  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.`);
5212
5217
  const a = e.availability ?? {};
5213
- const rng = (v, lo, hi, what) => {
5214
- if (v !== void 0 && (v < lo || v > hi)) say(`${at}.availability`, `${what} is between ${lo} and ${hi}; this says ${v}.`);
5218
+ const wholeNumber = (key, what) => {
5219
+ const v = a[key];
5220
+ if (v === void 0 || typeof v === "number" && Number.isInteger(v)) return true;
5221
+ 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)}.`);
5222
+ return false;
5223
+ };
5224
+ const rng = (v, lo, hi, what, key) => {
5225
+ if (v !== void 0 && wholeNumber(key, what) && (v < lo || v > hi)) say(`${at}.availability`, `${what} is between ${lo} and ${hi}; this says ${v}.`);
5215
5226
  };
5216
- rng(a.slotMinutes, 5, 480, "Slot length (minutes)");
5217
- rng(a.bufferMinutes, 0, 480, "The buffer (minutes)");
5218
- rng(a.leadMinutes, 0, 43200, "Lead time (minutes)");
5219
- rng(a.maxPerDay, 1, 100, "Appointments a day");
5220
- rng(a.days, 1, 90, "Days ahead");
5227
+ (a.windows ?? []).forEach((w, wi) => {
5228
+ const ww = w;
5229
+ 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)}.`);
5230
+ 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])}.`);
5231
+ });
5232
+ rng(a.slotMinutes, 5, 480, "Slot length (minutes)", "slotMinutes");
5233
+ rng(a.bufferMinutes, 0, 480, "The buffer (minutes)", "bufferMinutes");
5234
+ rng(a.leadMinutes, 0, 43200, "Lead time (minutes)", "leadMinutes");
5235
+ rng(a.maxPerDay, 1, 100, "Appointments a day", "maxPerDay");
5236
+ rng(a.days, 1, 90, "Days ahead", "days");
5221
5237
  if (a.timezone) {
5222
5238
  try {
5223
5239
  new Intl.DateTimeFormat("en-US", { timeZone: a.timezone });
@@ -5873,6 +5889,8 @@ var RESTRICTED_KEY = /(^|_)(member_id|ssn|social_security|dob|date_of_birth|birt
5873
5889
  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/;
5874
5890
  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)($|_)/;
5875
5891
  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");
5892
+ var GOOD_OUTCOME = /^(completed?|done|paid|approved|won|closed won|booked|confirmed|delivered|resolved|published|signed|passed|hired|accepted)$/i;
5893
+ var BAD_OUTCOME = /^(cancell?ed|skipped|lost|closed lost|denied|rejected|no[- ]show|failed|overdue|declined|void(ed)?|refunded|churned|expired|blocked)$/i;
5876
5894
  function sensitivityFloorOf(table, f) {
5877
5895
  const text = `${f.key} ${norm(f.label)}`;
5878
5896
  const billingTable = /(invoice|bill|statement|receipt|payment|charge|fee|order|account)/i.test(`${table.token} ${table.labelSingular ?? ""}`);
@@ -6350,7 +6368,719 @@ async function runTemplateDoor(source, door, organizationId, id, options = {}) {
6350
6368
  function planTables(plan) {
6351
6369
  return plan.steps.flatMap((s) => (s.made ?? []).filter((m) => m.kind === "table").map((m) => m.ref));
6352
6370
  }
6371
+
6372
+ // src/templates/describe/shape.ts
6373
+ var GALLERY_ONLY = ["catalogueId", "strengths", "provenance", "walk", "agent", "requires", "persona", "extraAgents", "workflows", "guide", "__kind"];
6374
+ function describeSpec(template) {
6375
+ const s = { ...template };
6376
+ for (const k of GALLERY_ONLY) delete s[k];
6377
+ s.specVersion = 1;
6378
+ if (typeof s.version !== "number") s.version = 1;
6379
+ if (typeof s.audience !== "string") s.audience = "organization";
6380
+ if (typeof s.cleanupTag !== "string" && typeof s.id === "string") s.cleanupTag = `describe:${s.id}`;
6381
+ for (const k of ["relationships", "sharedBlocks", "views", "forms", "dimensions", "extras"]) if (!Array.isArray(s[k])) s[k] = [];
6382
+ if (Array.isArray(s.tables)) s.tables = s.tables.map(describeTable);
6383
+ return s;
6384
+ }
6385
+ var TABLE_DEFAULTS = { type: "entity", display: "grid", weight: "light", ordered: false };
6386
+ var CONTACT_KINDS = /* @__PURE__ */ new Set(["phone", "email"]);
6387
+ function describeTable(t) {
6388
+ const own = Array.isArray(t.fields) ? t.fields : null;
6389
+ const shape = { token: String(t.token ?? ""), labelSingular: String(t.labelSingular ?? ""), subject: t.subject, fields: own ?? [] };
6390
+ const fields = own ? own.map((f) => describeField(f, shape)) : t.fields;
6391
+ return {
6392
+ ...TABLE_DEFAULTS,
6393
+ ...t,
6394
+ ...typeof t.labelPlural !== "string" && typeof t.name === "string" ? { labelPlural: t.name } : {},
6395
+ fields,
6396
+ rows: Array.isArray(t.rows) ? t.rows : []
6397
+ };
6398
+ }
6399
+ function describeField(f, table) {
6400
+ const contact = CONTACT_KINDS.has(String(f.parityType ?? ""));
6401
+ const floor = sensitivityFloorOf(table, { key: String(f.key ?? ""), label: String(f.label ?? ""), parityType: String(f.parityType ?? "") });
6402
+ return {
6403
+ ...contact ? { format: f.parityType } : {},
6404
+ sensitivity: floor ?? "internal",
6405
+ contextPolicy: floor ? "exclude" : "include",
6406
+ ...f
6407
+ };
6408
+ }
6409
+
6410
+ // src/templates/describe/merge.ts
6411
+ var clone = (v) => v === void 0 ? v : JSON.parse(JSON.stringify(v));
6412
+ var labelOfKey = (key) => {
6413
+ const w = key.replace(/_/g, " ").trim();
6414
+ return w ? w[0].toUpperCase() + w.slice(1) : key;
6415
+ };
6416
+ var field = (t, key) => t.fields.find((f) => f.key === key);
6417
+ function skeletonTable(p) {
6418
+ return {
6419
+ token: p.token,
6420
+ name: p.name,
6421
+ labelSingular: p.labelSingular,
6422
+ labelPlural: p.name,
6423
+ subject: p.subject,
6424
+ titleField: p.titleField.key,
6425
+ describes: p.purpose.slice(0, 140),
6426
+ fields: [],
6427
+ rows: []
6428
+ };
6429
+ }
6430
+ function ensureSkeleton(t, p) {
6431
+ if (typeof t.name !== "string") t.name = p.name;
6432
+ if (typeof t.labelSingular !== "string") t.labelSingular = p.labelSingular;
6433
+ if (typeof t.subject !== "string") t.subject = p.subject;
6434
+ if (typeof t.describes !== "string" || !t.describes.trim()) t.describes = p.purpose.slice(0, 140);
6435
+ if (!Array.isArray(t.fields)) t.fields = [];
6436
+ if (typeof t.titleField !== "string" || !t.titleField) t.titleField = p.titleField.key;
6437
+ if (!field(t, p.titleField.key)) t.fields.unshift({ key: p.titleField.key, label: p.titleField.label, parityType: "text", required: true });
6438
+ if (p.status && !field(t, "status")) {
6439
+ t.fields.push({ key: "status", label: "Status", parityType: "select", required: true, choices: [...p.status.choices], default: p.status.default });
6440
+ }
6441
+ if (p.slot && !field(t, "slot")) t.fields.push({ key: "slot", label: "Visit time", parityType: "datetime", format: "datetime", required: true });
6442
+ for (const l of p.links) {
6443
+ if (field(t, l.key)) continue;
6444
+ t.fields.push({ key: l.key, label: l.label, parityType: "relation", relationTarget: l.target, ...l.many ? { multi: true } : { relationMax: 1 } });
6445
+ }
6446
+ }
6447
+ function addExistingKeys(t, p, existing) {
6448
+ if (!p.reuse) return;
6449
+ const e = existing.find((x) => x.id === p.reuse.existing_table_id);
6450
+ if (!e) return;
6451
+ for (const key of p.reuse.keepKeys) {
6452
+ if (field(t, key)) continue;
6453
+ const ef = e.fields.find((f) => f.key === key);
6454
+ if (!ef) continue;
6455
+ t.fields.push({ key, label: ef.label, parityType: ef.kind, ...ef.choices?.length ? { choices: [...ef.choices] } : {} });
6456
+ }
6457
+ }
6458
+ function addRequest(t, r) {
6459
+ if (field(t, r.key)) return;
6460
+ const f = { key: r.key, label: r.label, parityType: r.parityType };
6461
+ if (r.choices?.length) f.choices = [...r.choices];
6462
+ if (r.askOfSubject) f.askOfSubject = true;
6463
+ if (r.askOfGuardian) f.askOfGuardian = true;
6464
+ if (r.required) f.required = true;
6465
+ if (r.help) f.help = r.help;
6466
+ t.fields.push(f);
6467
+ }
6468
+ var CHOICE = ["select", "multi_select", "list"];
6469
+ var STAGE_KEY = /(^|_)(status|stage)$/;
6470
+ function viewHolds(v, tables) {
6471
+ const t = tables.get(v.table);
6472
+ if (!t) return false;
6473
+ const has = (k) => typeof k === "string" && !!field(t, k);
6474
+ const x = v;
6475
+ const kindOf = (k) => t.fields.find((f) => f.key === k)?.parityType;
6476
+ if (v.kind === "kanban" && !(has(x.groupBy) && CHOICE.includes(kindOf(x.groupBy)))) return false;
6477
+ if (v.kind === "calendar" && !(has(x.dateField) && kindOf(x.dateField) === "datetime")) return false;
6478
+ if (v.kind === "timeline" && !(has(x.startField) && has(x.endField))) return false;
6479
+ if (v.kind === "gallery" && !has(x.coverField)) return false;
6480
+ if (x.groupBy !== void 0 && !has(x.groupBy)) return false;
6481
+ for (const k of Object.keys(x.filters ?? {})) if (!has(k)) return false;
6482
+ for (const k of Object.keys(x.anyOf ?? {})) if (!has(k)) return false;
6483
+ for (const s of x.sorts ?? []) if (!has(s.field)) return false;
6484
+ return typeof v.token === "string" && !!v.token && typeof v.name === "string" && !!v.name;
6485
+ }
6486
+ function fallbackView(t) {
6487
+ const plural = typeof t.name === "string" ? t.name : labelOfKey(t.token);
6488
+ const stage = t.fields.find((f) => STAGE_KEY.test(f.key) && CHOICE.includes(f.parityType));
6489
+ if (stage) return { token: `${t.token}_board`, name: `${plural} board`, table: t.token, kind: "kanban", groupBy: stage.key, isDefault: true };
6490
+ if (t.fields.some((f) => f.key === "slot" && f.parityType === "datetime")) {
6491
+ return { token: `${t.token}_calendar`, name: `${plural} calendar`, table: t.token, kind: "calendar", dateField: "slot", isDefault: true };
6492
+ }
6493
+ return { token: `${t.token}_list`, name: `All ${plural.toLowerCase()}`, table: t.token, kind: "grid", isDefault: true, sorts: [{ field: String(t.titleField), direction: "asc" }] };
6494
+ }
6495
+ function mergeViews(plan, part, tables) {
6496
+ const given = Array.isArray(part?.views) ? part.views : [];
6497
+ const seen = /* @__PURE__ */ new Set();
6498
+ const kept = [];
6499
+ for (const v of given) {
6500
+ if (!v || !viewHolds(v, tables) || seen.has(v.token)) continue;
6501
+ seen.add(v.token);
6502
+ kept.push(clone(v));
6503
+ }
6504
+ const defaulted = /* @__PURE__ */ new Set();
6505
+ for (const v of kept) {
6506
+ if (!v.isDefault) continue;
6507
+ if (defaulted.has(v.table)) delete v.isDefault;
6508
+ else defaulted.add(v.table);
6509
+ }
6510
+ if (kept.length) return kept;
6511
+ const wanted = [...new Set(plan.views.map((v) => v.table))].filter((tok) => tables.has(tok));
6512
+ const targets = wanted.length ? wanted : [...tables.keys()].slice(0, 1);
6513
+ return targets.map((tok) => fallbackView(tables.get(tok)));
6514
+ }
6515
+ function sharedBlocksFor(tables) {
6516
+ const own = new Set(tables.map((t) => t.token));
6517
+ const out = [];
6518
+ for (const id of SHARED_BLOCK_IDS) {
6519
+ const token = SHARED_BLOCKS[id].table;
6520
+ if (own.has(token)) continue;
6521
+ if (tables.some((t) => t.fields.some((f) => f.parityType === "relation" && f.relationTarget === token))) out.push({ block: id, rows: [] });
6522
+ }
6523
+ return out;
6524
+ }
6525
+ function reusesOf(plan) {
6526
+ return plan.tables.filter((t) => t.reuse).map((t) => ({ token: t.token, existing_table_id: t.reuse.existing_table_id }));
6527
+ }
6528
+ function mergeDescribeParts(plan, tables, doors, views, existingTables = []) {
6529
+ const out = clone(Array.isArray(tables?.tables) ? tables.tables : []);
6530
+ const byToken = new Map(out.map((t) => [t.token, t]));
6531
+ for (const p of plan.tables) {
6532
+ let t = byToken.get(p.token);
6533
+ if (!t) {
6534
+ t = skeletonTable(p);
6535
+ out.push(t);
6536
+ byToken.set(p.token, t);
6537
+ }
6538
+ ensureSkeleton(t, p);
6539
+ addExistingKeys(t, p, existingTables);
6540
+ }
6541
+ for (const r of doors?.fieldRequests ?? []) {
6542
+ const t = byToken.get(r.table);
6543
+ if (t) addRequest(t, r);
6544
+ }
6545
+ const spec = {
6546
+ specVersion: 1,
6547
+ version: 1,
6548
+ id: plan.id,
6549
+ useCase: plan.useCase,
6550
+ industry: plan.industry,
6551
+ vertical: plan.vertical,
6552
+ job: plan.job,
6553
+ teaches: plan.teaches,
6554
+ audience: plan.audience,
6555
+ cleanupTag: `describe:${plan.id}`,
6556
+ business: clone(plan.business),
6557
+ tables: out,
6558
+ sharedBlocks: sharedBlocksFor(out),
6559
+ relationships: clone(tables?.relationships ?? []),
6560
+ views: mergeViews(plan, views, byToken),
6561
+ forms: clone(doors?.forms ?? []),
6562
+ dimensions: [],
6563
+ extras: doors?.booking ? [clone(doors.booking)] : []
6564
+ };
6565
+ return spec;
6566
+ }
6567
+
6568
+ // src/templates/describe/autofix.ts
6569
+ var clone2 = (v) => JSON.parse(JSON.stringify(v));
6570
+ var SENSITIVITY_RANK2 = ["public", "internal", "confidential", "restricted"];
6571
+ var isChoice2 = (f) => ["select", "multi_select", "list"].includes(f.parityType);
6572
+ var isComputed2 = (f) => ["formula", "lookup", "rollup", "autonumber", "created_time", "modified_time"].includes(f.parityType) || !!f.source && f.source !== "manual";
6573
+ var MINOR_VERTICAL = /pediatric|paediatric|youth|child|kids?\b|school|daycare|day care|junior|teen|infant|nursery|camp\b/i;
6574
+ var NEUTRAL_ORDER = ["amber", "blue", "violet", "teal", "slate", "orange", "yellow", "pink", "brown", "gray", "green", "red"];
6575
+ var GOOD_ORDER = ["green", "teal", "blue", "violet", "slate", "amber", "orange", "yellow", "pink", "brown", "gray"];
6576
+ var BAD_ORDER = ["red", "orange", "gray", "slate", "brown", "pink", "amber", "yellow", "violet", "blue", "teal"];
6577
+ var DISTINCT_UP_TO = 7;
6578
+ var lower = (s) => s.trim().toLowerCase();
6579
+ function raiseSensitivity(spec, fixes) {
6580
+ for (const t of spec.tables) {
6581
+ for (const f of t.fields) {
6582
+ const at = `tables.${t.token}.fields.${f.key}`;
6583
+ const floor = sensitivityFloorOf(t, f);
6584
+ const rank = (s) => SENSITIVITY_RANK2.indexOf(s);
6585
+ if (floor && rank(f.sensitivity) < rank(floor)) {
6586
+ fixes.push(`${at}: sensitivity ${f.sensitivity} raised to ${floor}`);
6587
+ f.sensitivity = floor;
6588
+ }
6589
+ if (floor === "restricted" && f.contextPolicy !== "exclude") {
6590
+ fixes.push(`${at}: contextPolicy ${f.contextPolicy} set to exclude (restricted)`);
6591
+ f.contextPolicy = "exclude";
6592
+ }
6593
+ if (/note/.test(`${f.key} ${lower(f.label)}`) && f.sensitivity === "public") {
6594
+ fixes.push(`${at}: notes are never public; raised to internal`);
6595
+ f.sensitivity = "internal";
6596
+ }
6597
+ }
6598
+ }
6599
+ }
6600
+ function flagAskedOfSubject(spec, ctx, fixes) {
6601
+ const minor = MINOR_VERTICAL.test(`${spec.vertical ?? ""} ${ctx.plan?.vertical ?? ""}`);
6602
+ const byToken = new Map(spec.tables.map((t) => [t.token, t]));
6603
+ for (const form of spec.forms) {
6604
+ if (form.audience !== "public") continue;
6605
+ const t = byToken.get(form.table);
6606
+ if (!t) continue;
6607
+ for (const q2 of form.fields ?? []) {
6608
+ const f = t.fields.find((x) => x.key === q2.fieldKey);
6609
+ if (!f) continue;
6610
+ if (f.sensitivity !== "confidential" && f.sensitivity !== "restricted") continue;
6611
+ if (f.askOfSubject || f.askOfGuardian) continue;
6612
+ if (minor) f.askOfGuardian = true;
6613
+ else f.askOfSubject = true;
6614
+ fixes.push(`tables.${t.token}.fields.${f.key}: ${minor ? "askOfGuardian" : "askOfSubject"} set (asked on the public form "${form.token}")`);
6615
+ }
6616
+ }
6617
+ }
6618
+ var DOOR_KEYS = ["slot", "status", "booked_with"];
6619
+ function colorOf(name) {
6620
+ return /^booked$/i.test(name) ? "blue" : "slate";
6621
+ }
6622
+ function fixBooking(spec, fixes) {
6623
+ for (const e of spec.extras) {
6624
+ if (e.kind !== "booking") continue;
6625
+ const b = e;
6626
+ const t = spec.tables.find((x) => x.token === b.table);
6627
+ if (t) {
6628
+ const at = `tables.${t.token}.fields`;
6629
+ const slot = t.fields.find((f) => f.key === "slot");
6630
+ if (!slot) {
6631
+ t.fields.push({ key: "slot", label: "Visit time", parityType: "datetime", format: "datetime", required: true, sensitivity: "internal", contextPolicy: "include" });
6632
+ fixes.push(`${at}.slot: added the required datetime the booking page writes`);
6633
+ } else {
6634
+ if (slot.parityType !== "datetime") {
6635
+ slot.parityType = "datetime";
6636
+ fixes.push(`${at}.slot: made a datetime`);
6637
+ }
6638
+ if (!slot.required) {
6639
+ slot.required = true;
6640
+ fixes.push(`${at}.slot: made required`);
6641
+ }
6642
+ if (slot.format !== "datetime") slot.format = "datetime";
6643
+ }
6644
+ let status = t.fields.find((f) => f.key === "status");
6645
+ if (!status) {
6646
+ status = { key: "status", label: "Status", parityType: "select", required: true, choices: ["Booked", "Cancelled"], default: "Booked", choiceColors: { Booked: "blue", Cancelled: "slate" }, sensitivity: "internal", contextPolicy: "include" };
6647
+ t.fields.push(status);
6648
+ fixes.push(`${at}.status: added a Booked / Cancelled choice field`);
6649
+ } else {
6650
+ if (!isChoice2(status)) {
6651
+ status.parityType = "select";
6652
+ status.choices = status.choices ?? [];
6653
+ fixes.push(`${at}.status: made a choice field`);
6654
+ }
6655
+ const have = new Set((status.choices ?? []).map(lower));
6656
+ for (const w of ["Booked", "Cancelled"]) {
6657
+ if (have.has(lower(w))) continue;
6658
+ status.choices = [...status.choices ?? [], w];
6659
+ status.choiceColors = { ...status.choiceColors ?? {}, [w]: colorOf(w) };
6660
+ fixes.push(`${at}.status: added the choice "${w}" the booking page writes`);
6661
+ }
6662
+ }
6663
+ }
6664
+ const before = b.questions?.length ?? 0;
6665
+ b.questions = (b.questions ?? []).filter((q2) => !DOOR_KEYS.includes(q2.fieldKey));
6666
+ if (b.questions.length !== before) fixes.push(`extras.${b.token}.questions: dropped ${before - b.questions.length} question(s) on slot/status/booked_with`);
6667
+ }
6668
+ }
6669
+ var DEFAULT_DAYS_AHEAD = 28;
6670
+ var AVAILABILITY_NUMBERS = ["slotMinutes", "bufferMinutes", "leadMinutes", "maxPerDay"];
6671
+ function fixAvailability(spec, fixes) {
6672
+ for (const e of spec.extras) {
6673
+ if (e.kind !== "booking") continue;
6674
+ const b = e;
6675
+ const a = b.availability;
6676
+ if (!a) continue;
6677
+ const at = `extras.${b.token}.availability`;
6678
+ if (Array.isArray(a.days)) {
6679
+ const weekdays = [...new Set(a.days.filter((d) => Number.isInteger(d) && d >= 0 && d <= 6))];
6680
+ a.days = DEFAULT_DAYS_AHEAD;
6681
+ fixes.push(`${at}.days: a weekday list is not "days ahead"; set to ${DEFAULT_DAYS_AHEAD} (weekdays belong in windows)`);
6682
+ const windows = Array.isArray(a.windows) ? a.windows : [];
6683
+ const model = windows[0];
6684
+ if (model && /^\d{2}:\d{2}$/.test(String(model.from))) {
6685
+ for (const d of weekdays.sort((x, y) => x - y)) {
6686
+ if (windows.some((w) => w.weekday === d)) continue;
6687
+ windows.push({ weekday: d, from: model.from, to: model.to });
6688
+ fixes.push(`${at}.windows: added a ${model.from}\u2013${model.to} window on weekday ${d} (it was listed in days)`);
6689
+ }
6690
+ a.windows = windows;
6691
+ }
6692
+ } else if (a.days !== void 0) {
6693
+ const n = typeof a.days === "string" && a.days.trim() !== "" ? Number(a.days) : a.days;
6694
+ if (typeof n === "number" && Number.isFinite(n) && n !== a.days) {
6695
+ a.days = Math.round(n);
6696
+ fixes.push(`${at}.days: written as one whole number (${a.days})`);
6697
+ } else if (typeof n === "number" && Number.isFinite(n) && !Number.isInteger(n)) {
6698
+ a.days = Math.round(n);
6699
+ fixes.push(`${at}.days: rounded to ${a.days}`);
6700
+ }
6701
+ }
6702
+ for (const k of AVAILABILITY_NUMBERS) {
6703
+ const v = a[k];
6704
+ if (v === void 0) continue;
6705
+ const n = typeof v === "string" && v.trim() !== "" ? Number(v) : v;
6706
+ if (typeof n === "number" && Number.isFinite(n) && (!Number.isInteger(n) || n !== v)) {
6707
+ a[k] = Math.round(n);
6708
+ fixes.push(`${at}.${k}: written as one whole number (${a[k]})`);
6709
+ }
6710
+ }
6711
+ for (const w of Array.isArray(a.windows) ? a.windows : []) {
6712
+ if (typeof w.weekday === "string" && /^\d$/.test(w.weekday)) {
6713
+ w.weekday = Number(w.weekday);
6714
+ fixes.push(`${at}.windows: a weekday written as a whole number`);
6715
+ }
6716
+ }
6717
+ }
6718
+ }
6719
+ function fixViewTokens(spec, fixes) {
6720
+ const taken = /* @__PURE__ */ new Set();
6721
+ const claim = (t) => typeof t === "string" && t.trim() !== "" && taken.add(t);
6722
+ for (const v of spec.views) claim(v.token);
6723
+ for (const f of spec.forms) claim(f.token);
6724
+ for (const e of spec.extras) claim(e.token);
6725
+ const snake = (x) => x.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "view";
6726
+ spec.views.forEach((v, i) => {
6727
+ if (typeof v.token === "string" && v.token.trim() !== "") return;
6728
+ const base = `${snake(String(v.table ?? "table"))}_${snake(String(v.kind ?? "view"))}`;
6729
+ let token = base;
6730
+ for (let n = 2; taken.has(token); n++) token = `${base}_${n}`;
6731
+ taken.add(token);
6732
+ v.token = token;
6733
+ fixes.push(`views[${i}]: had no token; named "${token}" (table and kind)`);
6734
+ });
6735
+ }
6736
+ function fixColours(spec, fixes) {
6737
+ for (const t of spec.tables) {
6738
+ for (const f of t.fields) {
6739
+ if (!isChoice2(f) || !f.choices?.length) continue;
6740
+ const choices = f.choices;
6741
+ const palette = CHOICE_COLORS;
6742
+ const colors = {};
6743
+ for (const [k, v] of Object.entries(f.choiceColors ?? {})) if (choices.includes(k) && palette.includes(v)) colors[k] = v;
6744
+ const orderOf = (c) => GOOD_OUTCOME.test(c) ? GOOD_ORDER : BAD_OUTCOME.test(c) ? BAD_ORDER : NEUTRAL_ORDER;
6745
+ const holder = (color, except) => choices.find((c) => c !== except && colors[c] === color);
6746
+ const unused = (order) => order.find((c) => !choices.some((x) => colors[x] === c));
6747
+ const need = Math.min(choices.length, DISTINCT_UP_TO);
6748
+ const seen = /* @__PURE__ */ new Set();
6749
+ for (const c of choices) {
6750
+ const col = colors[c];
6751
+ if (col && seen.has(col) && new Set(Object.values(colors)).size < need) {
6752
+ const next = unused(orderOf(c));
6753
+ if (next) colors[c] = next;
6754
+ }
6755
+ if (colors[c]) seen.add(colors[c]);
6756
+ }
6757
+ for (const c of choices) {
6758
+ if (colors[c]) continue;
6759
+ colors[c] = unused(orderOf(c)) ?? orderOf(c)[choices.indexOf(c) % orderOf(c).length];
6760
+ }
6761
+ for (const c of choices) {
6762
+ const wrong = GOOD_OUTCOME.test(c) ? "red" : BAD_OUTCOME.test(c) ? "green" : null;
6763
+ if (!wrong || colors[c] !== wrong) continue;
6764
+ const want = GOOD_OUTCOME.test(c) ? "green" : "red";
6765
+ const other = holder(want, c);
6766
+ if (!other) colors[c] = want;
6767
+ else if (!(want === "red" ? GOOD_OUTCOME.test(other) : BAD_OUTCOME.test(other))) {
6768
+ colors[other] = wrong;
6769
+ colors[c] = want;
6770
+ } else colors[c] = unused(orderOf(c).filter((x) => x !== wrong)) ?? colors[c];
6771
+ }
6772
+ const ordered = {};
6773
+ for (const c of choices) ordered[c] = colors[c];
6774
+ const was = f.choiceColors ?? {};
6775
+ if (Object.keys(was).length !== choices.length || choices.some((c) => was[c] !== ordered[c])) {
6776
+ f.choiceColors = ordered;
6777
+ fixes.push(`tables.${t.token}.fields.${f.key}: choice colours filled and checked (good never red, bad never green)`);
6778
+ }
6779
+ }
6780
+ }
6781
+ }
6782
+ function fixRelationships(spec, ctx, fixes) {
6783
+ const everyTable = allTables(spec);
6784
+ const byToken = new Map(everyTable.map((t) => [t.token, t]));
6785
+ const rels = spec.relationships ?? [];
6786
+ for (const t of spec.tables) {
6787
+ for (const f of t.fields) {
6788
+ if (f.parityType !== "relation" || !f.relationTarget) continue;
6789
+ if (rels.some((r) => r.fromTable === t.token && r.fromField === f.key)) continue;
6790
+ const target = byToken.get(f.relationTarget);
6791
+ const toName = lower(target?.labelSingular ?? f.relationTarget.replace(/_/g, " "));
6792
+ const fromName = lower(t.labelSingular ?? t.token.replace(/_/g, " "));
6793
+ const many = !!f.multi || f.relationMax !== void 0 && f.relationMax !== 1;
6794
+ const planned = ctx.plan?.tables.find((p) => p.token === t.token && p.links.some((l) => l.key === f.key))?.purpose?.trim();
6795
+ const describes = planned && planned.length >= 15 && planned.length <= 140 ? planned : many ? `A ${fromName} lists its ${toName} records.` : `A ${fromName} belongs to one ${toName}.`;
6796
+ const taken = /* @__PURE__ */ new Set([...rels.map((r) => r.inverseKey), ...(target?.fields ?? []).map((x) => x.key)]);
6797
+ let inverseKey = `${t.token}s`;
6798
+ if (taken.has(inverseKey)) inverseKey = `${t.token}_${f.key}s`;
6799
+ for (let i = 2; taken.has(inverseKey); i++) inverseKey = `${t.token}_${f.key}s_${i}`;
6800
+ const toPerson = target?.subject === "person" && f.relationTarget !== "team_member";
6801
+ rels.push({ describes, fromTable: t.token, fromField: f.key, toTable: f.relationTarget, flavor: "referenced", cardinality: many ? "many" : "one", onDelete: toPerson ? "restrict" : "set_null", inverseKey });
6802
+ fixes.push(`relationships: stated ${t.token}.${f.key} \u2192 ${f.relationTarget}`);
6803
+ }
6804
+ }
6805
+ spec.relationships = rels;
6806
+ }
6807
+ function askRequired(spec, fixes) {
6808
+ for (const form of spec.forms) {
6809
+ const t = spec.tables.find((x) => x.token === form.table);
6810
+ if (!t) continue;
6811
+ const asked = new Set((form.fields ?? []).map((q2) => q2.fieldKey));
6812
+ for (const f of t.fields) {
6813
+ if (!f.required || isComputed2(f) || f.default !== void 0 || asked.has(f.key)) continue;
6814
+ form.fields = [...form.fields ?? [], { fieldKey: f.key, ask: `What is the ${lower(f.label)}?`, required: true }];
6815
+ fixes.push(`forms.${form.token}.fields.${f.key}: asked (a required field no question covered); reword in repair`);
6816
+ }
6817
+ }
6818
+ }
6819
+ var REALISM = ["childDates", "withinParent", "statusAgrees", "countMatches", "datePairs"];
6820
+ function switchJob(spec, fixes) {
6821
+ if (spec.job !== "crm_pipeline" && spec.job !== "billing") return;
6822
+ 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)));
6823
+ if (!linked) return;
6824
+ const declared = spec.tables.some((t) => REALISM.some((k) => Array.isArray(t[k]) && t[k].length > 0));
6825
+ if (declared) return;
6826
+ const to = spec.job === "crm_pipeline" ? "work_orders" : "intake";
6827
+ fixes.push(`job: ${spec.job} switched to ${to} (linked tables, no realism declarations)`);
6828
+ spec.job = to;
6829
+ }
6830
+ function autofixDescribeSpec(input, ctx = {}) {
6831
+ const spec = describeSpec(clone2(input));
6832
+ const autoFixes = [];
6833
+ raiseSensitivity(spec, autoFixes);
6834
+ flagAskedOfSubject(spec, ctx, autoFixes);
6835
+ fixBooking(spec, autoFixes);
6836
+ fixAvailability(spec, autoFixes);
6837
+ fixViewTokens(spec, autoFixes);
6838
+ fixColours(spec, autoFixes);
6839
+ fixRelationships(spec, ctx, autoFixes);
6840
+ askRequired(spec, autoFixes);
6841
+ switchJob(spec, autoFixes);
6842
+ return { spec, autoFixes };
6843
+ }
6844
+ function describeCheck(spec, ctx = {}) {
6845
+ const fixed = autofixDescribeSpec(spec, ctx);
6846
+ const v = validateTemplate(fixed.spec, { profile: "describe" });
6847
+ return { ...fixed, problems: v.problems.map((p) => ({ at: p.at, says: p.says })) };
6848
+ }
6849
+
6850
+ // src/templates/describe/repair.ts
6851
+ var clone3 = (v) => JSON.parse(JSON.stringify(v));
6852
+ function parseAt(at) {
6853
+ const out = [];
6854
+ const re = /([A-Za-z_][A-Za-z0-9_]*)(?:\[(\d+)\])?(?: \(([^)]*)\))?/g;
6855
+ for (let m = re.exec(at); m; m = re.exec(at)) {
6856
+ const named = m[3] !== void 0 && m[3] !== "" && m[3] !== "undefined" && m[3] !== "null";
6857
+ out.push({ root: m[1], ...named ? { id: m[3] } : m[2] !== void 0 ? { id: `#${m[2]}` } : {} });
6858
+ }
6859
+ return out;
6860
+ }
6861
+ function pathOfAt(at) {
6862
+ const segs = parseAt(at);
6863
+ const [a, b] = segs;
6864
+ if (!a) return "spec";
6865
+ if ((a.root === "tables" || a.root === "forms") && a.id !== void 0) {
6866
+ if (b && (b.root === "fields" || b.root === "questions") && b.id) return `${a.root}.${a.id}.fields.${b.id}`;
6867
+ return `${a.root}.${a.id}`;
6868
+ }
6869
+ if (a.root === "extras" && a.id) {
6870
+ if (b && b.root === "questions" && b.id) return `extras.${a.id}.questions.${b.id}`;
6871
+ return b && b.root === "availability" ? `extras.${a.id}.availability` : `extras.${a.id}`;
6872
+ }
6873
+ if (a.root === "views" && a.id) return `views.${a.id}`;
6874
+ if (a.root === "business") return "business";
6875
+ return "spec";
6876
+ }
6877
+ var idOf = (o) => typeof o.key === "string" ? o.key : typeof o.fieldKey === "string" ? o.fieldKey : typeof o.token === "string" ? o.token : void 0;
6878
+ function findIn(list, id) {
6879
+ if (!Array.isArray(list)) return void 0;
6880
+ if (/^#\d+$/.test(id)) return list[Number(id.slice(1))];
6881
+ return list.find((x) => x && idOf(x) === id);
6882
+ }
6883
+ function resolvePath(spec, path) {
6884
+ const s = spec;
6885
+ const parts = path.split(".");
6886
+ const [root, id, sub, subId] = parts;
6887
+ if (root === "spec" || !root) return s;
6888
+ if (root === "business") return s.business;
6889
+ if (root === "relationships") {
6890
+ const rels = s.relationships ?? [];
6891
+ return rels.find((r) => r.fromTable === id && (parts[2] === void 0 || r.fromField === parts[2]));
6892
+ }
6893
+ let item;
6894
+ if (root === "extras") item = findIn(s.extras, id ?? "") ?? (s.extras ?? []).find((e) => e.kind === id);
6895
+ else if (root === "tables" || root === "forms" || root === "views") item = findIn(s[root], id ?? "");
6896
+ if (!item) return void 0;
6897
+ if (!sub) return item;
6898
+ if (sub === "availability") return item.availability;
6899
+ if ((sub === "fields" || sub === "questions") && subId) return findIn(item[sub], subId);
6900
+ return void 0;
6901
+ }
6902
+ function specBrief(spec, problems) {
6903
+ const s = spec;
6904
+ 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) };
6905
+ for (const p of problems) {
6906
+ const root = parseAt(p.at)[0]?.root;
6907
+ if (root && root !== "spec" && root in s && !(root in brief)) brief[root] = clone3(s[root]);
6908
+ }
6909
+ return brief;
6910
+ }
6911
+ function tableBrief(t) {
6912
+ const { fields, rows: _rows, ...own } = t;
6913
+ 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 } : {} })) };
6914
+ }
6915
+ function repairSlices(spec, problems) {
6916
+ const byPath = /* @__PURE__ */ new Map();
6917
+ for (const p of problems) {
6918
+ const first = parseAt(p.at)[0];
6919
+ const rel = first?.root === "relationships" && first.id !== void 0 && /^#\d+$/.test(first.id) ? spec.relationships?.[Number(first.id.slice(1))] : void 0;
6920
+ const path = rel ? `relationships.${rel.fromTable}.${rel.fromField}` : pathOfAt(p.at);
6921
+ let slice = byPath.get(path);
6922
+ if (!slice) {
6923
+ const found = resolvePath(spec, path);
6924
+ const isTable = path.startsWith("tables.") && path.split(".").length === 2;
6925
+ const value = path === "spec" ? specBrief(spec, problems.filter((q2) => pathOfAt(q2.at) === "spec")) : isTable && found ? tableBrief(found) : found ? clone3(found) : null;
6926
+ slice = { path, slice: value, problems: [] };
6927
+ byPath.set(path, slice);
6928
+ }
6929
+ slice.problems.push({ at: p.at, says: p.says });
6930
+ }
6931
+ return [...byPath.values()];
6932
+ }
6933
+ var IMMUTABLE = /* @__PURE__ */ new Set(["key", "token", "fieldKey"]);
6934
+ function applyRepairPatches(input, patches) {
6935
+ const spec = clone3(input);
6936
+ const applied = [];
6937
+ const skipped = [];
6938
+ for (const patch of patches ?? []) {
6939
+ const path = String(patch?.path ?? "");
6940
+ const set = patch?.set;
6941
+ if (!path || !set || typeof set !== "object" || Array.isArray(set)) {
6942
+ skipped.push({ path, why: "a patch needs a path and a set object" });
6943
+ continue;
6944
+ }
6945
+ let target = resolvePath(spec, path);
6946
+ if (!target) {
6947
+ const parts = path.split(".");
6948
+ const parent = resolvePath(spec, parts.slice(0, 2).join("."));
6949
+ const [root, , sub, subId] = parts;
6950
+ if (parent && root === "tables" && sub === "fields" && subId && parts.length === 4 && typeof set.label === "string" && typeof set.parityType === "string") {
6951
+ const f = { key: subId, sensitivity: "internal", contextPolicy: "include", ...set };
6952
+ parent.fields.push(f);
6953
+ target = f;
6954
+ applied.push(path);
6955
+ continue;
6956
+ }
6957
+ if (parent && root === "forms" && sub === "fields" && subId && parts.length === 4 && typeof set.ask === "string") {
6958
+ (parent.fields ??= []).push({ fieldKey: subId, ...set });
6959
+ applied.push(path);
6960
+ continue;
6961
+ }
6962
+ skipped.push({ path, why: "no such piece in the spec" });
6963
+ continue;
6964
+ }
6965
+ for (const [k, v] of Object.entries(set)) {
6966
+ if (IMMUTABLE.has(k) && v !== target[k]) {
6967
+ skipped.push({ path: `${path}.${k}`, why: `${k} is never renamed by a patch` });
6968
+ continue;
6969
+ }
6970
+ if (v === null) delete target[k];
6971
+ else target[k] = v;
6972
+ }
6973
+ applied.push(path);
6974
+ }
6975
+ return { spec, applied, skipped };
6976
+ }
6977
+ function lastResortTrim(input, problems) {
6978
+ const spec = clone3(input);
6979
+ const notes = [];
6980
+ const droppedViews = /* @__PURE__ */ new Set();
6981
+ const views = spec.views;
6982
+ for (const p of problems) {
6983
+ const segs = parseAt(p.at);
6984
+ const [a, b] = segs;
6985
+ const last = segs[segs.length - 1];
6986
+ if (!a || !a.id) continue;
6987
+ if (a.root === "views") {
6988
+ const v = findIn(views, a.id);
6989
+ if (v && views.length - droppedViews.size > 1 && !droppedViews.has(v)) {
6990
+ droppedViews.add(v);
6991
+ notes.push(`Left out the view "${v.name}": it did not pass the store's check.`);
6992
+ }
6993
+ continue;
6994
+ }
6995
+ if (last?.root === "help") {
6996
+ 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;
6997
+ if (holder && "help" in holder) {
6998
+ delete holder.help;
6999
+ notes.push(`Left out the help text on "${b?.id}": it did not pass the store's check.`);
7000
+ }
7001
+ continue;
7002
+ }
7003
+ if (last?.root === "ask" && b?.id) {
7004
+ const holder = a.root === "forms" ? findIn(spec.forms, a.id) : a.root === "extras" ? findIn(spec.extras, a.id) : void 0;
7005
+ const list = (a.root === "forms" ? holder?.fields : holder?.questions) ?? null;
7006
+ const table = holder?.table;
7007
+ const field2 = spec.tables.find((t) => t.token === table)?.fields.find((f) => f.key === b.id);
7008
+ const q2 = list?.find((x) => x.fieldKey === b.id);
7009
+ if (list && q2 && !q2.required && !field2?.required) {
7010
+ list.splice(list.indexOf(q2), 1);
7011
+ notes.push(`Left out the question on "${b.id}": it did not pass the store's check.`);
7012
+ }
7013
+ }
7014
+ }
7015
+ if (droppedViews.size) spec.views = spec.views.filter((x) => !droppedViews.has(x));
7016
+ return { spec, notes };
7017
+ }
7018
+
7019
+ // src/templates/describe/package.ts
7020
+ var WORTH_SAYING = /^job:|: added |: asked |: dropped |dropped \d+ question/;
7021
+ var NOTES_MAX = 7;
7022
+ function packageDescribeResult(spec, plan, autoFixes, reuses, extraNotes = []) {
7023
+ const tokens = new Set(spec.tables.map((t) => t.token));
7024
+ const left = plan?.leaveOut?.length ? [`Left out: ${plan.leaveOut.join("; ")}.`] : [];
7025
+ const said = autoFixes.filter((f) => WORTH_SAYING.test(f)).map((f) => f.startsWith("job:") ? `Used a simpler setup type: ${f.slice(5)}.` : f);
7026
+ const notes = [...plan?.assumptions ?? [], ...left, ...said, ...extraNotes].map((n) => n.trim()).filter(Boolean).slice(0, NOTES_MAX);
7027
+ return {
7028
+ __kind: "describe_template_result",
7029
+ template: JSON.parse(JSON.stringify(spec)),
7030
+ notes,
7031
+ reuses: (reuses ?? []).filter((r) => r && tokens.has(r.token))
7032
+ };
7033
+ }
7034
+
7035
+ // src/templates/describe/vocabulary.ts
7036
+ var BOOKING_AVAILABILITY_FACTS = {
7037
+ timezone: "text \u2014 an IANA time zone such as America/Los_Angeles; defaults to the business's",
7038
+ slotMinutes: "one whole number 5 to 480 \u2014 how long one appointment is, in minutes",
7039
+ bufferMinutes: "one whole number 0 to 480 \u2014 rest between appointments, in minutes",
7040
+ leadMinutes: "one whole number 0 to 43200 \u2014 how much notice a person must give, in minutes",
7041
+ maxPerDay: "one whole number 1 to 100 \u2014 appointments accepted in one day",
7042
+ 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",
7043
+ 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"
7044
+ };
7045
+ var VIEW_SHAPE_FACTS = {
7046
+ token: "snake_case, unique among every view, form and extra \u2014 always written",
7047
+ name: 'what a person clicks, such as "Intake board"',
7048
+ table: "the token of one of the template's tables",
7049
+ kind: TEMPLATE_VIEW_KINDS,
7050
+ groupBy: "kanban only, required: the key of a select field whose choices become the columns",
7051
+ dateField: "calendar only, required: the key of a datetime field",
7052
+ startField_endField: "timeline only, required: keys of two datetime fields",
7053
+ coverField: "gallery only, required: the key of a file field"
7054
+ };
7055
+ function describeVocabulary() {
7056
+ return {
7057
+ field_kinds: RESOLVABLE_FIELD_TYPE_WORDS,
7058
+ table_types: TABLE_TYPES,
7059
+ table_displays: TABLE_DISPLAYS,
7060
+ storage_modes: STORAGE_MODES,
7061
+ field_sensitivities: FIELD_SENSITIVITIES,
7062
+ context_policies: CONTEXT_POLICIES,
7063
+ field_sources: FIELD_SOURCES,
7064
+ relation_flavors: RELATION_FLAVORS,
7065
+ relation_cardinalities: RELATION_CARDINALITIES,
7066
+ on_delete: ON_DELETE,
7067
+ industry_groups: INDUSTRY_GROUPS,
7068
+ template_jobs: TEMPLATE_JOBS,
7069
+ teaches_features: TEACHES_FEATURES,
7070
+ choice_colors: CHOICE_COLORS,
7071
+ template_view_kinds: TEMPLATE_VIEW_KINDS,
7072
+ template_audiences: TEMPLATE_AUDIENCES,
7073
+ form_audiences: ["public", "member", "staff"],
7074
+ extra_kinds_for_describe: ["booking"],
7075
+ booking_window: { weekday: "0 = Sunday \u2026 6 = Saturday", from: "HH:MM", to: "HH:MM later than from" },
7076
+ booking_availability: BOOKING_AVAILABILITY_FACTS,
7077
+ view_shape: VIEW_SHAPE_FACTS,
7078
+ 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"
7079
+ };
7080
+ }
6353
7081
  export {
7082
+ BAD_OUTCOME,
7083
+ BOOKING_AVAILABILITY_FACTS,
6354
7084
  CAPABILITY_WORDS,
6355
7085
  CATALOGUE_QUOTAS,
6356
7086
  CATALOGUE_STATUSES,
@@ -6360,6 +7090,8 @@ export {
6360
7090
  DESIGNATED_REAL_NUMBERS,
6361
7091
  ENTITY_REFERENCE_KINDS,
6362
7092
  FORMULA_FUNCTIONS,
7093
+ GALLERY_ONLY,
7094
+ GOOD_OUTCOME,
6363
7095
  INDUSTRY_GROUPS,
6364
7096
  INSTALL_PLAN_VERSION,
6365
7097
  MAIN_TABLE_ROWS_MAX,
@@ -6387,11 +7119,14 @@ export {
6387
7119
  TEMPLATE_VIEW_KINDS,
6388
7120
  TIME_BOUND_STAGE,
6389
7121
  TITLE_DISTINCT_SHARE,
7122
+ VIEW_SHAPE_FACTS,
6390
7123
  agentCopyRequest,
6391
7124
  allTables,
6392
7125
  anyOfWhere,
7126
+ applyRepairPatches,
6393
7127
  assertTemplate,
6394
7128
  attachmentProblems,
7129
+ autofixDescribeSpec,
6395
7130
  automationHeld,
6396
7131
  blockTable,
6397
7132
  bookingArgs,
@@ -6408,12 +7143,16 @@ export {
6408
7143
  deferredRelationFields,
6409
7144
  deferredRelations,
6410
7145
  derivedFieldSpec,
7146
+ describeCheck,
7147
+ describeSpec,
7148
+ describeVocabulary,
6411
7149
  digestSpec,
6412
7150
  documentBody,
6413
7151
  domainsIn,
6414
7152
  drillDownDashboard,
6415
7153
  evalFormula,
6416
7154
  extrasProblems,
7155
+ fallbackView,
6417
7156
  footprintOf,
6418
7157
  foreignTextProblems,
6419
7158
  formArgs,
@@ -6423,13 +7162,18 @@ export {
6423
7162
  foundationTokens,
6424
7163
  isSelfDeclaredField,
6425
7164
  isTimedField,
7165
+ lastResortTrim,
6426
7166
  membersAddRowsSteps,
7167
+ mergeDescribeParts,
6427
7168
  normalizeForPhones,
6428
7169
  notificationParts,
6429
7170
  numberText,
6430
7171
  numberedStreet,
7172
+ packageDescribeResult,
7173
+ parseAt,
6431
7174
  parseFormula,
6432
7175
  parseNumeric,
7176
+ pathOfAt,
6433
7177
  peopleOf,
6434
7178
  phonesOutsideFictionalRange,
6435
7179
  planTables,
@@ -6437,9 +7181,12 @@ export {
6437
7181
  relationFieldSpec,
6438
7182
  relationPatches,
6439
7183
  relativeHasTime,
7184
+ repairSlices,
6440
7185
  requiredCircleProblem,
6441
7186
  resolveDateToken,
7187
+ resolvePath,
6442
7188
  resolveRelativeDates,
7189
+ reusesOf,
6443
7190
  rowActionList,
6444
7191
  rowLevels,
6445
7192
  rowsFor,