@jarenjs/linq 0.49.2 → 0.56.0

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 (77) hide show
  1. package/ARCHITECTURE.md +217 -0
  2. package/README.md +559 -17
  3. package/docs/APP-PEN.md +1143 -0
  4. package/docs/CONTRACT-PEN.md +1217 -0
  5. package/docs/DB-CLIENT.md +814 -0
  6. package/docs/FLOW-PEN.md +1026 -0
  7. package/docs/FORMS-PEN.md +940 -0
  8. package/docs/JSLT-PEN.md +955 -0
  9. package/docs/LINQ-FORMAT.md +771 -383
  10. package/docs/MIGRATION-PEN.md +781 -0
  11. package/docs/MODEL-PEN.md +1083 -0
  12. package/docs/QUERY-PEN.md +1636 -0
  13. package/docs/SCHEMA-PEN.md +1218 -0
  14. package/package.json +57 -4
  15. package/src/app/action.js +255 -0
  16. package/src/app/capture.js +63 -0
  17. package/src/app/define.js +260 -0
  18. package/src/app/index.js +20 -0
  19. package/src/app/patch.js +277 -0
  20. package/src/app/sub.js +106 -0
  21. package/src/async.js +329 -75
  22. package/src/capture-root.js +82 -0
  23. package/src/concurrency.js +9 -4
  24. package/src/contract/define.js +269 -0
  25. package/src/contract/http.js +247 -0
  26. package/src/contract/index.js +23 -0
  27. package/src/contract/operation.js +342 -0
  28. package/src/db/handle.js +86 -0
  29. package/src/db/include.js +316 -0
  30. package/src/db/index.js +19 -0
  31. package/src/db/live.js +43 -0
  32. package/src/db/membership.js +37 -0
  33. package/src/db/open.js +82 -0
  34. package/src/document.js +143 -13
  35. package/src/effect.js +65 -0
  36. package/src/errors.js +69 -6
  37. package/src/expression.js +437 -36
  38. package/src/flow/capture.js +33 -0
  39. package/src/flow/dag.js +302 -0
  40. package/src/flow/fsm.js +328 -0
  41. package/src/flow/index.js +22 -0
  42. package/src/forms/index.js +43 -0
  43. package/src/forms/rules.js +170 -0
  44. package/src/forms/submit.js +177 -0
  45. package/src/index.js +4 -2
  46. package/src/jslt/body.js +226 -0
  47. package/src/jslt/index.js +18 -0
  48. package/src/jslt/rules.js +207 -0
  49. package/src/json-boundary.js +90 -0
  50. package/src/migration/define.js +323 -0
  51. package/src/migration/index.js +15 -0
  52. package/src/migration/steps.js +248 -0
  53. package/src/model/collection.js +171 -0
  54. package/src/model/define.js +125 -0
  55. package/src/model/entity.js +307 -0
  56. package/src/model/index.js +47 -0
  57. package/src/model/relation.js +85 -0
  58. package/src/provider.js +137 -20
  59. package/src/schema/brand.js +31 -0
  60. package/src/schema/builders.js +526 -0
  61. package/src/schema/check.js +29 -0
  62. package/src/schema/emit.js +394 -0
  63. package/src/schema/factories.js +239 -0
  64. package/src/schema/index.js +37 -0
  65. package/src/schema-of.js +24 -0
  66. package/src/sequence.js +233 -103
  67. package/src/sources.js +10 -3
  68. package/types/app.d.ts +293 -0
  69. package/types/contract.d.ts +371 -0
  70. package/types/db.d.ts +188 -0
  71. package/types/flow.d.ts +285 -0
  72. package/types/forms.d.ts +253 -0
  73. package/types/index.d.ts +231 -26
  74. package/types/jslt.d.ts +193 -0
  75. package/types/migration.d.ts +201 -0
  76. package/types/model.d.ts +493 -0
  77. package/types/schema.d.ts +494 -0
