@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.
- package/CHANGELOG.md +18 -0
- package/dist/app-table/index.cjs +2 -1
- package/dist/app-table/index.cjs.map +1 -1
- package/dist/app-table/index.js +2 -1
- package/dist/app-table/index.js.map +1 -1
- package/dist/memory/index.cjs.map +1 -1
- package/dist/memory/index.d.cts +1 -1
- package/dist/memory/index.d.ts +1 -1
- package/dist/memory/index.js.map +1 -1
- package/dist/react/index.cjs +2 -1
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.js +2 -1
- package/dist/react/index.js.map +1 -1
- package/dist/templates/index.cjs +1006 -155
- package/dist/templates/index.cjs.map +1 -1
- package/dist/templates/index.d.cts +278 -4
- package/dist/templates/index.d.ts +278 -4
- package/dist/templates/index.js +1006 -155
- package/dist/templates/index.js.map +1 -1
- package/dist/{types-DlRG9iZh.d.cts → types-D9HN8YZS.d.cts} +1 -1
- package/dist/{types-bdEWPW4p.d.ts → types-DvhixdUB.d.ts} +1 -1
- package/package.json +9 -5
package/dist/templates/index.cjs
CHANGED
|
@@ -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
|
|
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 =
|
|
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((
|
|
1972
|
-
const fat = `${at}.fields[${fi}] (${
|
|
1973
|
-
if (!/^[a-z][a-z0-9_]*$/.test(
|
|
1974
|
-
say(`${fat}.key`, `"${
|
|
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(
|
|
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 "${
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
2001
|
-
say(`${fat}.relationTarget`, `It points at the table "${
|
|
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 (
|
|
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
|
|
2019
|
-
if (
|
|
2020
|
-
say(`${rat}.values.${
|
|
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?.[
|
|
2023
|
-
if (value !== void 0 &&
|
|
2024
|
-
if (typeof value === "string" && !
|
|
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.${
|
|
2027
|
-
`"${value}" is not one of "${
|
|
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
|
|
2052
|
-
if (!
|
|
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(
|
|
2076
|
+
} else if (!RELATION_TYPES.has(field2.parityType)) {
|
|
2055
2077
|
say(
|
|
2056
2078
|
`${at}.fromField`,
|
|
2057
|
-
`"${rel.fromField}" is a ${
|
|
2079
|
+
`"${rel.fromField}" is a ${field2.parityType} field, so it cannot hold a relation. Declare it as a relation.`
|
|
2058
2080
|
);
|
|
2059
|
-
} else if (
|
|
2081
|
+
} else if (field2.relationTarget && field2.relationTarget !== rel.toTable) {
|
|
2060
2082
|
say(
|
|
2061
2083
|
`${at}.fromField`,
|
|
2062
|
-
`The field points at "${
|
|
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(
|
|
2221
|
-
if (!
|
|
2242
|
+
function choiceDisplayFormat(field2) {
|
|
2243
|
+
if (!field2.choices?.length || !field2.choiceColors) return void 0;
|
|
2222
2244
|
return {
|
|
2223
|
-
id:
|
|
2245
|
+
id: field2.parityType === "multi_select" ? "multi_choice" : "choice",
|
|
2224
2246
|
options: {
|
|
2225
|
-
choices:
|
|
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(
|
|
2315
|
+
function timedFieldSpec(field2) {
|
|
2294
2316
|
return {
|
|
2295
|
-
key:
|
|
2296
|
-
label:
|
|
2317
|
+
key: field2.key,
|
|
2318
|
+
label: field2.label,
|
|
2297
2319
|
type: "datetime",
|
|
2298
2320
|
kind: "datetime",
|
|
2299
|
-
...
|
|
2300
|
-
...
|
|
2301
|
-
sensitivity:
|
|
2302
|
-
context_policy:
|
|
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(
|
|
2306
|
-
if (isTimedField(
|
|
2307
|
-
const allowed =
|
|
2308
|
-
const displayFormat = choiceDisplayFormat(
|
|
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:
|
|
2311
|
-
label:
|
|
2312
|
-
type:
|
|
2313
|
-
...
|
|
2314
|
-
...
|
|
2315
|
-
...
|
|
2316
|
-
...
|
|
2317
|
-
...
|
|
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
|
-
...
|
|
2341
|
+
...field2.default !== void 0 ? { default: field2.default } : {},
|
|
2320
2342
|
...allowed,
|
|
2321
|
-
sensitivity:
|
|
2322
|
-
context_policy:
|
|
2343
|
+
sensitivity: field2.sensitivity,
|
|
2344
|
+
context_policy: field2.contextPolicy
|
|
2323
2345
|
};
|
|
2324
2346
|
}
|
|
2325
|
-
function relationFieldSpec(
|
|
2347
|
+
function relationFieldSpec(field2, targetTableId) {
|
|
2326
2348
|
return {
|
|
2327
|
-
key:
|
|
2328
|
-
label:
|
|
2349
|
+
key: field2.key,
|
|
2350
|
+
label: field2.label,
|
|
2329
2351
|
type: "relation",
|
|
2330
2352
|
relation_target: targetTableId,
|
|
2331
|
-
...
|
|
2332
|
-
...
|
|
2333
|
-
...
|
|
2334
|
-
sensitivity:
|
|
2335
|
-
context_policy:
|
|
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
|
|
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(
|
|
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
|
|
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) =>
|
|
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(
|
|
2453
|
+
function derivedFieldSpec(field2) {
|
|
2432
2454
|
const base = {
|
|
2433
|
-
key:
|
|
2434
|
-
label:
|
|
2435
|
-
type:
|
|
2436
|
-
via:
|
|
2437
|
-
sensitivity:
|
|
2438
|
-
context_policy:
|
|
2439
|
-
...
|
|
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
|
|
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(
|
|
2509
|
+
function formulaFieldSpec(field2) {
|
|
2488
2510
|
return {
|
|
2489
|
-
key:
|
|
2490
|
-
label:
|
|
2511
|
+
key: field2.key,
|
|
2512
|
+
label: field2.label,
|
|
2491
2513
|
type: "formula",
|
|
2492
|
-
formula_text:
|
|
2493
|
-
...
|
|
2494
|
-
sensitivity:
|
|
2495
|
-
context_policy:
|
|
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(
|
|
3135
|
+
function ruleBreak(field2, v) {
|
|
3114
3136
|
const t = storeText(v) ?? "";
|
|
3115
3137
|
if (t === "") return null;
|
|
3116
|
-
if (
|
|
3117
|
-
for (const rule of
|
|
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 "${
|
|
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 "${
|
|
3124
|
-
if (rule.min !== void 0 && t.length < rule.min) return `"${t}" is shorter than "${
|
|
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 "${
|
|
3149
|
+
if (!new RegExp(String(rule.value)).test(t)) return `"${t}" does not match "${field2.label}"'s own pattern.`;
|
|
3128
3150
|
} catch {
|
|
3129
|
-
return `"${
|
|
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
|
|
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: (
|
|
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
|
|
3293
|
-
const word = `field.${
|
|
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 (
|
|
3296
|
-
if (
|
|
3297
|
-
if (
|
|
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
|
|
3509
|
-
if (!all.includes(
|
|
3510
|
-
const re = new RegExp(`(^|[^a-z0-9])${esc(
|
|
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((
|
|
3858
|
-
const at = `${tat}.fields (${
|
|
3859
|
-
if (isChoice(
|
|
3860
|
-
const colors =
|
|
3861
|
-
|
|
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(
|
|
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(
|
|
3869
|
-
if (distinct < need) say(`${at}.choiceColors`, `${
|
|
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(
|
|
3872
|
-
const kinds2 = new Set((
|
|
3873
|
-
if (!kinds2.has("min") || !kinds2.has("max")) say(`${at}.rules`, `"${
|
|
3874
|
-
const seeds = table.rows.map((r) => parseNumeric(storeText(r.values?.[
|
|
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 ${
|
|
3879
|
-
for (const rule of
|
|
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 "${
|
|
3882
|
-
if (rule.kind === "min" && bound < -1e3 * Math.abs(lo)) say(`${at}.rules`, `The least "${
|
|
3883
|
-
if (rule.kind === "min" && bound < 0 && lo >= 0) say(`${at}.rules`, `"${
|
|
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 (
|
|
3888
|
-
const via =
|
|
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 ${
|
|
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 (!
|
|
3894
|
-
if (
|
|
3895
|
-
if (
|
|
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 (
|
|
3899
|
-
if (isRelation(
|
|
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
|
|
3910
|
-
if (!isRelation(
|
|
3911
|
-
const target = byToken.get(
|
|
3912
|
-
const v = row.values?.[
|
|
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.${
|
|
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
|
|
3921
|
-
const v = row.values?.[
|
|
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
|
|
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.${
|
|
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.${
|
|
3933
|
-
if (rule.min !== void 0 && t.length < rule.min) say(`${rat}.values.${
|
|
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 (${
|
|
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.${
|
|
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
|
|
3947
|
-
if (!(
|
|
3948
|
-
const vals = table.rows.map((r) => storeText(r.values?.[
|
|
3949
|
-
if (new Set(vals).size !== vals.length) say(`${tat}.fields (${
|
|
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
|
|
4508
|
-
if (!
|
|
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 =
|
|
5179
|
-
const BAD =
|
|
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
|
|
5357
|
-
|
|
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
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
|
|
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
|
|
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) =>
|
|
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
|