melusine 0.1.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 +249 -0
- package/SPEC.md +145 -0
- package/dist/catalog.d.ts +104 -0
- package/dist/cli.d.ts +27 -0
- package/dist/compile.d.ts +1 -0
- package/dist/generate.d.ts +11 -0
- package/dist/index.d.ts +23 -0
- package/dist/parse.d.ts +27 -0
- package/dist/run.d.ts +54 -0
- package/dist/scaffold.d.ts +18 -0
- package/dist/types.d.ts +542 -0
- package/dist/validate.d.ts +28 -0
- package/dist/walk.d.ts +24 -0
- package/examples/README.md +21 -0
- package/examples/onboarding/README.md +16 -0
- package/examples/onboarding/catalog.js +29 -0
- package/examples/onboarding/generated/onboarding.test.mjs +15 -0
- package/examples/onboarding/journey.md +18 -0
- package/examples/onboarding/onboarding-session.mjs +21 -0
- package/examples/order-fulfillment/README.md +16 -0
- package/examples/order-fulfillment/catalog.js +59 -0
- package/examples/order-fulfillment/generated/order-fulfillment.test.mjs +15 -0
- package/examples/order-fulfillment/journey.md +32 -0
- package/examples/order-fulfillment/order-workflow.mjs +48 -0
- package/examples/vending/README.md +16 -0
- package/examples/vending/catalog.js +32 -0
- package/examples/vending/generated/vending.test.mjs +15 -0
- package/examples/vending/journey.md +21 -0
- package/examples/vending/vending-machine.mjs +16 -0
- package/package.json +39 -0
- package/src/catalog.js +485 -0
- package/src/cli.js +331 -0
- package/src/compile.js +3 -0
- package/src/generate.js +52 -0
- package/src/index.js +28 -0
- package/src/parse.js +263 -0
- package/src/run.js +258 -0
- package/src/scaffold.js +142 -0
- package/src/types.js +330 -0
- package/src/validate.js +171 -0
- package/src/walk.js +57 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Order Fulfillment Example
|
|
2
|
+
|
|
3
|
+
This example shows nested decisions: stock, payment, and fulfillment speed.
|
|
4
|
+
|
|
5
|
+
Files:
|
|
6
|
+
|
|
7
|
+
- `journey.md` defines the Mermaid flow and setup args.
|
|
8
|
+
- `catalog.js` exposes reusable order tasks and scorers.
|
|
9
|
+
- `order-workflow.mjs` is the toy system under test.
|
|
10
|
+
- `generated/order-fulfillment.test.mjs` is the generated wrapper.
|
|
11
|
+
|
|
12
|
+
Run it:
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
node src/cli.js test examples/order-fulfillment/journey.md --catalog examples/order-fulfillment/catalog.js
|
|
16
|
+
```
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { scorer, task } from 'melusine';
|
|
4
|
+
import { OrderWorkflow } from './order-workflow.mjs';
|
|
5
|
+
|
|
6
|
+
const expectedExpressEvents = 'reserved,paid,express';
|
|
7
|
+
const expectedStandardEvents = 'reserved,paid,standard';
|
|
8
|
+
|
|
9
|
+
export const catalog = {
|
|
10
|
+
start: task(({ args }) => new OrderWorkflow(args[0]), { as: 'order', requiredArgs: 1 }),
|
|
11
|
+
|
|
12
|
+
stock: scorer(({ context }) => context.order.hasStock()),
|
|
13
|
+
|
|
14
|
+
reserve: task(({ args, context }) => {
|
|
15
|
+
context.order.reserve(args[0]);
|
|
16
|
+
}, { requiredArgs: 1 }),
|
|
17
|
+
|
|
18
|
+
outOfStock: scorer(() => ({
|
|
19
|
+
pass: false,
|
|
20
|
+
message: 'expected stock for SKU-42',
|
|
21
|
+
})),
|
|
22
|
+
|
|
23
|
+
payment: scorer(({ context }) => context.order.isPaymentApproved()),
|
|
24
|
+
|
|
25
|
+
capture: task(({ context }) => {
|
|
26
|
+
context.order.capturePayment();
|
|
27
|
+
}),
|
|
28
|
+
|
|
29
|
+
paymentDeclined: scorer(() => ({
|
|
30
|
+
pass: false,
|
|
31
|
+
message: 'expected approved payment',
|
|
32
|
+
})),
|
|
33
|
+
|
|
34
|
+
expedite: scorer(({ context }) => context.order.needsExpedite()),
|
|
35
|
+
|
|
36
|
+
express: task(({ context }) => {
|
|
37
|
+
context.order.scheduleExpress();
|
|
38
|
+
}),
|
|
39
|
+
|
|
40
|
+
expressReady: scorer(({ context }) => ({
|
|
41
|
+
pass: context.order.fulfillment === 'express' && context.order.events.join(',') === expectedExpressEvents,
|
|
42
|
+
actual: { fulfillment: context.order.fulfillment, events: context.order.events.join(',') },
|
|
43
|
+
expected: { fulfillment: 'express', events: expectedExpressEvents },
|
|
44
|
+
message: 'express fulfillment should be ready',
|
|
45
|
+
})),
|
|
46
|
+
|
|
47
|
+
standard: task(({ context }) => {
|
|
48
|
+
context.order.scheduleStandard();
|
|
49
|
+
}),
|
|
50
|
+
|
|
51
|
+
standardReady: scorer(({ context }) => ({
|
|
52
|
+
pass: context.order.fulfillment === 'standard' && context.order.events.join(',') === expectedStandardEvents,
|
|
53
|
+
actual: { fulfillment: context.order.fulfillment, events: context.order.events.join(',') },
|
|
54
|
+
expected: { fulfillment: 'standard', events: expectedStandardEvents },
|
|
55
|
+
message: 'standard fulfillment should be ready',
|
|
56
|
+
})),
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export default catalog;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { formatRunFailure, runText } from "melusine";
|
|
5
|
+
import * as catalogModule from "../catalog.js";
|
|
6
|
+
|
|
7
|
+
const catalog = (catalogModule.default ?? catalogModule.catalog);
|
|
8
|
+
const journeyUrl = new URL("../journey.md", import.meta.url);
|
|
9
|
+
|
|
10
|
+
test("journey: order-fulfillment-nested", async () => {
|
|
11
|
+
assert.ok(catalog && typeof catalog === 'object', 'catalog module did not export a catalog object');
|
|
12
|
+
const text = await readFile(journeyUrl, 'utf8');
|
|
13
|
+
const result = await runText(text, catalog);
|
|
14
|
+
assert.equal(result.ok, true, formatRunFailure(result));
|
|
15
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
journey: order-fulfillment-nested
|
|
3
|
+
nodes:
|
|
4
|
+
start:
|
|
5
|
+
args: [{ stock: 2, paymentApproved: true, expedited: false }]
|
|
6
|
+
as: order
|
|
7
|
+
reserve: { args: ["SKU-42"] }
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Order fulfillment journey
|
|
11
|
+
|
|
12
|
+
```mermaid
|
|
13
|
+
graph TD
|
|
14
|
+
start(["Order received"]) --> stock
|
|
15
|
+
stock{"Stock available?"}
|
|
16
|
+
stock -->|yes| reserve
|
|
17
|
+
stock -->|no| outOfStock
|
|
18
|
+
reserve["Reserve inventory"] --> payment
|
|
19
|
+
payment{"Payment approved?"}
|
|
20
|
+
payment -->|yes| capture
|
|
21
|
+
payment -->|no| paymentDeclined
|
|
22
|
+
capture["Capture payment"] --> expedite
|
|
23
|
+
expedite{"Expedite requested?"}
|
|
24
|
+
expedite -->|yes| express
|
|
25
|
+
expedite -->|no| standard
|
|
26
|
+
express["Schedule express fulfillment"] --> expressReady
|
|
27
|
+
standard["Schedule standard fulfillment"] --> standardReady
|
|
28
|
+
outOfStock(["Out of stock"])
|
|
29
|
+
paymentDeclined(["Payment declined"])
|
|
30
|
+
expressReady(["Express fulfillment ready"])
|
|
31
|
+
standardReady(["Standard fulfillment ready"])
|
|
32
|
+
```
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export class OrderWorkflow {
|
|
2
|
+
constructor({ stock, paymentApproved, expedited }) {
|
|
3
|
+
this.stock = stock;
|
|
4
|
+
this.paymentApproved = paymentApproved;
|
|
5
|
+
this.expedited = expedited;
|
|
6
|
+
this.reservedSku = null;
|
|
7
|
+
this.paid = false;
|
|
8
|
+
this.fulfillment = null;
|
|
9
|
+
this.events = [];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
hasStock() {
|
|
13
|
+
return this.stock > 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
reserve(sku) {
|
|
17
|
+
if (!this.hasStock()) throw new Error('out of stock');
|
|
18
|
+
this.stock -= 1;
|
|
19
|
+
this.reservedSku = sku;
|
|
20
|
+
this.events.push('reserved');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
isPaymentApproved() {
|
|
24
|
+
return this.paymentApproved;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
capturePayment() {
|
|
28
|
+
if (!this.paymentApproved) throw new Error('payment declined');
|
|
29
|
+
this.paid = true;
|
|
30
|
+
this.events.push('paid');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
needsExpedite() {
|
|
34
|
+
return this.expedited;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
scheduleExpress() {
|
|
38
|
+
if (!this.paid) throw new Error('payment required');
|
|
39
|
+
this.fulfillment = 'express';
|
|
40
|
+
this.events.push('express');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
scheduleStandard() {
|
|
44
|
+
if (!this.paid) throw new Error('payment required');
|
|
45
|
+
this.fulfillment = 'standard';
|
|
46
|
+
this.events.push('standard');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Vending Example
|
|
2
|
+
|
|
3
|
+
This example shows a single decision scorer. The `enough` scorer selects the `yes` branch when the machine has enough credit.
|
|
4
|
+
|
|
5
|
+
Files:
|
|
6
|
+
|
|
7
|
+
- `journey.md` defines the Mermaid flow and item args.
|
|
8
|
+
- `catalog.js` exposes reusable vending tasks and scorers.
|
|
9
|
+
- `vending-machine.mjs` is the toy system under test.
|
|
10
|
+
- `generated/vending.test.mjs` is the generated wrapper.
|
|
11
|
+
|
|
12
|
+
Run it:
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
node src/cli.js test examples/vending/journey.md --catalog examples/vending/catalog.js
|
|
16
|
+
```
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { scorer, task } from 'melusine';
|
|
4
|
+
import { VendingMachine } from './vending-machine.mjs';
|
|
5
|
+
|
|
6
|
+
export const catalog = {
|
|
7
|
+
start: task(() => new VendingMachine(), { as: 'machine' }),
|
|
8
|
+
|
|
9
|
+
insert: task(({ args, context }) => {
|
|
10
|
+
context.machine.insert(args[0]);
|
|
11
|
+
}, { requiredArgs: 1 }),
|
|
12
|
+
|
|
13
|
+
enough: scorer(({ context }) => context.machine.credit >= 125),
|
|
14
|
+
|
|
15
|
+
dispense: task(({ args, context }) => {
|
|
16
|
+
context.machine.dispense(args[0]);
|
|
17
|
+
}, { requiredArgs: 1 }),
|
|
18
|
+
|
|
19
|
+
gotCola: scorer(({ context }) => ({
|
|
20
|
+
pass: context.machine.lastDispensed === 'cola',
|
|
21
|
+
actual: context.machine.lastDispensed,
|
|
22
|
+
expected: 'cola',
|
|
23
|
+
message: 'machine should dispense cola',
|
|
24
|
+
})),
|
|
25
|
+
|
|
26
|
+
fail: scorer(() => ({
|
|
27
|
+
pass: false,
|
|
28
|
+
message: 'expected enough credit for cola',
|
|
29
|
+
})),
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export default catalog;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { formatRunFailure, runText } from "melusine";
|
|
5
|
+
import * as catalogModule from "../catalog.js";
|
|
6
|
+
|
|
7
|
+
const catalog = (catalogModule.default ?? catalogModule.catalog);
|
|
8
|
+
const journeyUrl = new URL("../journey.md", import.meta.url);
|
|
9
|
+
|
|
10
|
+
test("journey: vending-purchase", async () => {
|
|
11
|
+
assert.ok(catalog && typeof catalog === 'object', 'catalog module did not export a catalog object');
|
|
12
|
+
const text = await readFile(journeyUrl, 'utf8');
|
|
13
|
+
const result = await runText(text, catalog);
|
|
14
|
+
assert.equal(result.ok, true, formatRunFailure(result));
|
|
15
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
journey: vending-purchase
|
|
3
|
+
nodes:
|
|
4
|
+
start: { as: machine }
|
|
5
|
+
insert: { args: [150] }
|
|
6
|
+
dispense: { args: ["cola"] }
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Vending purchase journey
|
|
10
|
+
|
|
11
|
+
```mermaid
|
|
12
|
+
graph TD
|
|
13
|
+
start(["Customer approaches with $1.50"]) --> insert
|
|
14
|
+
insert["Insert coins"] --> enough
|
|
15
|
+
enough{"Enough credit for cola?"}
|
|
16
|
+
enough -->|yes| dispense
|
|
17
|
+
enough -->|no| fail
|
|
18
|
+
dispense["Dispense cola"] --> gotCola
|
|
19
|
+
gotCola(["Got cola"])
|
|
20
|
+
fail(["Insufficient credit"])
|
|
21
|
+
```
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export class VendingMachine {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.credit = 0;
|
|
4
|
+
this.lastDispensed = null;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
insert(cents) {
|
|
8
|
+
this.credit += cents;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
dispense(item) {
|
|
12
|
+
if (this.credit < 125) throw new Error('insufficient credit');
|
|
13
|
+
this.credit -= 125;
|
|
14
|
+
this.lastDispensed = item;
|
|
15
|
+
}
|
|
16
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "melusine",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Compile Mermaid journey flowcharts into framework-agnostic test plans.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=20"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./src/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"bin": {
|
|
16
|
+
"melusine": "./src/cli.js"
|
|
17
|
+
},
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"files": [
|
|
20
|
+
"src",
|
|
21
|
+
"dist",
|
|
22
|
+
"examples",
|
|
23
|
+
"README.md",
|
|
24
|
+
"SPEC.md"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsc -p tsconfig.json",
|
|
28
|
+
"generate:example": "npm run generate:examples",
|
|
29
|
+
"generate:examples": "node src/cli.js compile examples/onboarding/journey.md --catalog examples/onboarding/catalog.js --out examples/onboarding/generated/onboarding.test.mjs && node src/cli.js compile examples/vending/journey.md --catalog examples/vending/catalog.js --out examples/vending/generated/vending.test.mjs && node src/cli.js compile examples/order-fulfillment/journey.md --catalog examples/order-fulfillment/catalog.js --out examples/order-fulfillment/generated/order-fulfillment.test.mjs",
|
|
30
|
+
"test": "npm run build && node --test test/*.test.js"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"yaml": "^2.8.1"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^24.0.0",
|
|
37
|
+
"typescript": "^5.8.0"
|
|
38
|
+
}
|
|
39
|
+
}
|