@fiducian/spec 0.4.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.
@@ -0,0 +1,25 @@
1
+ export interface InteractionSignature {
2
+ from: string;
3
+ to: string;
4
+ op: string;
5
+ fields: string[];
6
+ }
7
+ export interface Constraint {
8
+ kind: "true" | "eq" | "fieldEq";
9
+ left?: string;
10
+ right?: string | boolean | number;
11
+ }
12
+ export interface ChainStep {
13
+ signature: InteractionSignature;
14
+ constraint: Constraint;
15
+ optional?: boolean;
16
+ }
17
+ export interface PropertySpec {
18
+ modal: "omega" | "theta" | "diamond" | "none";
19
+ steps: ChainStep[];
20
+ }
21
+ export declare class ParseError extends Error {
22
+ constructor(message: string);
23
+ }
24
+ export declare function parseProperty(source: string): PropertySpec;
25
+ export declare function parseWaltzFile(source: string): PropertySpec;
package/dist/index.js ADDED
@@ -0,0 +1,131 @@
1
+ export class ParseError extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = "ParseError";
5
+ }
6
+ }
7
+ function tokenize(source) {
8
+ const tokens = [];
9
+ let i = 0;
10
+ while (i < source.length) {
11
+ const ch = source[i];
12
+ if (/\s/.test(ch)) {
13
+ i++;
14
+ continue;
15
+ }
16
+ if ("();:{},".includes(ch)) {
17
+ tokens.push(ch);
18
+ i++;
19
+ continue;
20
+ }
21
+ if (ch === "-" && source[i + 1] === ">") {
22
+ tokens.push("->");
23
+ i += 2;
24
+ continue;
25
+ }
26
+ if (ch === "=" && source[i + 1] === "=") {
27
+ tokens.push("==");
28
+ i += 2;
29
+ continue;
30
+ }
31
+ const match = source.slice(i).match(/^[A-Za-z_][A-Za-z0-9_]*/);
32
+ if (match) {
33
+ tokens.push(match[0]);
34
+ i += match[0].length;
35
+ continue;
36
+ }
37
+ throw new ParseError(`Unexpected character '${ch}' at ${i}`);
38
+ }
39
+ return tokens;
40
+ }
41
+ export function parseProperty(source) {
42
+ const tokens = tokenize(source.replace(/\/\/[^\n]*/g, ""));
43
+ let index = 0;
44
+ const peek = () => tokens[index];
45
+ const consume = (expected) => {
46
+ const tok = tokens[index++];
47
+ if (expected && tok !== expected) {
48
+ throw new ParseError(`Expected '${expected}' but got '${tok ?? "EOF"}'`);
49
+ }
50
+ return tok;
51
+ };
52
+ let modal = "none";
53
+ if (peek() === "Omega" || peek() === "O") {
54
+ consume();
55
+ consume("(");
56
+ modal = "omega";
57
+ }
58
+ else if (peek() === "Theta" || peek() === "T") {
59
+ consume();
60
+ consume("(");
61
+ modal = "theta";
62
+ }
63
+ else if (peek() === "Diamond" || peek() === "D") {
64
+ consume();
65
+ consume("(");
66
+ modal = "diamond";
67
+ }
68
+ const steps = [];
69
+ while (index < tokens.length) {
70
+ if (peek() === ")")
71
+ break;
72
+ if (peek() === ";") {
73
+ consume(";");
74
+ continue;
75
+ }
76
+ let optional = false;
77
+ if (peek() === "optional") {
78
+ consume("optional");
79
+ optional = true;
80
+ }
81
+ consume("send");
82
+ const from = consume();
83
+ consume("->");
84
+ const to = consume();
85
+ consume("{");
86
+ const fields = [];
87
+ while (peek() !== "}") {
88
+ if (peek() === ",")
89
+ consume(",");
90
+ fields.push(consume());
91
+ }
92
+ consume("}");
93
+ consume(":");
94
+ const constraint = parseConstraint(tokens, () => consume(), () => peek());
95
+ steps.push({
96
+ signature: { from, to, op: "send", fields },
97
+ constraint,
98
+ ...(optional ? { optional: true } : {}),
99
+ });
100
+ }
101
+ if (modal !== "none") {
102
+ consume(")");
103
+ }
104
+ if (steps.length === 0) {
105
+ throw new ParseError("Property must contain at least one chain step");
106
+ }
107
+ return { modal, steps };
108
+ }
109
+ function parseConstraint(_tokens, consume, peek) {
110
+ const first = peek();
111
+ if (first === "true") {
112
+ consume();
113
+ return { kind: "true" };
114
+ }
115
+ const left = consume();
116
+ if (peek() === "==") {
117
+ consume();
118
+ const rightTok = consume();
119
+ if (rightTok === "true" || rightTok === "false") {
120
+ return { kind: "eq", left, right: rightTok === "true" };
121
+ }
122
+ if (/^\d+$/.test(rightTok)) {
123
+ return { kind: "eq", left, right: Number(rightTok) };
124
+ }
125
+ return { kind: "fieldEq", left, right: rightTok };
126
+ }
127
+ throw new ParseError(`Invalid constraint starting with '${first}'`);
128
+ }
129
+ export function parseWaltzFile(source) {
130
+ return parseProperty(source.trim());
131
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,31 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { parseWaltzFile } from "./index.js";
4
+ test("parseWaltzFile parses chained property", () => {
5
+ const spec = parseWaltzFile(`
6
+ Omega(
7
+ send client->order{orderId} : true
8
+ ;
9
+ send order->kitchen{orderId, accepted} : accepted == true
10
+ ;
11
+ send order->billing{orderId, charged} : charged == true
12
+ )
13
+ `);
14
+ assert.equal(spec.modal, "omega");
15
+ assert.equal(spec.steps.length, 3);
16
+ assert.equal(spec.steps[0].signature.from, "client");
17
+ assert.equal(spec.steps[1].signature.to, "kitchen");
18
+ });
19
+ test("parseWaltzFile parses optional steps", () => {
20
+ const spec = parseWaltzFile(`
21
+ Omega(
22
+ send client->order{orderId} : true
23
+ ;
24
+ optional send order->audit{orderId} : true
25
+ ;
26
+ send order->kitchen{orderId, accepted} : accepted == true
27
+ )
28
+ `);
29
+ assert.equal(spec.steps[1].optional, true);
30
+ assert.equal(spec.steps[2].optional, undefined);
31
+ });
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@fiducian/spec",
3
+ "version": "0.4.0",
4
+ "description": "Minimal WALTZ spec parser for FiduciaTrace",
5
+ "license": "AGPL-3.0-or-later",
6
+ "keywords": ["fiducia", "fiducia", "waltz", "spec", "verification"],
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/thesnmc/Fiducian.git",
10
+ "directory": "trace/packages/spec"
11
+ },
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "files": ["dist"],
16
+ "type": "module",
17
+ "main": "./dist/index.js",
18
+ "types": "./dist/index.d.ts",
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/index.d.ts",
22
+ "import": "./dist/index.js"
23
+ }
24
+ },
25
+ "scripts": {
26
+ "build": "tsc -p tsconfig.json",
27
+ "test": "node --test dist/**/*.test.js",
28
+ "prepublishOnly": "npm run build"
29
+ }
30
+ }