ngx-t-forms-types 0.0.30 → 0.0.33

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.
Files changed (63) hide show
  1. package/dist/interfaces/Form/formSubmissionHandleInterface.d.ts +6 -0
  2. package/dist/interfaces/FormBuilder/DefaultEelement.js +76 -0
  3. package/dist/interfaces/FormBuilder/DefaultInputConfigInterface.d.ts +13 -0
  4. package/dist/interfaces/FormBuilder/FormInputKeys.d.ts +6 -0
  5. package/dist/interfaces/FormBuilder/FormInputKeys.js +6 -0
  6. package/dist/interfaces/FormBuilder/inputConfig/ElementEditConfig.js +188 -3
  7. package/dist/interfaces/formInput/APIDataFetchingConfigurationInterface.d.ts +34 -0
  8. package/dist/interfaces/formInput/BasicFormInputInterface.d.ts +1 -0
  9. package/dist/interfaces/formInput/BasicFormInputInterface.js +1 -0
  10. package/dist/interfaces/formInput/IMscoaAccount.d.ts +19 -3
  11. package/dist/interfaces/formInput/ISelectInputInterface.d.ts +12 -0
  12. package/dist/interfaces/formInput/MultipleInterface.d.ts +10 -0
  13. package/dist/interfaces/formInput/WorkflowDocumentPicker.d.ts +2 -0
  14. package/dist/schemas/FormInputSchema.js +41 -2
  15. package/dist/schemas/MatOptionsSchema.js +7 -0
  16. package/dist/schemas/MscoaConfigSchema.js +1 -0
  17. package/dist/schemas/index.d.ts +2 -1
  18. package/dist/schemas/index.js +2 -1
  19. package/dist/skillet/authoring-rules.d.ts +41 -0
  20. package/dist/skillet/authoring-rules.js +61 -0
  21. package/dist/skillet/extra-members.d.ts +143 -0
  22. package/dist/skillet/extra-members.js +79 -0
  23. package/dist/skillet/form.d.ts +170 -0
  24. package/dist/skillet/form.js +139 -0
  25. package/dist/skillet/index.d.ts +43 -0
  26. package/dist/skillet/index.js +46 -0
  27. package/dist/skillet/input-members.d.ts +375 -0
  28. package/dist/skillet/input-members.js +186 -0
  29. package/dist/skillet/internal/coupling.d.ts +63 -0
  30. package/dist/skillet/internal/coupling.js +69 -0
  31. package/dist/skillet/internal/editor-guidance.d.ts +64 -0
  32. package/dist/skillet/internal/editor-guidance.js +291 -0
  33. package/dist/skillet/members/calculated-field.d.ts +155 -0
  34. package/dist/skillet/members/calculated-field.js +105 -0
  35. package/dist/skillet/members/conditional.d.ts +69 -0
  36. package/dist/skillet/members/conditional.js +16 -0
  37. package/dist/skillet/members/document-picker.d.ts +182 -0
  38. package/dist/skillet/members/document-picker.js +111 -0
  39. package/dist/skillet/members/mat-options.d.ts +187 -0
  40. package/dist/skillet/members/mat-options.js +92 -0
  41. package/dist/skillet/members/mscoa.d.ts +182 -0
  42. package/dist/skillet/members/mscoa.js +129 -0
  43. package/dist/skillet/members/pagination.d.ts +59 -0
  44. package/dist/skillet/members/pagination.js +16 -0
  45. package/dist/skillet/members/table.d.ts +178 -0
  46. package/dist/skillet/members/table.js +105 -0
  47. package/dist/skillet/members/validators.d.ts +197 -0
  48. package/dist/skillet/members/validators.js +74 -0
  49. package/dist/skillet/members/value.d.ts +154 -0
  50. package/dist/skillet/members/value.js +97 -0
  51. package/dist/skillet/shared-types.d.ts +57 -0
  52. package/dist/skillet/shared-types.js +69 -0
  53. package/dist/skillet/tests/house-rules.spec.d.ts +1 -0
  54. package/dist/skillet/tests/house-rules.spec.js +162 -0
  55. package/dist/skillet/tests/skillet-coupling.spec.d.ts +1 -0
  56. package/dist/skillet/tests/skillet-coupling.spec.js +191 -0
  57. package/dist/skillet/tests/variant-assembly.spec.d.ts +1 -0
  58. package/dist/skillet/tests/variant-assembly.spec.js +643 -0
  59. package/dist/skillet/variants.d.ts +138 -0
  60. package/dist/skillet/variants.js +536 -0
  61. package/dist/skillet/workflow-context.d.ts +95 -0
  62. package/dist/skillet/workflow-context.js +128 -0
  63. package/package.json +78 -60
