@mantaq/traversal 0.0.1
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/LICENSE +21 -0
- package/README.md +8 -0
- package/dist/index.d.mts +99 -0
- package/dist/index.mjs +479 -0
- package/package.json +41 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Anders Can
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# @mantaq/traversal
|
|
2
|
+
|
|
3
|
+
Graph extraction, traversal algorithms, and runtime tracking for
|
|
4
|
+
[@mantaq/core](https://www.npmjs.com/package/@mantaq/core) actors.
|
|
5
|
+
|
|
6
|
+
Build the state-machine graph from an actor, walk it with traversal
|
|
7
|
+
algorithms, and instrument coverage as the actor runs — useful for
|
|
8
|
+
verifying that every declared transition is reachable and exercised.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { AnyActor, Snapshot } from "@mantaq/core";
|
|
2
|
+
//#region src/types.d.ts
|
|
3
|
+
interface GraphNode {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
isActive: boolean;
|
|
7
|
+
isFinal: boolean;
|
|
8
|
+
isInitial?: boolean;
|
|
9
|
+
}
|
|
10
|
+
interface GraphEdge {
|
|
11
|
+
id: string;
|
|
12
|
+
source: string;
|
|
13
|
+
target: string;
|
|
14
|
+
label: string;
|
|
15
|
+
isActive: boolean;
|
|
16
|
+
isInternal?: boolean;
|
|
17
|
+
isUndetermined?: boolean;
|
|
18
|
+
payload?: {
|
|
19
|
+
action?: string;
|
|
20
|
+
};
|
|
21
|
+
contexts?: string[];
|
|
22
|
+
}
|
|
23
|
+
interface ActorGraph {
|
|
24
|
+
nodes: GraphNode[];
|
|
25
|
+
edges: GraphEdge[];
|
|
26
|
+
}
|
|
27
|
+
interface StateVisit {
|
|
28
|
+
stateName: string;
|
|
29
|
+
timestamp: number;
|
|
30
|
+
}
|
|
31
|
+
interface TransitionRecord {
|
|
32
|
+
from: string;
|
|
33
|
+
event: string;
|
|
34
|
+
to: string | undefined;
|
|
35
|
+
timestamp: number;
|
|
36
|
+
}
|
|
37
|
+
interface EffectRecord {
|
|
38
|
+
stateName: string;
|
|
39
|
+
timestamp: number;
|
|
40
|
+
}
|
|
41
|
+
interface HistoryEntry {
|
|
42
|
+
type: "state_visit" | "transition" | "effect" | "send";
|
|
43
|
+
data: StateVisit | TransitionRecord | EffectRecord | {
|
|
44
|
+
event: string;
|
|
45
|
+
timestamp: number;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
//#endregion
|
|
49
|
+
//#region src/graph.d.ts
|
|
50
|
+
declare const INITIAL_NODE_ID = "__initial__";
|
|
51
|
+
declare function collectActiveStates(snapshot: Snapshot, prefix: string, activeSet: Set<string>): void;
|
|
52
|
+
declare function buildGraph(actor: AnyActor, options?: {
|
|
53
|
+
internalIds?: Set<string>;
|
|
54
|
+
sampleContext?: Record<string, unknown>;
|
|
55
|
+
sampleContexts?: Record<string, Record<string, unknown>>;
|
|
56
|
+
}): ActorGraph;
|
|
57
|
+
//#endregion
|
|
58
|
+
//#region src/history.d.ts
|
|
59
|
+
declare class History {
|
|
60
|
+
constructor();
|
|
61
|
+
append(entry: HistoryEntry): void;
|
|
62
|
+
entries(): readonly HistoryEntry[];
|
|
63
|
+
stateVisits(): StateVisit[];
|
|
64
|
+
transitions(): TransitionRecord[];
|
|
65
|
+
effects(): EffectRecord[];
|
|
66
|
+
sends(): Array<{
|
|
67
|
+
event: string;
|
|
68
|
+
timestamp: number;
|
|
69
|
+
}>;
|
|
70
|
+
visitedStates(): Set<string>;
|
|
71
|
+
firedTransitions(): Set<string>;
|
|
72
|
+
reset(): void;
|
|
73
|
+
}
|
|
74
|
+
//#endregion
|
|
75
|
+
//#region src/instrument.d.ts
|
|
76
|
+
interface InstrumentedActor {
|
|
77
|
+
history: History;
|
|
78
|
+
send(event: unknown): void;
|
|
79
|
+
state: AnyActor["state"];
|
|
80
|
+
snapshot(): Snapshot;
|
|
81
|
+
regions: Record<string, AnyActor>;
|
|
82
|
+
context?: Record<string, unknown>;
|
|
83
|
+
clock: AnyActor["clock"];
|
|
84
|
+
on(event: "change", fn: (snapshot: Snapshot) => void): () => void;
|
|
85
|
+
on(event: "error", fn: (error: unknown) => void): () => void;
|
|
86
|
+
on(event: "done", fn: () => void): () => void;
|
|
87
|
+
settled(): Promise<void>;
|
|
88
|
+
options?: AnyActor["options"];
|
|
89
|
+
}
|
|
90
|
+
declare function instrument(actor: AnyActor): InstrumentedActor;
|
|
91
|
+
//#endregion
|
|
92
|
+
//#region src/algorithms.d.ts
|
|
93
|
+
declare function reachable(graph: ActorGraph, fromId: string, toId: string): boolean;
|
|
94
|
+
declare function allPaths(graph: ActorGraph, fromId: string, toId: string): string[][];
|
|
95
|
+
declare function findCycles(graph: ActorGraph): string[][];
|
|
96
|
+
declare function unreachableNodes(graph: ActorGraph, fromId: string): string[];
|
|
97
|
+
declare function shortestPath(graph: ActorGraph, fromId: string, toId: string): string[] | null;
|
|
98
|
+
//#endregion
|
|
99
|
+
export { type ActorGraph, type EffectRecord, type GraphEdge, type GraphNode, History, type HistoryEntry, INITIAL_NODE_ID, type InstrumentedActor, type StateVisit, type TransitionRecord, allPaths, buildGraph, collectActiveStates, findCycles, instrument, reachable, shortestPath, unreachableNodes };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
import { Either } from "@mantaq/utils";
|
|
2
|
+
//#region src/graph.ts
|
|
3
|
+
const INITIAL_NODE_ID = "__initial__";
|
|
4
|
+
function collectActiveStates(snapshot, prefix, activeSet) {
|
|
5
|
+
if (!snapshot || !snapshot.path) return;
|
|
6
|
+
const currentName = snapshot.path[snapshot.path.length - 1];
|
|
7
|
+
if (!currentName) return;
|
|
8
|
+
const fullId = nodeId(prefix, currentName);
|
|
9
|
+
activeSet.add(fullId);
|
|
10
|
+
if (snapshot.regions && !snapshot.done) for (const [regionName, regionSnap] of Object.entries(snapshot.regions)) collectActiveStates(regionSnap, regionName, activeSet);
|
|
11
|
+
}
|
|
12
|
+
function nodeId(prefix, name) {
|
|
13
|
+
return prefix ? `${prefix}.${name}` : name;
|
|
14
|
+
}
|
|
15
|
+
function buildNodesFromStates(states, activeSet, pathPrefix) {
|
|
16
|
+
return states.map((stateRef) => {
|
|
17
|
+
const nid = nodeId(pathPrefix, stateRef.name);
|
|
18
|
+
return {
|
|
19
|
+
id: nid,
|
|
20
|
+
label: stateRef.name,
|
|
21
|
+
isActive: activeSet.has(nid),
|
|
22
|
+
isFinal: stateRef.isFinal
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
function invokeHandler(handler, eventId, ctx, actor) {
|
|
27
|
+
return Either.from(() => {
|
|
28
|
+
const result = handler({ id: eventId }, {
|
|
29
|
+
context: ctx,
|
|
30
|
+
actor
|
|
31
|
+
});
|
|
32
|
+
return {
|
|
33
|
+
targetName: result?.state?.name,
|
|
34
|
+
emitNames: result?.emit?.map((e) => e.id).filter((id) => Boolean(id)) ?? []
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
function upsertEdge(sink, traversal, eventId, handled) {
|
|
39
|
+
const undetermined = !handled.targetName;
|
|
40
|
+
const targetId = handled.targetName ? nodeId(traversal.pathPrefix, handled.targetName) : `${sink.sourceId}-undetermined-${eventId}`;
|
|
41
|
+
const edgeId = `${sink.sourceId}-${eventId}-${targetId}`;
|
|
42
|
+
const existing = sink.edgeMap.get(edgeId);
|
|
43
|
+
if (existing) existing.contexts.push(sink.ctxName);
|
|
44
|
+
else sink.edgeMap.set(edgeId, {
|
|
45
|
+
id: edgeId,
|
|
46
|
+
source: sink.sourceId,
|
|
47
|
+
target: targetId,
|
|
48
|
+
label: eventId,
|
|
49
|
+
isActive: traversal.activeSet.has(sink.sourceId),
|
|
50
|
+
isInternal: traversal.internalIds?.has(eventId),
|
|
51
|
+
isUndetermined: undetermined,
|
|
52
|
+
contexts: [sink.ctxName],
|
|
53
|
+
...handled.emitNames.length > 0 && { payload: { action: `emit(${handled.emitNames.join(", ")})` } }
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function processStateTransitions(stateTransitions, pass) {
|
|
57
|
+
const stateTransitionedEvents = /* @__PURE__ */ new Set();
|
|
58
|
+
for (const [eventId, handler] of Object.entries(stateTransitions)) {
|
|
59
|
+
if (typeof handler !== "function") continue;
|
|
60
|
+
for (const ctxName of pass.traversal.contextNames) {
|
|
61
|
+
const ctx = pass.traversal.contexts[ctxName];
|
|
62
|
+
const handled = Either.getOrElse(invokeHandler(handler, eventId, ctx, pass.traversal.actor), () => ({
|
|
63
|
+
targetName: void 0,
|
|
64
|
+
emitNames: []
|
|
65
|
+
}));
|
|
66
|
+
if (handled.targetName) stateTransitionedEvents.add(eventId);
|
|
67
|
+
upsertEdge({
|
|
68
|
+
edgeMap: pass.edgeMap,
|
|
69
|
+
sourceId: pass.sourceId,
|
|
70
|
+
ctxName
|
|
71
|
+
}, pass.traversal, eventId, handled);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return stateTransitionedEvents;
|
|
75
|
+
}
|
|
76
|
+
function mergeAnyTransitions(anyTransitions, stateTransitionedEvents, pass) {
|
|
77
|
+
for (const [eventId, handler] of Object.entries(anyTransitions)) {
|
|
78
|
+
if (typeof handler !== "function") continue;
|
|
79
|
+
if (stateTransitionedEvents.has(eventId)) continue;
|
|
80
|
+
for (const ctxName of pass.traversal.contextNames) {
|
|
81
|
+
const ctx = pass.traversal.contexts[ctxName];
|
|
82
|
+
const handled = Either.getOrElse(invokeHandler(handler, eventId, ctx, pass.traversal.actor), () => ({
|
|
83
|
+
targetName: void 0,
|
|
84
|
+
emitNames: []
|
|
85
|
+
}));
|
|
86
|
+
upsertEdge({
|
|
87
|
+
edgeMap: pass.edgeMap,
|
|
88
|
+
sourceId: pass.sourceId,
|
|
89
|
+
ctxName
|
|
90
|
+
}, pass.traversal, eventId, handled);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function collectTransitionsForState(stateTransitions, anyTransitions, sourceId, traversal) {
|
|
95
|
+
const edgeMap = /* @__PURE__ */ new Map();
|
|
96
|
+
const pass = {
|
|
97
|
+
edgeMap,
|
|
98
|
+
sourceId,
|
|
99
|
+
traversal
|
|
100
|
+
};
|
|
101
|
+
const stateTransitionedEvents = stateTransitions ? processStateTransitions(stateTransitions, pass) : /* @__PURE__ */ new Set();
|
|
102
|
+
if (anyTransitions) mergeAnyTransitions(anyTransitions, stateTransitionedEvents, pass);
|
|
103
|
+
return edgeMap;
|
|
104
|
+
}
|
|
105
|
+
function buildEdgesFromTransitions(states, transitions, traversal) {
|
|
106
|
+
if (!transitions) return [];
|
|
107
|
+
const anyTransitions = transitions["Any"];
|
|
108
|
+
const edges = [];
|
|
109
|
+
for (const stateRef of states) {
|
|
110
|
+
const sourceId = nodeId(traversal.pathPrefix, stateRef.name);
|
|
111
|
+
const edgeMap = collectTransitionsForState(transitions[stateRef.name], anyTransitions, sourceId, traversal);
|
|
112
|
+
edges.push(...edgeMap.values());
|
|
113
|
+
}
|
|
114
|
+
return edges;
|
|
115
|
+
}
|
|
116
|
+
function addNodesForActor(actor, pathPrefix, activeSet) {
|
|
117
|
+
return buildNodesFromStates(actor.options?.states ?? [], activeSet, pathPrefix);
|
|
118
|
+
}
|
|
119
|
+
function addEdgesForActor(actor, traversal) {
|
|
120
|
+
return buildEdgesFromTransitions(actor.options?.states ?? [], actor.options?.transitions, traversal);
|
|
121
|
+
}
|
|
122
|
+
function recurseRegions(actor, traversal) {
|
|
123
|
+
const nodes = [];
|
|
124
|
+
const edges = [];
|
|
125
|
+
if (actor.regions) for (const [regionName, childActor] of Object.entries(actor.regions)) {
|
|
126
|
+
const child = buildForActor(childActor, {
|
|
127
|
+
...traversal,
|
|
128
|
+
pathPrefix: nodeId(traversal.pathPrefix, regionName)
|
|
129
|
+
});
|
|
130
|
+
nodes.push(...child.nodes);
|
|
131
|
+
edges.push(...child.edges);
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
nodes,
|
|
135
|
+
edges
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function addEffectSelfLoops(actor, pathPrefix, activeSet) {
|
|
139
|
+
const edges = [];
|
|
140
|
+
const effects = actor.options?.effects;
|
|
141
|
+
if (effects) {
|
|
142
|
+
for (const [stateName, fns] of Object.entries(effects)) if (Array.isArray(fns) && fns.length > 0) {
|
|
143
|
+
const sid = nodeId(pathPrefix, stateName);
|
|
144
|
+
edges.push({
|
|
145
|
+
id: `${sid}-effect:${stateName}-${sid}`,
|
|
146
|
+
source: sid,
|
|
147
|
+
target: sid,
|
|
148
|
+
label: `effect:${stateName}`,
|
|
149
|
+
isActive: activeSet.has(sid),
|
|
150
|
+
isInternal: true
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return edges;
|
|
155
|
+
}
|
|
156
|
+
function buildForActor(actor, traversal) {
|
|
157
|
+
if (!actor) return {
|
|
158
|
+
nodes: [],
|
|
159
|
+
edges: []
|
|
160
|
+
};
|
|
161
|
+
const nodes = addNodesForActor(actor, traversal.pathPrefix, traversal.activeSet);
|
|
162
|
+
const edges = addEdgesForActor(actor, traversal);
|
|
163
|
+
const region = recurseRegions(actor, traversal);
|
|
164
|
+
nodes.push(...region.nodes);
|
|
165
|
+
edges.push(...region.edges);
|
|
166
|
+
edges.push(...addEffectSelfLoops(actor, traversal.pathPrefix, traversal.activeSet));
|
|
167
|
+
return {
|
|
168
|
+
nodes,
|
|
169
|
+
edges
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
function addInitialNode(actor, nodes, edges) {
|
|
173
|
+
const initialName = actor.options?.initial?.name;
|
|
174
|
+
if (!initialName) return;
|
|
175
|
+
const initNodeId = nodeId("", INITIAL_NODE_ID);
|
|
176
|
+
const targetId = nodeId("", initialName);
|
|
177
|
+
nodes.push({
|
|
178
|
+
id: initNodeId,
|
|
179
|
+
label: "",
|
|
180
|
+
isActive: false,
|
|
181
|
+
isFinal: false,
|
|
182
|
+
isInitial: true
|
|
183
|
+
});
|
|
184
|
+
edges.push({
|
|
185
|
+
id: `${initNodeId}->${targetId}`,
|
|
186
|
+
source: initNodeId,
|
|
187
|
+
target: targetId,
|
|
188
|
+
label: "",
|
|
189
|
+
isActive: true
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
function collectActorsFromSnapshot(snapshot) {
|
|
193
|
+
const activeSet = /* @__PURE__ */ new Set();
|
|
194
|
+
collectActiveStates(snapshot, "", activeSet);
|
|
195
|
+
return activeSet;
|
|
196
|
+
}
|
|
197
|
+
function collectNamedContexts(options) {
|
|
198
|
+
return options?.sampleContexts ?? (options?.sampleContext ? { default: options.sampleContext } : void 0);
|
|
199
|
+
}
|
|
200
|
+
function buildGraph(actor, options) {
|
|
201
|
+
if (!actor) return {
|
|
202
|
+
nodes: [],
|
|
203
|
+
edges: []
|
|
204
|
+
};
|
|
205
|
+
return Either.match(Either.from(() => {
|
|
206
|
+
const activeSet = collectActorsFromSnapshot(actor.snapshot());
|
|
207
|
+
const contexts = collectNamedContexts(options) ?? { default: { ...actor.context } };
|
|
208
|
+
const { nodes, edges } = buildForActor(actor, {
|
|
209
|
+
actor,
|
|
210
|
+
pathPrefix: "",
|
|
211
|
+
activeSet,
|
|
212
|
+
internalIds: options?.internalIds,
|
|
213
|
+
contexts,
|
|
214
|
+
contextNames: Object.keys(contexts)
|
|
215
|
+
});
|
|
216
|
+
addInitialNode(actor, nodes, edges);
|
|
217
|
+
return {
|
|
218
|
+
nodes,
|
|
219
|
+
edges
|
|
220
|
+
};
|
|
221
|
+
}), (error) => {
|
|
222
|
+
console.error("[mantaq/traversal] buildGraph failed:", error);
|
|
223
|
+
return {
|
|
224
|
+
nodes: [],
|
|
225
|
+
edges: []
|
|
226
|
+
};
|
|
227
|
+
}, (result) => result);
|
|
228
|
+
}
|
|
229
|
+
//#endregion
|
|
230
|
+
//#region src/history.ts
|
|
231
|
+
const entriesMap = /* @__PURE__ */ new WeakMap();
|
|
232
|
+
function entriesOfType(entries, type) {
|
|
233
|
+
return entries.filter((e) => e.type === type).map((e) => e.data);
|
|
234
|
+
}
|
|
235
|
+
var History = class {
|
|
236
|
+
constructor() {
|
|
237
|
+
entriesMap.set(this, []);
|
|
238
|
+
}
|
|
239
|
+
append(entry) {
|
|
240
|
+
entriesMap.get(this).push(entry);
|
|
241
|
+
}
|
|
242
|
+
entries() {
|
|
243
|
+
return entriesMap.get(this);
|
|
244
|
+
}
|
|
245
|
+
stateVisits() {
|
|
246
|
+
return entriesOfType(entriesMap.get(this), "state_visit");
|
|
247
|
+
}
|
|
248
|
+
transitions() {
|
|
249
|
+
return entriesOfType(entriesMap.get(this), "transition");
|
|
250
|
+
}
|
|
251
|
+
effects() {
|
|
252
|
+
return entriesOfType(entriesMap.get(this), "effect");
|
|
253
|
+
}
|
|
254
|
+
sends() {
|
|
255
|
+
return entriesOfType(entriesMap.get(this), "send");
|
|
256
|
+
}
|
|
257
|
+
visitedStates() {
|
|
258
|
+
const result = /* @__PURE__ */ new Set();
|
|
259
|
+
for (const visit of this.stateVisits()) result.add(visit.stateName);
|
|
260
|
+
return result;
|
|
261
|
+
}
|
|
262
|
+
firedTransitions() {
|
|
263
|
+
const result = /* @__PURE__ */ new Set();
|
|
264
|
+
for (const t of this.transitions()) result.add(`${t.from}:${t.event}`);
|
|
265
|
+
return result;
|
|
266
|
+
}
|
|
267
|
+
reset() {
|
|
268
|
+
const arr = entriesMap.get(this);
|
|
269
|
+
arr.length = 0;
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
//#endregion
|
|
273
|
+
//#region src/instrument.ts
|
|
274
|
+
function wrapWithProxy(actor, hooks) {
|
|
275
|
+
const { history, origSend, pendingEventId } = hooks;
|
|
276
|
+
return {
|
|
277
|
+
get history() {
|
|
278
|
+
return history;
|
|
279
|
+
},
|
|
280
|
+
get state() {
|
|
281
|
+
return actor.state;
|
|
282
|
+
},
|
|
283
|
+
get regions() {
|
|
284
|
+
return actor.regions;
|
|
285
|
+
},
|
|
286
|
+
get context() {
|
|
287
|
+
return actor.context;
|
|
288
|
+
},
|
|
289
|
+
get clock() {
|
|
290
|
+
return actor.clock;
|
|
291
|
+
},
|
|
292
|
+
get options() {
|
|
293
|
+
return actor.options;
|
|
294
|
+
},
|
|
295
|
+
send(event) {
|
|
296
|
+
const { id: eventId } = trackSendEvent(history, event);
|
|
297
|
+
pendingEventId.value = eventId;
|
|
298
|
+
try {
|
|
299
|
+
origSend(event);
|
|
300
|
+
} finally {
|
|
301
|
+
pendingEventId.value = void 0;
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
snapshot() {
|
|
305
|
+
return actor.snapshot();
|
|
306
|
+
},
|
|
307
|
+
on: actor.on.bind(actor),
|
|
308
|
+
settled: actor.settled.bind(actor)
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
function recordTransition(history, from, to, eventId) {
|
|
312
|
+
history.append({
|
|
313
|
+
type: "transition",
|
|
314
|
+
data: {
|
|
315
|
+
from,
|
|
316
|
+
event: eventId,
|
|
317
|
+
to,
|
|
318
|
+
timestamp: Date.now()
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
history.append({
|
|
322
|
+
type: "state_visit",
|
|
323
|
+
data: {
|
|
324
|
+
stateName: to,
|
|
325
|
+
timestamp: Date.now()
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
history.append({
|
|
329
|
+
type: "effect",
|
|
330
|
+
data: {
|
|
331
|
+
stateName: to,
|
|
332
|
+
timestamp: Date.now()
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
function trackSendEvent(history, event) {
|
|
337
|
+
const eventId = event?.id ?? "unknown";
|
|
338
|
+
history.append({
|
|
339
|
+
type: "send",
|
|
340
|
+
data: {
|
|
341
|
+
event: eventId,
|
|
342
|
+
timestamp: Date.now()
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
return { id: eventId };
|
|
346
|
+
}
|
|
347
|
+
function instrument(actor) {
|
|
348
|
+
const history = new History();
|
|
349
|
+
const origSend = actor.send.bind(actor);
|
|
350
|
+
let prevStateName = actor.state.name;
|
|
351
|
+
const pendingEventId = { value: void 0 };
|
|
352
|
+
history.append({
|
|
353
|
+
type: "state_visit",
|
|
354
|
+
data: {
|
|
355
|
+
stateName: actor.state.name,
|
|
356
|
+
timestamp: Date.now()
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
const wrapped = wrapWithProxy(actor, {
|
|
360
|
+
history,
|
|
361
|
+
origSend,
|
|
362
|
+
pendingEventId
|
|
363
|
+
});
|
|
364
|
+
actor.on("change", () => {
|
|
365
|
+
const currentName = actor.state.name;
|
|
366
|
+
if (currentName !== prevStateName) {
|
|
367
|
+
recordTransition(history, prevStateName, currentName, pendingEventId.value ?? "unknown");
|
|
368
|
+
prevStateName = currentName;
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
return wrapped;
|
|
372
|
+
}
|
|
373
|
+
//#endregion
|
|
374
|
+
//#region src/algorithms.ts
|
|
375
|
+
function buildAdjacency(graph) {
|
|
376
|
+
const adj = {};
|
|
377
|
+
for (const node of graph.nodes) adj[node.id] = [];
|
|
378
|
+
for (const edge of graph.edges) if (adj[edge.source]) adj[edge.source].push({
|
|
379
|
+
target: edge.target,
|
|
380
|
+
label: edge.label
|
|
381
|
+
});
|
|
382
|
+
return adj;
|
|
383
|
+
}
|
|
384
|
+
function bfsWalk(adj, start, visit) {
|
|
385
|
+
const visited = {};
|
|
386
|
+
const queue = [start];
|
|
387
|
+
while (queue.length > 0) {
|
|
388
|
+
const current = queue.shift();
|
|
389
|
+
if (visited[current]) continue;
|
|
390
|
+
visited[current] = true;
|
|
391
|
+
if (visit(current)) break;
|
|
392
|
+
const neighbors = adj[current] || [];
|
|
393
|
+
for (let i = 0; i < neighbors.length; i++) {
|
|
394
|
+
const target = neighbors[i].target;
|
|
395
|
+
if (!visited[target]) queue.push(target);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return visited;
|
|
399
|
+
}
|
|
400
|
+
function reachable(graph, fromId, toId) {
|
|
401
|
+
return bfsWalk(buildAdjacency(graph), fromId, (node) => node === toId)[toId] === true;
|
|
402
|
+
}
|
|
403
|
+
function allPaths(graph, fromId, toId) {
|
|
404
|
+
const adj = buildAdjacency(graph);
|
|
405
|
+
const results = [];
|
|
406
|
+
const visited = {};
|
|
407
|
+
function dfs(current, path) {
|
|
408
|
+
if (current === toId) {
|
|
409
|
+
results.push(path.slice());
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
if (visited[current]) return;
|
|
413
|
+
visited[current] = true;
|
|
414
|
+
const neighbors = adj[current] || [];
|
|
415
|
+
for (let i = 0; i < neighbors.length; i++) {
|
|
416
|
+
path.push(neighbors[i].target);
|
|
417
|
+
dfs(neighbors[i].target, path);
|
|
418
|
+
path.pop();
|
|
419
|
+
}
|
|
420
|
+
delete visited[current];
|
|
421
|
+
}
|
|
422
|
+
dfs(fromId, [fromId]);
|
|
423
|
+
return results;
|
|
424
|
+
}
|
|
425
|
+
function findCycles(graph) {
|
|
426
|
+
const adj = buildAdjacency(graph);
|
|
427
|
+
const cycles = [];
|
|
428
|
+
const visited = {};
|
|
429
|
+
const inStack = {};
|
|
430
|
+
const path = [];
|
|
431
|
+
function dfs(node) {
|
|
432
|
+
if (inStack[node]) {
|
|
433
|
+
const cycleStart = path.indexOf(node);
|
|
434
|
+
if (cycleStart !== -1) cycles.push(path.slice(cycleStart).concat(node));
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
if (visited[node]) return;
|
|
438
|
+
visited[node] = true;
|
|
439
|
+
inStack[node] = true;
|
|
440
|
+
path.push(node);
|
|
441
|
+
const neighbors = adj[node] || [];
|
|
442
|
+
for (let i = 0; i < neighbors.length; i++) dfs(neighbors[i].target);
|
|
443
|
+
path.pop();
|
|
444
|
+
delete inStack[node];
|
|
445
|
+
}
|
|
446
|
+
for (const node of graph.nodes) dfs(node.id);
|
|
447
|
+
return cycles;
|
|
448
|
+
}
|
|
449
|
+
function unreachableNodes(graph, fromId) {
|
|
450
|
+
const visited = bfsWalk(buildAdjacency(graph), fromId, () => false);
|
|
451
|
+
const result = [];
|
|
452
|
+
for (const node of graph.nodes) if (!visited[node.id]) result.push(node.id);
|
|
453
|
+
return result;
|
|
454
|
+
}
|
|
455
|
+
function shortestPath(graph, fromId, toId) {
|
|
456
|
+
const adj = buildAdjacency(graph);
|
|
457
|
+
const visited = {};
|
|
458
|
+
const queue = [{
|
|
459
|
+
node: fromId,
|
|
460
|
+
path: [fromId]
|
|
461
|
+
}];
|
|
462
|
+
while (queue.length > 0) {
|
|
463
|
+
const { node: current, path } = queue.shift();
|
|
464
|
+
if (current === toId) return path;
|
|
465
|
+
if (visited[current]) continue;
|
|
466
|
+
visited[current] = true;
|
|
467
|
+
const neighbors = adj[current] || [];
|
|
468
|
+
for (let i = 0; i < neighbors.length; i++) {
|
|
469
|
+
const target = neighbors[i].target;
|
|
470
|
+
if (!visited[target]) queue.push({
|
|
471
|
+
node: target,
|
|
472
|
+
path: path.concat(target)
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
//#endregion
|
|
479
|
+
export { History, INITIAL_NODE_ID, allPaths, buildGraph, collectActiveStates, findCycles, instrument, reachable, shortestPath, unreachableNodes };
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mantaq/traversal",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Graph extraction, traversal algorithms, and runtime tracking for mantaq actors.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/AndersCan/mantaq",
|
|
9
|
+
"directory": "packages/traversal"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
14
|
+
"type": "module",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": "./dist/index.mjs",
|
|
17
|
+
"./package.json": "./package.json"
|
|
18
|
+
},
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@mantaq/core": "0.0.1",
|
|
24
|
+
"@mantaq/utils": "0.0.1"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/node": "^26.2.0",
|
|
28
|
+
"@typescript/native-preview": "7.0.0-dev.20260707.2",
|
|
29
|
+
"@vitest/coverage-v8": "4.1.10",
|
|
30
|
+
"typescript": "^7.0.2",
|
|
31
|
+
"vite": "npm:@voidzero-dev/vite-plus-core@0.2.8",
|
|
32
|
+
"vite-plus": "0.2.8"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "vp pack",
|
|
36
|
+
"dev": "vp pack --watch",
|
|
37
|
+
"test": "vp test",
|
|
38
|
+
"check": "vp check",
|
|
39
|
+
"coverage": "vp test run --coverage"
|
|
40
|
+
}
|
|
41
|
+
}
|