@fiducian/compiler 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,18 @@
1
+ import type { TraceEvent, Violation } from "@fiducian/conductor";
2
+ import type { PropertySpec } from "@fiducian/spec";
3
+ export interface MonitorState {
4
+ step: number;
5
+ bindings: Record<string, unknown>;
6
+ status: "watching" | "passed" | "violated";
7
+ violation?: Violation;
8
+ }
9
+ export interface Monitor {
10
+ onEvent(event: TraceEvent): MonitorState;
11
+ snapshot(): MonitorState;
12
+ }
13
+ export declare function compileMonitor(spec: PropertySpec): Monitor;
14
+ export declare function evaluateTrace(spec: PropertySpec, events: TraceEvent[]): {
15
+ passed: boolean;
16
+ violations: Violation[];
17
+ passedContexts: string[];
18
+ };
package/dist/index.js ADDED
@@ -0,0 +1,147 @@
1
+ function matchesSignature(event, from, to) {
2
+ return event.from === from && event.to === to && event.op === "send";
3
+ }
4
+ function evalConstraint(constraint, payload, bindings) {
5
+ switch (constraint.kind) {
6
+ case "true":
7
+ return true;
8
+ case "eq": {
9
+ const value = payload[constraint.left] ?? bindings[constraint.left];
10
+ return value === constraint.right;
11
+ }
12
+ case "fieldEq": {
13
+ const left = payload[constraint.left] ?? bindings[constraint.left];
14
+ const right = payload[String(constraint.right)] ??
15
+ bindings[String(constraint.right)];
16
+ return left === right;
17
+ }
18
+ default:
19
+ return false;
20
+ }
21
+ }
22
+ export function compileMonitor(spec) {
23
+ const steps = spec.steps;
24
+ const createState = () => ({
25
+ step: 0,
26
+ bindings: {},
27
+ status: "watching",
28
+ });
29
+ let state = createState();
30
+ const contexts = new Map();
31
+ const onEvent = (event) => {
32
+ if (spec.modal === "omega" || spec.modal === "none") {
33
+ return onEventForContext(event, contexts, steps, createState);
34
+ }
35
+ return onEventForContext(event, contexts, steps, createState);
36
+ };
37
+ return {
38
+ onEvent,
39
+ snapshot: () => ({ ...state }),
40
+ };
41
+ }
42
+ function allRemainingOptional(steps, step) {
43
+ return step >= steps.length || steps.slice(step).every((s) => s.optional);
44
+ }
45
+ function onEventForContext(event, contexts, steps, createState) {
46
+ let ctxState = contexts.get(event.contextId);
47
+ if (!ctxState) {
48
+ ctxState = createState();
49
+ contexts.set(event.contextId, ctxState);
50
+ }
51
+ if (ctxState.status !== "watching") {
52
+ return ctxState;
53
+ }
54
+ while (ctxState.step < steps.length &&
55
+ steps[ctxState.step].optional &&
56
+ !matchesSignature(event, steps[ctxState.step].signature.from, steps[ctxState.step].signature.to)) {
57
+ let matchesLater = false;
58
+ for (let j = ctxState.step + 1; j < steps.length; j++) {
59
+ const sig = steps[j].signature;
60
+ if (matchesSignature(event, sig.from, sig.to)) {
61
+ matchesLater = true;
62
+ break;
63
+ }
64
+ }
65
+ if (matchesLater) {
66
+ ctxState.step += 1;
67
+ continue;
68
+ }
69
+ break;
70
+ }
71
+ for (let future = ctxState.step + 1; future < steps.length; future++) {
72
+ if (steps[future].optional)
73
+ continue;
74
+ const futureSig = steps[future].signature;
75
+ if (matchesSignature(event, futureSig.from, futureSig.to)) {
76
+ ctxState.status = "violated";
77
+ ctxState.violation = {
78
+ contextId: event.contextId,
79
+ step: ctxState.step,
80
+ message: `Out-of-order step: saw ${futureSig.from}->${futureSig.to} before completing step ${ctxState.step + 1} (${steps[ctxState.step].signature.from}->${steps[ctxState.step].signature.to})`,
81
+ event,
82
+ };
83
+ return ctxState;
84
+ }
85
+ }
86
+ const expected = steps[ctxState.step];
87
+ if (!expected) {
88
+ ctxState.status = "passed";
89
+ return ctxState;
90
+ }
91
+ const { signature, constraint } = expected;
92
+ if (!matchesSignature(event, signature.from, signature.to)) {
93
+ return ctxState;
94
+ }
95
+ for (const field of signature.fields) {
96
+ if (field in event.payload) {
97
+ ctxState.bindings[field] = event.payload[field];
98
+ }
99
+ }
100
+ if (!evalConstraint(constraint, event.payload, ctxState.bindings)) {
101
+ ctxState.status = "violated";
102
+ ctxState.violation = {
103
+ contextId: event.contextId,
104
+ step: ctxState.step,
105
+ message: `Constraint failed at step ${ctxState.step + 1}: ${signature.from}->${signature.to}`,
106
+ event,
107
+ };
108
+ return ctxState;
109
+ }
110
+ ctxState.step += 1;
111
+ if (allRemainingOptional(steps, ctxState.step)) {
112
+ ctxState.status = "passed";
113
+ }
114
+ return ctxState;
115
+ }
116
+ export function evaluateTrace(spec, events) {
117
+ const monitor = compileMonitor(spec);
118
+ const violations = [];
119
+ const passedContexts = new Set();
120
+ const contextStates = new Map();
121
+ for (const event of events) {
122
+ const state = monitor.onEvent(event);
123
+ contextStates.set(event.contextId, state);
124
+ }
125
+ for (const [contextId, state] of contextStates) {
126
+ if (state.status === "violated" && state.violation) {
127
+ violations.push(state.violation);
128
+ }
129
+ else if (state.status === "passed") {
130
+ passedContexts.add(contextId);
131
+ }
132
+ else if (state.step > 0 && !allRemainingOptional(spec.steps, state.step)) {
133
+ violations.push({
134
+ contextId,
135
+ message: `Incomplete chain: stopped at step ${state.step + 1} of ${spec.steps.length}`,
136
+ });
137
+ }
138
+ }
139
+ const passed = violations.length === 0 &&
140
+ passedContexts.size > 0 &&
141
+ [...contextStates.values()].every((s) => s.status === "passed" || s.step === 0);
142
+ return {
143
+ passed,
144
+ violations,
145
+ passedContexts: [...passedContexts],
146
+ };
147
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,57 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { parseWaltzFile } from "@fiducian/spec";
4
+ import { evaluateTrace } from "./index.js";
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
+ function ev(contextId, from, to, payload) {
15
+ return {
16
+ contextId,
17
+ from,
18
+ to,
19
+ op: "send",
20
+ payload,
21
+ timestamp: 0,
22
+ };
23
+ }
24
+ test("evaluateTrace passes correct order", () => {
25
+ const result = evaluateTrace(spec, [
26
+ ev("c1", "client", "order", { orderId: "42" }),
27
+ ev("c1", "order", "kitchen", { orderId: "42", accepted: true }),
28
+ ev("c1", "order", "billing", { orderId: "42", charged: true }),
29
+ ]);
30
+ assert.equal(result.passed, true);
31
+ assert.equal(result.violations.length, 0);
32
+ });
33
+ test("evaluateTrace fails when billing before kitchen", () => {
34
+ const result = evaluateTrace(spec, [
35
+ ev("c1", "client", "order", { orderId: "42" }),
36
+ ev("c1", "order", "billing", { orderId: "42", charged: true }),
37
+ ev("c1", "order", "kitchen", { orderId: "42", accepted: true }),
38
+ ]);
39
+ assert.equal(result.passed, false);
40
+ assert.ok(result.violations.length > 0);
41
+ });
42
+ test("evaluateTrace skips optional audit step", () => {
43
+ const optionalSpec = parseWaltzFile(`
44
+ Omega(
45
+ send client->order{orderId} : true
46
+ ;
47
+ optional send order->audit{orderId} : true
48
+ ;
49
+ send order->kitchen{orderId, accepted} : accepted == true
50
+ )
51
+ `);
52
+ const result = evaluateTrace(optionalSpec, [
53
+ ev("c1", "client", "order", { orderId: "42" }),
54
+ ev("c1", "order", "kitchen", { orderId: "42", accepted: true }),
55
+ ]);
56
+ assert.equal(result.passed, true);
57
+ });
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@fiducian/compiler",
3
+ "version": "0.4.0",
4
+ "description": "Compile WALTZ specs into FiduciaTrace monitor state machines",
5
+ "license": "AGPL-3.0-or-later",
6
+ "keywords": ["fiducia", "fiducia", "compiler", "waltz", "monitor"],
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/thesnmc/Fiducian.git",
10
+ "directory": "trace/packages/compiler"
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
+ "dependencies": {
26
+ "@fiducian/spec": "0.4.0"
27
+ },
28
+ "scripts": {
29
+ "build": "tsc -p tsconfig.json",
30
+ "test": "node --test dist/**/*.test.js",
31
+ "prepublishOnly": "npm run build"
32
+ }
33
+ }