@very-coffee/statespace 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/LICENSE +21 -0
- package/README.md +74 -0
- package/package.json +52 -0
- package/src/codex/adapters.ts +8 -0
- package/src/codex/entity.ts +7 -0
- package/src/codex/index.ts +2 -0
- package/src/constraint/adapters.ts +66 -0
- package/src/constraint/domain.ts +61 -0
- package/src/constraint/index.ts +2 -0
- package/src/effect/adapters.ts +131 -0
- package/src/effect/domain.ts +97 -0
- package/src/effect/index.ts +2 -0
- package/src/effect/libs.ts +97 -0
- package/src/explorer/adapters.ts +155 -0
- package/src/explorer/domain.ts +57 -0
- package/src/explorer/index.ts +2 -0
- package/src/index.ts +9 -0
- package/src/morphisms/category.ts +231 -0
- package/src/morphisms/compile.ts +93 -0
- package/src/morphisms/domain.ts +111 -0
- package/src/morphisms/index.ts +21 -0
- package/src/morphisms/objects.ts +64 -0
- package/src/morphisms/result.ts +22 -0
- package/src/path/adapters.ts +126 -0
- package/src/path/domain.ts +54 -0
- package/src/path/index.ts +2 -0
- package/src/statespace/adapters.ts +54 -0
- package/src/statespace/domain.ts +61 -0
- package/src/statespace/index.ts +2 -0
- package/src/studies/bfs.ts +96 -0
- package/src/studies/dfs.ts +10 -0
- package/src/transition/adapters.ts +78 -0
- package/src/transition/domain.ts +51 -0
- package/src/transition/index.ts +2 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import Queue from "queue";
|
|
2
|
+
import type { Codex } from "../codex/entity";
|
|
3
|
+
import type { ExecutableStateSpace, Schema } from "../statespace/domain";
|
|
4
|
+
import type { HashedTransition, IExplorer, MarkovChain, MarkovGraph, StudyConfig } from "./domain";
|
|
5
|
+
|
|
6
|
+
export class Explorer<T extends object> implements IExplorer<T> {
|
|
7
|
+
private _graph: MarkovGraph = new Map();
|
|
8
|
+
private _uniqueStates: number = 0;
|
|
9
|
+
private _totalOperations: number = 0;
|
|
10
|
+
|
|
11
|
+
constructor(
|
|
12
|
+
private readonly stateSpace: ExecutableStateSpace<T>,
|
|
13
|
+
private readonly codex: Codex<T>,
|
|
14
|
+
) {}
|
|
15
|
+
|
|
16
|
+
private async _processTransition(
|
|
17
|
+
name: string,
|
|
18
|
+
initialState: T,
|
|
19
|
+
transitionMap: Map<string, MarkovChain[2]>,
|
|
20
|
+
context?: unknown,
|
|
21
|
+
): Promise<HashedTransition<T> | null> {
|
|
22
|
+
this._totalOperations++;
|
|
23
|
+
const result = this.stateSpace.apply(initialState, name, context);
|
|
24
|
+
|
|
25
|
+
if (result.success) {
|
|
26
|
+
const resultStateHash = await this.encode(result.state);
|
|
27
|
+
|
|
28
|
+
const transitionEntry = transitionMap.get(resultStateHash);
|
|
29
|
+
if (!transitionEntry) {
|
|
30
|
+
this._uniqueStates++;
|
|
31
|
+
transitionMap.set(resultStateHash, [
|
|
32
|
+
{
|
|
33
|
+
name: result.name,
|
|
34
|
+
...("path" in result.effect ? { path: result.effect.path } : {}),
|
|
35
|
+
cost: result.effect.cost,
|
|
36
|
+
meta: result.effect.meta,
|
|
37
|
+
},
|
|
38
|
+
1,
|
|
39
|
+
]);
|
|
40
|
+
} else {
|
|
41
|
+
transitionEntry[1]++;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return { result, hash: resultStateHash };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async neighbors(initialState: T, context?: unknown): Promise<HashedTransition<T>[]> {
|
|
51
|
+
const neighbors: HashedTransition<T>[] = [];
|
|
52
|
+
for await (const neighbor of this.neighborIterator(initialState, context)) {
|
|
53
|
+
neighbors.push(neighbor);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return neighbors;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async *neighborIterator(initialState: T, context?: unknown): AsyncGenerator<HashedTransition<T>> {
|
|
60
|
+
const initialStateHash = await this.encode(initialState);
|
|
61
|
+
let transitionMap = this._graph.get(initialStateHash);
|
|
62
|
+
if (!transitionMap) {
|
|
63
|
+
this._uniqueStates++;
|
|
64
|
+
transitionMap = new Map();
|
|
65
|
+
this._graph.set(initialStateHash, transitionMap);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Use queue package for better async processing with streaming results
|
|
69
|
+
const results: HashedTransition<T>[] = [];
|
|
70
|
+
let processedCount = 0;
|
|
71
|
+
|
|
72
|
+
const q = new Queue({
|
|
73
|
+
concurrency: Infinity, // Process all transitions in parallel
|
|
74
|
+
autostart: true,
|
|
75
|
+
results: [],
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Add each transition as a job to the queue
|
|
79
|
+
for (const transition of this.stateSpace.transitions) {
|
|
80
|
+
q.push(async () => {
|
|
81
|
+
const hashedTransition = await this._processTransition(
|
|
82
|
+
transition.name,
|
|
83
|
+
initialState,
|
|
84
|
+
transitionMap,
|
|
85
|
+
context,
|
|
86
|
+
);
|
|
87
|
+
if (hashedTransition) {
|
|
88
|
+
results.push(hashedTransition);
|
|
89
|
+
}
|
|
90
|
+
processedCount++;
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Stream results as they become available
|
|
95
|
+
while (processedCount < this.stateSpace.transitions.length || results.length > 0) {
|
|
96
|
+
const next = results.shift();
|
|
97
|
+
if (next) {
|
|
98
|
+
yield next;
|
|
99
|
+
} else {
|
|
100
|
+
// Small delay to prevent busy waiting
|
|
101
|
+
await new Promise((resolve) => setTimeout(resolve, 1));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async encode(state: T): Promise<string> {
|
|
107
|
+
return this.codex.encode(state);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async decode(key: string): Promise<T> {
|
|
111
|
+
return this.codex.decode(key);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Runs a study with this explorer instance, providing a unified interface
|
|
116
|
+
* for executing different types of algorithms and studies.
|
|
117
|
+
*/
|
|
118
|
+
async study<TResult>(
|
|
119
|
+
study: (config: StudyConfig<T>) => Promise<TResult>,
|
|
120
|
+
config: Omit<StudyConfig<T>, "explorer">,
|
|
121
|
+
): Promise<TResult> {
|
|
122
|
+
// Reset internal state for clean study execution
|
|
123
|
+
this.resetState();
|
|
124
|
+
|
|
125
|
+
return await study({
|
|
126
|
+
explorer: this,
|
|
127
|
+
...config,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Resets the internal state tracking for a fresh study execution
|
|
133
|
+
*/
|
|
134
|
+
resetState(): void {
|
|
135
|
+
this._graph.clear();
|
|
136
|
+
this._uniqueStates = 0;
|
|
137
|
+
this._totalOperations = 0;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
get graph(): MarkovGraph {
|
|
141
|
+
return this._graph;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
get uniqueStates(): number {
|
|
145
|
+
return this._uniqueStates;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
get totalOperations(): number {
|
|
149
|
+
return this._totalOperations;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
get shape(): Schema<T> {
|
|
153
|
+
return this.stateSpace.shape;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Hash } from "../codex/entity";
|
|
2
|
+
import type { Metadata } from "../effect/domain";
|
|
3
|
+
import type { Schema } from "../statespace/domain";
|
|
4
|
+
import type { TransitionResult, TransitionSuccess } from "../transition/domain";
|
|
5
|
+
|
|
6
|
+
export interface IExplorer<T extends object> {
|
|
7
|
+
readonly graph: MarkovGraph;
|
|
8
|
+
readonly uniqueStates: number;
|
|
9
|
+
readonly totalOperations: number;
|
|
10
|
+
readonly shape: Schema<T>;
|
|
11
|
+
|
|
12
|
+
neighbors(initialState: T, context?: unknown): Promise<HashedTransition<T>[]>;
|
|
13
|
+
neighborIterator(initialState: T, context?: unknown): AsyncGenerator<HashedTransition<T>>;
|
|
14
|
+
encode(state: T): Promise<string>;
|
|
15
|
+
decode(key: string): Promise<T>;
|
|
16
|
+
study<TResult>(
|
|
17
|
+
study: (config: StudyConfig<T>) => Promise<TResult>,
|
|
18
|
+
config: Omit<StudyConfig<T>, "explorer">,
|
|
19
|
+
): Promise<TResult>;
|
|
20
|
+
resetState(): void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type StudyResult<T extends object> = {
|
|
24
|
+
lastTransition: TransitionResult<T> | null;
|
|
25
|
+
exitReason: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export interface StudyConfig<T extends object> {
|
|
29
|
+
explorer: IExplorer<T>;
|
|
30
|
+
initialState: T;
|
|
31
|
+
context?: unknown;
|
|
32
|
+
exitConditions: ((
|
|
33
|
+
explorer: IExplorer<T>,
|
|
34
|
+
) => StudyResult<T> | null | Promise<StudyResult<T> | null>)[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type MarkovChain = [
|
|
38
|
+
Hash,
|
|
39
|
+
Hash,
|
|
40
|
+
[
|
|
41
|
+
{
|
|
42
|
+
name: string;
|
|
43
|
+
/** Present for path-focused effects; omitted for whole-state transforms. */
|
|
44
|
+
path?: string;
|
|
45
|
+
meta?: Metadata;
|
|
46
|
+
cost?: number | null | undefined;
|
|
47
|
+
},
|
|
48
|
+
number, // number of times this transition has been taken
|
|
49
|
+
],
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
export type MarkovGraph = Map<MarkovChain[0], Map<MarkovChain[1], MarkovChain[2]>>;
|
|
53
|
+
|
|
54
|
+
export type HashedTransition<T extends object> = {
|
|
55
|
+
result: TransitionSuccess<T>;
|
|
56
|
+
hash: Hash;
|
|
57
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from "./codex";
|
|
2
|
+
export * from "./constraint";
|
|
3
|
+
export * from "./effect";
|
|
4
|
+
export * from "./explorer";
|
|
5
|
+
export * from "./path";
|
|
6
|
+
export * from "./statespace";
|
|
7
|
+
export * from "./studies/bfs";
|
|
8
|
+
export * from "./studies/dfs";
|
|
9
|
+
export * from "./transition";
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Arrow,
|
|
3
|
+
CompositionCertificate,
|
|
4
|
+
MorphismDef,
|
|
5
|
+
MorphismError,
|
|
6
|
+
ObjectValue,
|
|
7
|
+
SemanticObject,
|
|
8
|
+
} from "./domain";
|
|
9
|
+
import { classify, classifyAs, objectByKey } from "./objects";
|
|
10
|
+
import { err, flatMap, ok, type Result } from "./result";
|
|
11
|
+
|
|
12
|
+
/** Preserve literal morphism name unions from a const definition list. */
|
|
13
|
+
export function defineMorphisms<TState extends object, TContext = unknown>() {
|
|
14
|
+
return <
|
|
15
|
+
const TDefs extends readonly MorphismDef<
|
|
16
|
+
TState,
|
|
17
|
+
TContext,
|
|
18
|
+
string,
|
|
19
|
+
string,
|
|
20
|
+
// biome-ignore lint/suspicious/noExplicitAny: open params slot for parameterized defs
|
|
21
|
+
any
|
|
22
|
+
>[],
|
|
23
|
+
>(
|
|
24
|
+
definitions: TDefs,
|
|
25
|
+
): TDefs => definitions;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Seal a morphism definition with fixed params into a certified Arrow.
|
|
30
|
+
* Source membership and target closure are enforced on every run.
|
|
31
|
+
*/
|
|
32
|
+
export function instantiate<TState extends object, TContext, S extends string, T extends string, P>(
|
|
33
|
+
def: MorphismDef<TState, TContext, S, T, P>,
|
|
34
|
+
params: P,
|
|
35
|
+
objects: readonly SemanticObject<TState>[],
|
|
36
|
+
/** Override transition name when params distinguish instances. */
|
|
37
|
+
instanceName?: string,
|
|
38
|
+
): Arrow<TState, S, T, P> {
|
|
39
|
+
if (!objectByKey(objects, def.source)) {
|
|
40
|
+
throw new Error(`Unknown source object: ${def.source}`);
|
|
41
|
+
}
|
|
42
|
+
if (!objectByKey(objects, def.target)) {
|
|
43
|
+
throw new Error(`Unknown target object: ${def.target}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const name = instanceName ?? def.name;
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
name,
|
|
50
|
+
source: def.source,
|
|
51
|
+
target: def.target,
|
|
52
|
+
params,
|
|
53
|
+
run: (value, context) => {
|
|
54
|
+
if (value.object !== def.source) {
|
|
55
|
+
return err({
|
|
56
|
+
type: "wrong-source",
|
|
57
|
+
expected: def.source,
|
|
58
|
+
actual: value.object,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
// Re-check live containment in case the witness is stale.
|
|
62
|
+
const sourceCheck = classifyAs(value.state, objects, def.source);
|
|
63
|
+
if (!sourceCheck.ok) return sourceCheck;
|
|
64
|
+
|
|
65
|
+
let nextState: TState;
|
|
66
|
+
try {
|
|
67
|
+
const effectResult = def.run(sourceCheck.value, context as TContext, params);
|
|
68
|
+
if (!effectResult.ok) return effectResult;
|
|
69
|
+
nextState = effectResult.value;
|
|
70
|
+
} catch (cause) {
|
|
71
|
+
return err({
|
|
72
|
+
type: "effect-failed",
|
|
73
|
+
cause: cause instanceof Error ? cause.message : String(cause),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const targetCheck = classify(nextState, objects);
|
|
78
|
+
if (!targetCheck.ok) {
|
|
79
|
+
return err({
|
|
80
|
+
type: "invalid-target",
|
|
81
|
+
expected: def.target,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
if (targetCheck.value.object !== def.target) {
|
|
85
|
+
return err({
|
|
86
|
+
type: "invalid-target",
|
|
87
|
+
expected: def.target,
|
|
88
|
+
actual: targetCheck.value.object,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
return ok({ object: def.target, state: nextState });
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Identity endomorphism for an object key. */
|
|
97
|
+
export function identityArrow<TState extends object, K extends string>(
|
|
98
|
+
objectKey: K,
|
|
99
|
+
): Arrow<TState, K, K> {
|
|
100
|
+
return {
|
|
101
|
+
name: `id_${objectKey}`,
|
|
102
|
+
source: objectKey,
|
|
103
|
+
target: objectKey,
|
|
104
|
+
run: (value) => {
|
|
105
|
+
if (value.object !== objectKey) {
|
|
106
|
+
return err({
|
|
107
|
+
type: "wrong-source",
|
|
108
|
+
expected: objectKey,
|
|
109
|
+
actual: value.object,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return ok({ object: objectKey, state: value.state });
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Compose arrows left-to-right (f then g). Compatible pairs always compose;
|
|
119
|
+
* connectivity failures yield a failed certificate and a failing apply.
|
|
120
|
+
*/
|
|
121
|
+
export function composeArrows<TState extends object>(
|
|
122
|
+
arrows: readonly Arrow<TState>[],
|
|
123
|
+
): {
|
|
124
|
+
certificate: CompositionCertificate;
|
|
125
|
+
arrow: Arrow<TState>;
|
|
126
|
+
} {
|
|
127
|
+
if (arrows.length === 0) {
|
|
128
|
+
const error = "Empty composition.";
|
|
129
|
+
return {
|
|
130
|
+
certificate: { ok: false, intermediates: [], steps: [], error },
|
|
131
|
+
arrow: {
|
|
132
|
+
name: "(empty)",
|
|
133
|
+
source: "",
|
|
134
|
+
target: "",
|
|
135
|
+
run: () => err({ type: "compose", reason: error }),
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
for (let i = 0; i < arrows.length - 1; i++) {
|
|
141
|
+
const left = arrows[i];
|
|
142
|
+
const right = arrows[i + 1];
|
|
143
|
+
if (!left || !right) continue;
|
|
144
|
+
if (left.target !== right.source) {
|
|
145
|
+
const reason = `Cannot compose ${left.name} → ${right.name}: ${left.target} ≠ ${right.source}`;
|
|
146
|
+
return {
|
|
147
|
+
certificate: {
|
|
148
|
+
ok: false,
|
|
149
|
+
source: arrows[0]?.source,
|
|
150
|
+
intermediates: [],
|
|
151
|
+
steps: arrows.map((a) => a.name),
|
|
152
|
+
error: reason,
|
|
153
|
+
},
|
|
154
|
+
arrow: {
|
|
155
|
+
name: `(invalid:${left.name}∘${right.name})`,
|
|
156
|
+
source: arrows[0]?.source ?? "",
|
|
157
|
+
target: arrows[arrows.length - 1]?.target ?? "",
|
|
158
|
+
run: () => err({ type: "compose", reason }),
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const first = arrows[0];
|
|
165
|
+
const last = arrows[arrows.length - 1];
|
|
166
|
+
if (!first || !last) {
|
|
167
|
+
const error = "Empty composition.";
|
|
168
|
+
return {
|
|
169
|
+
certificate: { ok: false, intermediates: [], steps: [], error },
|
|
170
|
+
arrow: {
|
|
171
|
+
name: "(empty)",
|
|
172
|
+
source: "",
|
|
173
|
+
target: "",
|
|
174
|
+
run: () => err({ type: "compose", reason: error }),
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const intermediates = arrows.slice(0, -1).map((a) => a.target);
|
|
180
|
+
const certificate: CompositionCertificate = {
|
|
181
|
+
ok: true,
|
|
182
|
+
source: first.source,
|
|
183
|
+
target: last.target,
|
|
184
|
+
intermediates,
|
|
185
|
+
steps: arrows.map((a) => a.name),
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const arrow: Arrow<TState> = {
|
|
189
|
+
name: arrows.map((a) => a.name).join("∘"),
|
|
190
|
+
source: first.source,
|
|
191
|
+
target: last.target,
|
|
192
|
+
run: (value, context) => {
|
|
193
|
+
let current: Result<ObjectValue<TState, string>, MorphismError> = ok(value);
|
|
194
|
+
for (const step of arrows) {
|
|
195
|
+
current = flatMap(current, (v) => step.run(v, context));
|
|
196
|
+
if (!current.ok) return current;
|
|
197
|
+
}
|
|
198
|
+
return current;
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
return { certificate, arrow };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Binary compose: f then g (requires f.target === g.source). */
|
|
206
|
+
export function compose<
|
|
207
|
+
TState extends object,
|
|
208
|
+
A extends string,
|
|
209
|
+
B extends string,
|
|
210
|
+
C extends string,
|
|
211
|
+
>(f: Arrow<TState, A, B>, g: Arrow<TState, B, C>): Arrow<TState, A, C> {
|
|
212
|
+
const { certificate, arrow } = composeArrows([f, g]);
|
|
213
|
+
if (!certificate.ok) {
|
|
214
|
+
return {
|
|
215
|
+
name: `${f.name}∘${g.name}`,
|
|
216
|
+
source: f.source,
|
|
217
|
+
target: g.target,
|
|
218
|
+
run: () =>
|
|
219
|
+
err({
|
|
220
|
+
type: "compose",
|
|
221
|
+
reason: certificate.error ?? "Cannot compose",
|
|
222
|
+
}),
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
name: arrow.name,
|
|
227
|
+
source: f.source,
|
|
228
|
+
target: g.target,
|
|
229
|
+
run: arrow.run,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { EffectResult } from "../effect/domain";
|
|
2
|
+
import { StateSpaceRepository } from "../statespace/adapters";
|
|
3
|
+
import type { Schema, StateSpace } from "../statespace/domain";
|
|
4
|
+
import type { Transition } from "../transition/domain";
|
|
5
|
+
import type { Arrow, MorphismSpace, SemanticObject } from "./domain";
|
|
6
|
+
import { formatMorphismError } from "./domain";
|
|
7
|
+
import { classify } from "./objects";
|
|
8
|
+
|
|
9
|
+
function toTransition<TState extends object>(
|
|
10
|
+
arrow: Arrow<TState>,
|
|
11
|
+
objects: readonly SemanticObject<TState>[],
|
|
12
|
+
): Transition<TState> {
|
|
13
|
+
return {
|
|
14
|
+
name: arrow.name,
|
|
15
|
+
constraints: [],
|
|
16
|
+
effect: {
|
|
17
|
+
operation: "transform",
|
|
18
|
+
transform: (state, context) => {
|
|
19
|
+
const classified = classify(state, objects);
|
|
20
|
+
if (!classified.ok) {
|
|
21
|
+
return {
|
|
22
|
+
success: false,
|
|
23
|
+
error: formatMorphismError(classified.error),
|
|
24
|
+
} satisfies EffectResult<TState>;
|
|
25
|
+
}
|
|
26
|
+
const result = arrow.run(classified.value, context);
|
|
27
|
+
if (!result.ok) {
|
|
28
|
+
return {
|
|
29
|
+
success: false,
|
|
30
|
+
error: formatMorphismError(result.error),
|
|
31
|
+
} satisfies EffectResult<TState>;
|
|
32
|
+
}
|
|
33
|
+
return { success: true, state: result.value.state };
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Compile a finite set of instantiated arrows into a StateSpace plus morphism
|
|
41
|
+
* helpers. No effectPath — transitions are whole-state transforms.
|
|
42
|
+
*/
|
|
43
|
+
export function createMorphismSpace<
|
|
44
|
+
TState extends object,
|
|
45
|
+
const TArrows extends readonly Arrow<TState>[],
|
|
46
|
+
>(options: {
|
|
47
|
+
shape: Schema<TState>;
|
|
48
|
+
objects: readonly SemanticObject<TState>[];
|
|
49
|
+
/** Instantiated arrows; context type is whatever each arrow accepts. */
|
|
50
|
+
morphisms: TArrows;
|
|
51
|
+
}): MorphismSpace<TState, TArrows> {
|
|
52
|
+
const { shape, objects, morphisms } = options;
|
|
53
|
+
|
|
54
|
+
const objectKeys = new Set(objects.map((o) => o.key));
|
|
55
|
+
if (objectKeys.size !== objects.length) {
|
|
56
|
+
throw new Error("Duplicate semantic object key.");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const seen = new Set<string>();
|
|
60
|
+
for (const arrow of morphisms) {
|
|
61
|
+
if (seen.has(arrow.name)) {
|
|
62
|
+
throw new Error(`Duplicate morphism name: ${arrow.name}`);
|
|
63
|
+
}
|
|
64
|
+
seen.add(arrow.name);
|
|
65
|
+
if (!objectKeys.has(arrow.source)) {
|
|
66
|
+
throw new Error(`Unknown source object: ${arrow.source}`);
|
|
67
|
+
}
|
|
68
|
+
if (!objectKeys.has(arrow.target)) {
|
|
69
|
+
throw new Error(`Unknown target object: ${arrow.target}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const byName = new Map<string, TArrows[number]>(morphisms.map((a) => [a.name, a]));
|
|
74
|
+
|
|
75
|
+
const stateSpace: StateSpace<TState> = {
|
|
76
|
+
shape,
|
|
77
|
+
transitions: morphisms.map((arrow) => toTransition(arrow, objects)),
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
let executable: ReturnType<typeof StateSpaceRepository.makeExecutable<TState>> | undefined;
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
objects,
|
|
84
|
+
morphisms,
|
|
85
|
+
stateSpace,
|
|
86
|
+
shape,
|
|
87
|
+
arrowOf: (name) => byName.get(name),
|
|
88
|
+
makeExecutable: () => {
|
|
89
|
+
if (!executable) executable = StateSpaceRepository.makeExecutable(stateSpace);
|
|
90
|
+
return executable;
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { ExecutableStateSpace, Schema, StateSpace } from "../statespace/domain";
|
|
2
|
+
import type { Result } from "./result";
|
|
3
|
+
|
|
4
|
+
export type MorphismError =
|
|
5
|
+
| { type: "unavailable"; reason: string }
|
|
6
|
+
| { type: "unclassified"; reason: string }
|
|
7
|
+
| { type: "ambiguous"; keys: readonly string[] }
|
|
8
|
+
| { type: "wrong-source"; expected: string; actual: string }
|
|
9
|
+
| { type: "invalid-target"; expected: string; actual?: string }
|
|
10
|
+
| { type: "unknown-object"; key: string }
|
|
11
|
+
| { type: "effect-failed"; cause: string }
|
|
12
|
+
| { type: "compose"; reason: string };
|
|
13
|
+
|
|
14
|
+
/** Certified membership of `state` in semantic object `object`. */
|
|
15
|
+
export type ObjectValue<TState extends object, K extends string> = {
|
|
16
|
+
readonly object: K;
|
|
17
|
+
readonly state: TState;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type SemanticObject<TState extends object, K extends string = string> = {
|
|
21
|
+
readonly key: K;
|
|
22
|
+
readonly contains: (state: TState) => boolean;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Declarative morphism. `run` returns the next raw state; the category seals
|
|
27
|
+
* source membership and target closure around it. Witness keys are `string` so
|
|
28
|
+
* heterogeneous definition lists remain assignable (source checked at seal time).
|
|
29
|
+
*/
|
|
30
|
+
export type MorphismDef<
|
|
31
|
+
TState extends object,
|
|
32
|
+
TContext = unknown,
|
|
33
|
+
S extends string = string,
|
|
34
|
+
T extends string = string,
|
|
35
|
+
TParams = undefined,
|
|
36
|
+
> = {
|
|
37
|
+
readonly name: string;
|
|
38
|
+
readonly source: S;
|
|
39
|
+
readonly target: T;
|
|
40
|
+
readonly run: (
|
|
41
|
+
value: ObjectValue<TState, string>,
|
|
42
|
+
context: TContext,
|
|
43
|
+
params: TParams,
|
|
44
|
+
) => Result<TState, MorphismError>;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Instantiated arrow: params closed; run is total into Result.
|
|
49
|
+
* Context stays `unknown` (same as StateSpace.apply) so arrows compose in
|
|
50
|
+
* heterogeneous lists; typed context is enforced by the sealed definition.
|
|
51
|
+
*/
|
|
52
|
+
export type Arrow<
|
|
53
|
+
TState extends object,
|
|
54
|
+
S extends string = string,
|
|
55
|
+
T extends string = string,
|
|
56
|
+
TParams = unknown,
|
|
57
|
+
> = {
|
|
58
|
+
readonly name: string;
|
|
59
|
+
readonly source: S;
|
|
60
|
+
readonly target: T;
|
|
61
|
+
readonly params?: TParams;
|
|
62
|
+
readonly run: (
|
|
63
|
+
value: ObjectValue<TState, string>,
|
|
64
|
+
context?: unknown,
|
|
65
|
+
) => Result<ObjectValue<TState, string>, MorphismError>;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export type CompositionCertificate = {
|
|
69
|
+
readonly ok: boolean;
|
|
70
|
+
readonly source?: string;
|
|
71
|
+
readonly target?: string;
|
|
72
|
+
readonly intermediates: readonly string[];
|
|
73
|
+
readonly steps: readonly string[];
|
|
74
|
+
readonly error?: string;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export type MorphismSpace<
|
|
78
|
+
TState extends object,
|
|
79
|
+
TArrows extends readonly Arrow<TState>[] = readonly Arrow<TState>[],
|
|
80
|
+
> = {
|
|
81
|
+
readonly objects: readonly SemanticObject<TState>[];
|
|
82
|
+
readonly morphisms: TArrows;
|
|
83
|
+
readonly stateSpace: StateSpace<TState>;
|
|
84
|
+
readonly shape: Schema<TState>;
|
|
85
|
+
readonly arrowOf: (name: TArrows[number]["name"]) => TArrows[number] | undefined;
|
|
86
|
+
readonly makeExecutable: () => ExecutableStateSpace<TState>;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/** Format a typed morphism error for TransitionResult.error. */
|
|
90
|
+
export function formatMorphismError(error: MorphismError): string {
|
|
91
|
+
switch (error.type) {
|
|
92
|
+
case "unavailable":
|
|
93
|
+
return error.reason;
|
|
94
|
+
case "unclassified":
|
|
95
|
+
return error.reason;
|
|
96
|
+
case "ambiguous":
|
|
97
|
+
return `Ambiguous region: ${error.keys.join(", ")}`;
|
|
98
|
+
case "wrong-source":
|
|
99
|
+
return `Expected region ${error.expected}, got ${error.actual}`;
|
|
100
|
+
case "invalid-target":
|
|
101
|
+
return error.actual
|
|
102
|
+
? `Effect left region ${error.actual}; expected ${error.expected}`
|
|
103
|
+
: `Effect left unclassified region; expected ${error.expected}`;
|
|
104
|
+
case "unknown-object":
|
|
105
|
+
return `Unknown object: ${error.key}`;
|
|
106
|
+
case "effect-failed":
|
|
107
|
+
return error.cause;
|
|
108
|
+
case "compose":
|
|
109
|
+
return error.reason;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export {
|
|
2
|
+
compose,
|
|
3
|
+
composeArrows,
|
|
4
|
+
defineMorphisms,
|
|
5
|
+
identityArrow,
|
|
6
|
+
instantiate,
|
|
7
|
+
} from "./category";
|
|
8
|
+
export { createMorphismSpace } from "./compile";
|
|
9
|
+
export type {
|
|
10
|
+
Arrow,
|
|
11
|
+
CompositionCertificate,
|
|
12
|
+
MorphismDef,
|
|
13
|
+
MorphismError,
|
|
14
|
+
MorphismSpace,
|
|
15
|
+
ObjectValue,
|
|
16
|
+
SemanticObject,
|
|
17
|
+
} from "./domain";
|
|
18
|
+
export { formatMorphismError } from "./domain";
|
|
19
|
+
export { classify, classifyAs, defineObjects, objectByKey } from "./objects";
|
|
20
|
+
export type { Result } from "./result";
|
|
21
|
+
export { err, flatMap, mapError, ok } from "./result";
|