@jxsuite/formulas 0.0.1

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/README.md ADDED
@@ -0,0 +1,58 @@
1
+ # @jxsuite/formulas
2
+
3
+ Composite **pure formulas** for Jx, authored as declarative `$expression` JSON
4
+ (spec §19.4c). Each formula is a named, parameterized computation — `clamp`,
5
+ `sum`, `average`, `truncate`, `initials`, … — whose body is built exclusively
6
+ from blessed operators, pure standard-library method operators (§19.4d), and
7
+ blessed globals. No JavaScript source anywhere: every formula is inspectable
8
+ data that the visual editor can open, badge with live values, and recompose.
9
+
10
+ ## Consumption model: copy-in, not a project dependency
11
+
12
+ This package is a **studio/tooling dependency**, deliberately _not_ a runtime
13
+ or project dependency. Jx documents stay self-contained plain JSON:
14
+
15
+ - **In Jx Studio** — the formula catalog lists these formulas alongside
16
+ operators and blessed globals. Picking one **copies its state entry into the
17
+ document** (vendoring the JSON) and inserts a `call` node referencing it.
18
+ From that moment the project owns the formula: no package resolution at
19
+ build or run time, no version coupling, and the user can edit their copy
20
+ like any other named formula.
21
+ - **Programmatically** — `formulaEntries()` returns the catalog as
22
+ ready-to-merge `state` entries for tooling that seeds documents or
23
+ `project.json` state (starters, importers, AI agents):
24
+
25
+ ```ts
26
+ import { catalog, formulaEntries } from "@jxsuite/formulas";
27
+
28
+ const state = { ...formulaEntries() };
29
+ // → { clamp: { $expression: …, parameters: […] }, sum: …, … }
30
+ ```
31
+
32
+ Because consumption is copy-in, publishing a new package version never changes
33
+ existing projects — they upgrade a formula by re-inserting it.
34
+
35
+ ## Authoring a formula
36
+
37
+ One folder per formula under `formulas/`:
38
+
39
+ ```
40
+ formulas/<name>/formula.json # metadata: name, description, parameters (CEM convention)
41
+ formulas/<name>/expression.json # the pure $expression body over $args/<name> refs
42
+ ```
43
+
44
+ Then regenerate the catalog module:
45
+
46
+ ```sh
47
+ bun run generate
48
+ ```
49
+
50
+ The generator (`bin/generate.ts`) emits `src/catalog.ts` and **fails the build
51
+ on anything impure**: unknown or mutating operators, or `call` targets outside
52
+ the blessed pure globals (`window#/Math/…`, `window#/JSON/…`, …). Descriptions
53
+ are mandatory — they double as editor tooltips and the LLM contract.
54
+
55
+ ## Releases
56
+
57
+ Versioned independently via release-please (`formulas` component); published to
58
+ npm with provenance by the repo's publish workflow when a release is cut.
@@ -0,0 +1,19 @@
1
+ {
2
+ "operator": "?:",
3
+ "target": { "$ref": "$args/values/length" },
4
+ "value": {
5
+ "operator": "/",
6
+ "target": {
7
+ "operator": "reduce",
8
+ "target": { "$ref": "$args/values" },
9
+ "initial": 0,
10
+ "value": {
11
+ "operator": "+",
12
+ "target": { "$ref": "$reduce/acc" },
13
+ "value": { "$ref": "$map/item" }
14
+ }
15
+ },
16
+ "value": { "$ref": "$args/values/length" }
17
+ },
18
+ "initial": 0
19
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "average",
3
+ "description": "Arithmetic mean of an array of numbers; 0 for an empty array.",
4
+ "parameters": [{ "name": "values", "type": { "text": "number[]" } }]
5
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "operator": "+",
3
+ "target": {
4
+ "operator": "toUpperCase",
5
+ "target": { "operator": "charAt", "target": { "$ref": "$args/text" }, "value": 0 }
6
+ },
7
+ "value": { "operator": "slice", "target": { "$ref": "$args/text" }, "value": 1 }
8
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "capitalize",
3
+ "description": "Uppercase the first character of a string, leaving the rest unchanged (the CSS text-transform: capitalize shape for a single word).",
4
+ "parameters": [{ "name": "text", "type": { "text": "string" } }]
5
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "operator": "call",
3
+ "target": { "$ref": "window#/Math/min" },
4
+ "value": [
5
+ {
6
+ "operator": "call",
7
+ "target": { "$ref": "window#/Math/max" },
8
+ "value": [{ "$ref": "$args/value" }, { "$ref": "$args/min" }]
9
+ },
10
+ { "$ref": "$args/max" }
11
+ ]
12
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "clamp",
3
+ "description": "Bound a number to the [min, max] range (the TC39 Math.clamp shape): returns min when below, max when above, the value itself otherwise.",
4
+ "parameters": [
5
+ { "name": "value", "type": { "text": "number" } },
6
+ { "name": "min", "type": { "text": "number" } },
7
+ { "name": "max", "type": { "text": "number" } }
8
+ ]
9
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "operator": "filter",
3
+ "target": { "$ref": "$args/values" },
4
+ "value": { "operator": "!", "target": { "operator": "!", "target": { "$ref": "$map/item" } } }
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "compact",
3
+ "description": "A copy of an array with the falsy elements removed (Array.prototype.filter on truthiness).",
4
+ "parameters": [{ "name": "values", "type": { "text": "unknown[]" } }]
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "operator": "??",
3
+ "target": { "$ref": "$args/values/length" },
4
+ "value": 0
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "count",
3
+ "description": "The length of an array or string, or 0 when the value is missing.",
4
+ "parameters": [{ "name": "values", "type": { "text": "unknown[] | string" } }]
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "operator": "??",
3
+ "target": { "$ref": "$args/values/0" },
4
+ "value": null
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "first",
3
+ "description": "The first element of an array (index 0), or null when empty.",
4
+ "parameters": [{ "name": "values", "type": { "text": "unknown[]" } }]
5
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "operator": "join",
3
+ "target": {
4
+ "operator": "map",
5
+ "target": { "operator": "split", "target": { "$ref": "$args/name" }, "value": " " },
6
+ "value": {
7
+ "operator": "toUpperCase",
8
+ "target": { "operator": "charAt", "target": { "$ref": "$map/item" }, "value": 0 }
9
+ }
10
+ },
11
+ "value": ""
12
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "initials",
3
+ "description": "The uppercase first letters of each whitespace-separated word, joined (e.g. \"ada lovelace\" → \"AL\").",
4
+ "parameters": [{ "name": "name", "type": { "text": "string" } }]
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "operator": "===",
3
+ "target": { "operator": "??", "target": { "$ref": "$args/value/length" }, "value": 0 },
4
+ "value": 0
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "isEmpty",
3
+ "description": "True when an array or string has no elements (length 0 — a missing value counts as empty).",
4
+ "parameters": [{ "name": "value", "type": { "text": "unknown[] | string" } }]
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "operator": "at",
3
+ "target": { "$ref": "$args/values" },
4
+ "value": -1
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "last",
3
+ "description": "The last element of an array (Array.prototype.at(-1)), or undefined when empty.",
4
+ "parameters": [{ "name": "values", "type": { "text": "unknown[]" } }]
5
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "operator": "reduce",
3
+ "target": { "$ref": "$args/values" },
4
+ "initial": { "$ref": "$args/values/0" },
5
+ "value": {
6
+ "operator": "call",
7
+ "target": { "$ref": "window#/Math/max" },
8
+ "value": [{ "$ref": "$reduce/acc" }, { "$ref": "$map/item" }]
9
+ }
10
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "max",
3
+ "description": "The largest number in an array (Math.max folded over the values); undefined for an empty array.",
4
+ "parameters": [{ "name": "values", "type": { "text": "number[]" } }]
5
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "operator": "reduce",
3
+ "target": { "$ref": "$args/values" },
4
+ "initial": { "$ref": "$args/values/0" },
5
+ "value": {
6
+ "operator": "call",
7
+ "target": { "$ref": "window#/Math/min" },
8
+ "value": [{ "$ref": "$reduce/acc" }, { "$ref": "$map/item" }]
9
+ }
10
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "min",
3
+ "description": "The smallest number in an array (Math.min folded over the values); undefined for an empty array.",
4
+ "parameters": [{ "name": "values", "type": { "text": "number[]" } }]
5
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "operator": "?:",
3
+ "target": { "$ref": "$args/whole" },
4
+ "value": {
5
+ "operator": "*",
6
+ "target": {
7
+ "operator": "/",
8
+ "target": { "$ref": "$args/part" },
9
+ "value": { "$ref": "$args/whole" }
10
+ },
11
+ "value": 100
12
+ },
13
+ "initial": 0
14
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "percent",
3
+ "description": "part / whole as a percentage (0 when the whole is 0 or missing).",
4
+ "parameters": [
5
+ { "name": "part", "type": { "text": "number" } },
6
+ { "name": "whole", "type": { "text": "number" } }
7
+ ]
8
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "operator": "/",
3
+ "target": {
4
+ "operator": "call",
5
+ "target": { "$ref": "window#/Math/round" },
6
+ "value": [
7
+ {
8
+ "operator": "*",
9
+ "target": { "$ref": "$args/value" },
10
+ "value": {
11
+ "operator": "call",
12
+ "target": { "$ref": "window#/Math/pow" },
13
+ "value": [10, { "$ref": "$args/digits" }]
14
+ }
15
+ }
16
+ ]
17
+ },
18
+ "value": {
19
+ "operator": "call",
20
+ "target": { "$ref": "window#/Math/pow" },
21
+ "value": [10, { "$ref": "$args/digits" }]
22
+ }
23
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "roundTo",
3
+ "description": "Round a number to the given decimal places (Math.round against a Math.pow(10, digits) scale).",
4
+ "parameters": [
5
+ { "name": "value", "type": { "text": "number" } },
6
+ { "name": "digits", "type": { "text": "number" }, "default": 0 }
7
+ ]
8
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "operator": "reduce",
3
+ "target": { "$ref": "$args/values" },
4
+ "initial": 0,
5
+ "value": {
6
+ "operator": "+",
7
+ "target": { "$ref": "$reduce/acc" },
8
+ "value": { "$ref": "$map/item" }
9
+ }
10
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "sum",
3
+ "description": "Add every number in an array (Array.prototype.reduce with +, seeded at 0).",
4
+ "parameters": [{ "name": "values", "type": { "text": "number[]" } }]
5
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "operator": "?:",
3
+ "target": {
4
+ "operator": ">",
5
+ "target": { "$ref": "$args/text/length" },
6
+ "value": { "$ref": "$args/length" }
7
+ },
8
+ "value": {
9
+ "operator": "+",
10
+ "target": {
11
+ "operator": "slice",
12
+ "target": { "$ref": "$args/text" },
13
+ "value": [0, { "$ref": "$args/length" }]
14
+ },
15
+ "value": "…"
16
+ },
17
+ "initial": { "$ref": "$args/text" }
18
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "truncate",
3
+ "description": "Shorten a string to at most the given length, appending a horizontal ellipsis when cut.",
4
+ "parameters": [
5
+ { "name": "text", "type": { "text": "string" } },
6
+ { "name": "length", "type": { "text": "number" } }
7
+ ]
8
+ }
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@jxsuite/formulas",
3
+ "version": "0.0.1",
4
+ "description": "Composite pure formulas for Jx, authored as declarative $expression JSON",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/jxsuite/jx.git",
9
+ "directory": "packages/formulas"
10
+ },
11
+ "files": [
12
+ "src/",
13
+ "formulas/"
14
+ ],
15
+ "type": "module",
16
+ "exports": {
17
+ ".": "./src/index.ts",
18
+ "./catalog": "./src/catalog.ts"
19
+ },
20
+ "publishConfig": {
21
+ "provenance": false
22
+ },
23
+ "scripts": {
24
+ "generate": "bun run bin/generate.ts",
25
+ "test": "bun test --isolate"
26
+ },
27
+ "dependencies": {
28
+ "@jxsuite/schema": "workspace:^"
29
+ },
30
+ "devDependencies": {
31
+ "@jxsuite/runtime": "workspace:^"
32
+ }
33
+ }
package/src/catalog.ts ADDED
@@ -0,0 +1,537 @@
1
+ /**
2
+ * GENERATED by bin/generate.ts — do not edit. Regenerate with `bun run generate`.
3
+ *
4
+ * Every entry is a named formula (spec §19.4c): a pure $expression body over $args/<name>
5
+ * parameters, verified against the blessed operator and global sets at generation time.
6
+ */
7
+
8
+ import type { CemParameter, JxExpressionNode } from "@jxsuite/schema/types";
9
+
10
+ export interface FormulaCatalogEntry {
11
+ name: string;
12
+ description: string;
13
+ parameters: (string | CemParameter)[];
14
+ expression: JxExpressionNode;
15
+ }
16
+
17
+ export const catalog: FormulaCatalogEntry[] = [
18
+ {
19
+ description: "Arithmetic mean of an array of numbers; 0 for an empty array.",
20
+ expression: {
21
+ operator: "?:",
22
+ target: {
23
+ $ref: "$args/values/length",
24
+ },
25
+ value: {
26
+ operator: "/",
27
+ target: {
28
+ operator: "reduce",
29
+ target: {
30
+ $ref: "$args/values",
31
+ },
32
+ initial: 0,
33
+ value: {
34
+ operator: "+",
35
+ target: {
36
+ $ref: "$reduce/acc",
37
+ },
38
+ value: {
39
+ $ref: "$map/item",
40
+ },
41
+ },
42
+ },
43
+ value: {
44
+ $ref: "$args/values/length",
45
+ },
46
+ },
47
+ initial: 0,
48
+ },
49
+ name: "average",
50
+ parameters: [
51
+ {
52
+ name: "values",
53
+ type: {
54
+ text: "number[]",
55
+ },
56
+ },
57
+ ],
58
+ },
59
+ {
60
+ description:
61
+ "Uppercase the first character of a string, leaving the rest unchanged (the CSS text-transform: capitalize shape for a single word).",
62
+ expression: {
63
+ operator: "+",
64
+ target: {
65
+ operator: "toUpperCase",
66
+ target: {
67
+ operator: "charAt",
68
+ target: {
69
+ $ref: "$args/text",
70
+ },
71
+ value: 0,
72
+ },
73
+ },
74
+ value: {
75
+ operator: "slice",
76
+ target: {
77
+ $ref: "$args/text",
78
+ },
79
+ value: 1,
80
+ },
81
+ },
82
+ name: "capitalize",
83
+ parameters: [
84
+ {
85
+ name: "text",
86
+ type: {
87
+ text: "string",
88
+ },
89
+ },
90
+ ],
91
+ },
92
+ {
93
+ description:
94
+ "Bound a number to the [min, max] range (the TC39 Math.clamp shape): returns min when below, max when above, the value itself otherwise.",
95
+ expression: {
96
+ operator: "call",
97
+ target: {
98
+ $ref: "window#/Math/min",
99
+ },
100
+ value: [
101
+ {
102
+ operator: "call",
103
+ target: {
104
+ $ref: "window#/Math/max",
105
+ },
106
+ value: [
107
+ {
108
+ $ref: "$args/value",
109
+ },
110
+ {
111
+ $ref: "$args/min",
112
+ },
113
+ ],
114
+ },
115
+ {
116
+ $ref: "$args/max",
117
+ },
118
+ ],
119
+ },
120
+ name: "clamp",
121
+ parameters: [
122
+ {
123
+ name: "value",
124
+ type: {
125
+ text: "number",
126
+ },
127
+ },
128
+ {
129
+ name: "min",
130
+ type: {
131
+ text: "number",
132
+ },
133
+ },
134
+ {
135
+ name: "max",
136
+ type: {
137
+ text: "number",
138
+ },
139
+ },
140
+ ],
141
+ },
142
+ {
143
+ description:
144
+ "A copy of an array with the falsy elements removed (Array.prototype.filter on truthiness).",
145
+ expression: {
146
+ operator: "filter",
147
+ target: {
148
+ $ref: "$args/values",
149
+ },
150
+ value: {
151
+ operator: "!",
152
+ target: {
153
+ operator: "!",
154
+ target: {
155
+ $ref: "$map/item",
156
+ },
157
+ },
158
+ },
159
+ },
160
+ name: "compact",
161
+ parameters: [
162
+ {
163
+ name: "values",
164
+ type: {
165
+ text: "unknown[]",
166
+ },
167
+ },
168
+ ],
169
+ },
170
+ {
171
+ description: "The length of an array or string, or 0 when the value is missing.",
172
+ expression: {
173
+ operator: "??",
174
+ target: {
175
+ $ref: "$args/values/length",
176
+ },
177
+ value: 0,
178
+ },
179
+ name: "count",
180
+ parameters: [
181
+ {
182
+ name: "values",
183
+ type: {
184
+ text: "unknown[] | string",
185
+ },
186
+ },
187
+ ],
188
+ },
189
+ {
190
+ description: "The first element of an array (index 0), or null when empty.",
191
+ expression: {
192
+ operator: "??",
193
+ target: {
194
+ $ref: "$args/values/0",
195
+ },
196
+ value: null,
197
+ },
198
+ name: "first",
199
+ parameters: [
200
+ {
201
+ name: "values",
202
+ type: {
203
+ text: "unknown[]",
204
+ },
205
+ },
206
+ ],
207
+ },
208
+ {
209
+ description:
210
+ 'The uppercase first letters of each whitespace-separated word, joined (e.g. "ada lovelace" → "AL").',
211
+ expression: {
212
+ operator: "join",
213
+ target: {
214
+ operator: "map",
215
+ target: {
216
+ operator: "split",
217
+ target: {
218
+ $ref: "$args/name",
219
+ },
220
+ value: " ",
221
+ },
222
+ value: {
223
+ operator: "toUpperCase",
224
+ target: {
225
+ operator: "charAt",
226
+ target: {
227
+ $ref: "$map/item",
228
+ },
229
+ value: 0,
230
+ },
231
+ },
232
+ },
233
+ value: "",
234
+ },
235
+ name: "initials",
236
+ parameters: [
237
+ {
238
+ name: "name",
239
+ type: {
240
+ text: "string",
241
+ },
242
+ },
243
+ ],
244
+ },
245
+ {
246
+ description:
247
+ "True when an array or string has no elements (length 0 — a missing value counts as empty).",
248
+ expression: {
249
+ operator: "===",
250
+ target: {
251
+ operator: "??",
252
+ target: {
253
+ $ref: "$args/value/length",
254
+ },
255
+ value: 0,
256
+ },
257
+ value: 0,
258
+ },
259
+ name: "isEmpty",
260
+ parameters: [
261
+ {
262
+ name: "value",
263
+ type: {
264
+ text: "unknown[] | string",
265
+ },
266
+ },
267
+ ],
268
+ },
269
+ {
270
+ description: "The last element of an array (Array.prototype.at(-1)), or undefined when empty.",
271
+ expression: {
272
+ operator: "at",
273
+ target: {
274
+ $ref: "$args/values",
275
+ },
276
+ value: -1,
277
+ },
278
+ name: "last",
279
+ parameters: [
280
+ {
281
+ name: "values",
282
+ type: {
283
+ text: "unknown[]",
284
+ },
285
+ },
286
+ ],
287
+ },
288
+ {
289
+ description:
290
+ "The largest number in an array (Math.max folded over the values); undefined for an empty array.",
291
+ expression: {
292
+ operator: "reduce",
293
+ target: {
294
+ $ref: "$args/values",
295
+ },
296
+ initial: {
297
+ $ref: "$args/values/0",
298
+ },
299
+ value: {
300
+ operator: "call",
301
+ target: {
302
+ $ref: "window#/Math/max",
303
+ },
304
+ value: [
305
+ {
306
+ $ref: "$reduce/acc",
307
+ },
308
+ {
309
+ $ref: "$map/item",
310
+ },
311
+ ],
312
+ },
313
+ },
314
+ name: "max",
315
+ parameters: [
316
+ {
317
+ name: "values",
318
+ type: {
319
+ text: "number[]",
320
+ },
321
+ },
322
+ ],
323
+ },
324
+ {
325
+ description:
326
+ "The smallest number in an array (Math.min folded over the values); undefined for an empty array.",
327
+ expression: {
328
+ operator: "reduce",
329
+ target: {
330
+ $ref: "$args/values",
331
+ },
332
+ initial: {
333
+ $ref: "$args/values/0",
334
+ },
335
+ value: {
336
+ operator: "call",
337
+ target: {
338
+ $ref: "window#/Math/min",
339
+ },
340
+ value: [
341
+ {
342
+ $ref: "$reduce/acc",
343
+ },
344
+ {
345
+ $ref: "$map/item",
346
+ },
347
+ ],
348
+ },
349
+ },
350
+ name: "min",
351
+ parameters: [
352
+ {
353
+ name: "values",
354
+ type: {
355
+ text: "number[]",
356
+ },
357
+ },
358
+ ],
359
+ },
360
+ {
361
+ description: "part / whole as a percentage (0 when the whole is 0 or missing).",
362
+ expression: {
363
+ operator: "?:",
364
+ target: {
365
+ $ref: "$args/whole",
366
+ },
367
+ value: {
368
+ operator: "*",
369
+ target: {
370
+ operator: "/",
371
+ target: {
372
+ $ref: "$args/part",
373
+ },
374
+ value: {
375
+ $ref: "$args/whole",
376
+ },
377
+ },
378
+ value: 100,
379
+ },
380
+ initial: 0,
381
+ },
382
+ name: "percent",
383
+ parameters: [
384
+ {
385
+ name: "part",
386
+ type: {
387
+ text: "number",
388
+ },
389
+ },
390
+ {
391
+ name: "whole",
392
+ type: {
393
+ text: "number",
394
+ },
395
+ },
396
+ ],
397
+ },
398
+ {
399
+ description:
400
+ "Round a number to the given decimal places (Math.round against a Math.pow(10, digits) scale).",
401
+ expression: {
402
+ operator: "/",
403
+ target: {
404
+ operator: "call",
405
+ target: {
406
+ $ref: "window#/Math/round",
407
+ },
408
+ value: [
409
+ {
410
+ operator: "*",
411
+ target: {
412
+ $ref: "$args/value",
413
+ },
414
+ value: {
415
+ operator: "call",
416
+ target: {
417
+ $ref: "window#/Math/pow",
418
+ },
419
+ value: [
420
+ 10,
421
+ {
422
+ $ref: "$args/digits",
423
+ },
424
+ ],
425
+ },
426
+ },
427
+ ],
428
+ },
429
+ value: {
430
+ operator: "call",
431
+ target: {
432
+ $ref: "window#/Math/pow",
433
+ },
434
+ value: [
435
+ 10,
436
+ {
437
+ $ref: "$args/digits",
438
+ },
439
+ ],
440
+ },
441
+ },
442
+ name: "roundTo",
443
+ parameters: [
444
+ {
445
+ name: "value",
446
+ type: {
447
+ text: "number",
448
+ },
449
+ },
450
+ {
451
+ name: "digits",
452
+ type: {
453
+ text: "number",
454
+ },
455
+ default: 0,
456
+ },
457
+ ],
458
+ },
459
+ {
460
+ description: "Add every number in an array (Array.prototype.reduce with +, seeded at 0).",
461
+ expression: {
462
+ operator: "reduce",
463
+ target: {
464
+ $ref: "$args/values",
465
+ },
466
+ initial: 0,
467
+ value: {
468
+ operator: "+",
469
+ target: {
470
+ $ref: "$reduce/acc",
471
+ },
472
+ value: {
473
+ $ref: "$map/item",
474
+ },
475
+ },
476
+ },
477
+ name: "sum",
478
+ parameters: [
479
+ {
480
+ name: "values",
481
+ type: {
482
+ text: "number[]",
483
+ },
484
+ },
485
+ ],
486
+ },
487
+ {
488
+ description:
489
+ "Shorten a string to at most the given length, appending a horizontal ellipsis when cut.",
490
+ expression: {
491
+ operator: "?:",
492
+ target: {
493
+ operator: ">",
494
+ target: {
495
+ $ref: "$args/text/length",
496
+ },
497
+ value: {
498
+ $ref: "$args/length",
499
+ },
500
+ },
501
+ value: {
502
+ operator: "+",
503
+ target: {
504
+ operator: "slice",
505
+ target: {
506
+ $ref: "$args/text",
507
+ },
508
+ value: [
509
+ 0,
510
+ {
511
+ $ref: "$args/length",
512
+ },
513
+ ],
514
+ },
515
+ value: "…",
516
+ },
517
+ initial: {
518
+ $ref: "$args/text",
519
+ },
520
+ },
521
+ name: "truncate",
522
+ parameters: [
523
+ {
524
+ name: "text",
525
+ type: {
526
+ text: "string",
527
+ },
528
+ },
529
+ {
530
+ name: "length",
531
+ type: {
532
+ text: "number",
533
+ },
534
+ },
535
+ ],
536
+ },
537
+ ] as unknown as FormulaCatalogEntry[];
package/src/index.ts ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The `jxsuite formulas` package — composite pure formulas, authored as declarative $expression
3
+ * JSON.
4
+ *
5
+ * Each catalog entry is a named formula (spec §19.4c): a pure body over $args/<name> parameters
6
+ * built from blessed operators, pure standard-library methods (§19.4d), and blessed globals.
7
+ * `formulaEntries()` returns them as ready-to-merge `state` entries — drop them into a document or
8
+ * project `state` and invoke via the `call` operator.
9
+ */
10
+
11
+ import { catalog } from "./catalog.ts";
12
+
13
+ import type { JxExpressionDef } from "@jxsuite/schema/types";
14
+
15
+ export { catalog } from "./catalog.ts";
16
+ export type { FormulaCatalogEntry } from "./catalog.ts";
17
+
18
+ /** The catalog as named-formula state entries, keyed by formula name. */
19
+ export function formulaEntries(): Record<string, JxExpressionDef> {
20
+ const entries: Record<string, JxExpressionDef> = {};
21
+ for (const formula of catalog) {
22
+ entries[formula.name] = {
23
+ $description: formula.description,
24
+ $expression: formula.expression,
25
+ parameters: formula.parameters,
26
+ };
27
+ }
28
+ return entries;
29
+ }