@@ -0,0 +1,291 @@
1
+ import { SpecialElementKeys } from '../../interfaces/FormBuilder/FormInputKeys.js';
2
+ import { getElementEditorConfig } from '../../interfaces/FormBuilder/inputConfig/ElementEditConfig.js';
3
+ /** Joins a `deepBind` path into the key this module indexes rows by. */
4
+ const pathKey = (path) => path.join('.');
5
+ function collectRows() {
6
+ const rows = [];
7
+ for (const section of getElementEditorConfig.editorSections ?? []) {
8
+ for (const element of section.elements ?? []) {
9
+ rows.push(element);
10
+ }
11
+ }
12
+ return rows;
13
+ }
14
+ /**
15
+ * Rows belonging to a `secondaryElementEditorConfig`, indexed by the path of
16
+ * the row that opens them and then by their own path within that editor.
17
+ *
18
+ * ## Why these are indexed separately rather than prefixed into `rowsByPath`
19
+ *
20
+ * A secondary editor's rows bind RELATIVE to the record it edits, so making
21
+ * them globally addressable means prefixing them with something. The obvious
22
+ * prefix — the opening row's `deepBind` — is right in one case and wrong in the
23
+ * other, which is the whole reason for this split:
24
+ *
25
+ * - `workflowPickerConfig.presetFilters` opens a `RecordListManager` over the
26
+ * filters themselves, so its inner `op` row really is
27
+ * `workflowPickerConfig.presetFilters[].op`.
28
+ * - `mscoaConfig` opens a composite editor whose secondary rows edit
29
+ * `dualCashExclusion.rules[]`, NOT `mscoaConfig` directly. Prefixing there
30
+ * would file the dual-cash `pattern` hint under `mscoaConfig.pattern` — a
31
+ * path nothing has, silently attaching accounting-exclusion guidance to
32
+ * whatever member later claimed that name.
33
+ *
34
+ * Nothing on the row says which of the two it is, so rather than guess, the
35
+ * caller states the container it is describing and looks rows up within it.
36
+ * Keeping this index apart from `rowsByPath` also means top-level descriptions
37
+ * are unchanged by its existence.
38
+ */
39
+ function collectSecondaryRows() {
40
+ const index = new Map();
41
+ for (const parent of collectRows()) {
42
+ const sections = parent.secondaryElementEditorConfig;
43
+ if (!sections?.length || !parent.deepBind?.length)
44
+ continue;
45
+ const parentKey = pathKey(parent.deepBind);
46
+ const inner = index.get(parentKey) ?? new Map();
47
+ for (const section of sections) {
48
+ for (const row of section.elements ?? []) {
49
+ if (!row.deepBind?.length)
50
+ continue;
51
+ const key = pathKey(row.deepBind);
52
+ const existing = inner.get(key);
53
+ if (existing)
54
+ existing.push(row);
55
+ else
56
+ inner.set(key, [row]);
57
+ }
58
+ }
59
+ if (inner.size)
60
+ index.set(parentKey, inner);
61
+ }
62
+ return index;
63
+ }
64
+ /**
65
+ * Every editor row, indexed by its `deepBind` path.
66
+ *
67
+ * `name` is not the index: it reads `'default'` on most rows, with the property
68
+ * actually being edited carried in `deepBind`. Nested paths
69
+ * (`matOptions.fetch.options`) are indexed too, so the sub-schemas for the
70
+ * pending members can draw on the same guidance when they are built.
71
+ */
72
+ const rowsByPath = (() => {
73
+ const index = new Map();
74
+ for (const row of collectRows()) {
75
+ if (!row.deepBind?.length)
76
+ continue;
77
+ const key = pathKey(row.deepBind);
78
+ const existing = index.get(key);
79
+ if (existing)
80
+ existing.push(row);
81
+ else
82
+ index.set(key, [row]);
83
+ }
84
+ return index;
85
+ })();
86
+ /**
87
+ * Expressions this module could not read, exposed so a test can assert the set
88
+ * is empty.
89
+ *
90
+ * The grammar in use is closed — `key === value`, joined only by `||` — but it
91
+ * is a string, so nothing stops a future edit reaching for `&&` or `!==`.
92
+ * Rather than throw at import time and take a consumer's app down over editor
93
+ * copy, unreadable expressions are skipped and recorded here, which turns a
94
+ * silent loss of guidance into a failing test.
95
+ */
96
+ export const UNREADABLE_EXPRESSIONS = [];
97
+ /**
98
+ * Renders one comparison as prose.
99
+ *
100
+ * `!==` is tested first because `===` is not a substring of it — splitting on
101
+ * `===` would leave `valueSource !` as the key and read the rule backwards,
102
+ * which is worse than not reading it at all.
103
+ */
104
+ function readComparison(clause) {
105
+ const negated = clause.includes('!==');
106
+ const parts = clause.split(negated ? '!==' : '===');
107
+ if (parts.length !== 2)
108
+ return null;
109
+ const key = parts[0].trim();
110
+ const value = parts[1].trim();
111
+ if (!key || !value)
112
+ return null;
113
+ return { key, negated, value };
114
+ }
115
+ /**
116
+ * Turns one applicability test into a clause such as
117
+ * `type is number or date`, or `null` if it cannot be read.
118
+ */
119
+ function readTest(test) {
120
+ if (test.expression) {
121
+ const expression = test.expression;
122
+ // `&&` remains unreadable. Unlike `!==`, it changes the SHAPE of the rule
123
+ // from "any of these" to "all of these", and every collapse below assumes
124
+ // alternatives — reading one as a disjunction would state the opposite of
125
+ // what the editor enforces.
126
+ if (expression.includes('&&')) {
127
+ UNREADABLE_EXPRESSIONS.push(expression);
128
+ return null;
129
+ }
130
+ const comparisons = expression.split('||').map(readComparison);
131
+ if (comparisons.some((c) => c === null)) {
132
+ UNREADABLE_EXPRESSIONS.push(expression);
133
+ return null;
134
+ }
135
+ const read = comparisons;
136
+ const phrase = (negated) => (negated ? 'is not' : 'is');
137
+ // Every clause in the observed grammar tests the same key against
138
+ // alternatives, which reads far better collapsed than repeated. Collapsing
139
+ // requires a shared operator as well as a shared key: `a === x || a !== y`
140
+ // states two different things about `a` and must stay spelled out.
141
+ const [first] = read;
142
+ const uniform = read.every((c) => c.key === first.key && c.negated === first.negated);
143
+ if (uniform) {
144
+ const values = read.map((c) => c.value);
145
+ const list = values.length === 1
146
+ ? values[0]
147
+ : `${values.slice(0, -1).join(', ')} or ${values[values.length - 1]}`;
148
+ return `${first.key} ${phrase(first.negated)} ${list}`;
149
+ }
150
+ return read
151
+ .map((c) => `${c.key} ${phrase(c.negated)} ${c.value}`)
152
+ .join(', or ');
153
+ }
154
+ if (test.testType === 'exists' && test.deepBind?.length) {
155
+ return `${pathKey(test.deepBind)} is set`;
156
+ }
157
+ return null;
158
+ }
159
+ /**
160
+ * Secondary-editor rows, indexed by opening path and then by their own path.
161
+ * See {@link collectSecondaryRows} for why these are kept out of
162
+ * {@link rowsByPath}.
163
+ */
164
+ const secondaryRowsByParent = collectSecondaryRows();
165
+ /** The applicability sentence for a property, if the editor declares one. */
166
+ function applicabilityIn(rows) {
167
+ if (!rows?.length)
168
+ return undefined;
169
+ const clauses = rows
170
+ .flatMap((row) => row.additionalTest ?? [])
171
+ .map(readTest)
172
+ .filter((clause) => clause !== null);
173
+ const unique = [...new Set(clauses)];
174
+ if (!unique.length)
175
+ return undefined;
176
+ return `Only applies when ${unique.join(', or when ')}.`;
177
+ }
178
+ /**
179
+ * Collapses the whitespace a hint picked up from its source.
180
+ *
181
+ * Several hints are written as indented multi-line template literals, so they
182
+ * arrive carrying newlines and runs of leading spaces. Rendered into a schema
183
+ * description those become blank lines and stray gaps before punctuation, which
184
+ * is exactly the kind of noise that makes a long system prompt harder to read.
185
+ */
186
+ const collapseWhitespace = (text) => text
187
+ .replace(/\s+/g, ' ')
188
+ .replace(/\s+([.,;:])/g, '$1')
189
+ // Pulling a hint flush can bring a stray trailing full stop up against the
190
+ // one ending the previous sentence — at least one hint is written with a
191
+ // lone `.` on its own line. Collapse the pair back to a single stop.
192
+ .replace(/([.!?])[\s]*\1+/g, '$1')
193
+ .trim();
194
+ /** The authored hint for a property, if the editor carries one. */
195
+ function hintIn(rows) {
196
+ const hint = rows
197
+ ?.map((row) => row.hint)
198
+ .find((h) => !!h?.trim());
199
+ return hint ? collapseWhitespace(hint) : undefined;
200
+ }
201
+ /** Ends a fragment with a full stop so composed sentences read cleanly. */
202
+ const terminate = (text) => /[.!?]$/.test(text) ? text : `${text}.`;
203
+ /**
204
+ * Composes the description for a property: the authored sentence, then the
205
+ * editor's hint where it adds something, then when the property applies.
206
+ *
207
+ * @param base authored, model-facing description of the property
208
+ * @param path the property's `deepBind` path — a single key for a top-level
209
+ * member, a longer path for a member of a nested config
210
+ */
211
+ export function describe(base, ...path) {
212
+ return compose(base, rowsByPath.get(pathKey(path)));
213
+ }
214
+ /**
215
+ * Composes a description for a member of a record edited by a SECONDARY
216
+ * editor — a preset filter within `workflowPickerConfig.presetFilters`, say.
217
+ *
218
+ * @param base authored, model-facing description of the property
219
+ * @param opensAt the `deepBind` path of the row that opens the editor
220
+ * @param path the property's path within the record being edited
221
+ */
222
+ export function describeIn(base, opensAt, ...path) {
223
+ return compose(base, secondaryRowsByParent.get(pathKey(opensAt))?.get(pathKey(path)));
224
+ }
225
+ /** Shared body of {@link describe} and {@link describeIn}. */
226
+ function compose(base, rows) {
227
+ const parts = [terminate(base)];
228
+ const hint = hintIn(rows);
229
+ // Skip a hint the authored text already covers, so composition cannot
230
+ // produce the same guidance twice in one description.
231
+ if (hint && !base.toLowerCase().includes(hint.toLowerCase())) {
232
+ parts.push(terminate(hint));
233
+ }
234
+ const applicability = applicabilityIn(rows);
235
+ if (applicability)
236
+ parts.push(applicability);
237
+ return parts.join(' ');
238
+ }
239
+ /**
240
+ * Numeric bounds the editor enforces on a property, ready to spread into a
241
+ * Skillet numeric node so the schema constrains what the settings panel does.
242
+ */
243
+ export function rangeFor(...path) {
244
+ const rows = rowsByPath.get(pathKey(path));
245
+ const row = rows?.find((r) => r.min !== undefined || r.max !== undefined);
246
+ if (!row)
247
+ return undefined;
248
+ const range = {};
249
+ if (typeof row.min === 'number')
250
+ range.minimum = row.min;
251
+ if (typeof row.max === 'number')
252
+ range.maximum = row.max;
253
+ return Object.keys(range).length ? range : undefined;
254
+ }
255
+ /** Property paths the editor config carries guidance for. Used by tests. */
256
+ export const GUIDED_PATHS = [...rowsByPath.keys()];
257
+ /**
258
+ * The top-level members the settings panel edits on EVERY element.
259
+ *
260
+ * A row named `SpecialElementKeys.Default` is not looked up in an element's
261
+ * `properties` at all: it is rendered for every element, gated only by its own
262
+ * `additionalTest` and `disabled` tests. So the members those rows edit are
263
+ * part of every element's surface in the builder while appearing in no
264
+ * element's property list — `readonly`, `onlySetTempErrorOnTouch` and
265
+ * `tourContent` among them. Read here so variant assembly can offer what the
266
+ * builder offers, from the same source the builder reads.
267
+ *
268
+ * Only ungated rows qualify. A gated row is conditional on a VALUE (`min`
269
+ * needs a numeric `type`; `calculatedFieldRules` needs the flag), which is not
270
+ * "always shown" and is handled elsewhere. Nested bindings (`script.onChange`,
271
+ * `matOptions.fetch.value.source`) are members of a nested object, not of the
272
+ * column, and are left to the modules that own those objects.
273
+ */
274
+ export const ALWAYS_SHOWN_MEMBERS = [
275
+ ...new Set(collectRows()
276
+ .filter((row) => row.name === SpecialElementKeys.Default &&
277
+ row.deepBind?.length === 1 &&
278
+ !row.additionalTest?.length &&
279
+ !row.disabled?.length)
280
+ .map((row) => row.deepBind[0])),
281
+ ];
282
+ /**
283
+ * Secondary-editor paths, as `openingPath -> propertyPath`. Used by tests to
284
+ * assert the nested editors are actually being reached: before these were
285
+ * collected, every hint inside a record-list editor was invisible here, and
286
+ * nothing failed to say so.
287
+ */
288
+ export const SECONDARY_GUIDED_PATHS = new Map([...secondaryRowsByParent].map(([parent, inner]) => [
289
+ parent,
290
+ [...inner.keys()],
291
+ ]));
@@ -0,0 +1,155 @@
1
+ import { s } from '@hashbrownai/core';
2
+ import type { CalculatedFieldRules } from '../../interfaces/formInput/calculatedFieldRules.js';
3
+ import { CalculationFunctions } from '../../interfaces/formInput/calculationVariableInterface.js';
4
+ /**
5
+ * The `calculatedFieldRules` member: a value computed from other fields rather
6
+ * than typed by the user.
7
+ *
8
+ * ## Why this is a draft
9
+ *
10
+ * `calculationVariableInterface` requires both `id` and `inputId` on every
11
+ * variable. Those are builder-assigned: `inputId` is the `id` of another input
12
+ * in the form, stamped when that input was created, and a model has no way to
13
+ * know it — the form it is authoring does not exist yet, so the ids do not
14
+ * either. Asked for one it will invent a plausible string, and the formula then
15
+ * references a field that is not there. The failure is quiet: the form saves,
16
+ * renders, and computes nothing.
17
+ *
18
+ * So the model binds by `formControlName`, which it genuinely does author, and
19
+ * the application resolves each one to that input's `id` on expansion. Both
20
+ * names are already on the interface — `formControlName` and
21
+ * `parentInputFormControl` sit alongside `inputId` and `parentInputId` — so the
22
+ * draft is not inventing a vocabulary. It fills in the half of each pair a
23
+ * model can actually know.
24
+ *
25
+ * ## The two ways a variable binds
26
+ *
27
+ * A variable reads EITHER one field's value directly, OR an aggregate over one
28
+ * column of a repeatable list. The second needs a set of members the first has
29
+ * no use for — `function`, `parentInputFormControl`, `applyFunctionToCol` — and
30
+ * Joi accepts them all on the same flat object, so nothing in the stored shape
31
+ * says which belong together. Offered flat, a model fills in
32
+ * `applyFunctionToCol` for a plain field and `function` for something with
33
+ * nothing to aggregate.
34
+ *
35
+ * They are therefore two branches of an `anyOf`, discriminated by an explicit
36
+ * `bindingType`. That key is NOT a member of `calculationVariableInterface`;
37
+ * like `endpointId` in `members/mat-options.ts` it exists only in the draft and
38
+ * is dropped on expansion. It earns the extra expansion step because an
39
+ * undiscriminated union of two objects sharing three of their keys is exactly
40
+ * the shape a model picks wrongly.
41
+ *
42
+ * The grouping is not invented here: `schemas/customValidationSchema.ts`
43
+ * annotates the same `parentInputId` + `function` pair as "Multiple-input
44
+ * (list) bindings", which is the distinction drawn.
45
+ */
46
+ /** `roundingMode` is a string union with no runtime enum to derive from. */
47
+ export declare const ROUNDING_MODE_VALUES: readonly ["FLOOR", "CEIL", "ROUND"];
48
+ interface VariableCommonDraft {
49
+ /** The token this variable is referenced by inside `formula`. */
50
+ variable: string;
51
+ label: string;
52
+ formControlName: string;
53
+ }
54
+ export interface FieldVariableDraft extends VariableCommonDraft {
55
+ bindingType: 'field';
56
+ }
57
+ export interface ListVariableDraft extends VariableCommonDraft {
58
+ bindingType: 'listAggregate';
59
+ parentInputFormControl: string;
60
+ function: CalculationFunctions;
61
+ applyFunctionToCol: string;
62
+ applyFunctionToLabel: string | null;
63
+ filterValuesByCol: string | null;
64
+ filterValuesByThisColLabel: string | null;
65
+ tableCell: boolean | null;
66
+ }
67
+ export type CalculationVariableDraft = FieldVariableDraft | ListVariableDraft;
68
+ export interface CalculatedFieldRulesDraft {
69
+ formula: string;
70
+ variables: CalculationVariableDraft[];
71
+ decimalPlaces: number | null;
72
+ roundingMode: (typeof ROUNDING_MODE_VALUES)[number] | null;
73
+ getFormulaFromAFormInput: boolean | null;
74
+ formControlWithFormula: string | null;
75
+ }
76
+ export declare const calculationVariableDraftSchema: import("@hashbrownai/core/src/schema/base").SchemaForUnion<{
77
+ bindingType: "field";
78
+ variable: string;
79
+ label: string;
80
+ formControlName: string;
81
+ } | {
82
+ bindingType: "listAggregate";
83
+ variable: string;
84
+ label: string;
85
+ formControlName: string;
86
+ parentInputFormControl: string;
87
+ function: CalculationFunctions;
88
+ applyFunctionToCol: string;
89
+ applyFunctionToLabel: string | null;
90
+ filterValuesByCol: string | null;
91
+ filterValuesByThisColLabel: string | null;
92
+ tableCell: boolean | null;
93
+ }>;
94
+ /**
95
+ * The editor's applicability text for this row is deliberately NOT composed in.
96
+ *
97
+ * `describe(..., 'calculatedFieldRules')` resolves to "Only applies when
98
+ * isCalculatedField is true", which is correct in the settings panel and
99
+ * incoherent here: `variants.ts` encodes that rule as the shape of the union
100
+ * and drops `isCalculatedField` from the schema entirely, so the sentence would
101
+ * point a model at a member it is never shown — an invitation to invent one.
102
+ *
103
+ * This is the same judgement `internal/editor-guidance.ts` records for `label`
104
+ * and `options`: derived text earns its place by telling a model something it
105
+ * does not already have, and a rule the structure already makes unbreakable is
106
+ * not that.
107
+ */
108
+ export declare const calculatedFieldRulesDraftSchema: s.ObjectType<{
109
+ formula: s.StringType;
110
+ variables: s.ArrayType<import("@hashbrownai/core/src/schema/base").SchemaForUnion<{
111
+ bindingType: "field";
112
+ variable: string;
113
+ label: string;
114
+ formControlName: string;
115
+ } | {
116
+ bindingType: "listAggregate";
117
+ variable: string;
118
+ label: string;
119
+ formControlName: string;
120
+ parentInputFormControl: string;
121
+ function: CalculationFunctions;
122
+ applyFunctionToCol: string;
123
+ applyFunctionToLabel: string | null;
124
+ filterValuesByCol: string | null;
125
+ filterValuesByThisColLabel: string | null;
126
+ tableCell: boolean | null;
127
+ }>>;
128
+ decimalPlaces: import("@hashbrownai/core/src/schema/base").SchemaForUnion<number | null>;
129
+ roundingMode: import("@hashbrownai/core/src/schema/base").SchemaForUnion<"FLOOR" | "CEIL" | "ROUND" | null>;
130
+ getFormulaFromAFormInput: import("@hashbrownai/core/src/schema/base").SchemaForUnion<boolean | null>;
131
+ formControlWithFormula: import("@hashbrownai/core/src/schema/base").SchemaForUnion<string | null>;
132
+ }>;
133
+ /**
134
+ * The draft is checked against its declared shape, not against
135
+ * `CalculatedFieldRules`: it deliberately differs, carrying `bindingType` and
136
+ * omitting the two identifiers the application supplies. What the compiler can
137
+ * still hold is that the node produces the draft it claims to.
138
+ */
139
+ export type _NoCalculatedFieldDraftDrift = [
140
+ s.Infer<typeof calculatedFieldRulesDraftSchema>
141
+ ] extends [CalculatedFieldRulesDraft] ? never : ['calculatedFieldRulesDraftSchema drifted from CalculatedFieldRulesDraft'];
142
+ /**
143
+ * The parts of the draft stored verbatim stay pinned to the real interface, so
144
+ * retyping `roundingMode` upstream surfaces here rather than in a form that
145
+ * silently rounds the wrong way.
146
+ */
147
+ export type _RoundingModeMatchesInterface = [
148
+ (typeof ROUNDING_MODE_VALUES)[number]
149
+ ] extends [NonNullable<CalculatedFieldRules['roundingMode']>] ? never : ['ROUNDING_MODE_VALUES drifted from CalculatedFieldRules'];
150
+ /**
151
+ * Members of a generated `calculatedFieldRules` the application must supply
152
+ * before Joi will accept it.
153
+ */
154
+ export declare const CALCULATED_FIELD_DRAFT_COMPLETION: readonly ["calculatedFieldRules.variables[].id", "calculatedFieldRules.variables[].inputId (resolved from formControlName)", "calculatedFieldRules.variables[].parentInputId (resolved from parentInputFormControl)", "calculatedFieldRules.variables[].bindingType (draft-only; dropped on expansion)"];
155
+ export {};
@@ -0,0 +1,105 @@
1
+ import { s } from '@hashbrownai/core';
2
+ import { CalculationFunctions } from '../../interfaces/formInput/calculationVariableInterface.js';
3
+ import { enumValues, exhaustive, optional } from '../internal/coupling.js';
4
+ /**
5
+ * The `calculatedFieldRules` member: a value computed from other fields rather
6
+ * than typed by the user.
7
+ *
8
+ * ## Why this is a draft
9
+ *
10
+ * `calculationVariableInterface` requires both `id` and `inputId` on every
11
+ * variable. Those are builder-assigned: `inputId` is the `id` of another input
12
+ * in the form, stamped when that input was created, and a model has no way to
13
+ * know it — the form it is authoring does not exist yet, so the ids do not
14
+ * either. Asked for one it will invent a plausible string, and the formula then
15
+ * references a field that is not there. The failure is quiet: the form saves,
16
+ * renders, and computes nothing.
17
+ *
18
+ * So the model binds by `formControlName`, which it genuinely does author, and
19
+ * the application resolves each one to that input's `id` on expansion. Both
20
+ * names are already on the interface — `formControlName` and
21
+ * `parentInputFormControl` sit alongside `inputId` and `parentInputId` — so the
22
+ * draft is not inventing a vocabulary. It fills in the half of each pair a
23
+ * model can actually know.
24
+ *
25
+ * ## The two ways a variable binds
26
+ *
27
+ * A variable reads EITHER one field's value directly, OR an aggregate over one
28
+ * column of a repeatable list. The second needs a set of members the first has
29
+ * no use for — `function`, `parentInputFormControl`, `applyFunctionToCol` — and
30
+ * Joi accepts them all on the same flat object, so nothing in the stored shape
31
+ * says which belong together. Offered flat, a model fills in
32
+ * `applyFunctionToCol` for a plain field and `function` for something with
33
+ * nothing to aggregate.
34
+ *
35
+ * They are therefore two branches of an `anyOf`, discriminated by an explicit
36
+ * `bindingType`. That key is NOT a member of `calculationVariableInterface`;
37
+ * like `endpointId` in `members/mat-options.ts` it exists only in the draft and
38
+ * is dropped on expansion. It earns the extra expansion step because an
39
+ * undiscriminated union of two objects sharing three of their keys is exactly
40
+ * the shape a model picks wrongly.
41
+ *
42
+ * The grouping is not invented here: `schemas/customValidationSchema.ts`
43
+ * annotates the same `parentInputId` + `function` pair as "Multiple-input
44
+ * (list) bindings", which is the distinction drawn.
45
+ */
46
+ /** `roundingMode` is a string union with no runtime enum to derive from. */
47
+ export const ROUNDING_MODE_VALUES = exhaustive()(['FLOOR', 'CEIL', 'ROUND']);
48
+ // --- schema -----------------------------------------------------------------
49
+ const variableToken = s.string('The name this variable is referenced by inside the formula. Keep it short and lower case, and spell it identically in the formula.', { pattern: '^[a-zA-Z][a-zA-Z0-9_]*$' });
50
+ const variableLabel = s.string('A human-readable name for this variable, shown when the formula is explained back to an administrator.');
51
+ const fieldVariableSchema = s.object("Reads one field's value directly.", {
52
+ bindingType: s.literal('field'),
53
+ variable: variableToken,
54
+ label: variableLabel,
55
+ formControlName: s.string('The formControlName of the field on this form supplying the value. It must be a field that actually exists in the form being authored.'),
56
+ });
57
+ const listVariableSchema = s.object('Aggregates one column across every row of a repeatable item list.', {
58
+ bindingType: s.literal('listAggregate'),
59
+ variable: variableToken,
60
+ label: variableLabel,
61
+ formControlName: s.string('The formControlName of the column being aggregated, as it is named inside the list.'),
62
+ parentInputFormControl: s.string('The formControlName of the item-list field whose rows are aggregated. It must be a multipleInput field in this form.'),
63
+ function: s.enumeration('How the column is reduced to a single value across the rows.', enumValues(CalculationFunctions)),
64
+ applyFunctionToCol: s.string('The column within each row the function is applied to.'),
65
+ applyFunctionToLabel: optional(s.string('Display label for the aggregated column.')),
66
+ filterValuesByCol: optional(s.string('Restricts the aggregate to rows matching a value in this column. Leave unset to aggregate every row.')),
67
+ filterValuesByThisColLabel: optional(s.string('Display label for the column the rows are filtered by.')),
68
+ tableCell: optional(s.boolean('Whether this variable resolves to a single table cell.')),
69
+ });
70
+ export const calculationVariableDraftSchema = s.anyOf([
71
+ fieldVariableSchema,
72
+ listVariableSchema,
73
+ ]);
74
+ /**
75
+ * The editor's applicability text for this row is deliberately NOT composed in.
76
+ *
77
+ * `describe(..., 'calculatedFieldRules')` resolves to "Only applies when
78
+ * isCalculatedField is true", which is correct in the settings panel and
79
+ * incoherent here: `variants.ts` encodes that rule as the shape of the union
80
+ * and drops `isCalculatedField` from the schema entirely, so the sentence would
81
+ * point a model at a member it is never shown — an invitation to invent one.
82
+ *
83
+ * This is the same judgement `internal/editor-guidance.ts` records for `label`
84
+ * and `options`: derived text earns its place by telling a model something it
85
+ * does not already have, and a rule the structure already makes unbreakable is
86
+ * not that.
87
+ */
88
+ export const calculatedFieldRulesDraftSchema = s.object('How this field computes its value from other fields on the form, so the user never types a figure the system can work out: a line total, VAT, an age. Whenever a total, average, minimum, maximum or count over an item list is used anywhere else — a decision gate, a validator, another formula, a list column, the next person — it must be a calculated field like this one on the form itself, aggregating the list; the roll-up shown beneath the list is for the user only.', {
89
+ formula: s.string('The arithmetic expression, written using the variable names declared below and nothing else. Every name in the formula must be declared as a variable, and every declared variable should appear in the formula.'),
90
+ variables: s.array('The fields the formula reads, one entry per variable name used in it.', calculationVariableDraftSchema, { minItems: 1 }),
91
+ decimalPlaces: optional(s.integer('How many decimal places the result is rounded to: 2 for money, 0 for a count.', { minimum: 0 })),
92
+ roundingMode: optional(s.enumeration("How the result is rounded at that precision: 'ROUND' to the nearest, which is right for money; 'FLOOR' or 'CEIL' only when the business rule says round down or up.", [...ROUNDING_MODE_VALUES])),
93
+ getFormulaFromAFormInput: optional(s.boolean('Whether the formula is read from another field on the form at runtime instead of being fixed here. Leave unset unless the form genuinely lets a user supply the formula.')),
94
+ formControlWithFormula: optional(s.string('The formControlName of the field supplying the formula. Only meaningful when the formula is read from a form input.')),
95
+ });
96
+ /**
97
+ * Members of a generated `calculatedFieldRules` the application must supply
98
+ * before Joi will accept it.
99
+ */
100
+ export const CALCULATED_FIELD_DRAFT_COMPLETION = [
101
+ 'calculatedFieldRules.variables[].id',
102
+ 'calculatedFieldRules.variables[].inputId (resolved from formControlName)',
103
+ 'calculatedFieldRules.variables[].parentInputId (resolved from parentInputFormControl)',
104
+ 'calculatedFieldRules.variables[].bindingType (draft-only; dropped on expansion)',
105
+ ];
@@ -0,0 +1,69 @@
1
+ import { s } from '@hashbrownai/core';
2
+ import type { ConditionalInputRule } from '../../interfaces/formInput/ConditionalInputRule.js';
3
+ import { type ObservedInputDraft } from './validators.js';
4
+ /**
5
+ * The `conditionalInputConfig` member: when this control is shown at all.
6
+ *
7
+ * ## Only half of the interface is offered
8
+ *
9
+ * `ConditionalInputRule` carries two shapes at once. The `expression` half is
10
+ * read by the engine; `formControlName`, `label`, `testType` and `valueMatch`
11
+ * are a legacy descriptive half, marked `@deprecated` on every member and never
12
+ * read by the runtime. Only the expression half is modelled, for the reason
13
+ * `DEPRECATED_INPUT_MEMBERS` gives in `input-members.ts`: a deprecated property
14
+ * left in a generation schema comes back on every form the model writes, and
15
+ * these four would come back describing a rule that does nothing.
16
+ *
17
+ * The consequence is worth being explicit about, because it is the one case
18
+ * here where narrowing changes behaviour rather than just shape: a rule with no
19
+ * `expression` is inert. Since the deprecated members are the only thing a rule
20
+ * could otherwise carry, every rule this schema produces is an active one.
21
+ *
22
+ * ## Polarity and combination come from the interface
23
+ *
24
+ * `ConditionalInputRule`'s own documentation states two things a model cannot
25
+ * infer from the types and gets wrong by default:
26
+ *
27
+ * - `true` means the input is REVEALED — the opposite polarity to a custom
28
+ * validator's expression, where `true` means invalid. The two share a DSL and
29
+ * are trivially confused.
30
+ * - Multiple rules combine with AND, and an empty list means always visible.
31
+ *
32
+ * Both are restated in the descriptions rather than paraphrased loosely, on the
33
+ * same principle that `internal/editor-guidance.ts` harvests the builder's
34
+ * hints instead of rewriting them.
35
+ *
36
+ * ## Why this is a draft
37
+ *
38
+ * `id` is a builder-assigned rule identifier, and the observed inputs bind by
39
+ * `inputId`. Both are handled exactly as in `members/validators.ts` — the model
40
+ * binds by `formControlName` and the application resolves it.
41
+ */
42
+ export interface ConditionalRuleDraft {
43
+ expression: string;
44
+ inputsObservedForChanges: ObservedInputDraft[];
45
+ }
46
+ export declare const conditionalInputConfigDraftSchema: s.ArrayType<s.ObjectType<{
47
+ expression: s.StringType;
48
+ inputsObservedForChanges: s.ArrayType<s.ObjectType<{
49
+ formControlName: s.StringType;
50
+ variable: s.StringType;
51
+ parentInputFormControl: import("@hashbrownai/core/src/schema/base").SchemaForUnion<string | null>;
52
+ function: import("@hashbrownai/core/src/schema/base").SchemaForUnion<import("../../index.js").CalculationFunctions | null>;
53
+ }>>;
54
+ }>>;
55
+ /** The node produces the draft it declares. */
56
+ export type _NoConditionalDraftDrift = [
57
+ s.Infer<typeof conditionalInputConfigDraftSchema>
58
+ ] extends [ConditionalRuleDraft[]] ? never : ['conditionalInputConfigDraftSchema drifted from ConditionalRuleDraft[]'];
59
+ /**
60
+ * `expression` is carried through expansion untouched, so it stays pinned to
61
+ * the stored interface.
62
+ */
63
+ export type _ExpressionMatchesStoredShape = [
64
+ ConditionalRuleDraft['expression']
65
+ ] extends [NonNullable<ConditionalInputRule['expression']>] ? never : ['ConditionalRuleDraft.expression drifted from ConditionalInputRule'];
66
+ /**
67
+ * Members of a generated rule the application must supply.
68
+ */
69
+ export declare const CONDITIONAL_DRAFT_COMPLETION: readonly ["conditionalInputConfig[].id", "conditionalInputConfig[].inputsObservedForChanges[].inputId (resolved from formControlName)", "conditionalInputConfig[].inputsObservedForChanges[].parentInputId (resolved from parentInputFormControl)"];
@@ -0,0 +1,16 @@
1
+ import { s } from '@hashbrownai/core';
2
+ import { describe } from '../internal/editor-guidance.js';
3
+ import { observedInputSchema } from './validators.js';
4
+ const conditionalRuleSchema = s.object('One rule deciding whether this control is shown.', {
5
+ expression: s.string("An expression in the form engine's comparison language — NOT JavaScript — that is TRUE when this control should be SHOWN. Like a validator expression, it is true when the condition it names holds — here that condition is the reason to reveal the field. The grammar is a comparison `variable OP value`, with OP one of ===, !==, ==, !=, >, <, >=, <=, or a keyword operator includes, in, startsWith, endsWith or matches; a bare variable tests truthiness; ! negates; comparisons combine with && and || and group with parentheses; a variable may be a dotted path (reason.length > 15). There is no arithmetic, no function or method call and no ternary: to compare against a computed figure, make the figure a calculated field and compare against that field. A date is compared as an ISO string. Reference other fields only by the variable names declared below, compare against the stored option value rather than its label (urgency === 'urgent'), and put quotes around every string literal. For 'either of two conditions' write one rule with || inside it."),
6
+ inputsObservedForChanges: s.array('The fields the expression reads, one entry per variable name used in it.', observedInputSchema, { minItems: 1 }),
7
+ });
8
+ export const conditionalInputConfigDraftSchema = s.array(describe('Rules controlling when this control is visible: a follow-up question, an exception, a branch, a rejection reason when a decision toggle is off. Every rule must hold for it to be shown; leave empty to show it always. Never hide a field the engine reads.', 'conditionalInputConfig'), conditionalRuleSchema);
9
+ /**
10
+ * Members of a generated rule the application must supply.
11
+ */
12
+ export const CONDITIONAL_DRAFT_COMPLETION = [
13
+ 'conditionalInputConfig[].id',
14
+ 'conditionalInputConfig[].inputsObservedForChanges[].inputId (resolved from formControlName)',
15
+ 'conditionalInputConfig[].inputsObservedForChanges[].parentInputId (resolved from parentInputFormControl)',
16
+ ];