@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,64 @@
1
+ import type { MorphismError, ObjectValue, SemanticObject } from "./domain";
2
+ import { err, ok, type Result } from "./result";
3
+
4
+ /** Preserve literal object key unions from a const definition list. */
5
+ export function defineObjects<TState extends object>() {
6
+ return <const TObjs extends readonly SemanticObject<TState, string>[]>(objects: TObjs): TObjs =>
7
+ objects;
8
+ }
9
+
10
+ export function objectByKey<TState extends object>(
11
+ objects: readonly SemanticObject<TState>[],
12
+ key: string,
13
+ ): SemanticObject<TState> | undefined {
14
+ return objects.find((object) => object.key === key);
15
+ }
16
+
17
+ /**
18
+ * Classify a state into exactly one object. Returns unclassified or ambiguous
19
+ * errors when zero or multiple predicates match.
20
+ */
21
+ export function classify<TState extends object>(
22
+ state: TState,
23
+ objects: readonly SemanticObject<TState>[],
24
+ ): Result<ObjectValue<TState, string>, MorphismError> {
25
+ const matches: SemanticObject<TState>[] = [];
26
+ for (const object of objects) {
27
+ if (object.contains(state)) matches.push(object);
28
+ }
29
+ if (matches.length === 0) {
30
+ return err({ type: "unclassified", reason: "State is unclassified." });
31
+ }
32
+ if (matches.length > 1) {
33
+ return err({ type: "ambiguous", keys: matches.map((m) => m.key) });
34
+ }
35
+ const match = matches[0];
36
+ if (!match) {
37
+ return err({ type: "unclassified", reason: "State is unclassified." });
38
+ }
39
+ return ok({ object: match.key, state });
40
+ }
41
+
42
+ /**
43
+ * Classify and require a specific source key (wrong-source when classified
44
+ * elsewhere).
45
+ */
46
+ export function classifyAs<TState extends object, K extends string>(
47
+ state: TState,
48
+ objects: readonly SemanticObject<TState>[],
49
+ expected: K,
50
+ ): Result<ObjectValue<TState, K>, MorphismError> {
51
+ if (!objectByKey(objects, expected)) {
52
+ return err({ type: "unknown-object", key: expected });
53
+ }
54
+ const classified = classify(state, objects);
55
+ if (!classified.ok) return classified;
56
+ if (classified.value.object !== expected) {
57
+ return err({
58
+ type: "wrong-source",
59
+ expected,
60
+ actual: classified.value.object,
61
+ });
62
+ }
63
+ return ok({ object: expected, state: classified.value.state });
64
+ }
@@ -0,0 +1,22 @@
1
+ export type Result<T, E> =
2
+ | { readonly ok: true; readonly value: T }
3
+ | { readonly ok: false; readonly error: E };
4
+
5
+ export function ok<T>(value: T): Result<T, never> {
6
+ return { ok: true, value };
7
+ }
8
+
9
+ export function err<E>(error: E): Result<never, E> {
10
+ return { ok: false, error };
11
+ }
12
+
13
+ export function flatMap<T, U, E>(
14
+ result: Result<T, E>,
15
+ fn: (value: T) => Result<U, E>,
16
+ ): Result<U, E> {
17
+ return result.ok ? fn(result.value) : result;
18
+ }
19
+
20
+ export function mapError<T, E, F>(result: Result<T, E>, fn: (error: E) => F): Result<T, F> {
21
+ return result.ok ? result : err(fn(result.error));
22
+ }
@@ -0,0 +1,126 @@
1
+ import type { IPathRepository, Path, Value } from "./domain";
2
+
3
+ export const PathRepository: IPathRepository = {
4
+ paths: (object, prefix = "") => {
5
+ type TState = typeof object;
6
+ type TPath = Path<TState>;
7
+
8
+ const _paths: TPath[] = [];
9
+
10
+ for (const key in object) {
11
+ if (Object.hasOwn(object, key)) {
12
+ const currentPath = prefix ? `${prefix}.${key}` : key;
13
+ _paths.push(currentPath as TPath);
14
+
15
+ const value = object[key];
16
+ if (value && typeof value === "object") {
17
+ if (Array.isArray(value)) {
18
+ // Add array indexing paths
19
+ for (let i = 0; i < value.length; i++) {
20
+ const indexPath = `${currentPath}[${i}]`;
21
+ _paths.push(indexPath as TPath);
22
+
23
+ // If array element is an object, recursively add its paths
24
+ if (value[i] && typeof value[i] === "object" && !Array.isArray(value[i])) {
25
+ _paths.push(...PathRepository.paths(value[i] as object, indexPath));
26
+ }
27
+ }
28
+ } else {
29
+ // Regular object traversal
30
+ _paths.push(...PathRepository.paths(value as object, currentPath));
31
+ }
32
+ }
33
+ }
34
+ }
35
+
36
+ return _paths;
37
+ },
38
+
39
+ isPath: (path, object) => {
40
+ if (path === ".") {
41
+ return false;
42
+ }
43
+ return (PathRepository.paths(object) as string[]).includes(path);
44
+ },
45
+
46
+ isPathRef: (maybeRef, state) =>
47
+ typeof maybeRef === "string" &&
48
+ maybeRef.startsWith("$") &&
49
+ PathRepository.isPath(maybeRef.slice(1), state),
50
+
51
+ valueFromPath: (path, state) => {
52
+ type TState = typeof state;
53
+ type TPath = typeof path;
54
+ type TValue = Value<TState, TPath>;
55
+
56
+ // Parse path segments that can include array indexing
57
+ const segments = PathRepository.parsePathSegments(path);
58
+
59
+ let current: unknown = state;
60
+ for (const segment of segments) {
61
+ if (segment.type === "property") {
62
+ if (current && typeof current === "object" && segment.key in current) {
63
+ current = (current as Record<string, unknown>)[segment.key];
64
+ } else {
65
+ throw new Error(`Invalid path: ${path}`);
66
+ }
67
+ } else if (segment.type === "index") {
68
+ if (Array.isArray(current) && segment.index >= 0 && segment.index < current.length) {
69
+ current = current[segment.index];
70
+ } else {
71
+ throw new Error(`Invalid path: ${path}`);
72
+ }
73
+ }
74
+ }
75
+
76
+ return current as TValue;
77
+ },
78
+
79
+ // Helper method to parse path segments
80
+ parsePathSegments: (path: string) => {
81
+ const segments: Array<{ type: "property"; key: string } | { type: "index"; index: number }> =
82
+ [];
83
+ let i = 0;
84
+
85
+ while (i < path.length) {
86
+ if (path[i] === "[") {
87
+ // Parse array index
88
+ const start = i + 1;
89
+ const end = path.indexOf("]", start);
90
+ if (end === -1) {
91
+ throw new Error(`Invalid path: unclosed bracket in ${path}`);
92
+ }
93
+ const indexStr = path.slice(start, end);
94
+ const index = parseInt(indexStr, 10);
95
+ if (Number.isNaN(index)) {
96
+ throw new Error(`Invalid path: non-numeric array index in ${path}`);
97
+ }
98
+ segments.push({ type: "index", index });
99
+ i = end + 1;
100
+
101
+ // Skip dot after bracket if present
102
+ if (i < path.length && path[i] === ".") {
103
+ i++;
104
+ }
105
+ } else {
106
+ // Parse property name
107
+ let end = i;
108
+ while (end < path.length && path[end] !== "." && path[end] !== "[") {
109
+ end++;
110
+ }
111
+ const key = path.slice(i, end);
112
+ if (key) {
113
+ segments.push({ type: "property", key });
114
+ }
115
+ i = end;
116
+
117
+ // Skip dot
118
+ if (i < path.length && path[i] === ".") {
119
+ i++;
120
+ }
121
+ }
122
+ }
123
+
124
+ return segments;
125
+ },
126
+ };
@@ -0,0 +1,54 @@
1
+ export type Path<TState> = TState extends object
2
+ ? {
3
+ [K in keyof TState]-?: K extends string
4
+ ? TState[K] extends object
5
+ ? TState[K] extends (infer U)[]
6
+ ? K | `${K}[${number}]` | `${K}[${number}].${Path<U>}`
7
+ : K | `${K}.${Path<TState[K]>}`
8
+ : K
9
+ : never;
10
+ }[keyof TState]
11
+ : never;
12
+
13
+ export type Value<TState, TPath extends string> = TPath extends `${infer K}[${string}].${infer R}`
14
+ ? K extends keyof TState
15
+ ? TState[K] extends (infer U)[]
16
+ ? Value<U, R>
17
+ : never
18
+ : never
19
+ : TPath extends `${infer K}[${string}]`
20
+ ? K extends keyof TState
21
+ ? TState[K] extends (infer U)[]
22
+ ? U
23
+ : never
24
+ : never
25
+ : TPath extends `${infer K}.${infer R}`
26
+ ? K extends keyof TState
27
+ ? Value<TState[K], R>
28
+ : never
29
+ : TPath extends keyof TState
30
+ ? TState[TPath]
31
+ : never;
32
+
33
+ export type PathReference<TState extends object, TPath extends Path<TState>> = `$${TPath}`;
34
+
35
+ export interface IPathRepository {
36
+ readonly paths: <TState extends object>(state: TState, prefix?: string) => Path<TState>[];
37
+
38
+ readonly isPath: <TState extends object, TPath extends string>(
39
+ path: TPath,
40
+ state: TState,
41
+ ) => boolean;
42
+
43
+ // TODO: return errors
44
+ readonly isPathRef: <TState extends object>(maybeRef: unknown, state: TState) => boolean;
45
+
46
+ readonly valueFromPath: <TState extends object, TPath extends Path<TState> = Path<TState>>(
47
+ path: TPath,
48
+ state: TState,
49
+ ) => Value<TState, TPath>;
50
+
51
+ readonly parsePathSegments: (
52
+ path: string,
53
+ ) => Array<{ type: "property"; key: string } | { type: "index"; index: number }>;
54
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./adapters";
2
+ export * from "./domain";
@@ -0,0 +1,54 @@
1
+ import Ajv from "ajv";
2
+ import { TransitionRepository } from "../transition/adapters";
3
+ import type { TransitionFn, TransitionSuccess } from "../transition/domain";
4
+ import type { ExecutableStateSpace, IStateSpaceRepository, StateSpace } from "./domain";
5
+
6
+ const ajv = new Ajv({
7
+ strict: false,
8
+ validateSchema: false,
9
+ });
10
+
11
+ export const StateSpaceRepository: IStateSpaceRepository = {
12
+ makeExecutable: <TState extends object>(
13
+ stateSpace: StateSpace<TState>,
14
+ ): ExecutableStateSpace<TState> => {
15
+ const validator = ajv.compile(stateSpace.shape);
16
+ const byName = new Map<string, TransitionFn<TState>>();
17
+
18
+ for (const transition of stateSpace.transitions) {
19
+ if (byName.has(transition.name)) {
20
+ throw new Error(`Duplicate transition name: ${transition.name}`);
21
+ }
22
+ byName.set(transition.name, TransitionRepository.makeExecutable(transition, validator));
23
+ }
24
+
25
+ const apply: ExecutableStateSpace<TState>["apply"] = (state, name, context) => {
26
+ const fn = byName.get(name);
27
+ if (!fn) {
28
+ return {
29
+ success: false,
30
+ name,
31
+ state,
32
+ error: `Unknown transition: ${name}`,
33
+ };
34
+ }
35
+ return fn(state, context);
36
+ };
37
+
38
+ return {
39
+ shape: stateSpace.shape,
40
+ transitions: stateSpace.transitions,
41
+ apply,
42
+ enabled: (state, context) => {
43
+ const neighbors: TransitionSuccess<TState>[] = [];
44
+ for (const transition of stateSpace.transitions) {
45
+ const result = apply(state, transition.name, context);
46
+ if (result.success) {
47
+ neighbors.push(result);
48
+ }
49
+ }
50
+ return neighbors;
51
+ },
52
+ };
53
+ },
54
+ };
@@ -0,0 +1,61 @@
1
+ import type { JSONSchemaType } from "ajv";
2
+ import type { Transition, TransitionResult, TransitionSuccess } from "../transition/domain";
3
+
4
+ export type Schema<T> = JSONSchemaType<T>;
5
+
6
+ export type StateSpace<TState extends object> = {
7
+ shape: Schema<TState>;
8
+ transitions: Transition<TState>[];
9
+ };
10
+
11
+ export interface ExecutableStateSpace<TState extends object> {
12
+ shape: Schema<TState>;
13
+ transitions: Transition<TState>[];
14
+ apply(state: TState, name: string, context?: unknown): TransitionResult<TState>;
15
+ enabled(state: TState, context?: unknown): TransitionSuccess<TState>[];
16
+ }
17
+
18
+ /**
19
+ * State space with the state parameter erased.
20
+ * Use for heterogeneous registries (catalogs, kernels) where many
21
+ * StateSpace<T_i> share one Map. Not a covariant subtype of StateSpace<object>.
22
+ *
23
+ * Prefer `StateSpace<T>` at machine definition sites. Use `AnyStateSpace` /
24
+ * `eraseStateSpace` only when putting spaces into a mixed collection.
25
+ */
26
+ // biome-ignore lint/suspicious/noExplicitAny: intentional erase for heterogeneous registries
27
+ export type AnyStateSpace = StateSpace<any>;
28
+
29
+ /**
30
+ * Erase T for registry storage. Prefer this over casting to StateSpace<object>.
31
+ * Accepts a structural shape so unions of StateSpace<T_i> type-check
32
+ * (AJV JSONSchemaType blocks StateSpace<T> assigning to StateSpace<any> otherwise).
33
+ */
34
+ export function eraseStateSpace(space: {
35
+ readonly shape: unknown;
36
+ readonly transitions: readonly unknown[];
37
+ }): AnyStateSpace {
38
+ return space as AnyStateSpace;
39
+ }
40
+
41
+ /**
42
+ * Executable state space with the state parameter erased.
43
+ * Same registry use case as AnyStateSpace. Calling apply/enabled on an erased
44
+ * space means you are responsible for passing a state that matches the original T.
45
+ */
46
+ // biome-ignore lint/suspicious/noExplicitAny: intentional erase for heterogeneous registries
47
+ export type AnyExecutableStateSpace = ExecutableStateSpace<any>;
48
+
49
+ /** Erase T for registry storage. Prefer this over casting to ExecutableStateSpace<object>. */
50
+ export function eraseExecutableStateSpace<TState extends object>(
51
+ space: ExecutableStateSpace<TState>,
52
+ ): AnyExecutableStateSpace {
53
+ // AJV JSONSchemaType keeps Schema<T> from assigning to Schema<any>; erase here.
54
+ return space as AnyExecutableStateSpace;
55
+ }
56
+
57
+ export interface IStateSpaceRepository {
58
+ readonly makeExecutable: <TState extends object>(
59
+ stateSpace: StateSpace<TState>,
60
+ ) => ExecutableStateSpace<TState>;
61
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./adapters";
2
+ export * from "./domain";
@@ -0,0 +1,96 @@
1
+ import { type ICompare, PriorityQueue } from "@datastructures-js/priority-queue";
2
+ import type { StudyConfig, StudyResult } from "../explorer";
3
+ import type { TransitionResult, TransitionSuccess } from "../transition/domain";
4
+
5
+ export interface BFSConfig<T extends object> extends StudyConfig<T> {
6
+ comparator?: ICompare<TransitionResult<T>>;
7
+ }
8
+
9
+ export interface BFSResult<T extends object> extends StudyResult<T> {
10
+ reconstructPath?: (goalStateHash: string) => Promise<TransitionSuccess<T>[]>;
11
+ }
12
+
13
+ export async function bfs<T extends object>({
14
+ explorer,
15
+ initialState,
16
+ exitConditions,
17
+ context,
18
+ comparator = (a, b) => (a.effect?.cost ?? 0) - (b.effect?.cost ?? 0),
19
+ }: BFSConfig<T>): Promise<BFSResult<T>> {
20
+ explorer.resetState();
21
+
22
+ const queue = new PriorityQueue<TransitionSuccess<T>>(comparator);
23
+ const visited = new Set<string>();
24
+ const parents = new Map<string, { parentHash: string; transition: TransitionSuccess<T> }>();
25
+
26
+ const initialHash = await explorer.encode(initialState);
27
+ visited.add(initialHash);
28
+
29
+ const initialNeighbors = await explorer.neighbors(initialState, context);
30
+ for (const neighbor of initialNeighbors) {
31
+ if (neighbor.result.success) {
32
+ queue.enqueue(neighbor.result);
33
+ parents.set(neighbor.hash, {
34
+ parentHash: initialHash,
35
+ transition: neighbor.result,
36
+ });
37
+ }
38
+ }
39
+
40
+ const reconstructPath = async (goalStateHash: string): Promise<TransitionSuccess<T>[]> => {
41
+ const path: TransitionSuccess<T>[] = [];
42
+ let currentHash = goalStateHash;
43
+
44
+ while (parents.has(currentHash)) {
45
+ const parent = parents.get(currentHash);
46
+ if (!parent) break;
47
+ path.unshift(parent.transition);
48
+ currentHash = parent.parentHash;
49
+ }
50
+
51
+ return path;
52
+ };
53
+
54
+ let lastTransition: TransitionResult<T> | null = null;
55
+
56
+ while (!queue.isEmpty()) {
57
+ const current = queue.dequeue();
58
+ if (!current) break;
59
+ lastTransition = current;
60
+
61
+ // Check async or sync exit conditions
62
+ for (const exitCondition of exitConditions) {
63
+ const maybeExit = exitCondition(explorer) as
64
+ | StudyResult<T>
65
+ | null
66
+ | Promise<StudyResult<T> | null>;
67
+
68
+ const exitResult = await Promise.resolve(maybeExit);
69
+ if (exitResult) {
70
+ return {
71
+ ...exitResult,
72
+ reconstructPath,
73
+ };
74
+ }
75
+ }
76
+
77
+ const currentHash = await explorer.encode(current.state);
78
+ if (visited.has(currentHash)) continue;
79
+ visited.add(currentHash);
80
+
81
+ const neighbors = await explorer.neighbors(current.state, context);
82
+ for (const neighbor of neighbors) {
83
+ const { result, hash } = neighbor;
84
+ if (result.success && !visited.has(hash)) {
85
+ queue.enqueue(result);
86
+ parents.set(hash, { parentHash: currentHash, transition: result });
87
+ }
88
+ }
89
+ }
90
+
91
+ return {
92
+ lastTransition: lastTransition,
93
+ exitReason: "Search exhausted without meeting exit conditions",
94
+ reconstructPath,
95
+ };
96
+ }
@@ -0,0 +1,10 @@
1
+ import type { StudyConfig, StudyResult } from "../explorer";
2
+
3
+ export interface DFSConfig<T extends object> extends StudyConfig<T> {}
4
+
5
+ /**
6
+ * DFS implementation that follows one path deeply before backtracking.
7
+ */
8
+ export async function dfs<T extends object>(_config: DFSConfig<T>): Promise<StudyResult<T>> {
9
+ throw new Error("DFS is not implemented");
10
+ }
@@ -0,0 +1,78 @@
1
+ import { ConstraintRepository } from "../constraint/adapters";
2
+ import { EffectRepository } from "../effect/adapters";
3
+ import type { ITransitionRepository, TransitionFailure, TransitionSuccess } from "./domain";
4
+
5
+ export const TransitionRepository: ITransitionRepository = {
6
+ apply: (state, transition, validator, context) =>
7
+ TransitionRepository.makeExecutable(transition, validator)(state, context),
8
+
9
+ validateConstraints: (phase, constraints, state) => {
10
+ const constrainstOfPhase = constraints.filter((constraint) => constraint.phase === phase);
11
+ try {
12
+ return constrainstOfPhase.every((constraint) => {
13
+ const result = ConstraintRepository.apply(constraint, state, constraint.path, phase);
14
+ return result.success;
15
+ });
16
+ } catch (_error) {
17
+ return false;
18
+ }
19
+ },
20
+
21
+ makeExecutable: (transition, validator) => (state, context) => {
22
+ const { effect, constraints } = transition;
23
+
24
+ type TState = typeof state;
25
+
26
+ const isValidBefore = TransitionRepository.validateConstraints(
27
+ "before_transition",
28
+ constraints,
29
+ state,
30
+ );
31
+
32
+ if (!isValidBefore) {
33
+ return {
34
+ success: false,
35
+ name: transition.name,
36
+ state,
37
+ error: "Constraints failed before transition",
38
+ effect: effect,
39
+ } satisfies TransitionFailure<TState>;
40
+ }
41
+
42
+ const result = EffectRepository.apply(state, transition, validator, context);
43
+
44
+ if (!result.success) {
45
+ return {
46
+ success: false,
47
+ name: transition.name,
48
+ state,
49
+ error: result.error,
50
+ effect: effect,
51
+ } satisfies TransitionFailure<TState>;
52
+ }
53
+
54
+ // Check after constraints
55
+ const isValidAfter = TransitionRepository.validateConstraints(
56
+ "after_transition",
57
+ constraints,
58
+ result.state,
59
+ );
60
+
61
+ if (!isValidAfter) {
62
+ return {
63
+ success: false,
64
+ name: transition.name,
65
+ state,
66
+ error: "Constraints failed after transition",
67
+ effect: effect,
68
+ } satisfies TransitionFailure<TState>;
69
+ }
70
+
71
+ return {
72
+ success: true,
73
+ name: transition.name,
74
+ state: result.state,
75
+ effect: effect,
76
+ } satisfies TransitionSuccess<TState>;
77
+ },
78
+ } as const;
@@ -0,0 +1,51 @@
1
+ import type { Constraint } from "../constraint/domain";
2
+ import type { Effect } from "../effect/domain";
3
+
4
+ export interface Transition<TState extends object> {
5
+ name: string;
6
+ effect: Effect<TState>;
7
+ constraints: Constraint<TState>[]; // Constraints could point to any path of state, not exclusively TPath
8
+ }
9
+
10
+ export type TransitionFn<TState extends object> = (
11
+ state: TState,
12
+ context?: unknown,
13
+ ) => TransitionResult<TState>;
14
+
15
+ export type TransitionSuccess<TState extends object> = {
16
+ success: true;
17
+ name: string;
18
+ state: TState;
19
+ effect: Effect<TState>;
20
+ };
21
+ export type TransitionFailure<TState extends object> = {
22
+ success: false;
23
+ name: string;
24
+ state: TState;
25
+ error: string;
26
+ effect?: Effect<TState>;
27
+ };
28
+
29
+ export type TransitionResult<TState extends object> =
30
+ | TransitionSuccess<TState>
31
+ | TransitionFailure<TState>;
32
+
33
+ export interface ITransitionRepository {
34
+ readonly apply: <TState extends object>(
35
+ state: TState,
36
+ transition: Transition<TState>,
37
+ validator: (state: TState) => boolean,
38
+ context?: unknown,
39
+ ) => TransitionResult<TState>;
40
+
41
+ readonly makeExecutable: <TState extends object>(
42
+ transition: Transition<TState>,
43
+ validator: (state: TState) => boolean,
44
+ ) => TransitionFn<TState>;
45
+
46
+ readonly validateConstraints: <TState extends object>(
47
+ phase: "before_transition" | "after_transition",
48
+ constraints: Constraint<TState>[],
49
+ state: TState,
50
+ ) => boolean;
51
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./adapters";
2
+ export * from "./domain";