@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.
@@ -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
+ };
@@ -0,0 +1,2 @@
1
+ export { Explorer } from "./adapters";
2
+ export * from "./domain";
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";