@kindgi/flow 0.0.0-bootstrap.0 → 0.1.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 +201 -0
- package/README.md +262 -2
- package/dist/define.d.ts +36 -0
- package/dist/define.d.ts.map +1 -0
- package/dist/define.js +29 -0
- package/dist/define.js.map +1 -0
- package/dist/errors.d.ts +252 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +4 -0
- package/dist/errors.js.map +1 -0
- package/dist/evaluator.d.ts +69 -0
- package/dist/evaluator.d.ts.map +1 -0
- package/dist/evaluator.js +220 -0
- package/dist/evaluator.js.map +1 -0
- package/dist/flow.schema.json +517 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/loader.d.ts +43 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +1051 -0
- package/dist/loader.js.map +1 -0
- package/dist/scheduler.d.ts +91 -0
- package/dist/scheduler.d.ts.map +1 -0
- package/dist/scheduler.js +187 -0
- package/dist/scheduler.js.map +1 -0
- package/dist/types.d.ts +792 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +71 -0
- package/dist/types.js.map +1 -0
- package/package.json +51 -4
- package/src/define.ts +44 -0
- package/src/errors.ts +306 -0
- package/src/evaluator.ts +254 -0
- package/src/flow.schema.json +517 -0
- package/src/index.ts +85 -0
- package/src/loader.ts +1382 -0
- package/src/scheduler.ts +274 -0
- package/src/types.ts +853 -0
package/src/evaluator.ts
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
import type { NodeId } from '@kindgi/types';
|
|
5
|
+
|
|
6
|
+
import type { Expr, LiteralOperand, Mapping, Operand } from './types.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The evaluation environment. Passed by the kernel at edge dispatch time —
|
|
10
|
+
* one instance per edge evaluation — and when resolving a `Mapping`.
|
|
11
|
+
* Consumers should treat every branch as read-only.
|
|
12
|
+
*
|
|
13
|
+
* Every root is optional at construction; a missing root resolves any
|
|
14
|
+
* dependent path to MISSING (`MISSING_PATH`), which composes cleanly with
|
|
15
|
+
* `exists` and `notExists` predicates.
|
|
16
|
+
*/
|
|
17
|
+
export interface EvalEnv {
|
|
18
|
+
readonly runInput?: unknown;
|
|
19
|
+
readonly state?: unknown;
|
|
20
|
+
/**
|
|
21
|
+
* Outputs of previously-completed nodes, keyed by NodeId. Only nodes whose
|
|
22
|
+
* `step.completed` entry exists in the journal appear here — a fan-in edge
|
|
23
|
+
* whose predicate references a not-yet-completed sibling resolves that
|
|
24
|
+
* path to MISSING.
|
|
25
|
+
*/
|
|
26
|
+
readonly nodeOutputs?: ReadonlyMap<NodeId, unknown>;
|
|
27
|
+
/**
|
|
28
|
+
* Only populated when evaluating a loop node's `exitCondition`. `undefined`
|
|
29
|
+
* outside that context; predicates referencing this root then resolve to
|
|
30
|
+
* MISSING (composes cleanly with `exists` / `notExists`).
|
|
31
|
+
*/
|
|
32
|
+
readonly iterationIndex?: number;
|
|
33
|
+
/**
|
|
34
|
+
* Only populated when evaluating a loop node's `exitCondition`. Holds
|
|
35
|
+
* the just-completed iteration's `$loop-end` output (or the previous
|
|
36
|
+
* iteration's output for while+before at iteration i>0; null for
|
|
37
|
+
* while+before iteration 0).
|
|
38
|
+
*/
|
|
39
|
+
readonly iterationOutput?: unknown;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Distinguishing "resolved to `undefined`" from "path does not exist". */
|
|
43
|
+
const MISSING = Symbol('flow.evaluator.missing');
|
|
44
|
+
type Resolved = unknown | typeof MISSING;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Evaluate a predicate expression against an environment.
|
|
48
|
+
*
|
|
49
|
+
* Semantics summary:
|
|
50
|
+
* - Comparisons on missing operands (path resolves to MISSING) → false.
|
|
51
|
+
* - `exists` on MISSING → false; on `undefined` value at a real path → true.
|
|
52
|
+
* - `truthy` / `falsy` use the JS truthiness rules on the resolved value;
|
|
53
|
+
* MISSING is treated as falsy.
|
|
54
|
+
* - `in` / `notIn` require `set` to be an array; a non-array `set` → false.
|
|
55
|
+
* - `and` / `or` are short-circuiting; empty children are rejected at load time.
|
|
56
|
+
*
|
|
57
|
+
* The evaluator is intentionally total — it never throws for a well-formed
|
|
58
|
+
* expression, so kernel edge evaluation cannot fail due to predicate execution.
|
|
59
|
+
*/
|
|
60
|
+
export function evaluateExpr(expr: Expr, env: EvalEnv): boolean {
|
|
61
|
+
switch (expr.op) {
|
|
62
|
+
case 'eq':
|
|
63
|
+
case 'ne':
|
|
64
|
+
case 'lt':
|
|
65
|
+
case 'lte':
|
|
66
|
+
case 'gt':
|
|
67
|
+
case 'gte':
|
|
68
|
+
return compare(expr.op, resolveOperand(expr.left, env), resolveOperand(expr.right, env));
|
|
69
|
+
case 'in':
|
|
70
|
+
case 'notIn':
|
|
71
|
+
return evalMembership(expr.op, expr, env);
|
|
72
|
+
case 'exists':
|
|
73
|
+
return resolveOperand(expr.value, env) !== MISSING;
|
|
74
|
+
case 'notExists':
|
|
75
|
+
return resolveOperand(expr.value, env) === MISSING;
|
|
76
|
+
case 'truthy':
|
|
77
|
+
return evalTruthy(resolveOperand(expr.value, env));
|
|
78
|
+
case 'falsy':
|
|
79
|
+
return evalFalsy(resolveOperand(expr.value, env));
|
|
80
|
+
case 'and':
|
|
81
|
+
return expr.children.every((c) => evaluateExpr(c, env));
|
|
82
|
+
case 'or':
|
|
83
|
+
return expr.children.some((c) => evaluateExpr(c, env));
|
|
84
|
+
case 'not':
|
|
85
|
+
return !evaluateExpr(expr.child, env);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Resolve a `Mapping` against an environment: literals as given, paths
|
|
91
|
+
* via `resolvePath`. A path that doesn't resolve omits its key — the
|
|
92
|
+
* result never carries a mapped key whose value is missing. Total, like
|
|
93
|
+
* `evaluateExpr`: it never throws for a well-formed mapping.
|
|
94
|
+
*/
|
|
95
|
+
export function resolveMapping(mapping: Mapping, env: EvalEnv): Record<string, unknown> {
|
|
96
|
+
const out: Record<string, unknown> = {};
|
|
97
|
+
for (const [key, operand] of Object.entries(mapping)) {
|
|
98
|
+
const value = resolveOperand(operand, env);
|
|
99
|
+
if (value !== MISSING) out[key] = value;
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function evalMembership(
|
|
105
|
+
op: 'in' | 'notIn',
|
|
106
|
+
expr: { readonly value: Operand; readonly set: Operand },
|
|
107
|
+
env: EvalEnv,
|
|
108
|
+
): boolean {
|
|
109
|
+
const value = resolveOperand(expr.value, env);
|
|
110
|
+
const set = resolveOperand(expr.set, env);
|
|
111
|
+
if (value === MISSING || set === MISSING) return false;
|
|
112
|
+
if (!Array.isArray(set)) return false;
|
|
113
|
+
const found = set.some((item) => deepEqual(item, value));
|
|
114
|
+
return op === 'in' ? found : !found;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function evalTruthy(v: Resolved): boolean {
|
|
118
|
+
if (v === MISSING) return false;
|
|
119
|
+
return Boolean(v);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function evalFalsy(v: Resolved): boolean {
|
|
123
|
+
if (v === MISSING) return true;
|
|
124
|
+
return !v;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function compare(
|
|
128
|
+
op: 'eq' | 'ne' | 'lt' | 'lte' | 'gt' | 'gte',
|
|
129
|
+
left: Resolved,
|
|
130
|
+
right: Resolved,
|
|
131
|
+
): boolean {
|
|
132
|
+
if (op === 'eq' || op === 'ne') return compareEquality(op, left, right);
|
|
133
|
+
return compareOrdering(op, left, right);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function compareEquality(op: 'eq' | 'ne', left: Resolved, right: Resolved): boolean {
|
|
137
|
+
if (left === MISSING || right === MISSING) return op === 'ne' ? left !== right : false;
|
|
138
|
+
const equal = deepEqual(left, right);
|
|
139
|
+
return op === 'eq' ? equal : !equal;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function compareOrdering(
|
|
143
|
+
op: 'lt' | 'lte' | 'gt' | 'gte',
|
|
144
|
+
left: Resolved,
|
|
145
|
+
right: Resolved,
|
|
146
|
+
): boolean {
|
|
147
|
+
if (left === MISSING || right === MISSING) return false;
|
|
148
|
+
if (typeof left !== typeof right) return false;
|
|
149
|
+
if (typeof left !== 'number' && typeof left !== 'string') return false;
|
|
150
|
+
const l = left as number | string;
|
|
151
|
+
const r = right as number | string;
|
|
152
|
+
switch (op) {
|
|
153
|
+
case 'lt':
|
|
154
|
+
return l < r;
|
|
155
|
+
case 'lte':
|
|
156
|
+
return l <= r;
|
|
157
|
+
case 'gt':
|
|
158
|
+
return l > r;
|
|
159
|
+
case 'gte':
|
|
160
|
+
return l >= r;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function resolveOperand(operand: Operand, env: EvalEnv): Resolved {
|
|
165
|
+
if (isLiteral(operand)) return operand.literal;
|
|
166
|
+
return resolvePath(operand.path, env);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function isLiteral(operand: Operand): operand is LiteralOperand {
|
|
170
|
+
return 'literal' in operand;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Exported for tests and diagnostics; keep small and predictable.
|
|
174
|
+
export function resolvePath(path: string, env: EvalEnv): Resolved {
|
|
175
|
+
const segments = path.split('.');
|
|
176
|
+
const [root, ...rest] = segments;
|
|
177
|
+
if (root === undefined) return MISSING;
|
|
178
|
+
|
|
179
|
+
if (root === 'runInput') {
|
|
180
|
+
return walk(env.runInput, rest);
|
|
181
|
+
}
|
|
182
|
+
if (root === 'state') {
|
|
183
|
+
return walk(env.state, rest);
|
|
184
|
+
}
|
|
185
|
+
if (root === 'nodeOutputs') {
|
|
186
|
+
const [nodeId, ...tail] = rest;
|
|
187
|
+
if (nodeId === undefined || nodeId === '') return MISSING;
|
|
188
|
+
if (env.nodeOutputs === undefined) return MISSING;
|
|
189
|
+
if (!env.nodeOutputs.has(nodeId as NodeId)) return MISSING;
|
|
190
|
+
return walk(env.nodeOutputs.get(nodeId as NodeId), tail);
|
|
191
|
+
}
|
|
192
|
+
if (root === 'iterationIndex') {
|
|
193
|
+
if (env.iterationIndex === undefined) return MISSING;
|
|
194
|
+
if (rest.length > 0) return MISSING;
|
|
195
|
+
return env.iterationIndex;
|
|
196
|
+
}
|
|
197
|
+
if (root === 'iterationOutput') {
|
|
198
|
+
if (env.iterationOutput === undefined) return MISSING;
|
|
199
|
+
return walk(env.iterationOutput, rest);
|
|
200
|
+
}
|
|
201
|
+
return MISSING;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function walk(base: unknown, segments: readonly string[]): Resolved {
|
|
205
|
+
let current: unknown = base;
|
|
206
|
+
if (segments.length === 0) return current === undefined ? MISSING : current;
|
|
207
|
+
for (const seg of segments) {
|
|
208
|
+
if (current === null || current === undefined) return MISSING;
|
|
209
|
+
if (typeof current !== 'object') return MISSING;
|
|
210
|
+
const asRecord = current as Readonly<Record<string, unknown>>;
|
|
211
|
+
if (!(seg in asRecord)) return MISSING;
|
|
212
|
+
current = asRecord[seg];
|
|
213
|
+
}
|
|
214
|
+
return current === undefined ? MISSING : current;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Structural JSON equality. Handles primitives, arrays, and plain objects. */
|
|
218
|
+
function deepEqual(a: unknown, b: unknown): boolean {
|
|
219
|
+
if (a === b) return true;
|
|
220
|
+
if (a === null || b === null) return false;
|
|
221
|
+
if (typeof a !== typeof b) return false;
|
|
222
|
+
if (typeof a !== 'object') return false;
|
|
223
|
+
if (Array.isArray(a) || Array.isArray(b)) return arrayEqual(a, b);
|
|
224
|
+
return objectEqual(a as object, b as object);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function arrayEqual(a: unknown, b: unknown): boolean {
|
|
228
|
+
if (!Array.isArray(a) || !Array.isArray(b)) return false;
|
|
229
|
+
if (a.length !== b.length) return false;
|
|
230
|
+
for (let i = 0; i < a.length; i += 1) {
|
|
231
|
+
if (!deepEqual(a[i], b[i])) return false;
|
|
232
|
+
}
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function objectEqual(a: object, b: object): boolean {
|
|
237
|
+
const ka = Object.keys(a).sort();
|
|
238
|
+
const kb = Object.keys(b).sort();
|
|
239
|
+
if (ka.length !== kb.length) return false;
|
|
240
|
+
for (let i = 0; i < ka.length; i += 1) if (ka[i] !== kb[i]) return false;
|
|
241
|
+
const aRec = a as Readonly<Record<string, unknown>>;
|
|
242
|
+
const bRec = b as Readonly<Record<string, unknown>>;
|
|
243
|
+
for (const k of ka) {
|
|
244
|
+
if (!deepEqual(aRec[k as string], bRec[k as string])) return false;
|
|
245
|
+
}
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Distinguishes "path resolved but value was undefined" from "path did not
|
|
251
|
+
* exist at all". Exported so tests and downstream diagnostics can identify
|
|
252
|
+
* the missing marker without depending on a private symbol.
|
|
253
|
+
*/
|
|
254
|
+
export const MISSING_PATH: typeof MISSING = MISSING;
|