@panagos/jojoscript 0.9.3 → 1.0.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.
- package/README.md +848 -25
- package/bin/jojo.js +257 -23
- package/package.json +2 -2
- package/src/check.js +253 -0
- package/src/compiler.js +36 -37
- package/src/contracts.js +124 -0
- package/src/describe.js +63 -0
- package/src/effect-parser.js +4 -2
- package/src/formatter.js +105 -0
- package/src/index.js +31 -0
- package/src/match-parser.js +212 -14
- package/src/named-pipelines.js +87 -0
- package/src/parse.js +124 -0
- package/src/pipeline-explain.js +132 -0
- package/src/pipeline-ir.js +9 -7
- package/src/pipeline-parser.js +24 -2
- package/src/pipeline-plan.js +203 -0
- package/src/purity.js +101 -0
- package/src/runtime/a2a.js +322 -0
- package/src/runtime/checkpoint-store.js +26 -5
- package/src/runtime/checkpoint.js +3 -2
- package/src/runtime/effects.js +136 -2
- package/src/runtime/errors.js +14 -0
- package/src/runtime/index.js +15 -3
- package/src/runtime/mcp.js +237 -35
- package/src/runtime/serialization.js +97 -0
- package/src/runtime/tagged-unions.js +29 -0
- package/src/simulate.js +156 -0
- package/src/type-declarations.js +83 -0
- package/src/why.js +146 -0
package/README.md
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
# JojoScript 0.
|
|
1
|
+
# JojoScript 1.0.0
|
|
2
2
|
|
|
3
|
-
JojoScript is a
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
support, and VS Code editor support.
|
|
3
|
+
JojoScript is a lightweight language and runtime for data pipelines and agent
|
|
4
|
+
workflows, built on JavaScript.
|
|
5
|
+
|
|
6
|
+
It combines expressive pipeline syntax, lazy data processing, resumable
|
|
7
|
+
execution and effect handling with MCP tool serving and A2A agent
|
|
8
|
+
communication. It compiles to ordinary JavaScript and keeps the familiar
|
|
9
|
+
JavaScript ecosystem accessible, without requiring a heavyweight framework.
|
|
11
10
|
|
|
12
11
|
See [CHANGELOG.md](CHANGELOG.md) for release history and
|
|
13
12
|
[example/](example/) for a complete runnable application that exercises
|
|
@@ -23,8 +22,17 @@ npm install @panagos/jojoscript
|
|
|
23
22
|
|
|
24
23
|
```bash
|
|
25
24
|
jojo build <file.jojo|directory> [--out <directory>] [--watch]
|
|
26
|
-
jojo run <file.jojo> [--out <directory>]
|
|
27
|
-
jojo graph <file.jojo> [--format text|dot]
|
|
25
|
+
jojo run <file.jojo> [--out <directory>] [--deterministic [--seed <n>]]
|
|
26
|
+
jojo graph <file.jojo> [--format text|dot] [--pipeline <name>]
|
|
27
|
+
jojo explain <file.jojo> [--format text|json] [--pipeline <name>]
|
|
28
|
+
jojo plan <file.jojo> [--format json] [--pipeline <name>]
|
|
29
|
+
jojo generate <plan.json>
|
|
30
|
+
jojo check <file.jojo|plan.json> [--format text|json] [--machine]
|
|
31
|
+
jojo describe <file.jojo> [--pipeline <name>]
|
|
32
|
+
jojo simulate <file.jojo> [--input <records>] [--format text|json] [--pipeline <name>]
|
|
33
|
+
jojo why <file.jojo> [--format text|json] [--pipeline <name>]
|
|
34
|
+
jojo format <file.jojo|directory> [--check]
|
|
35
|
+
jojo parse <file.jojo> [--json]
|
|
28
36
|
jojo profile <file.jojo>
|
|
29
37
|
jojo resume <file.jojo> [--out <directory>]
|
|
30
38
|
jojo replay <file.jojo> --from <checkpoint> [--out <directory>]
|
|
@@ -36,7 +44,17 @@ jojo --help
|
|
|
36
44
|
- `jojo build src` compiles every `.jojo` file under `src` to `src/dist/*.js` (or `--out <dir>`).
|
|
37
45
|
- `jojo build src --watch` rebuilds whenever a `.jojo` file under `src` changes.
|
|
38
46
|
- `jojo run app.jojo` compiles a single file and immediately runs it with Node — useful for a fast dev loop.
|
|
47
|
+
- `jojo run app.jojo --deterministic --seed 42` runs the program in [deterministic execution mode](#deterministic-execution-mode): seeded `random()`, a fixed clock, sequential `parallel()`, and deterministic effect handlers only.
|
|
39
48
|
- `jojo graph app.jojo` prints a static pipeline graph; use `--format dot` for Graphviz output.
|
|
49
|
+
- `jojo explain app.jojo` describes what each pipeline means (stages, effects, properties) without running it; use `--format json` for tools and LLMs. See [Explaining pipelines](#explaining-pipelines).
|
|
50
|
+
- `jojo plan app.jojo` prints a canonical JSON execution plan, and `jojo generate plan.json` turns a plan back into validated JojoScript. See [Machine-readable execution plans](#machine-readable-execution-plans).
|
|
51
|
+
- `graph`, `explain`, and `plan` accept `--pipeline <name>` to select one pipeline by its (inferred or [declared](#named-pipelines)) name.
|
|
52
|
+
- `jojo check app.jojo` (or `plan.json`) reports structured diagnostics with codes, lines, and suggestions, and exits 1 on errors; `--machine` prints JSON. See [Checking programs](#checking-programs).
|
|
53
|
+
- `jojo describe app.jojo` prints one JSON document describing the pipelines, effects, functions, and types in a file. See [Describing programs](#describing-programs).
|
|
54
|
+
- `jojo simulate app.jojo --input 1000000` estimates item counts, effect calls, and risks for a given input size without running anything. See [Simulating pipelines](#simulating-pipelines).
|
|
55
|
+
- `jojo why app.jojo` answers questions such as "why is this pipeline not deterministic?" in plain language. See [Asking why](#asking-why).
|
|
56
|
+
- `jojo format src` reformats `.jojo` files in place; `--check` lists unformatted files and exits 1. See [Formatting](#formatting).
|
|
57
|
+
- `jojo parse app.jojo --json` lists every syntax construct (imports, effects, types, functions, pipelines, handle blocks, matches) with its line. See [Parsing programs](#parsing-programs).
|
|
40
58
|
- `jojo profile app.jojo` executes a pipeline and prints per-node item counts, duration, and throughput.
|
|
41
59
|
- `jojo resume app.jojo` reuses completed file-backed checkpoints; `jojo replay app.jojo --from name` starts from a selected checkpoint.
|
|
42
60
|
- `jojo test app.jojo` compiles the file and runs its co-located `<name>.test.js` test when present.
|
|
@@ -137,6 +155,69 @@ async fn loadUsers() {
|
|
|
137
155
|
}
|
|
138
156
|
```
|
|
139
157
|
|
|
158
|
+
### Pure and effectful functions
|
|
159
|
+
|
|
160
|
+
`pure` and `effectful` declare a function's role. They compile away to an
|
|
161
|
+
ordinary function, and may follow `export` or surround `async`:
|
|
162
|
+
|
|
163
|
+
```jojo
|
|
164
|
+
effect storeUser(user)
|
|
165
|
+
|
|
166
|
+
export pure fn normalizeUser(user) {
|
|
167
|
+
return { ...user, email: user.email.trim().toLowerCase() }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
effectful fn saveUser(user) {
|
|
171
|
+
return storeUser(user)
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
The compiler rejects a `pure fn` that uses a declared `effect` or an
|
|
176
|
+
`effectful fn`, whether it calls it or passes it along (`map(saveUser)`):
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
pure fn "normalizeUser" uses effectful function "saveUser" (line 9)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
The check is static and name-based. Strings, comments, property access
|
|
183
|
+
(`user.saveUser`), and recursion are ignored, and calls to unannotated
|
|
184
|
+
functions are allowed. `jojo explain` reports a stage that uses an
|
|
185
|
+
`effectful fn` as `effectful` and the pipeline as non-deterministic, exactly as
|
|
186
|
+
it does for declared effects. See
|
|
187
|
+
[example/src/application/purity-example.jojo](example/src/application/purity-example.jojo).
|
|
188
|
+
|
|
189
|
+
### Function contracts
|
|
190
|
+
|
|
191
|
+
A block function can state what it expects and what it guarantees. Each
|
|
192
|
+
clause goes on its own line between the parameter list and the opening `{`:
|
|
193
|
+
|
|
194
|
+
```jojo
|
|
195
|
+
fn applyDiscount(price, percent)
|
|
196
|
+
requires price >= 0
|
|
197
|
+
requires percent >= 0 && percent <= 100
|
|
198
|
+
ensures result >= 0 && result <= price
|
|
199
|
+
{
|
|
200
|
+
return price - price * percent / 100
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
`requires` clauses are checked on entry, in order, before the body runs.
|
|
205
|
+
`ensures` clauses are checked after it, with the return value available as
|
|
206
|
+
`result` (async functions are awaited first). The body runs inside an arrow
|
|
207
|
+
function, so `this` and `arguments` keep referring to the declared function.
|
|
208
|
+
A broken clause throws `ContractError`:
|
|
209
|
+
|
|
210
|
+
```
|
|
211
|
+
ContractError: Contract violated: applyDiscount requires percent >= 0 && percent <= 100
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
The error carries `functionName`, `kind` (`"requires"` or `"ensures"`), and
|
|
215
|
+
`condition`, and is non-retryable, so `retry()` never repeats a call that
|
|
216
|
+
broke its contract. Contract clauses keep the source line count. Contracts
|
|
217
|
+
apply to block functions only, and a function using `ensures` should not
|
|
218
|
+
have a parameter named `result`. See
|
|
219
|
+
[example/src/application/contracts-example.jojo](example/src/application/contracts-example.jojo).
|
|
220
|
+
|
|
140
221
|
### Optional type annotations
|
|
141
222
|
|
|
142
223
|
Parameters and return values can be annotated. Annotations are **never
|
|
@@ -198,8 +279,77 @@ category := match true {
|
|
|
198
279
|
}
|
|
199
280
|
```
|
|
200
281
|
|
|
201
|
-
|
|
202
|
-
|
|
282
|
+
#### Structured patterns
|
|
283
|
+
|
|
284
|
+
Object patterns match on the shape of a value. Arms can be separated by
|
|
285
|
+
newlines instead of commas:
|
|
286
|
+
|
|
287
|
+
```jojo
|
|
288
|
+
message := match result {
|
|
289
|
+
{ status: "ok", value } => value
|
|
290
|
+
{ status: "error", error: { code: 404 } } => "not found"
|
|
291
|
+
{ status: "error", error } => handle(error)
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Inside an object pattern:
|
|
296
|
+
|
|
297
|
+
- a literal or expression (`status: "ok"`, `code: Status.OK`) is compared with `===`;
|
|
298
|
+
- a bare name (`value`) binds that field, and the field must be present;
|
|
299
|
+
- `key: name` binds the field under another name, and `key: { ... }` matches a nested object;
|
|
300
|
+
- quoted keys (`"content-type": "json"`) are supported.
|
|
301
|
+
|
|
302
|
+
A match with at least one object pattern may omit the `_` arm. If no arm
|
|
303
|
+
matches, it throws `TypeError: No match arm matched the value`, so an
|
|
304
|
+
unexpected shape fails loudly instead of returning `undefined`. A match whose
|
|
305
|
+
patterns are all plain expressions still requires a final `_` arm and
|
|
306
|
+
compiles exactly as before. See
|
|
307
|
+
[example/src/application/structured-match-example.jojo](example/src/application/structured-match-example.jojo).
|
|
308
|
+
|
|
309
|
+
### Tagged unions
|
|
310
|
+
|
|
311
|
+
`type` declares a tagged union: a value that is exactly one of a fixed set of
|
|
312
|
+
variants. Each variant has a constructor:
|
|
313
|
+
|
|
314
|
+
```jojo
|
|
315
|
+
type PaymentResult =
|
|
316
|
+
| Paid(amount)
|
|
317
|
+
| Declined(reason)
|
|
318
|
+
| Pending
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
becomes:
|
|
322
|
+
|
|
323
|
+
```js
|
|
324
|
+
const PaymentResult = defineType("PaymentResult", { Paid: ["amount"], Declined: ["reason"], Pending: [] }); const { Paid, Declined, Pending } = PaymentResult
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
`Paid(40)` returns the frozen plain object `{ tag: "Paid", amount: 40 }`, so
|
|
328
|
+
values serialize to JSON unchanged. Constructors reject the wrong number of
|
|
329
|
+
arguments (`Paid expects 1 argument (amount), got 0`), and fieldless variants
|
|
330
|
+
are called with no arguments (`Pending()`). `export type` exports the type and
|
|
331
|
+
all of its constructors. Variant names must start with an uppercase letter,
|
|
332
|
+
variants and fields must be unique, and no field may be named `tag`.
|
|
333
|
+
|
|
334
|
+
Constructor patterns match a variant by its tag and bind its fields by
|
|
335
|
+
position, so they also match values decoded from JSON:
|
|
336
|
+
|
|
337
|
+
```jojo
|
|
338
|
+
message := match result {
|
|
339
|
+
Paid(amount) => `paid ${amount}`
|
|
340
|
+
Declined("insufficient funds") => "ask for another card"
|
|
341
|
+
Declined(reason) => `declined: ${reason}`
|
|
342
|
+
Pending() => "waiting for the bank"
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
Pattern arguments can be literals, `_`, bindings, or nested object and
|
|
347
|
+
constructor patterns. Like object patterns, a match over constructor patterns
|
|
348
|
+
may omit `_` and throws `TypeError` when nothing matches. A capitalized call
|
|
349
|
+
such as `Ok(v)` inside a `match` is always a constructor pattern. Avoid naming
|
|
350
|
+
a variant `Error`, since its constructor would shadow JavaScript's `Error` in
|
|
351
|
+
that module. See
|
|
352
|
+
[example/src/application/tagged-unions-example.jojo](example/src/application/tagged-unions-example.jojo).
|
|
203
353
|
|
|
204
354
|
### Pipelines
|
|
205
355
|
|
|
@@ -222,6 +372,52 @@ A stage can have no arguments (`users |> unique`) or arguments
|
|
|
222
372
|
inputs, including `await` expressions and pipelines inside `return`
|
|
223
373
|
statements, function bodies, and object literals.
|
|
224
374
|
|
|
375
|
+
#### Named pipelines
|
|
376
|
+
|
|
377
|
+
A `pipeline` block declares a named, reusable pipeline. Its first line names
|
|
378
|
+
the source, and the remaining lines are stages:
|
|
379
|
+
|
|
380
|
+
```jojo
|
|
381
|
+
export pipeline enrichOrders {
|
|
382
|
+
source orders
|
|
383
|
+
|
|
384
|
+
|> filter(order => order.active)
|
|
385
|
+
|> map(addVat)
|
|
386
|
+
|> checkpoint("enriched")
|
|
387
|
+
|> toArrayAsync
|
|
388
|
+
|> await
|
|
389
|
+
}
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
It compiles to a function. An identifier source becomes its single parameter
|
|
393
|
+
(`enrichOrders(orders)`); any other source expression gives a function with
|
|
394
|
+
no parameters. A pipeline with a `|> await` stage (or an awaited source) becomes an
|
|
395
|
+
`async` function; an `await` inside a stage callback does not. Also,
|
|
396
|
+
blank lines inside the block are allowed. Tools see the pipeline under its
|
|
397
|
+
declared name, so `jojo explain app.jojo --pipeline enrichOrders` (and the same
|
|
398
|
+
option on `graph` and `plan`) selects it directly. See
|
|
399
|
+
[example/src/application/named-pipeline-example.jojo](example/src/application/named-pipeline-example.jojo).
|
|
400
|
+
|
|
401
|
+
#### Stage IDs
|
|
402
|
+
|
|
403
|
+
A stage can be labelled with a trailing `@id` (letters, digits, `_`, and `-`,
|
|
404
|
+
starting with a letter or `_`):
|
|
405
|
+
|
|
406
|
+
```jojo
|
|
407
|
+
saved := orders
|
|
408
|
+
|> filter(order => order.paid) @paid-only
|
|
409
|
+
|> map(assignCarrier) @pick-carrier
|
|
410
|
+
|> checkpoint("prepared")
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
The label is removed from the compiled code. It becomes the stage's node id in
|
|
414
|
+
runtime metadata (checkpoint, inspect, parallel, batch, and `jojo profile`),
|
|
415
|
+
in `jojo graph`, in `jojo explain` (`id` and `@pick-carrier` in the text), and
|
|
416
|
+
in `jojo plan` (an `id` field that `jojo generate` writes back). Tools and
|
|
417
|
+
LLMs can therefore refer to "the `@pick-carrier` stage" instead of a position
|
|
418
|
+
in the source. IDs must be unique within a pipeline. They are not part of the
|
|
419
|
+
pipeline's hash, so renaming a label never invalidates saved checkpoints.
|
|
420
|
+
|
|
225
421
|
#### Placeholder argument
|
|
226
422
|
|
|
227
423
|
By default the piped value is inserted as the stage's first argument. The
|
|
@@ -326,6 +522,381 @@ auto-imported):
|
|
|
326
522
|
See [ITERATOR_PROPOSAL.md](ITERATOR_PROPOSAL.md) and [ITERATORS.md](ITERATORS.md)
|
|
327
523
|
for the full laziness/memory model.
|
|
328
524
|
|
|
525
|
+
## Explaining pipelines
|
|
526
|
+
|
|
527
|
+
`jojo explain` lets a person, a tool, or an LLM ask JojoScript what a program
|
|
528
|
+
means instead of inferring it from source. It is static: nothing is executed.
|
|
529
|
+
|
|
530
|
+
```jojo
|
|
531
|
+
effect enrichOrder(order)
|
|
532
|
+
|
|
533
|
+
export async fn enrichPaidOrders(orders) {
|
|
534
|
+
return handle {
|
|
535
|
+
enrichOrder: order => ({ ...order, region: "EU" })
|
|
536
|
+
} {
|
|
537
|
+
return orders
|
|
538
|
+
|> filter(order => order.status == "paid")
|
|
539
|
+
|> parallel(4)
|
|
540
|
+
|> map(enrichOrder)
|
|
541
|
+
|> checkpoint("enriched-orders")
|
|
542
|
+
|> toArrayAsync
|
|
543
|
+
|> await
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
```
|
|
549
|
+
$ jojo explain orders.jojo
|
|
550
|
+
PIPELINE enrichPaidOrders
|
|
551
|
+
|
|
552
|
+
STAGES
|
|
553
|
+
1 source orders
|
|
554
|
+
2 filter pure
|
|
555
|
+
3 parallel concurrency=4
|
|
556
|
+
4 map effectful: enrichOrder
|
|
557
|
+
5 checkpoint enriched-orders
|
|
558
|
+
6 toArrayAsync pure
|
|
559
|
+
7 await control
|
|
560
|
+
|
|
561
|
+
EFFECTS
|
|
562
|
+
enrichOrder (handler declared)
|
|
563
|
+
|
|
564
|
+
PROPERTIES
|
|
565
|
+
lazy: true
|
|
566
|
+
parallel: true
|
|
567
|
+
resumable: true
|
|
568
|
+
deterministic: false
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
`jojo explain orders.jojo --format json` prints the same information as an
|
|
572
|
+
array of `{ name, id, stages, effects, properties }` objects, and
|
|
573
|
+
`explainPipelines(source)` / `renderExplanationText(explanation)` are exported
|
|
574
|
+
from `@panagos/jojoscript` for programmatic use.
|
|
575
|
+
|
|
576
|
+
- **name** is inferred: the variable or property a pipeline is assigned to
|
|
577
|
+
(`paid := orders |> ...`), the `fn` name of an expression function, or the
|
|
578
|
+
enclosing named function of a `return` pipeline; otherwise `pipeline-N`.
|
|
579
|
+
- **stages** list each stage's type, raw `args`, referenced `effects`, and a
|
|
580
|
+
`detail`: the source expression, `concurrency=N` for `parallel`, `size=N` for
|
|
581
|
+
`batch`, the checkpoint name, `control` for `await`/`catch`/`orElse`/`pass`,
|
|
582
|
+
or `pure`/`effectful: <effects>`.
|
|
583
|
+
- **effects** are the declared effects the pipeline references, each with the
|
|
584
|
+
same best-effort `staticallyHandled` signal as `jojo graph`.
|
|
585
|
+
- **properties**: `lazy` (no `sort`/`sortBy`/`groupBy`/`partition`
|
|
586
|
+
materialization boundary), `parallel` (has a `parallel` stage), `resumable`
|
|
587
|
+
(has a `checkpoint` stage), `deterministic` (references no declared effect).
|
|
588
|
+
|
|
589
|
+
Purity and determinism only reflect references to declared `effect`s and
|
|
590
|
+
`effectful fn`s (see [Pure and effectful functions](#pure-and-effectful-functions)): a stage
|
|
591
|
+
calling ordinary JavaScript such as `Math.random()` or `fetch()` directly is
|
|
592
|
+
still reported as `pure`. Route external work through effects to make it
|
|
593
|
+
visible. See
|
|
594
|
+
[example/src/application/explain-example.jojo](example/src/application/explain-example.jojo).
|
|
595
|
+
|
|
596
|
+
## Machine-readable execution plans
|
|
597
|
+
|
|
598
|
+
`jojo plan` gives every pipeline a stable, simple JSON representation, and
|
|
599
|
+
`jojo generate` turns such a plan back into JojoScript. A tool or an LLM can
|
|
600
|
+
read a program as data, or write a plan and have JojoScript validate it:
|
|
601
|
+
|
|
602
|
+
```
|
|
603
|
+
Jojo source -> jojo plan -> JSON plan -> LLM
|
|
604
|
+
LLM -> JSON plan -> jojo generate -> validated Jojo program
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
```jojo
|
|
608
|
+
export fn topCustomers(customers) {
|
|
609
|
+
return customers
|
|
610
|
+
|> filter(customer => customer.active)
|
|
611
|
+
|> sortBy(customer => -customer.spend)
|
|
612
|
+
|> take(2)
|
|
613
|
+
|> map(customer => customer.name)
|
|
614
|
+
|> toArray
|
|
615
|
+
}
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
```
|
|
619
|
+
$ jojo plan customers.jojo --format=json
|
|
620
|
+
{
|
|
621
|
+
"version": 1,
|
|
622
|
+
"pipelines": [
|
|
623
|
+
{
|
|
624
|
+
"pipeline": "topCustomers",
|
|
625
|
+
"id": "…",
|
|
626
|
+
"stages": [
|
|
627
|
+
{ "type": "source", "expression": "customers" },
|
|
628
|
+
{ "type": "filter", "expression": "customer => customer.active" },
|
|
629
|
+
{ "type": "sortBy", "expression": "customer => -customer.spend" },
|
|
630
|
+
{ "type": "take", "args": ["2"] },
|
|
631
|
+
{ "type": "map", "expression": "customer => customer.name" },
|
|
632
|
+
{ "type": "toArray" }
|
|
633
|
+
]
|
|
634
|
+
}
|
|
635
|
+
]
|
|
636
|
+
}
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
(Printed with two-space indentation; condensed here.)
|
|
640
|
+
|
|
641
|
+
The plan format, version 1:
|
|
642
|
+
|
|
643
|
+
| Field | Meaning |
|
|
644
|
+
|---|---|
|
|
645
|
+
| `version` | Always `1` for this format |
|
|
646
|
+
| `pipelines[].pipeline` | Inferred name (same rules as `jojo explain`), or `null` for an unnamed pipeline |
|
|
647
|
+
| `pipelines[].id` | The pipeline's deterministic hash (informational, ignored by `generate`) |
|
|
648
|
+
| `pipelines[].stages[0]` | `{ "type": "source", "expression": "..." }` |
|
|
649
|
+
| `parallel` / `batch` | `concurrency` / `size` as a positive integer |
|
|
650
|
+
| `checkpoint` | `name` as a string |
|
|
651
|
+
| `map`, `filter`, `flatMap`, `tap`, `mapAsync`, `filterAsync`, `find`, `some`, `every`, `sortBy`, `distinctBy`, `groupBy`, `partition`, `catch` | `expression`: the single function argument as source text |
|
|
652
|
+
| any other stage | `args`: argument source strings, omitted when there are none |
|
|
653
|
+
| `.prop` / `.method()` | `{ "type": ".length" }` reads a property; `{ "type": ".trim", "args": [] }` calls it |
|
|
654
|
+
| `id` (any stage after the source) | Optional [stage ID](#stage-ids) such as `"pick-carrier"`, unique within the pipeline |
|
|
655
|
+
|
|
656
|
+
A stage whose setting is not a literal (`parallel(limit)`) keeps it in `args`
|
|
657
|
+
instead, so a plan is always lossless. Arguments stay source text: the plan
|
|
658
|
+
describes computation without evaluating it.
|
|
659
|
+
|
|
660
|
+
`jojo generate plan.json` (or `planToJojo(plan)`) emits one
|
|
661
|
+
`name := source |> ...` statement per pipeline. It rejects unknown fields,
|
|
662
|
+
wrong types, misplaced `source` stages, and duplicate names with the exact path
|
|
663
|
+
of the problem (for example
|
|
664
|
+
`Invalid plan: pipelines[0].stages[2].concurrency must be a positive integer`),
|
|
665
|
+
then compiles the program and checks that it parses as JavaScript, without
|
|
666
|
+
running it. The generated program contains only the pipelines: their source
|
|
667
|
+
variables, custom stage functions, and effects must be supplied by
|
|
668
|
+
surrounding code. Validation is not a sandbox: expressions are code, so
|
|
669
|
+
review a generated program as you would any code you did not write.
|
|
670
|
+
|
|
671
|
+
`planPipelines(source)` and `planToJojo(plan)` are exported from
|
|
672
|
+
`@panagos/jojoscript`. See
|
|
673
|
+
[example/src/application/plan-example.jojo](example/src/application/plan-example.jojo).
|
|
674
|
+
|
|
675
|
+
## Checking programs
|
|
676
|
+
|
|
677
|
+
`jojo check` validates a program, or a JSON plan, without running it. It is
|
|
678
|
+
designed for LLM-generated code: every problem is a structured diagnostic with
|
|
679
|
+
a stable code, a line, and a concrete suggestion, instead of a raw stack trace.
|
|
680
|
+
|
|
681
|
+
```
|
|
682
|
+
$ jojo check generated.jojo
|
|
683
|
+
ERROR E301
|
|
684
|
+
line 8
|
|
685
|
+
Call divide(1, 0) violates "requires b != 0"
|
|
686
|
+
suggestion: Pass arguments to "divide" that satisfy b != 0
|
|
687
|
+
|
|
688
|
+
1 error, 0 warnings
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
`--machine` (or `--format json`) prints the same result as JSON:
|
|
692
|
+
|
|
693
|
+
```json
|
|
694
|
+
{
|
|
695
|
+
"valid": false,
|
|
696
|
+
"errors": [
|
|
697
|
+
{
|
|
698
|
+
"code": "E301",
|
|
699
|
+
"severity": "error",
|
|
700
|
+
"line": 8,
|
|
701
|
+
"message": "Call divide(1, 0) violates \"requires b != 0\"",
|
|
702
|
+
"suggestion": "Pass arguments to \"divide\" that satisfy b != 0"
|
|
703
|
+
}
|
|
704
|
+
],
|
|
705
|
+
"warnings": []
|
|
706
|
+
}
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
The command exits with code 1 when there is at least one error, so a
|
|
710
|
+
generate-check-fix loop can rely on the exit code.
|
|
711
|
+
|
|
712
|
+
| Code | Severity | Meaning |
|
|
713
|
+
|---|---|---|
|
|
714
|
+
| `E100` | error | The compiler rejects the program (with its line when known) |
|
|
715
|
+
| `E101` | error | The compiled JavaScript does not parse (checked by Node's own parser) |
|
|
716
|
+
| `W102` | warning | A declared effect is used but no `handle { ... }` in the file handles it; a caller may still install one |
|
|
717
|
+
| `E201` | error | A `pure fn` uses a declared effect or an `effectful fn` |
|
|
718
|
+
| `E301` | error | A call with literal arguments breaks a `requires` clause |
|
|
719
|
+
| `E401` | error | A `match` over a declared tagged union misses a variant and has no `_` arm |
|
|
720
|
+
| `E402` | error | A constructor pattern has a different number of fields than its variant |
|
|
721
|
+
| `E700` | error | `jojo check plan.json`: the plan is invalid (message names the exact field) |
|
|
722
|
+
|
|
723
|
+
Contract clauses are only evaluated statically when they use nothing but the
|
|
724
|
+
function's parameters, literals, and operators, so `jojo check` never runs
|
|
725
|
+
program code. `checkSource(source)`, `checkPlan(plan)`, and
|
|
726
|
+
`renderCheckText(result)` are exported from `@panagos/jojoscript`. See
|
|
727
|
+
[example/src/application/check-example.jojo](example/src/application/check-example.jojo).
|
|
728
|
+
|
|
729
|
+
## Describing programs
|
|
730
|
+
|
|
731
|
+
`jojo describe` prints one self-describing JSON document for a file, so a tool
|
|
732
|
+
or an LLM can learn what a program contains without reading its source:
|
|
733
|
+
|
|
734
|
+
```json
|
|
735
|
+
{
|
|
736
|
+
"language": "jojo",
|
|
737
|
+
"version": "1",
|
|
738
|
+
"pipelines": [
|
|
739
|
+
{
|
|
740
|
+
"name": "openInvoices",
|
|
741
|
+
"inputs": ["records"],
|
|
742
|
+
"effects": [],
|
|
743
|
+
"stages": [
|
|
744
|
+
{ "id": "unpaid", "operation": "filter", "expression": "record => !record.paid" },
|
|
745
|
+
{ "id": "classify", "operation": "map", "expression": "toInvoice" },
|
|
746
|
+
{ "id": "node-3", "operation": "checkpoint", "name": "open-invoices" }
|
|
747
|
+
],
|
|
748
|
+
"properties": { "lazy": true, "parallel": false, "resumable": true, "deterministic": true }
|
|
749
|
+
}
|
|
750
|
+
],
|
|
751
|
+
"effects": [{ "name": "storeInvoice", "params": ["invoice"], "handled": true }],
|
|
752
|
+
"functions": [
|
|
753
|
+
{ "name": "toInvoice", "line": 12, "purity": "pure", "requires": ["record.amount >= 0"], "ensures": [] }
|
|
754
|
+
],
|
|
755
|
+
"types": [
|
|
756
|
+
{ "name": "Invoice", "variants": [{ "name": "Open", "fields": ["id", "amount"] }, { "name": "Settled", "fields": ["id"] }] }
|
|
757
|
+
]
|
|
758
|
+
}
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
- `pipelines` uses the same names, effects, and properties as `jojo explain`,
|
|
762
|
+
and the same stage fields as `jojo plan`. A pipeline whose source is an
|
|
763
|
+
identifier lists it in `inputs`. A stage's `id` is its
|
|
764
|
+
[stage ID](#stage-ids), or the runtime node id (`node-3`) used by
|
|
765
|
+
`jojo graph` and `jojo profile`.
|
|
766
|
+
- `effects` lists declared effects and whether a handler is statically
|
|
767
|
+
visible in the file.
|
|
768
|
+
- `functions` lists functions that declare a purity or contracts.
|
|
769
|
+
- `types` lists tagged unions with their variants and fields.
|
|
770
|
+
|
|
771
|
+
`--pipeline <name>` limits `pipelines` to one pipeline. `describeSource(source)`
|
|
772
|
+
is exported from `@panagos/jojoscript`. See
|
|
773
|
+
[example/src/application/describe-example.jojo](example/src/application/describe-example.jojo).
|
|
774
|
+
|
|
775
|
+
## Parsing programs
|
|
776
|
+
|
|
777
|
+
`jojo parse <file.jojo> --json` prints the syntax-level view of a file: every
|
|
778
|
+
JojoScript construct the compiler recognizes, with its line, ordered by line.
|
|
779
|
+
Where `jojo describe` summarizes what a program means, `jojo parse` lists what
|
|
780
|
+
it contains, so a tool or an LLM can locate and edit constructs precisely.
|
|
781
|
+
|
|
782
|
+
```json
|
|
783
|
+
{
|
|
784
|
+
"language": "jojo",
|
|
785
|
+
"version": "1",
|
|
786
|
+
"nodes": [
|
|
787
|
+
{ "kind": "type", "line": 6, "name": "Decision", "exported": true,
|
|
788
|
+
"variants": [{ "name": "Approved", "fields": ["id"] }, { "name": "Escalated", "fields": ["id", "team"] }] },
|
|
789
|
+
{ "kind": "function", "line": 10, "name": "decide", "params": ["refund"], "exported": false,
|
|
790
|
+
"async": false, "purity": null, "body": "expression", "requires": [], "ensures": [] },
|
|
791
|
+
{ "kind": "match", "line": 13, "patterns": ["Approved(id)", "Escalated(id, team)"] },
|
|
792
|
+
{ "kind": "pipeline", "line": 20, "name": "routeRefunds", "declared": false, "source": "refunds",
|
|
793
|
+
"stages": [{ "type": "map", "args": ["decide"], "id": null }] }
|
|
794
|
+
]
|
|
795
|
+
}
|
|
796
|
+
```
|
|
797
|
+
|
|
798
|
+
| Kind | Fields |
|
|
799
|
+
|---|---|
|
|
800
|
+
| `import` | `from`, `names` |
|
|
801
|
+
| `effect` | `name`, `params` |
|
|
802
|
+
| `type` | `name`, `exported`, `variants` (`name`, `fields`) |
|
|
803
|
+
| `function` | `name`, `params` (as written, including defaults), `exported`, `async`, `purity`, `body` (`"block"` or `"expression"`), `requires`, `ensures` |
|
|
804
|
+
| `pipeline` | `name`, `declared` (a `pipeline` block), `source`, `stages` (`type`, `args`, `id`) |
|
|
805
|
+
| `handle` | `handlers` |
|
|
806
|
+
| `match` | `patterns` |
|
|
807
|
+
|
|
808
|
+
JojoScript has no AST by design: nodes come from the same finders the compiler
|
|
809
|
+
uses, so `parse` never disagrees with what compiles. Invalid syntax is
|
|
810
|
+
reported as a single `jojo:` error line. `parseSource(source)` is exported
|
|
811
|
+
from `@panagos/jojoscript`. See
|
|
812
|
+
[example/src/application/parse-example.jojo](example/src/application/parse-example.jojo).
|
|
813
|
+
|
|
814
|
+
## Simulating pipelines
|
|
815
|
+
|
|
816
|
+
`jojo simulate` answers "what would this pipeline do with N records?" before
|
|
817
|
+
anything runs. No stage or effect is executed:
|
|
818
|
+
|
|
819
|
+
```
|
|
820
|
+
$ jojo simulate prices.jojo --input 250000 --pipeline pricedProducts
|
|
821
|
+
SIMULATION pricedProducts
|
|
822
|
+
|
|
823
|
+
INPUT
|
|
824
|
+
250,000 records
|
|
825
|
+
|
|
826
|
+
OPERATIONS
|
|
827
|
+
2 filter 250,000 items in
|
|
828
|
+
3 parallel ≤250,000 items in, 16 workers
|
|
829
|
+
4 map ≤250,000 items in, ≤250,000 calls to fetchPrice
|
|
830
|
+
5 checkpoint ≤250,000 items in, saved as "priced"
|
|
831
|
+
|
|
832
|
+
POTENTIAL PROBLEMS
|
|
833
|
+
⚠ stage 4: up to 250,000 calls to effect "fetchPrice"
|
|
834
|
+
⚠ stage 4: effect "fetchPrice" runs before checkpoint "priced": items processed before a failure are fetched again on retry (at-least-once)
|
|
835
|
+
```
|
|
836
|
+
|
|
837
|
+
Stages are numbered as in `jojo explain`, where the source is stage 1.
|
|
838
|
+
Item counts flow through the stages as upper bounds (`≤` once a filtering
|
|
839
|
+
stage such as `filter`, `unique`, or `distinctBy` has run). `take`, `skip`,
|
|
840
|
+
`chunk`, `batch`, and `window` change them exactly; `flatMap` and `groupBy`
|
|
841
|
+
make them unknown; `count`, `first`, `find`, `some`, `every`, and `reduce`
|
|
842
|
+
produce one result. A stage that uses a declared effect or an `effectful fn`
|
|
843
|
+
makes one call per item it receives.
|
|
844
|
+
|
|
845
|
+
Potential problems reported:
|
|
846
|
+
|
|
847
|
+
- 1,000 or more calls to one effect;
|
|
848
|
+
- an effect that runs before a checkpoint, and so may run again for items
|
|
849
|
+
processed before a failure (see
|
|
850
|
+
[Effects and checkpoint/resume](#effects-and-checkpointresume));
|
|
851
|
+
- a pipeline with effects but no checkpoint at all;
|
|
852
|
+
- `sort`, `sortBy`, `groupBy`, or `partition` holding 10,000 or more items in
|
|
853
|
+
memory.
|
|
854
|
+
|
|
855
|
+
The input defaults to 1,000 records. `--format json` prints the simulation as
|
|
856
|
+
data, and `--pipeline <name>` selects one pipeline. `simulatePipelines(source,
|
|
857
|
+
{ input })` and `renderSimulationText(simulation)` are exported from
|
|
858
|
+
`@panagos/jojoscript`. See
|
|
859
|
+
[example/src/application/simulate-example.jojo](example/src/application/simulate-example.jojo).
|
|
860
|
+
|
|
861
|
+
## Asking why
|
|
862
|
+
|
|
863
|
+
`jojo why` turns the static analysis into plain-language answers, numbered
|
|
864
|
+
like `jojo explain` (the source is stage 1):
|
|
865
|
+
|
|
866
|
+
```
|
|
867
|
+
$ jojo why onboarding.jojo --pipeline verifiedCustomers
|
|
868
|
+
WHY verifiedCustomers
|
|
869
|
+
|
|
870
|
+
Why is this pipeline not deterministic?
|
|
871
|
+
→ Stage 3 (mapAsync) uses effect verifyEmail; stage 5 (mapAsync) uses effectful function sendWelcome.
|
|
872
|
+
|
|
873
|
+
Why is this pipeline lazy?
|
|
874
|
+
→ Every stage passes items on one at a time, so no stage waits for the complete input.
|
|
875
|
+
|
|
876
|
+
Why is this pipeline resumable?
|
|
877
|
+
→ Checkpoint "verified" (stage 4) saves its items once they all complete; a rerun restores them instead of repeating stages 2-3.
|
|
878
|
+
|
|
879
|
+
Why can't stage 3 be moved before stage 2?
|
|
880
|
+
→ Stage 3 (mapAsync) uses effect verifyEmail and stage 2 (filter) decides which items reach it: moving it earlier would also run it for items that stage 2 drops.
|
|
881
|
+
|
|
882
|
+
Why does checkpoint "verified" not guarantee replay safety?
|
|
883
|
+
→ Effect verifyEmail runs before it (stage 3): if a run fails before "verified" completes, it runs again on retry (at-least-once, not exactly-once).
|
|
884
|
+
|
|
885
|
+
Why can stage 5 repeat after a failure?
|
|
886
|
+
→ Stage 5 (mapAsync) uses effectful function sendWelcome after the last checkpoint "verified": a rerun restores the checkpoint and runs it again for every restored item.
|
|
887
|
+
```
|
|
888
|
+
|
|
889
|
+
Every pipeline gets answers about determinism, laziness, and resumability.
|
|
890
|
+
Additional questions appear when they apply: an effectful stage behind a
|
|
891
|
+
filtering stage (`filter`, `filterAsync`, `unique`, `distinctBy`, `take`,
|
|
892
|
+
`skip`) cannot be moved before it, a `parallel` stage explains what it
|
|
893
|
+
parallelizes, and checkpoints explain which effects can run again on retry.
|
|
894
|
+
Answers only reflect declared effects and `effectful fn`s. `--format json`
|
|
895
|
+
prints `[{ name, answers: [{ question, answer }] }]`, and
|
|
896
|
+
`whyPipelines(source)` / `renderWhyText(why)` are exported from
|
|
897
|
+
`@panagos/jojoscript`. See
|
|
898
|
+
[example/src/application/why-example.jojo](example/src/application/why-example.jojo).
|
|
899
|
+
|
|
329
900
|
## Effect Handlers
|
|
330
901
|
|
|
331
902
|
> This is a deliberately simplified effect-handler model, not full
|
|
@@ -592,6 +1163,51 @@ declared itself `deterministic`, and the declaring effect's `sideEffecting`/
|
|
|
592
1163
|
`replayable` metadata (from `effect name(params)`'s options, or defaults —
|
|
593
1164
|
`sideEffecting: true`, `replayable: false`).
|
|
594
1165
|
|
|
1166
|
+
### Effect authorization
|
|
1167
|
+
|
|
1168
|
+
Applications can place a capability policy around a `handle` block with
|
|
1169
|
+
`withAuthorization(policy, fn)`. The policy receives the declared effect
|
|
1170
|
+
name, a shallow copy of its arguments, declared effect metadata, and the
|
|
1171
|
+
current execution context. It must explicitly return `true` or
|
|
1172
|
+
`{ allowed: true }` for the handler to run.
|
|
1173
|
+
|
|
1174
|
+
```js
|
|
1175
|
+
import { withAuthorization } from "@panagos/jojoscript/runtime"
|
|
1176
|
+
|
|
1177
|
+
const result = await withAuthorization(
|
|
1178
|
+
invocation => invocation.name === "readTender"
|
|
1179
|
+
? true
|
|
1180
|
+
: { allowed: false, reason: "Capability is not granted" },
|
|
1181
|
+
() => withHandlers({
|
|
1182
|
+
readTender: id => ({ id }),
|
|
1183
|
+
approveOffer: offer => approve(offer)
|
|
1184
|
+
}, () => program())
|
|
1185
|
+
)
|
|
1186
|
+
```
|
|
1187
|
+
|
|
1188
|
+
Authorization is opt-in; programs that do not use `withAuthorization` retain
|
|
1189
|
+
their existing unrestricted effect behavior. When enabled, every active
|
|
1190
|
+
authorization scope must allow each resolved effect invocation before its
|
|
1191
|
+
handler starts. This means an inner `handle` block cannot bypass an outer
|
|
1192
|
+
policy. A missing, malformed, denied, or failed policy decision throws
|
|
1193
|
+
`EffectAuthorizationError`; denial is non-retryable, so `retry()` does not
|
|
1194
|
+
repeat it. An unhandled effect still throws `UnhandledEffectError` before any
|
|
1195
|
+
authorization policy runs.
|
|
1196
|
+
|
|
1197
|
+
Authorization is evaluated before normal handlers, recording wrappers, and
|
|
1198
|
+
replay wrappers. Replay therefore does not bypass a policy, and a checkpoint
|
|
1199
|
+
resume only avoids authorization when it restores a completed checkpoint
|
|
1200
|
+
without invoking an upstream effect at all. Policies do not record arguments
|
|
1201
|
+
or decisions automatically, and this mechanism is not a sandbox: ordinary
|
|
1202
|
+
JavaScript and operations that do not pass through a declared effect are
|
|
1203
|
+
outside its enforcement boundary. Do not derive permission from model or
|
|
1204
|
+
remote-agent output; make policy decisions in trusted application code.
|
|
1205
|
+
|
|
1206
|
+
See
|
|
1207
|
+
[example/src/effects/effect-authorization.jojo](example/src/effects/effect-authorization.jojo)
|
|
1208
|
+
for a procurement workflow that permits reads and supplier lookup while
|
|
1209
|
+
blocking invitations and offer approval before their handlers run.
|
|
1210
|
+
|
|
595
1211
|
### Effects and checkpoint/resume
|
|
596
1212
|
|
|
597
1213
|
Consider:
|
|
@@ -629,6 +1245,13 @@ actually safe. See
|
|
|
629
1245
|
[example/src/effects/checkpoint-resume.jojo](example/src/effects/checkpoint-resume.jojo)
|
|
630
1246
|
and `test/effects-checkpoint.test.js` for both cases exercised end to end.
|
|
631
1247
|
|
|
1248
|
+
File-backed checkpoint records are validated when they are loaded: their
|
|
1249
|
+
pipeline ID and checkpoint ID must match the requested checkpoint, `items`
|
|
1250
|
+
must be an array, and a present `itemCount` must match the saved item count.
|
|
1251
|
+
Malformed records fail clearly rather than resuming with untrusted data. See
|
|
1252
|
+
[example/src/effects/checkpoint-integrity.jojo](example/src/effects/checkpoint-integrity.jojo)
|
|
1253
|
+
for a compiled pipeline test of this behavior.
|
|
1254
|
+
|
|
632
1255
|
### Record/replay
|
|
633
1256
|
|
|
634
1257
|
A handler map can be wrapped to record every call (arguments + result) to
|
|
@@ -653,6 +1276,44 @@ interface (`record`/`replayNext`), mirroring `CheckpointStore` — an
|
|
|
653
1276
|
extension point for a future backend, not a full production-grade
|
|
654
1277
|
persistence system.
|
|
655
1278
|
|
|
1279
|
+
### Deterministic execution mode
|
|
1280
|
+
|
|
1281
|
+
`now()` and `random()` are built-in capabilities, like `parallel()`. Outside
|
|
1282
|
+
any handler they behave like `Date.now()` and `Math.random()`, and a
|
|
1283
|
+
`handle { TIME: ..., RANDOM: ... }` block can replace them:
|
|
1284
|
+
|
|
1285
|
+
```jojo
|
|
1286
|
+
import { now, random } from "@panagos/jojoscript/runtime"
|
|
1287
|
+
|
|
1288
|
+
order := { id: Math.round(random() * 1000), createdAt: new Date(now()) }
|
|
1289
|
+
```
|
|
1290
|
+
|
|
1291
|
+
`deterministic(fn, { seed, startTime })` runs `fn` so that the same inputs
|
|
1292
|
+
always produce the same outputs:
|
|
1293
|
+
|
|
1294
|
+
- `random()` is a seeded generator (`seed` defaults to `1`);
|
|
1295
|
+
- `now()` starts at `startTime` (default `0`) and advances 1ms per read;
|
|
1296
|
+
- `parallel()` stages process one item at a time, in order;
|
|
1297
|
+
- every declared effect must resolve to a handler declared deterministic
|
|
1298
|
+
(`{ handle, deterministic: true }`, or `replayHandlers(...)`); any other
|
|
1299
|
+
handler throws a non-retryable `NondeterministicEffectError` before it runs;
|
|
1300
|
+
- checkpoints record `now()` as their `createdAt`, so reruns write identical
|
|
1301
|
+
checkpoints.
|
|
1302
|
+
|
|
1303
|
+
```js
|
|
1304
|
+
import { deterministic } from "@panagos/jojoscript/runtime"
|
|
1305
|
+
|
|
1306
|
+
const first = await deterministic(() => generateOrders(3), { seed: 2026 })
|
|
1307
|
+
const second = await deterministic(() => generateOrders(3), { seed: 2026 })
|
|
1308
|
+
// first and second are deep-equal
|
|
1309
|
+
```
|
|
1310
|
+
|
|
1311
|
+
`jojo run app.jojo --deterministic [--seed <n>]` runs a whole program, top-level
|
|
1312
|
+
code included, in this mode. Determinism covers JojoScript's capabilities:
|
|
1313
|
+
direct calls to `Date.now()`, `Math.random()`, or I/O outside declared effects
|
|
1314
|
+
are not intercepted. See
|
|
1315
|
+
[example/src/application/deterministic-example.jojo](example/src/application/deterministic-example.jojo).
|
|
1316
|
+
|
|
656
1317
|
### JavaScript interoperability
|
|
657
1318
|
|
|
658
1319
|
A handler is just a normal JavaScript function — sync or async — and can
|
|
@@ -739,6 +1400,38 @@ never through `|>`) and also uses pipeline syntax with overlapping names,
|
|
|
739
1400
|
the compiler merges the auto-imported names into the existing import
|
|
740
1401
|
instead of emitting a second, colliding import declaration.
|
|
741
1402
|
|
|
1403
|
+
### Deterministic serialization
|
|
1404
|
+
|
|
1405
|
+
`toJson(value, { indent })` and `fromJson(text)` give JojoScript values one
|
|
1406
|
+
canonical JSON form:
|
|
1407
|
+
|
|
1408
|
+
```jojo
|
|
1409
|
+
import { toJson, fromJson } from "@panagos/jojoscript/runtime"
|
|
1410
|
+
|
|
1411
|
+
snapshot := toJson({ name: "John", id: 42, placedAt: new Date(0) })
|
|
1412
|
+
// {"id":42,"name":"John","placedAt":{"$type":"Date","value":"1970-01-01T00:00:00.000Z"}}
|
|
1413
|
+
|
|
1414
|
+
order := fromJson(snapshot) // order.placedAt is a Date again
|
|
1415
|
+
```
|
|
1416
|
+
|
|
1417
|
+
- Object keys are sorted recursively, so equal values always serialize to
|
|
1418
|
+
identical text, whatever order their properties were created in.
|
|
1419
|
+
- Values plain JSON would lose or change are written as `{ "$type": ... }`
|
|
1420
|
+
objects: `Date`, `Map`, `Set`, `BigInt`, `undefined`, `NaN`, `Infinity`,
|
|
1421
|
+
`-Infinity`, `-0`, and `Error` (name, message, own fields such as a
|
|
1422
|
+
`ContractError`'s `kind`, and `cause`). `fromJson` restores each exactly.
|
|
1423
|
+
- An ordinary object that has its own `$type` key is wrapped, so tags are
|
|
1424
|
+
never ambiguous. Tagged union values are plain objects and serialize as
|
|
1425
|
+
they are. Objects with a `toJSON` method serialize its result.
|
|
1426
|
+
- Functions, symbols, and circular structures throw a `TypeError`.
|
|
1427
|
+
|
|
1428
|
+
`FileCheckpointStore` writes and reads checkpoints in this format, so
|
|
1429
|
+
checkpointed dates, maps, and sets resume as themselves. Checkpoint files
|
|
1430
|
+
written as plain JSON before this change still load. Pipelines and execution
|
|
1431
|
+
plans have their own canonical form: the [`jojo plan`](#machine-readable-execution-plans)
|
|
1432
|
+
JSON. See
|
|
1433
|
+
[example/src/application/serialization-example.jojo](example/src/application/serialization-example.jojo).
|
|
1434
|
+
|
|
742
1435
|
### MCP support
|
|
743
1436
|
|
|
744
1437
|
JojoScript includes a small MCP-style JSON-RPC runtime for Node.js. It does
|
|
@@ -749,18 +1442,121 @@ JavaScript functions.
|
|
|
749
1442
|
import { createMcpServer, runMcpStdioServer } from "jojoscript/runtime"
|
|
750
1443
|
|
|
751
1444
|
const server = createMcpServer({
|
|
1445
|
+
name: "example-server",
|
|
1446
|
+
version: "1.0.0",
|
|
752
1447
|
tools: {
|
|
753
|
-
echo:
|
|
1448
|
+
echo: {
|
|
1449
|
+
description: "Returns the supplied value.",
|
|
1450
|
+
inputSchema: {
|
|
1451
|
+
type: "object",
|
|
1452
|
+
properties: {
|
|
1453
|
+
value: { type: "string" }
|
|
1454
|
+
},
|
|
1455
|
+
required: ["value"],
|
|
1456
|
+
additionalProperties: false
|
|
1457
|
+
},
|
|
1458
|
+
handler: args => ({
|
|
1459
|
+
content: [{ type: "text", text: args.value }]
|
|
1460
|
+
})
|
|
1461
|
+
}
|
|
754
1462
|
}
|
|
755
1463
|
})
|
|
756
1464
|
|
|
757
1465
|
await runMcpStdioServer(server)
|
|
758
1466
|
```
|
|
759
1467
|
|
|
760
|
-
The server
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
1468
|
+
The server supports the `initialize` handshake (protocol version
|
|
1469
|
+
`2025-06-18`), `notifications/initialized`, `ping`, `tools/list`, and
|
|
1470
|
+
`tools/call` over newline-delimited JSON-RPC on standard input/output. Clients
|
|
1471
|
+
must initialize before making requests. Tools declare an `inputSchema`; calls
|
|
1472
|
+
that do not conform are rejected before the handler runs. Handlers return a
|
|
1473
|
+
`CallToolResult` object with a `content` array and may return
|
|
1474
|
+
`structuredContent` when their tool declares an `outputSchema`. Thrown handler
|
|
1475
|
+
errors are returned as `CallToolResult` values with `isError: true`.
|
|
1476
|
+
|
|
1477
|
+
It is intentionally minimal: supporting tool registration, schema validation,
|
|
1478
|
+
request dispatch, structured errors, and the stdin/stdout transport without
|
|
1479
|
+
external NPM packages. It remains an MCP-style runtime until its supported
|
|
1480
|
+
protocol surface has been verified against MCP conformance tests and real
|
|
1481
|
+
clients.
|
|
1482
|
+
|
|
1483
|
+
### A2A client
|
|
1484
|
+
|
|
1485
|
+
JojoScript provides a dependency-free A2A 1.0 JSON-RPC client. It discovers a
|
|
1486
|
+
remote agent through `/.well-known/agent-card.json`, selects its advertised
|
|
1487
|
+
JSON-RPC interface, sends messages, and retrieves/polls task results.
|
|
1488
|
+
|
|
1489
|
+
```js
|
|
1490
|
+
import { createA2AClient } from "jojoscript/runtime"
|
|
1491
|
+
|
|
1492
|
+
const agent = createA2AClient("https://agent.example")
|
|
1493
|
+
await agent.discover()
|
|
1494
|
+
|
|
1495
|
+
const task = await agent.sendMessage({
|
|
1496
|
+
role: "ROLE_USER",
|
|
1497
|
+
parts: [{ kind: "data", data: { record: { id: "item-1" } } }],
|
|
1498
|
+
metadata: { requestId: "request-1" }
|
|
1499
|
+
})
|
|
1500
|
+
const result = await agent.waitForTask(task, {
|
|
1501
|
+
pollIntervalMs: 500,
|
|
1502
|
+
maxPollIntervalMs: 5000,
|
|
1503
|
+
timeoutMs: 30000
|
|
1504
|
+
})
|
|
1505
|
+
```
|
|
1506
|
+
|
|
1507
|
+
`sendMessage`, `getTask`, and `cancelTask` map to the A2A `SendMessage`,
|
|
1508
|
+
`GetTask`, and `CancelTask` operations. Messages, task IDs, context IDs,
|
|
1509
|
+
metadata, typed parts, and artifacts are returned unchanged. `waitForTask`
|
|
1510
|
+
accepts either a task ID or an in-progress task, then returns the final task
|
|
1511
|
+
for every terminal state—including failed, rejected, canceled,
|
|
1512
|
+
input-required, and authentication-required tasks—so callers must inspect the
|
|
1513
|
+
state before treating it as successful.
|
|
1514
|
+
|
|
1515
|
+
Waiting uses a five-minute default timeout, configurable polling and bounded
|
|
1516
|
+
backoff (`pollIntervalMs`, `maxPollIntervalMs`, and `backoffMultiplier`), and
|
|
1517
|
+
an optional `AbortSignal`. It rejects with an `A2AError` whose `code` clearly
|
|
1518
|
+
identifies timeouts, cancellation, network/HTTP failures, invalid discovery or
|
|
1519
|
+
JSON-RPC responses, and remote-agent errors. Pass `bearerToken`, custom
|
|
1520
|
+
headers, or an alternate `fetch` function in the client options. Discovery
|
|
1521
|
+
validates advertised JSON-RPC endpoints and only permits credential-free HTTP
|
|
1522
|
+
or HTTPS URLs.
|
|
1523
|
+
|
|
1524
|
+
JojoScript intentionally provides a lightweight A2A client rather than an
|
|
1525
|
+
A2A SDK or agent server. Its protocol behavior is covered by local fixtures;
|
|
1526
|
+
real-agent interoperability remains a separate integration concern.
|
|
1527
|
+
|
|
1528
|
+
### Resumable MCP-to-A2A enrichment
|
|
1529
|
+
|
|
1530
|
+
[example/src/application/mcp-a2a-resumable-enrichment.jojo](example/src/application/mcp-a2a-resumable-enrichment.jojo)
|
|
1531
|
+
combines the MCP server, A2A client, asynchronous pipelines, and checkpoints
|
|
1532
|
+
in one tool. `enrich_records` accepts a batch of `{ id, title }` records,
|
|
1533
|
+
delegates enrichment to an A2A agent, checkpoints a completed batch, and
|
|
1534
|
+
returns both text content and structured records to the MCP client.
|
|
1535
|
+
|
|
1536
|
+
```js
|
|
1537
|
+
import {
|
|
1538
|
+
FileCheckpointStore,
|
|
1539
|
+
runMcpStdioServer
|
|
1540
|
+
} from "jojoscript/runtime"
|
|
1541
|
+
import { createEnrichmentServer } from "./application/mcp-a2a-resumable-enrichment.js"
|
|
1542
|
+
|
|
1543
|
+
const server = createEnrichmentServer({
|
|
1544
|
+
agentUrl: process.env.A2A_AGENT_URL,
|
|
1545
|
+
checkpointStore: new FileCheckpointStore(".jojo-checkpoints")
|
|
1546
|
+
})
|
|
1547
|
+
|
|
1548
|
+
await runMcpStdioServer(server)
|
|
1549
|
+
```
|
|
1550
|
+
|
|
1551
|
+
Use a stable `jobId` for retries of the same batch. The checkpoint is saved
|
|
1552
|
+
only after every record has been enriched, so retrying a completed batch uses
|
|
1553
|
+
the saved records without calling the A2A agent again. This is **at-least-once
|
|
1554
|
+
delegation**, not exactly-once: if an A2A agent completes a record and the
|
|
1555
|
+
process fails before the enclosing batch checkpoint commits, the retry invokes
|
|
1556
|
+
that agent again. Remote agents should therefore use idempotency keys derived
|
|
1557
|
+
from the record and job ID when duplicate work is unsafe. The matching
|
|
1558
|
+
integration test demonstrates both the duplicate-before-commit case and the
|
|
1559
|
+
no-repeat-after-commit case.
|
|
764
1560
|
|
|
765
1561
|
## Example
|
|
766
1562
|
|
|
@@ -777,6 +1573,10 @@ running the same program with multiple handlers, deterministic testing,
|
|
|
777
1573
|
`parallel()` as an effect, effect visibility in the execution plan, and the
|
|
778
1574
|
checkpoint/resume interaction.
|
|
779
1575
|
|
|
1576
|
+
[example/src/application/a2a-enrichment.jojo](example/src/application/a2a-enrichment.jojo)
|
|
1577
|
+
demonstrates enriching a record through A2A discovery, message dispatch, and
|
|
1578
|
+
task polling against a local test agent.
|
|
1579
|
+
|
|
780
1580
|
```bash
|
|
781
1581
|
cd example
|
|
782
1582
|
npm install
|
|
@@ -804,10 +1604,36 @@ const total = users
|
|
|
804
1604
|
boundaries because they need the complete input. See
|
|
805
1605
|
[ITERATORS.md](ITERATORS.md) for the complete proposal and memory model.
|
|
806
1606
|
|
|
1607
|
+
## Formatting
|
|
1608
|
+
|
|
1609
|
+
`jojo format <file.jojo|directory>` reformats files in place and prints each
|
|
1610
|
+
file it changed. `--check` changes nothing: it lists the files that need
|
|
1611
|
+
formatting and exits with code 1 if there are any, which suits CI and
|
|
1612
|
+
generate-format-check loops.
|
|
1613
|
+
|
|
1614
|
+
The formatter is line-based, like the compiler: it normalizes indentation and
|
|
1615
|
+
whitespace but never rewrites expressions.
|
|
1616
|
+
|
|
1617
|
+
- A line that opens brackets indents their contents one level, however many
|
|
1618
|
+
it opens (`posts.reduce((groups, post) => {`).
|
|
1619
|
+
- `|>` stages, `requires` / `ensures` clauses, `| Variant` lines, ternary
|
|
1620
|
+
`?` / `:` lines, leading `.method()` chains, and the line after a trailing
|
|
1621
|
+
`:=` or `=>` are indented one extra level. Stages inside a
|
|
1622
|
+
`pipeline name { ... }` block line up with its `source` line.
|
|
1623
|
+
- Trailing whitespace is removed, Windows line endings become `\n`, runs of
|
|
1624
|
+
blank lines collapse to one, and the file ends with a single newline.
|
|
1625
|
+
- Block comments and template literal text are left untouched.
|
|
1626
|
+
|
|
1627
|
+
`formatSource(source, { indent })` is exported from `@panagos/jojoscript`, and
|
|
1628
|
+
the VS Code extension's `Format Document` uses the same formatter. Every file
|
|
1629
|
+
under `example/src` is kept formatted by a test.
|
|
1630
|
+
|
|
807
1631
|
## Editor support
|
|
808
1632
|
|
|
809
1633
|
See [editors/vscode](editors/vscode) for a VS Code extension providing syntax
|
|
810
|
-
highlighting
|
|
1634
|
+
highlighting for all JojoScript syntax (pipelines, stage IDs, named
|
|
1635
|
+
pipelines, `match` patterns, tagged unions, effects, purity, and contracts)
|
|
1636
|
+
and a `Format Document` provider for `.jojo` files. It is not
|
|
811
1637
|
published to the marketplace; package it with `vsce package` and install the
|
|
812
1638
|
resulting `.vsix` locally.
|
|
813
1639
|
|
|
@@ -815,8 +1641,6 @@ resulting `.vsix` locally.
|
|
|
815
1641
|
|
|
816
1642
|
- **No source maps.** Stack traces from compiled `dist/*.js` point at the
|
|
817
1643
|
generated JavaScript, not the original `.jojo` source.
|
|
818
|
-
- **No `jojo fmt` CLI command.** Formatting is only available through the
|
|
819
|
-
VS Code extension's `Format Document` command, not the `jojo` CLI.
|
|
820
1644
|
- **No regex-literal awareness in the lexer.** A regex containing `{`, `}`,
|
|
821
1645
|
`(`, or `)` can confuse brace/paren-depth-sensitive transforms (pipelines,
|
|
822
1646
|
`match`, destructuring, type annotations). Prefer `new RegExp(...)` in code
|
|
@@ -845,8 +1669,7 @@ resulting `.vsix` locally.
|
|
|
845
1669
|
actually consumed.** Constructing a lazy pipeline inside a `handle` block
|
|
846
1670
|
without consuming it there (no `toArray`/`toArrayAsync`/`reduce`/...) means
|
|
847
1671
|
effects pulled later, outside the block, won't see its handlers.
|
|
848
|
-
-
|
|
849
|
-
mentioned as a long-term direction (`IO`, `HTTP`, `FILE`,
|
|
850
|
-
`
|
|
1672
|
+
- **`PARALLEL`, `TIME`, and `RANDOM` are the only built-in effects.** The
|
|
1673
|
+
other capabilities mentioned as a long-term direction (`IO`, `HTTP`, `FILE`,
|
|
1674
|
+
`DATABASE`, `SLEEP`, `CHECKPOINT`) are not implemented; user-defined
|
|
851
1675
|
`effect` declarations cover the same need today.
|
|
852
|
-
|