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