@@ -0,0 +1,302 @@
1
+ //@ts-check
2
+ /**
3
+ * @file `defineDag()` — one `jaren-dag` 0.1 document (FLOW-FORMAT.md
4
+ * §6), deep-frozen, that `compileDag` takes unchanged. `input()`,
5
+ * `constant()`, `query()`, `jslt()`, `task()` and `output()` are the
6
+ * closed kind vocabulary §6 fixes — one pen method per kind, so a kind
7
+ * the format does not have cannot be spelled — and `edge()` is the
8
+ * wiring. `.checkpoint()` writes §7.6's opt-in `checkpoint: true`.
9
+ *
10
+ * A `query` node's document, a `task`'s props and an edge's `select`
11
+ * are captured over the node's input scope (§6.1) at `$`; a `jslt`
12
+ * node's stylesheet is the JSLT pen's document, or one written by hand.
13
+ * Node ids are literal types, so an edge from a node nothing declares
14
+ * is a compile error; at runtime it is `JL0102` naming the id, before
15
+ * the compiler's `JF0013`.
16
+ *
17
+ * What the pen does NOT judge is the compiler's, and every emitted
18
+ * document is compiled in the tests: the wiring rules (`JF0015`),
19
+ * acyclicity (`JF0016`), the exactly-one-output rule (`JF0017`) and
20
+ * task-registry resolution (`JF0018`).
21
+ */
22
+
23
+ import { cloneJson, deepFreeze, setObjectMember } from '@jarenjs/core/object';
24
+
25
+ import { LinqBuildError } from '../errors.js';
26
+ import { describeValue, requireJson, requireNameMap } from '../json-boundary.js';
27
+ import { queryMember } from './capture.js';
28
+
29
+ const DAG_VERSION = '0.1';
30
+
31
+ /** FLOW-FORMAT §6.1's scope, for a `JL0104` message. */
32
+ const SCOPE = "the node's input scope (FLOW-FORMAT §6.1)";
33
+
34
+ /** The node-declaration brand; the members it emits live under it. */
35
+ const NODE = Symbol.for('@jarenjs/linq/flow-node');
36
+ /** The edge brand; the members it emits live under it. */
37
+ const EDGE = Symbol.for('@jarenjs/linq/flow-edge');
38
+
39
+ /** The members `edge()` takes beside its two positional arguments. */
40
+ const EDGE_MEMBERS = Object.freeze(['port', 'select']);
41
+ /** The members `defineDag()` takes. */
42
+ const DAG_MEMBERS = Object.freeze(['nodes', 'edges']);
43
+
44
+ /** @param {any} value */
45
+ function isPlainObject(value) {
46
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
47
+ }
48
+
49
+ /**
50
+ * A member set the pen knows, or `JL0101` naming the one it does not.
51
+ * @param {any} spec
52
+ * @param {readonly string[]} members
53
+ * @param {string} what
54
+ */
55
+ function closedTo(spec, members, what) {
56
+ for (const key of Object.keys(spec)) {
57
+ if (!members.includes(key)) {
58
+ throw new LinqBuildError('JL0101',
59
+ `${what} does not take '${key}' — it takes ${members.join(', ')}`, `/${key}`);
60
+ }
61
+ }
62
+ }
63
+
64
+ /**
65
+ * One node declaration: immutable, its emitted members carried under
66
+ * the brand so `checkpoint` can be a method here and a member there.
67
+ * @param {any} members - the node's members, in document order
68
+ * @returns {any}
69
+ */
70
+ function node(members) {
71
+ const out = {
72
+ /**
73
+ * Declare this node's value durable (§7.6): a checkpoint store
74
+ * records it and a resumed run seeds it instead of re-evaluating.
75
+ * The value must be JSON — `JF2008` at save time otherwise, never
76
+ * a silent skip.
77
+ */
78
+ checkpoint() { return node({ ...members, checkpoint: true }); },
79
+ };
80
+ Object.defineProperty(out, NODE, { value: members, enumerable: false });
81
+ return Object.freeze(out);
82
+ }
83
+
84
+ /**
85
+ * The `input` node (§6): it yields the `run(input)` value — `null` when
86
+ * the caller passes none — and accepts no inbound edge.
87
+ * @returns {any} the node declaration
88
+ */
89
+ export function input() { return node({ kind: 'input' }); }
90
+
91
+ /**
92
+ * The `output` node (§6): its input-scope value IS the run's result.
93
+ * Exactly one per document (the compiler's `JF0017`), no outbound edge.
94
+ * @returns {any} the node declaration
95
+ */
96
+ export function output() { return node({ kind: 'output' }); }
97
+
98
+ /**
99
+ * A `const` node (§6): its literal value, delivered by reference.
100
+ * @param {any} value - any JSON value
101
+ * @returns {any} the node declaration
102
+ * @example
103
+ * constant({ threshold: 18 });
104
+ */
105
+ export function constant(value) {
106
+ if (value === undefined) {
107
+ throw new LinqBuildError('JL0101',
108
+ 'constant() takes the value the node yields — every JSON value, null included; '
109
+ + 'undefined is not one', '/value');
110
+ }
111
+ return node({ kind: 'const', value: cloneJson(requireJson(value, 'constant()')) });
112
+ }
113
+
114
+ /**
115
+ * A `query` node (§6): a Jaren JSON Query over the node's input scope.
116
+ * @param {any} document - `(v) => …` captured over `$`, or a query document
117
+ * @returns {any} the node declaration
118
+ * @example
119
+ * query((rows) => rows.all().age); // captured
120
+ * query('$.summary'); // a path document, verbatim
121
+ */
122
+ export function query(document) {
123
+ if (document === undefined) {
124
+ throw new LinqBuildError('JL0101',
125
+ 'query() takes a callback (v) => … captured over the node input, or a query document',
126
+ '/query');
127
+ }
128
+ return node({ kind: 'query', query: queryMember('query()', SCOPE, document) });
129
+ }
130
+
131
+ /**
132
+ * A `jslt` node (§6): a stylesheet transforming the node's input scope.
133
+ * @param {any} document - the JSLT pen's stylesheet (or rule array), or one by hand
134
+ * @returns {any} the node declaration
135
+ * @example
136
+ * jslt(stylesheet([rule('$', (v) => ({ names: [apply(v.all())] }))]));
137
+ */
138
+ export function jslt(document) {
139
+ if (document === undefined) {
140
+ throw new LinqBuildError('JL0101',
141
+ 'jslt() takes a stylesheet document — the JSLT pen\'s stylesheet(…) or rule array, '
142
+ + 'or one written by hand', '/stylesheet');
143
+ }
144
+ return node({ kind: 'jslt', stylesheet: cloneJson(requireJson(document, 'jslt()')) });
145
+ }
146
+
147
+ /**
148
+ * A `task` node (§7.2): a registered async handler, called as
149
+ * `handler({ with, input }, signal)`. The pen writes the NAME; the
150
+ * registry a host hands `compileDag` resolves it (`JF0018` when it
151
+ * cannot).
152
+ * @param {string} run - the registry handler name
153
+ * @param {any} [props] - `(v) => ({ … })` over the input scope, or a query document
154
+ * @returns {any} the node declaration
155
+ * @example
156
+ * task('llm', (v) => ({ prompt: v.instruction }));
157
+ */
158
+ export function task(run, props = undefined) {
159
+ if (typeof run !== 'string' || run === '') {
160
+ throw new LinqBuildError('JL0101',
161
+ `task() takes the handler name as a non-empty string, got ${describeValue(run)}`, '/run');
162
+ }
163
+ const members = { kind: 'task', run };
164
+ if (props !== undefined) members.with = queryMember('task() with', SCOPE, props);
165
+ return node(members);
166
+ }
167
+
168
+ /**
169
+ * One edge (§6): data flows from a node's result to a consumer's input
170
+ * scope. `port` names the delivery in a ported fan-in (§6.1); `select`
171
+ * is applied to the source value before delivery.
172
+ * @param {string} from - the producing node id
173
+ * @param {string} to - the consuming node id
174
+ * @param {{ port?: string, select?: any }} [options]
175
+ * @returns {any} the edge declaration
176
+ * @example
177
+ * edge('rows', 'adults');
178
+ * edge('adults', 'report', { port: 'rows', select: (v) => v.all().name });
179
+ */
180
+ export function edge(from, to, options = undefined) {
181
+ if (typeof from !== 'string' || from === '') {
182
+ throw new LinqBuildError('JL0101',
183
+ `edge() takes the producing node id as a non-empty string, got ${describeValue(from)}`,
184
+ '/from');
185
+ }
186
+ if (typeof to !== 'string' || to === '') {
187
+ throw new LinqBuildError('JL0101',
188
+ `edge() takes the consuming node id as a non-empty string, got ${describeValue(to)}`,
189
+ '/to');
190
+ }
191
+ const members = { from, to };
192
+ if (options !== undefined) {
193
+ if (!isPlainObject(options)) {
194
+ throw new LinqBuildError('JL0101',
195
+ `edge() options are { port?, select? }, got ${describeValue(options)}`);
196
+ }
197
+ closedTo(options, EDGE_MEMBERS, 'edge()');
198
+ if (options.port !== undefined) {
199
+ if (typeof options.port !== 'string' || options.port === '') {
200
+ throw new LinqBuildError('JL0101',
201
+ `edge() port is a non-empty string, got ${describeValue(options.port)}`, '/port');
202
+ }
203
+ members.port = options.port;
204
+ }
205
+ if (options.select !== undefined) {
206
+ members.select = queryMember('edge() select', 'the source value (FLOW-FORMAT §6)',
207
+ options.select);
208
+ }
209
+ }
210
+ const out = {};
211
+ Object.defineProperty(out, EDGE, { value: members, enumerable: false });
212
+ return Object.freeze(out);
213
+ }
214
+
215
+ /**
216
+ * Write a `jaren-dag` 0.1 document (FLOW-FORMAT.md §6).
217
+ *
218
+ * Every id an edge names must be declared — `JL0102` naming it, before
219
+ * the compiler's `JF0013`. Everything else about the wiring is
220
+ * `compileDag`'s.
221
+ *
222
+ * @param {any} spec - `{ nodes, edges }`
223
+ * @returns {any} the deep-frozen `$dag` 0.1 document
224
+ * @throws {LinqBuildError} `JL0101` a value the pen cannot spell;
225
+ * `JL0102` an edge on an undeclared node id
226
+ * @example
227
+ * const graph = defineDag({
228
+ * nodes: { rows: input(), adults: query('$[*]'), out: output() },
229
+ * edges: [edge('rows', 'adults'), edge('adults', 'out')],
230
+ * });
231
+ * await compileDag(graph).run([{ age: 20 }]);
232
+ */
233
+ export function defineDag(spec) {
234
+ if (!isPlainObject(spec)) {
235
+ throw new LinqBuildError('JL0101',
236
+ `defineDag() takes { nodes, edges }, got ${describeValue(spec)}`);
237
+ }
238
+ closedTo(spec, DAG_MEMBERS, 'defineDag()');
239
+ if (!isPlainObject(spec.nodes)) {
240
+ throw new LinqBuildError('JL0101',
241
+ `defineDag() nodes is a plain object of id → node declaration, got ${describeValue(spec.nodes)}`,
242
+ '/nodes');
243
+ }
244
+ requireNameMap(spec.nodes, 'defineDag() nodes', '/nodes');
245
+ const ids = Object.keys(spec.nodes);
246
+ if (ids.length === 0) {
247
+ throw new LinqBuildError('JL0101', 'defineDag() needs at least one node', '/nodes');
248
+ }
249
+ const nodes = {};
250
+ for (const id of ids) {
251
+ const declared = spec.nodes[id];
252
+ const members = isPlainObject(declared) ? declared[NODE] : undefined;
253
+ if (members === undefined) {
254
+ throw new LinqBuildError('JL0101',
255
+ `defineDag() node '${id}' is input(), constant(), query(), jslt(), task() or `
256
+ + `output(), got ${describeValue(declared)}`, `/nodes/${id}`);
257
+ }
258
+ setObjectMember(nodes, id, { ...members });
259
+ }
260
+
261
+ if (!Array.isArray(spec.edges)) {
262
+ throw new LinqBuildError('JL0101',
263
+ `defineDag() edges is an array of edge(from, to) declarations, got ${describeValue(spec.edges)}`,
264
+ '/edges');
265
+ }
266
+ const edges = spec.edges.map((declared, i) => {
267
+ const at = `/edges/${i}`;
268
+ const members = isPlainObject(declared) ? declared[EDGE] : undefined;
269
+ if (members === undefined) {
270
+ throw new LinqBuildError('JL0101',
271
+ `defineDag() edges[${i}] is edge(from, to, options?), got ${describeValue(declared)}`, at);
272
+ }
273
+ for (const end of ['from', 'to']) {
274
+ if (!Object.hasOwn(nodes, members[end])) {
275
+ throw new LinqBuildError('JL0102',
276
+ `edge ${i} names the node '${members[end]}', which "nodes" does not declare — the `
277
+ + `declared nodes are ${ids.map((id) => `'${id}'`).join(', ')}`, `${at}/${end}`);
278
+ }
279
+ }
280
+ return { ...members };
281
+ });
282
+
283
+ return deepFreeze({ $dag: DAG_VERSION, nodes, edges });
284
+ }
285
+
286
+ /**
287
+ * Bind a task registry to the graph it serves. Identity at runtime: the
288
+ * table is checked against the task names the document declares, so the
289
+ * registry `compileDag` resolves and the document agree at compile
290
+ * time; the runtime check remains `JF0018`.
291
+ * @template D
292
+ * @template T
293
+ * @param {D} dag - the pen's graph; the type argument only
294
+ * @param {T} tasks - handler name → handler
295
+ * @returns {T}
296
+ * @example
297
+ * compileDag(graph, { tasks: typedTasks(graph, { llm: askModel }) });
298
+ */
299
+ export function typedTasks(dag, tasks) {
300
+ void dag;
301
+ return tasks;
302
+ }
@@ -0,0 +1,328 @@
1
+ //@ts-check
2
+ /**
3
+ * @file `defineFsm()` — one `jaren-fsm` 0.1 document
4
+ * (FLOW-FORMAT.md §2), deep-frozen, that `compileFsm` and `fsmToApp`
5
+ * take unchanged. `state()` declares one state, `on()` one transition
6
+ * (`.when()` its guard, `.to()` its target, `.effects()` its
7
+ * descriptors) and `effect()` one `{ run, with? }`.
8
+ *
9
+ * Guards and `with` members are captured over FLOW-FORMAT §3's scope —
10
+ * `{ state, event, payload, context }` at `$` — so `(s) =>
11
+ * s.payload.fresh` writes `"$.payload.fresh"` and `() => ({ text:
12
+ * 'retrying' })` writes the constructor the format's own example
13
+ * carries. The one trap the format names is refused here: a guard given
14
+ * as a plain STRING is `JL0102`, because §3 makes a non-`$` literal
15
+ * vacuously TRUE and a picture's display annotation must never decide
16
+ * execution.
17
+ *
18
+ * State ids are literal types, so a transition into an undeclared state
19
+ * is a compile error; at runtime it is `JL0102` naming the state,
20
+ * before the compiler's `JF0006`. What the pen does NOT judge is the
21
+ * compiler's: duplicate ids (`JF0003`), a guard's operators (`JF0007`)
22
+ * and everything else §5.1 lists.
23
+ */
24
+
25
+ import { deepFreeze } from '@jarenjs/core/object';
26
+
27
+ import { LinqBuildError } from '../errors.js';
28
+ import { effectDescriptor, readEffects } from '../effect.js';
29
+ import { describeValue } from '../json-boundary.js';
30
+ import { isSchemaBuilder } from '../schema/brand.js';
31
+ import { queryMember } from './capture.js';
32
+
33
+ const FSM_VERSION = '0.1';
34
+
35
+ /** FLOW-FORMAT §3's scope, for a `JL0104` message. */
36
+ const SCOPE = 'the step scope { state, event, payload, context } (FLOW-FORMAT §3)';
37
+
38
+ /** The state-declaration brand: how `defineFsm` tells one apart. */
39
+ const STATE = Symbol.for('@jarenjs/linq/flow-state');
40
+ /** The transition brand; the entry it carries lives under it. */
41
+ const TRANSITION = Symbol.for('@jarenjs/linq/flow-transition');
42
+
43
+ /** The members `state()` takes, in the order §2 writes them. */
44
+ const STATE_MEMBERS = Object.freeze(['entry', 'exit', 'final']);
45
+ /** The members `on()` takes beside its two positional arguments. */
46
+ const ON_MEMBERS = Object.freeze(['payload']);
47
+ /** The members `defineFsm()` takes. */
48
+ const FSM_MEMBERS = Object.freeze(['initial', 'states', 'transitions', 'context']);
49
+
50
+ /** @param {any} value */
51
+ function isPlainObject(value) {
52
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
53
+ }
54
+
55
+ /**
56
+ * A member set the pen knows, or `JL0101` naming the one it does not.
57
+ * @param {any} spec
58
+ * @param {readonly string[]} members
59
+ * @param {string} what
60
+ * @param {string} [at]
61
+ */
62
+ function closedTo(spec, members, what, at = '') {
63
+ for (const key of Object.keys(spec)) {
64
+ if (!members.includes(key)) {
65
+ throw new LinqBuildError('JL0101',
66
+ `${what} does not take '${key}' — it takes ${members.join(', ')}`, `${at}/${key}`);
67
+ }
68
+ }
69
+ }
70
+
71
+ /**
72
+ * A state id: a non-empty string, or `JL0101`.
73
+ * @param {any} id
74
+ * @param {string} what
75
+ * @returns {string}
76
+ */
77
+ function readId(id, what) {
78
+ if (typeof id !== 'string' || id === '') {
79
+ throw new LinqBuildError('JL0101',
80
+ `${what} takes a state id — a non-empty string, got ${describeValue(id)}`);
81
+ }
82
+ return id;
83
+ }
84
+
85
+ /**
86
+ * One effect descriptor (§2): `{ run, with? }`, its props captured over
87
+ * the step scope. The engine never executes it — the host's registry
88
+ * does — so `run` is a name the pen writes and never resolves.
89
+ * @param {string} run - the host-registered handler name
90
+ * @param {any} [props] - `(s) => ({ … })`, or a query document
91
+ * @returns {any} the effect declaration
92
+ * @example
93
+ * effect('fetch', (s) => ({ url: s.context.url })); // { run, with }
94
+ * effect('toast', () => ({ text: 'retrying' })); // a constructor, not $const
95
+ */
96
+ export function effect(run, props = undefined) {
97
+ return effectDescriptor(run, props, (p) => queryMember('effect() with', SCOPE, p));
98
+ }
99
+
100
+ /**
101
+ * One state declaration (§2): `{ id, entry?, exit?, final? }`. A bare
102
+ * string in `states` is the format's own shorthand for `{ id }` and
103
+ * stays one — it is the shape the `jaren-workflow` projection carries.
104
+ * @param {string} id - the state id transitions refer to
105
+ * @param {{ entry?: readonly any[], exit?: readonly any[], final?: boolean }} [options]
106
+ * @returns {any} the state declaration
107
+ * @example
108
+ * state('loading', { entry: [effect('fetch', (s) => ({ url: s.context.url }))] });
109
+ * state('done', { final: true });
110
+ */
111
+ export function state(id, options = undefined) {
112
+ const out = { id: readId(id, 'state()') };
113
+ if (options !== undefined) {
114
+ if (!isPlainObject(options)) {
115
+ throw new LinqBuildError('JL0101',
116
+ `state() options are { entry?, exit?, final? }, got ${describeValue(options)}`);
117
+ }
118
+ closedTo(options, STATE_MEMBERS, 'state()');
119
+ if (options.entry !== undefined) out.entry = readEffects(options.entry, 'state() entry');
120
+ if (options.exit !== undefined) out.exit = readEffects(options.exit, 'state() exit');
121
+ if (options.final !== undefined) {
122
+ if (typeof options.final !== 'boolean') {
123
+ throw new LinqBuildError('JL0101',
124
+ `state() final is a boolean, got ${describeValue(options.final)}`, '/final');
125
+ }
126
+ out.final = options.final;
127
+ }
128
+ }
129
+ Object.defineProperty(out, STATE, { value: true, enumerable: false });
130
+ return Object.freeze(out);
131
+ }
132
+
133
+ /**
134
+ * A guard, as the document carries it: a captured body, or a query
135
+ * document by hand. A plain STRING is the one trap FLOW-FORMAT §3 names
136
+ * itself, and the pen refuses it.
137
+ * @param {any} value
138
+ * @returns {any}
139
+ */
140
+ function readGuard(value) {
141
+ if (typeof value === 'string') {
142
+ throw new LinqBuildError('JL0102',
143
+ 'a guard given as a plain string is asserted by effective boolean value, and a '
144
+ + 'non-empty literal is therefore VACUOUSLY TRUE (FLOW-FORMAT §3: a projected '
145
+ + `display guard must not change execution) — got ${JSON.stringify(value)}; pass a `
146
+ + 'body instead: .when((s) => s.payload.fresh)', '/guard');
147
+ }
148
+ return queryMember('when()', SCOPE, value);
149
+ }
150
+
151
+ /**
152
+ * The transition builder: immutable, one new builder per call, its
153
+ * entry carried under the brand.
154
+ * @param {any} entry
155
+ * @returns {any}
156
+ */
157
+ function transition(entry) {
158
+ const out = {
159
+ /**
160
+ * The transition's guard (§3), captured over the step scope.
161
+ * @param {any} guard - `(s) => …`, or a query document
162
+ */
163
+ when(guard) { return transition({ ...entry, guard: readGuard(guard) }); },
164
+ /**
165
+ * The state this transition enters.
166
+ * @param {string} to
167
+ */
168
+ to(to) { return transition({ ...entry, to: readId(to, 'to()') }); },
169
+ /**
170
+ * The transition's own effects, fired between exit and entry (§4).
171
+ * @param {readonly any[]} effects
172
+ */
173
+ effects(effects) {
174
+ return transition({ ...entry, effects: readEffects(effects, 'effects()') });
175
+ },
176
+ };
177
+ Object.defineProperty(out, TRANSITION, { value: entry, enumerable: false });
178
+ return Object.freeze(out);
179
+ }
180
+
181
+ /**
182
+ * One transition (§2), left open until `.to()` names its target.
183
+ * Document order is the whole priority scheme (§4), so the order of the
184
+ * `transitions` array is the order the pen writes.
185
+ * @param {string} from - the state this transition leaves
186
+ * @param {string|null} [event] - the event name; null or absent is a wildcard
187
+ * @param {{ payload?: any }} [options] - the event's payload schema; a
188
+ * TYPE only, the format carries no payload schema
189
+ * @returns {any} the transition builder
190
+ * @example
191
+ * on('draft', 'submit').to('review');
192
+ * on('review', 'approve').when((s) => s.payload.fresh).to('published');
193
+ * on('review').to('draft'); // a wildcard, listed last
194
+ */
195
+ export function on(from, event = null, options = undefined) {
196
+ const entry = { from: readId(from, 'on()') };
197
+ if (event !== null && event !== undefined) {
198
+ if (typeof event !== 'string' || event === '') {
199
+ throw new LinqBuildError('JL0101',
200
+ 'on() takes an event name as a non-empty string, or null for the wildcard that '
201
+ + `matches any event (FLOW-FORMAT §2), got ${describeValue(event)}`, '/event');
202
+ }
203
+ entry.event = event;
204
+ }
205
+ if (options !== undefined) {
206
+ if (!isPlainObject(options)) {
207
+ throw new LinqBuildError('JL0101',
208
+ `on() options are { payload? }, got ${describeValue(options)}`);
209
+ }
210
+ closedTo(options, ON_MEMBERS, 'on()');
211
+ if (options.payload !== undefined && !isSchemaBuilder(options.payload)) {
212
+ throw new LinqBuildError('JL0101',
213
+ 'on() payload is a schema-pen builder that types the event\'s payload — the format '
214
+ + `carries no payload schema, so nothing is emitted for it; got ${describeValue(options.payload)}`,
215
+ '/payload');
216
+ }
217
+ }
218
+ return transition(entry);
219
+ }
220
+
221
+ /**
222
+ * Write a `jaren-fsm` 0.1 document (FLOW-FORMAT.md §2).
223
+ *
224
+ * `initial` is declared, never guessed: the format requires the member
225
+ * and `null` is what a document that chooses no start state writes.
226
+ * Every state id `initial` and a transition name must be declared —
227
+ * `JL0102` naming it, before the compiler's `JF0004`/`JF0006`.
228
+ *
229
+ * @param {any} spec - `{ initial, states, transitions, context? }`
230
+ * @returns {any} the deep-frozen `$fsm` 0.1 document
231
+ * @throws {LinqBuildError} `JL0101` a value the pen cannot spell;
232
+ * `JL0102` a plain-string guard, or an undeclared state id
233
+ * @example
234
+ * const machine = defineFsm({
235
+ * initial: 'draft',
236
+ * states: ['draft', 'review', state('published', { final: true })],
237
+ * transitions: [on('draft', 'submit').to('review'), on('review', 'approve').to('published')],
238
+ * });
239
+ * compileFsm(machine).step('draft', 'submit').state; // 'review'
240
+ */
241
+ export function defineFsm(spec) {
242
+ if (!isPlainObject(spec)) {
243
+ throw new LinqBuildError('JL0101',
244
+ `defineFsm() takes { initial, states, transitions, context? }, got ${describeValue(spec)}`);
245
+ }
246
+ closedTo(spec, FSM_MEMBERS, 'defineFsm()');
247
+ if (spec.context !== undefined && !isSchemaBuilder(spec.context)) {
248
+ throw new LinqBuildError('JL0101',
249
+ 'defineFsm() context is a schema-pen builder that types the host data a guard reads '
250
+ + '(FLOW-FORMAT §3) — the format carries no context schema, so nothing is emitted for '
251
+ + `it; got ${describeValue(spec.context)}`, '/context');
252
+ }
253
+ if (!Array.isArray(spec.states)) {
254
+ throw new LinqBuildError('JL0101',
255
+ `defineFsm() states is an array of ids and state() declarations, got ${describeValue(spec.states)}`,
256
+ '/states');
257
+ }
258
+
259
+ /** @type {any[]} */
260
+ const states = [];
261
+ /** @type {Set<string>} */
262
+ const declared = new Set();
263
+ spec.states.forEach((entry, i) => {
264
+ if (typeof entry === 'string') {
265
+ declared.add(readId(entry, 'defineFsm() states'));
266
+ states.push(entry);
267
+ return;
268
+ }
269
+ if (!isPlainObject(entry) || entry[STATE] !== true) {
270
+ throw new LinqBuildError('JL0101',
271
+ `defineFsm() states[${i}] is an id or state(id, options?), got ${describeValue(entry)}`,
272
+ `/states/${i}`);
273
+ }
274
+ declared.add(entry.id);
275
+ states.push({ ...entry });
276
+ });
277
+
278
+ /**
279
+ * @param {any} id
280
+ * @param {string} what
281
+ * @param {string} at
282
+ */
283
+ const declaredId = (id, what, at) => {
284
+ if (!declared.has(id)) {
285
+ throw new LinqBuildError('JL0102',
286
+ `${what} names the state '${id}', which "states" does not declare — the declared `
287
+ + `states are ${[...declared].map((s) => `'${s}'`).join(', ')}`, at);
288
+ }
289
+ return id;
290
+ };
291
+
292
+ if (spec.initial === undefined) {
293
+ throw new LinqBuildError('JL0101',
294
+ 'defineFsm() needs an initial state — the format requires the member; pass null for a '
295
+ + 'machine that chooses none (a session then starts with an explicit state)', '/initial');
296
+ }
297
+ if (spec.initial !== null) {
298
+ declaredId(readId(spec.initial, 'defineFsm() initial'), 'defineFsm() initial', '/initial');
299
+ }
300
+
301
+ if (!Array.isArray(spec.transitions)) {
302
+ throw new LinqBuildError('JL0101',
303
+ `defineFsm() transitions is an array of on(…) declarations, got ${describeValue(spec.transitions)}`,
304
+ '/transitions');
305
+ }
306
+ const transitions = spec.transitions.map((declaration, i) => {
307
+ const at = `/transitions/${i}`;
308
+ const entry = isPlainObject(declaration) ? declaration[TRANSITION] : undefined;
309
+ if (entry === undefined) {
310
+ throw new LinqBuildError('JL0101',
311
+ `defineFsm() transitions[${i}] is on(from, event?).to(state), got `
312
+ + `${describeValue(declaration)}`, at);
313
+ }
314
+ if (entry.to === undefined) {
315
+ throw new LinqBuildError('JL0101',
316
+ `defineFsm() transitions[${i}] never named its target — on('${entry.from}'`
317
+ + `${entry.event === undefined ? '' : `, '${entry.event}'`}) needs .to(state)`, at);
318
+ }
319
+ const out = { from: declaredId(entry.from, `transition ${i}`, `${at}/from`) };
320
+ if (entry.event !== undefined) out.event = entry.event;
321
+ if (entry.guard !== undefined) out.guard = entry.guard;
322
+ out.to = declaredId(entry.to, `transition ${i}`, `${at}/to`);
323
+ if (entry.effects !== undefined) out.effects = entry.effects;
324
+ return out;
325
+ });
326
+
327
+ return deepFreeze({ $fsm: FSM_VERSION, initial: spec.initial, states, transitions });
328
+ }
@@ -0,0 +1,22 @@
1
+ //@ts-check
2
+ /**
3
+ * @file `@jarenjs/linq/flow` — the `jaren-fsm` and `jaren-dag` 0.1
4
+ * documents by code. `defineFsm()` writes the machine `compileFsm` and
5
+ * `fsmToApp` take; `defineDag()` writes the dataflow `compileDag`
6
+ * takes. Guards, effect props, node queries and edge selectors are
7
+ * callbacks captured over the scope the engine evaluates them in
8
+ * (FLOW-FORMAT §3 and §6.1), so a path is written, never typed as a
9
+ * string — which is also why a plain-string guard is refused here
10
+ * (§3 makes a literal one vacuously true).
11
+ *
12
+ * State ids, event names and node ids are literal types: a transition
13
+ * into an undeclared state or an edge from an undeclared node is a
14
+ * compile error before it is a `JL0102`, and long before the engine's
15
+ * `JF0006`/`JF0013`. The document is the deliverable — plain,
16
+ * deep-frozen JSON — and nothing here imports `@jarenjs/flow`.
17
+ */
18
+
19
+ export { defineFsm, state, on, effect } from './fsm.js';
20
+ export {
21
+ defineDag, input, output, constant, query, jslt, task, edge, typedTasks,
22
+ } from './dag.js';