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.
Files changed (41) hide show
  1. package/README.md +249 -0
  2. package/SPEC.md +145 -0
  3. package/dist/catalog.d.ts +104 -0
  4. package/dist/cli.d.ts +27 -0
  5. package/dist/compile.d.ts +1 -0
  6. package/dist/generate.d.ts +11 -0
  7. package/dist/index.d.ts +23 -0
  8. package/dist/parse.d.ts +27 -0
  9. package/dist/run.d.ts +54 -0
  10. package/dist/scaffold.d.ts +18 -0
  11. package/dist/types.d.ts +542 -0
  12. package/dist/validate.d.ts +28 -0
  13. package/dist/walk.d.ts +24 -0
  14. package/examples/README.md +21 -0
  15. package/examples/onboarding/README.md +16 -0
  16. package/examples/onboarding/catalog.js +29 -0
  17. package/examples/onboarding/generated/onboarding.test.mjs +15 -0
  18. package/examples/onboarding/journey.md +18 -0
  19. package/examples/onboarding/onboarding-session.mjs +21 -0
  20. package/examples/order-fulfillment/README.md +16 -0
  21. package/examples/order-fulfillment/catalog.js +59 -0
  22. package/examples/order-fulfillment/generated/order-fulfillment.test.mjs +15 -0
  23. package/examples/order-fulfillment/journey.md +32 -0
  24. package/examples/order-fulfillment/order-workflow.mjs +48 -0
  25. package/examples/vending/README.md +16 -0
  26. package/examples/vending/catalog.js +32 -0
  27. package/examples/vending/generated/vending.test.mjs +15 -0
  28. package/examples/vending/journey.md +21 -0
  29. package/examples/vending/vending-machine.mjs +16 -0
  30. package/package.json +39 -0
  31. package/src/catalog.js +485 -0
  32. package/src/cli.js +331 -0
  33. package/src/compile.js +3 -0
  34. package/src/generate.js +52 -0
  35. package/src/index.js +28 -0
  36. package/src/parse.js +263 -0
  37. package/src/run.js +258 -0
  38. package/src/scaffold.js +142 -0
  39. package/src/types.js +330 -0
  40. package/src/validate.js +171 -0
  41. 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
+ }