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