@exellix/graphs-studio-data-flow 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/package.json +33 -0
- package/src/graphContractMetadata.js +484 -0
- package/src/index.js +8 -0
- package/src/informationFlow.js +225 -0
- package/src/informationFlowFocus.js +332 -0
- package/src/informationFlowLayerDocument.js +69 -0
- package/src/informationFlowOutputSurface.js +339 -0
- package/src/ioLinking.js +236 -0
- package/src/lib/flatRuntimeInput.js +38 -0
- package/src/lib/memorixEntityContentTypes.js +116 -0
- package/src/lib/memorixScopedConfig.js +108 -0
- package/src/lib/nodeMetadataAccessors.js +59 -0
- package/src/lib/recordEligibilityRules.js +542 -0
- package/src/lib/recordFiltersJsonConditionsBridge.js +97 -0
- package/src/lib/taskNodeConfiguration.js +117 -0
- package/src/lib/webQueryTemplate.js +277 -0
- package/src/pathClassification.js +133 -0
- package/src/planning.js +3 -0
- package/src/planningSourceFamily.js +109 -0
- package/src/types.ts +246 -0
- package/src/webScopingPlanning.js +131 -0
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured record eligibility under `conditions.dataFilters` on task nodes.
|
|
3
|
+
* Supports legacy `string[]` (path presence) and `{ match, rules, groups? }`.
|
|
4
|
+
*
|
|
5
|
+
* @typedef {{ fieldId?: string, label?: string, path: string, type?: string, operator: string, value?: unknown }} RecordEligibilityRule
|
|
6
|
+
* @typedef {{ match?: 'all'|'any', rules?: RecordEligibilityRule[], groups?: unknown[] }} RecordEligibilityFilters
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { decodeJsonConditionsToRecordFilters } from './recordFiltersJsonConditionsBridge.js';
|
|
10
|
+
|
|
11
|
+
/** @param {unknown} v */
|
|
12
|
+
function isEmptyValue(v) {
|
|
13
|
+
if (v === null || v === undefined) return true;
|
|
14
|
+
if (typeof v === 'string') return v.trim() === '';
|
|
15
|
+
if (Array.isArray(v)) return v.length === 0;
|
|
16
|
+
if (typeof v === 'object') return Object.keys(v).length === 0;
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** @param {unknown} obj
|
|
21
|
+
* @param {string} dotPath */
|
|
22
|
+
function deepGet(obj, dotPath) {
|
|
23
|
+
const p = String(dotPath || '').trim();
|
|
24
|
+
if (!p || obj == null || typeof obj !== 'object') return undefined;
|
|
25
|
+
const parts = p.split('.').filter(Boolean);
|
|
26
|
+
let cur = obj;
|
|
27
|
+
for (const key of parts) {
|
|
28
|
+
if (cur == null || typeof cur !== 'object') return undefined;
|
|
29
|
+
cur = /** @type {Record<string, unknown>} */ (cur)[key];
|
|
30
|
+
}
|
|
31
|
+
return cur;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Resolve a rule path against execution/job memory (playground-style context).
|
|
36
|
+
* @param {{ execution?: Record<string, unknown>, jobMemory?: Record<string, unknown>, taskMemory?: Record<string, unknown> }} ctx
|
|
37
|
+
* @param {string} path
|
|
38
|
+
*/
|
|
39
|
+
export function resolveEligibilityPath(ctx, path) {
|
|
40
|
+
const raw = String(path || '').trim();
|
|
41
|
+
if (!raw) return undefined;
|
|
42
|
+
const execution = ctx.execution && typeof ctx.execution === 'object' && !Array.isArray(ctx.execution) ? ctx.execution : {};
|
|
43
|
+
const jobMemory = ctx.jobMemory && typeof ctx.jobMemory === 'object' && !Array.isArray(ctx.jobMemory) ? ctx.jobMemory : {};
|
|
44
|
+
const taskMemory = ctx.taskMemory && typeof ctx.taskMemory === 'object' && !Array.isArray(ctx.taskMemory) ? ctx.taskMemory : {};
|
|
45
|
+
if (raw.startsWith('execution.')) return deepGet(execution, raw.slice('execution.'.length));
|
|
46
|
+
if (raw.startsWith('jobMemory.')) return deepGet(jobMemory, raw.slice('jobMemory.'.length));
|
|
47
|
+
if (raw.startsWith('taskMemory.')) return deepGet(taskMemory, raw.slice('taskMemory.'.length));
|
|
48
|
+
if (raw.startsWith('input.')) {
|
|
49
|
+
const input = execution.input != null && typeof execution.input === 'object' && !Array.isArray(execution.input) ? execution.input : {};
|
|
50
|
+
return deepGet(input, raw.slice('input.'.length));
|
|
51
|
+
}
|
|
52
|
+
const input = execution.input != null && typeof execution.input === 'object' && !Array.isArray(execution.input) ? execution.input : {};
|
|
53
|
+
const fromInput = deepGet(input, raw);
|
|
54
|
+
if (fromInput !== undefined) return fromInput;
|
|
55
|
+
return deepGet(execution, raw);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const STRING_OPS = new Set([
|
|
59
|
+
'equals',
|
|
60
|
+
'doesNotEqual',
|
|
61
|
+
'contains',
|
|
62
|
+
'doesNotContain',
|
|
63
|
+
'startsWith',
|
|
64
|
+
'endsWith',
|
|
65
|
+
'isEmpty',
|
|
66
|
+
'isNotEmpty',
|
|
67
|
+
'matchesRegex',
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
const NUMBER_OPS = new Set([
|
|
71
|
+
'equals',
|
|
72
|
+
'doesNotEqual',
|
|
73
|
+
'greaterThan',
|
|
74
|
+
'greaterThanOrEqual',
|
|
75
|
+
'lessThan',
|
|
76
|
+
'lessThanOrEqual',
|
|
77
|
+
'between',
|
|
78
|
+
'isEmpty',
|
|
79
|
+
'isNotEmpty',
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
const BOOL_OPS = new Set(['isTrue', 'isFalse', 'isEmpty', 'isNotEmpty']);
|
|
83
|
+
|
|
84
|
+
const DATE_OPS = new Set(['before', 'after', 'between', 'inTheLast', 'olderThan', 'isEmpty', 'isNotEmpty']);
|
|
85
|
+
|
|
86
|
+
const ARRAY_OPS = new Set([
|
|
87
|
+
'contains',
|
|
88
|
+
'doesNotContain',
|
|
89
|
+
'containsAnyOf',
|
|
90
|
+
'containsAllOf',
|
|
91
|
+
'isEmpty',
|
|
92
|
+
'isNotEmpty',
|
|
93
|
+
]);
|
|
94
|
+
|
|
95
|
+
/** @param {string} type */
|
|
96
|
+
export function operatorsForFieldType(type) {
|
|
97
|
+
const t = String(type || 'unknown').toLowerCase();
|
|
98
|
+
if (t === 'number' || t === 'integer') return [...NUMBER_OPS];
|
|
99
|
+
if (t === 'boolean') return [...BOOL_OPS];
|
|
100
|
+
if (t === 'date' || t === 'datetime') return [...DATE_OPS];
|
|
101
|
+
if (t === 'array') return [...ARRAY_OPS];
|
|
102
|
+
return [...STRING_OPS];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** @param {number} ts */
|
|
106
|
+
function toMillis(ts) {
|
|
107
|
+
if (typeof ts === 'number' && Number.isFinite(ts)) return ts;
|
|
108
|
+
if (typeof ts === 'string' && ts.trim()) {
|
|
109
|
+
const d = Date.parse(ts);
|
|
110
|
+
return Number.isFinite(d) ? d : NaN;
|
|
111
|
+
}
|
|
112
|
+
return NaN;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** @param {unknown} v */
|
|
116
|
+
function asNumber(v) {
|
|
117
|
+
if (typeof v === 'number' && Number.isFinite(v)) return v;
|
|
118
|
+
if (typeof v === 'string' && v.trim() !== '') {
|
|
119
|
+
const n = Number(v);
|
|
120
|
+
return Number.isFinite(n) ? n : NaN;
|
|
121
|
+
}
|
|
122
|
+
return NaN;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* @param {RecordEligibilityRule} rule
|
|
127
|
+
* @param {unknown} actual
|
|
128
|
+
*/
|
|
129
|
+
export function evaluateRule(rule, actual) {
|
|
130
|
+
const op = String(rule.operator || '');
|
|
131
|
+
const val = rule.value;
|
|
132
|
+
const empty = isEmptyValue(actual);
|
|
133
|
+
|
|
134
|
+
if (op === 'isEmpty') return empty;
|
|
135
|
+
if (op === 'isNotEmpty') return !empty;
|
|
136
|
+
|
|
137
|
+
if (op === 'isTrue') return actual === true;
|
|
138
|
+
if (op === 'isFalse') return actual === false;
|
|
139
|
+
|
|
140
|
+
if (op === 'equals') {
|
|
141
|
+
const an = asNumber(actual);
|
|
142
|
+
const vn = asNumber(val);
|
|
143
|
+
if (Number.isFinite(an) && Number.isFinite(vn)) return an === vn;
|
|
144
|
+
return String(actual ?? '') === String(val ?? '');
|
|
145
|
+
}
|
|
146
|
+
if (op === 'doesNotEqual') {
|
|
147
|
+
const an = asNumber(actual);
|
|
148
|
+
const vn = asNumber(val);
|
|
149
|
+
if (Number.isFinite(an) && Number.isFinite(vn)) return an !== vn;
|
|
150
|
+
return String(actual ?? '') !== String(val ?? '');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (op === 'between' && (rule.type === 'date' || rule.type === 'datetime')) {
|
|
154
|
+
const at = toMillis(actual);
|
|
155
|
+
const o = val && typeof val === 'object' && !Array.isArray(val) ? val : {};
|
|
156
|
+
const lo = toMillis(/** @type {{ start?: unknown }} */ (o).start);
|
|
157
|
+
const hi = toMillis(/** @type {{ end?: unknown }} */ (o).end);
|
|
158
|
+
return Number.isFinite(at) && Number.isFinite(lo) && Number.isFinite(hi) && at >= lo && at <= hi;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const at = toMillis(actual);
|
|
162
|
+
const vt = toMillis(val);
|
|
163
|
+
if (op === 'before') return Number.isFinite(at) && Number.isFinite(vt) && at < vt;
|
|
164
|
+
if (op === 'after') return Number.isFinite(at) && Number.isFinite(vt) && at > vt;
|
|
165
|
+
if (op === 'inTheLast') {
|
|
166
|
+
const o = val && typeof val === 'object' && !Array.isArray(val) ? val : {};
|
|
167
|
+
const amount = asNumber(/** @type {{ amount?: unknown }} */ (o).amount);
|
|
168
|
+
const unit = String(/** @type {{ unit?: unknown }} */ (o).unit || 'days');
|
|
169
|
+
if (!Number.isFinite(at) || !Number.isFinite(amount) || amount < 0) return false;
|
|
170
|
+
const now = Date.now();
|
|
171
|
+
let ms = amount * 24 * 60 * 60 * 1000;
|
|
172
|
+
if (unit === 'hours') ms = amount * 60 * 60 * 1000;
|
|
173
|
+
if (unit === 'minutes') ms = amount * 60 * 1000;
|
|
174
|
+
return at >= now - ms && at <= now;
|
|
175
|
+
}
|
|
176
|
+
if (op === 'olderThan') {
|
|
177
|
+
const o = val && typeof val === 'object' && !Array.isArray(val) ? val : {};
|
|
178
|
+
const amount = asNumber(/** @type {{ amount?: unknown }} */ (o).amount);
|
|
179
|
+
const unit = String(/** @type {{ unit?: unknown }} */ (o).unit || 'days');
|
|
180
|
+
if (!Number.isFinite(at) || !Number.isFinite(amount) || amount < 0) return false;
|
|
181
|
+
const now = Date.now();
|
|
182
|
+
let ms = amount * 24 * 60 * 60 * 1000;
|
|
183
|
+
if (unit === 'hours') ms = amount * 60 * 60 * 1000;
|
|
184
|
+
if (unit === 'minutes') ms = amount * 60 * 1000;
|
|
185
|
+
return at < now - ms;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const an = asNumber(actual);
|
|
189
|
+
const vn = asNumber(val);
|
|
190
|
+
if (op === 'greaterThan') return Number.isFinite(an) && Number.isFinite(vn) && an > vn;
|
|
191
|
+
if (op === 'greaterThanOrEqual') return Number.isFinite(an) && Number.isFinite(vn) && an >= vn;
|
|
192
|
+
if (op === 'lessThan') return Number.isFinite(an) && Number.isFinite(vn) && an < vn;
|
|
193
|
+
if (op === 'lessThanOrEqual') return Number.isFinite(an) && Number.isFinite(vn) && an <= vn;
|
|
194
|
+
if (op === 'between') {
|
|
195
|
+
const lo = val && typeof val === 'object' && !Array.isArray(val) ? asNumber(/** @type {{ min?: unknown }} */ (val).min) : NaN;
|
|
196
|
+
const hi = val && typeof val === 'object' && !Array.isArray(val) ? asNumber(/** @type {{ max?: unknown }} */ (val).max) : NaN;
|
|
197
|
+
return Number.isFinite(an) && Number.isFinite(lo) && Number.isFinite(hi) && an >= lo && an <= hi;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (typeof actual === 'string' || typeof val === 'string') {
|
|
201
|
+
if (op === 'contains') return typeof actual === 'string' && typeof val === 'string' && actual.includes(val);
|
|
202
|
+
if (op === 'doesNotContain') return typeof actual === 'string' && typeof val === 'string' && !actual.includes(val);
|
|
203
|
+
if (op === 'startsWith') return typeof actual === 'string' && typeof val === 'string' && actual.startsWith(val);
|
|
204
|
+
if (op === 'endsWith') return typeof actual === 'string' && typeof val === 'string' && actual.endsWith(val);
|
|
205
|
+
if (op === 'matchesRegex') {
|
|
206
|
+
if (typeof actual !== 'string' || typeof val !== 'string') return false;
|
|
207
|
+
try {
|
|
208
|
+
return new RegExp(val).test(actual);
|
|
209
|
+
} catch {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const arr = Array.isArray(actual) ? actual : typeof actual === 'string' ? [actual] : actual != null ? [actual] : [];
|
|
216
|
+
const asStrings = (x) => (Array.isArray(x) ? x : [x]).map((y) => String(y));
|
|
217
|
+
if (op === 'contains' && Array.isArray(actual)) {
|
|
218
|
+
const needle = String(val ?? '');
|
|
219
|
+
return arr.some((x) => String(x).includes(needle));
|
|
220
|
+
}
|
|
221
|
+
if (op === 'doesNotContain' && Array.isArray(actual)) {
|
|
222
|
+
const needle = String(val ?? '');
|
|
223
|
+
return !arr.some((x) => String(x).includes(needle));
|
|
224
|
+
}
|
|
225
|
+
if (op === 'containsAnyOf') {
|
|
226
|
+
const needles = asStrings(val);
|
|
227
|
+
return needles.some((n) => arr.some((x) => String(x) === n || String(x).includes(n)));
|
|
228
|
+
}
|
|
229
|
+
if (op === 'containsAllOf') {
|
|
230
|
+
const needles = asStrings(val);
|
|
231
|
+
return needles.every((n) => arr.some((x) => String(x) === n || String(x).includes(n)));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* @param {unknown} raw
|
|
239
|
+
* @returns {RecordEligibilityFilters | null}
|
|
240
|
+
*/
|
|
241
|
+
export function normalizeConditionsDataFilters(raw) {
|
|
242
|
+
if (raw == null) return null;
|
|
243
|
+
if (Array.isArray(raw)) {
|
|
244
|
+
const paths = raw.map((x) => String(x).trim()).filter(Boolean);
|
|
245
|
+
if (paths.length === 0) return null;
|
|
246
|
+
return {
|
|
247
|
+
match: 'all',
|
|
248
|
+
rules: paths.map((path) => ({
|
|
249
|
+
path,
|
|
250
|
+
type: 'unknown',
|
|
251
|
+
operator: 'isNotEmpty',
|
|
252
|
+
label: humanizePathLabel(path),
|
|
253
|
+
})),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
if (typeof raw !== 'object' || Array.isArray(raw)) return null;
|
|
257
|
+
const o = /** @type {Record<string, unknown>} */ (raw);
|
|
258
|
+
const match = o.match === 'any' ? 'any' : 'all';
|
|
259
|
+
const rulesRaw = Array.isArray(o.rules) ? o.rules : [];
|
|
260
|
+
/** @type {RecordEligibilityRule[]} */
|
|
261
|
+
const rules = [];
|
|
262
|
+
for (const r of rulesRaw) {
|
|
263
|
+
if (!r || typeof r !== 'object' || Array.isArray(r)) continue;
|
|
264
|
+
const row = /** @type {Record<string, unknown>} */ (r);
|
|
265
|
+
const path = typeof row.path === 'string' ? row.path.trim() : '';
|
|
266
|
+
if (!path) continue;
|
|
267
|
+
const operator = typeof row.operator === 'string' ? row.operator.trim() : 'isNotEmpty';
|
|
268
|
+
rules.push({
|
|
269
|
+
fieldId: typeof row.fieldId === 'string' ? row.fieldId : undefined,
|
|
270
|
+
label: typeof row.label === 'string' ? row.label : undefined,
|
|
271
|
+
path,
|
|
272
|
+
type: typeof row.type === 'string' ? row.type : 'string',
|
|
273
|
+
operator,
|
|
274
|
+
value: row.value,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
if (rules.length === 0 && !Array.isArray(o.groups)) return null;
|
|
278
|
+
return { match, rules, groups: Array.isArray(o.groups) ? o.groups : undefined };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** @param {string} path */
|
|
282
|
+
export function humanizePathLabel(path) {
|
|
283
|
+
const tail = String(path || '').split('.').filter(Boolean).pop() || path;
|
|
284
|
+
return tail
|
|
285
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
286
|
+
.replace(/[_-]+/g, ' ')
|
|
287
|
+
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* @param {RecordEligibilityFilters | null} filters
|
|
292
|
+
* @param {{ execution?: Record<string, unknown>, jobMemory?: Record<string, unknown>, taskMemory?: Record<string, unknown> }} ctx
|
|
293
|
+
*/
|
|
294
|
+
export function evaluateRecordEligibilityFilters(filters, ctx) {
|
|
295
|
+
if (!filters || !Array.isArray(filters.rules) || filters.rules.length === 0) {
|
|
296
|
+
return { passed: true, matchedRules: 0, failedRules: 0, ruleResults: [], reason: 'no_rules' };
|
|
297
|
+
}
|
|
298
|
+
const mode = filters.match === 'any' ? 'any' : 'all';
|
|
299
|
+
/** @type {{ rule: RecordEligibilityRule, passed: boolean, actual: unknown }[]} */
|
|
300
|
+
const ruleResults = [];
|
|
301
|
+
for (const rule of filters.rules) {
|
|
302
|
+
const actual = resolveEligibilityPath(ctx, rule.path);
|
|
303
|
+
const passed = evaluateRule(rule, actual);
|
|
304
|
+
ruleResults.push({ rule, passed, actual });
|
|
305
|
+
}
|
|
306
|
+
const passedCount = ruleResults.filter((x) => x.passed).length;
|
|
307
|
+
const failedCount = ruleResults.length - passedCount;
|
|
308
|
+
const passed = mode === 'any' ? passedCount > 0 : failedCount === 0;
|
|
309
|
+
return {
|
|
310
|
+
passed,
|
|
311
|
+
matchedRules: passedCount,
|
|
312
|
+
failedRules: failedCount,
|
|
313
|
+
ruleResults,
|
|
314
|
+
mode,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Playground gate: OR across configured arms (narratives, structured data filters, ai filter).
|
|
320
|
+
* Narratives / AI are not evaluated here — reported with `evaluated: false` so execution is not blocked.
|
|
321
|
+
*
|
|
322
|
+
* @param {Record<string, unknown>|null|undefined} conditions
|
|
323
|
+
* @param {{ execution?: Record<string, unknown>, jobMemory?: Record<string, unknown>, taskMemory?: Record<string, unknown> }} ctx
|
|
324
|
+
*/
|
|
325
|
+
export function evaluateNodeConditionsForPlayground(conditions, ctx) {
|
|
326
|
+
const c = conditions && typeof conditions === 'object' && !Array.isArray(conditions) ? conditions : {};
|
|
327
|
+
const narratives = Array.isArray(c.narratives) ? c.narratives.map((x) => String(x)).filter(Boolean) : [];
|
|
328
|
+
const aiCond =
|
|
329
|
+
c.aiCondition && typeof c.aiCondition === 'object' && !Array.isArray(c.aiCondition)
|
|
330
|
+
? /** @type {Record<string, unknown>} */ (c.aiCondition)
|
|
331
|
+
: {};
|
|
332
|
+
const aiFilter =
|
|
333
|
+
typeof aiCond.condition === 'string'
|
|
334
|
+
? aiCond.condition.trim()
|
|
335
|
+
: typeof c.aiFilter === 'string'
|
|
336
|
+
? c.aiFilter.trim()
|
|
337
|
+
: '';
|
|
338
|
+
const df = normalizeConditionsDataFilters(c.dataFilters) || decodeJsonConditionsToRecordFilters(c.jsonConditions);
|
|
339
|
+
|
|
340
|
+
const arms = [];
|
|
341
|
+
if (narratives.length > 0) {
|
|
342
|
+
arms.push({
|
|
343
|
+
id: 'narratives',
|
|
344
|
+
passed: true,
|
|
345
|
+
evaluated: false,
|
|
346
|
+
note: 'Narrative eligibility is evaluated by the graph engine / Narrix runtime, not the playground execute stub.',
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
if (df && df.rules && df.rules.length > 0) {
|
|
350
|
+
const ev = evaluateRecordEligibilityFilters(df, ctx);
|
|
351
|
+
arms.push({
|
|
352
|
+
id: 'dataFilters',
|
|
353
|
+
passed: ev.passed,
|
|
354
|
+
evaluated: true,
|
|
355
|
+
detail: ev,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
if (aiFilter) {
|
|
359
|
+
arms.push({
|
|
360
|
+
id: 'aiCondition',
|
|
361
|
+
passed: true,
|
|
362
|
+
evaluated: false,
|
|
363
|
+
note: 'AI condition prompt is not evaluated in the playground execute stub; the graph engine should apply it.',
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (arms.length === 0) {
|
|
368
|
+
return { skip: false, arms: [], combinedPassed: true };
|
|
369
|
+
}
|
|
370
|
+
const combinedPassed = arms.some((a) => a.passed);
|
|
371
|
+
return {
|
|
372
|
+
skip: !combinedPassed,
|
|
373
|
+
arms,
|
|
374
|
+
combinedPassed,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Same evaluation rules as {@link evaluateNodeConditionsForPlayground}: `metadata.graphEntry.conditions`
|
|
380
|
+
* uses the identical payload shape as task `conditions`.
|
|
381
|
+
*
|
|
382
|
+
* @param {Record<string, unknown>|null|undefined} graphEntryConditions
|
|
383
|
+
* @param {{ execution?: Record<string, unknown>, jobMemory?: Record<string, unknown>, taskMemory?: Record<string, unknown> }} ctx
|
|
384
|
+
*/
|
|
385
|
+
export function evaluateGraphEntryConditionsForPlayground(graphEntryConditions, ctx) {
|
|
386
|
+
return evaluateNodeConditionsForPlayground(graphEntryConditions, ctx);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* @param {unknown} example
|
|
391
|
+
* @returns {Record<string, unknown>|null}
|
|
392
|
+
*/
|
|
393
|
+
export function tryParseJsonObjectExample(example) {
|
|
394
|
+
if (!example || typeof example !== 'object' || Array.isArray(example)) return null;
|
|
395
|
+
const format = typeof example.format === 'string' ? example.format : 'json';
|
|
396
|
+
const value = example.value;
|
|
397
|
+
if (typeof value !== 'string' || !value.trim()) return null;
|
|
398
|
+
if (format !== 'json') return null;
|
|
399
|
+
try {
|
|
400
|
+
const parsed = JSON.parse(value);
|
|
401
|
+
if (parsed != null && typeof parsed === 'object' && !Array.isArray(parsed)) return /** @type {Record<string, unknown>} */ (parsed);
|
|
402
|
+
} catch {
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Build field picker options for the Record Filters UI.
|
|
410
|
+
* @param {{
|
|
411
|
+
* graphData?: Record<string, unknown>|null,
|
|
412
|
+
* smartInputPaths?: { path: string, title?: string }[],
|
|
413
|
+
* runtimeReadPaths?: string[],
|
|
414
|
+
* }} p
|
|
415
|
+
*/
|
|
416
|
+
export function buildRecordFilterFieldCatalog(p) {
|
|
417
|
+
/** @type {{ id: string, label: string, path: string, section: string, type: string, enumValues?: string[] }[]} */
|
|
418
|
+
const out = [];
|
|
419
|
+
const seen = new Set();
|
|
420
|
+
|
|
421
|
+
const push = (id, label, path, section, type, enumValues) => {
|
|
422
|
+
const key = `${section}:${path}`;
|
|
423
|
+
if (seen.has(key)) return;
|
|
424
|
+
seen.add(key);
|
|
425
|
+
out.push({ id, label, path, section, type, ...(enumValues?.length ? { enumValues } : {}) });
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
const ge = p.graphData?.metadata && typeof p.graphData.metadata === 'object' && !Array.isArray(p.graphData.metadata)
|
|
429
|
+
? /** @type {Record<string, unknown>} */ (p.graphData.metadata).graphEntry
|
|
430
|
+
: null;
|
|
431
|
+
const geObj = ge && typeof ge === 'object' && !Array.isArray(ge) ? /** @type {Record<string, unknown>} */ (ge) : null;
|
|
432
|
+
const ex = geObj?.exampleInput ?? (Array.isArray(geObj?.exampleInputs) ? geObj.exampleInputs[0] : null);
|
|
433
|
+
const sample = tryParseJsonObjectExample(ex);
|
|
434
|
+
if (sample) {
|
|
435
|
+
for (const k of Object.keys(sample)) {
|
|
436
|
+
const v = sample[k];
|
|
437
|
+
const t = inferTypeFromValue(v);
|
|
438
|
+
push(`record:${k}`, humanizePathLabel(k), `input.${k}`, 'Record fields', t);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const inputs = geObj && Array.isArray(geObj.inputs) ? geObj.inputs : [];
|
|
443
|
+
for (const row of inputs) {
|
|
444
|
+
if (!row || typeof row !== 'object' || Array.isArray(row)) continue;
|
|
445
|
+
const r = /** @type {Record<string, unknown>} */ (row);
|
|
446
|
+
const pathStr = typeof r.path === 'string' ? r.path.trim() : '';
|
|
447
|
+
if (!pathStr) continue;
|
|
448
|
+
const name = typeof r.name === 'string' ? r.name : typeof r.title === 'string' ? r.title : humanizePathLabel(pathStr);
|
|
449
|
+
const fullPath = pathStr.startsWith('execution.') ? pathStr : pathStr.startsWith('input.') ? pathStr : `input.${pathStr}`;
|
|
450
|
+
push(`entry:${pathStr}`, String(name), fullPath, 'Record fields', inferTypeFromValue(r.example));
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
for (const sp of p.smartInputPaths || []) {
|
|
454
|
+
const path = String(sp.path || '').trim();
|
|
455
|
+
if (!path) continue;
|
|
456
|
+
const full = path.startsWith('input.') ? path : `input.${path.replace(/^\.+/, '')}`;
|
|
457
|
+
const title = String(sp.title || '').trim();
|
|
458
|
+
push(`smart:${path}`, title || humanizePathLabel(path), full, 'Smart input fields', 'string');
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
for (const path of p.runtimeReadPaths || []) {
|
|
462
|
+
const pth = String(path || '').trim();
|
|
463
|
+
if (!pth) continue;
|
|
464
|
+
push(`rt:${pth}`, humanizePathLabel(pth.split('.').pop() || pth), pth, 'Runtime fields', 'unknown');
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return out;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** @param {unknown} v */
|
|
471
|
+
function inferTypeFromValue(v) {
|
|
472
|
+
if (v == null) return 'string';
|
|
473
|
+
if (typeof v === 'number') return 'number';
|
|
474
|
+
if (typeof v === 'boolean') return 'boolean';
|
|
475
|
+
if (Array.isArray(v)) return 'array';
|
|
476
|
+
if (typeof v === 'string') {
|
|
477
|
+
if (/^\d{4}-\d{2}-\d{2}/.test(v) && Number.isFinite(Date.parse(v))) return 'date';
|
|
478
|
+
return 'string';
|
|
479
|
+
}
|
|
480
|
+
return 'string';
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* @param {RecordEligibilityFilters|null} filters
|
|
485
|
+
* @param {Record<string, unknown>[]} samples
|
|
486
|
+
*/
|
|
487
|
+
export function previewRecordEligibilityAgainstSamples(filters, samples) {
|
|
488
|
+
if (!filters || !filters.rules?.length || !Array.isArray(samples) || samples.length === 0) {
|
|
489
|
+
return { available: false, reason: 'No sample records or no rules to evaluate.', totalRecords: 0, matchedRecords: 0, previewRows: [] };
|
|
490
|
+
}
|
|
491
|
+
let matched = 0;
|
|
492
|
+
/** @type {Record<string, unknown>[]} */
|
|
493
|
+
const previewRows = [];
|
|
494
|
+
for (const rec of samples) {
|
|
495
|
+
const ctx = { execution: { input: rec } };
|
|
496
|
+
const ev = evaluateRecordEligibilityFilters(filters, ctx);
|
|
497
|
+
if (ev.passed) {
|
|
498
|
+
matched++;
|
|
499
|
+
if (previewRows.length < 25) previewRows.push(rec);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return {
|
|
503
|
+
available: true,
|
|
504
|
+
totalRecords: samples.length,
|
|
505
|
+
matchedRecords: matched,
|
|
506
|
+
previewRows,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Sample `execution.input`-shaped objects for Studio preview (graph examples + last run snapshot).
|
|
512
|
+
* @param {Record<string, unknown>|null|undefined} graphData
|
|
513
|
+
* @param {unknown} lastExec
|
|
514
|
+
*/
|
|
515
|
+
export function collectStudioRecordFilterSamples(graphData, lastExec) {
|
|
516
|
+
/** @type {Record<string, unknown>[]} */
|
|
517
|
+
const samples = [];
|
|
518
|
+
const push = (o) => {
|
|
519
|
+
if (o && typeof o === 'object' && !Array.isArray(o)) samples.push(/** @type {Record<string, unknown>} */ (o));
|
|
520
|
+
};
|
|
521
|
+
const md =
|
|
522
|
+
graphData?.metadata && typeof graphData.metadata === 'object' && !Array.isArray(graphData.metadata)
|
|
523
|
+
? /** @type {Record<string, unknown>} */ (graphData.metadata)
|
|
524
|
+
: null;
|
|
525
|
+
const ge = md?.graphEntry && typeof md.graphEntry === 'object' && !Array.isArray(md.graphEntry) ? md.graphEntry : null;
|
|
526
|
+
if (ge) {
|
|
527
|
+
const inputs = Array.isArray(ge.exampleInputs) ? ge.exampleInputs : [];
|
|
528
|
+
for (const ex of inputs) {
|
|
529
|
+
const o = tryParseJsonObjectExample(ex);
|
|
530
|
+
if (o) push(o);
|
|
531
|
+
}
|
|
532
|
+
const one = tryParseJsonObjectExample(ge.exampleInput);
|
|
533
|
+
if (one) push(one);
|
|
534
|
+
}
|
|
535
|
+
const le = lastExec && typeof lastExec === 'object' && !Array.isArray(lastExec) ? /** @type {Record<string, unknown>} */ (lastExec) : null;
|
|
536
|
+
const ep = le?.executionPreview && typeof le.executionPreview === 'object' && !Array.isArray(le.executionPreview)
|
|
537
|
+
? /** @type {Record<string, unknown>} */ (le.executionPreview)
|
|
538
|
+
: null;
|
|
539
|
+
const inp = ep?.input;
|
|
540
|
+
if (inp && typeof inp === 'object' && !Array.isArray(inp)) push(/** @type {Record<string, unknown>} */ (inp));
|
|
541
|
+
return samples;
|
|
542
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridge studio record-eligibility UI (`RecordEligibilityFilters`) and canonical
|
|
3
|
+
* `conditions.jsonConditions` (`{ condition, parameters?, negate? }`).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Studio convention — graph-engine accepts any non-empty `condition` string. */
|
|
7
|
+
export const RECORD_ELIGIBILITY_JSON_CONDITION_ID = 'exellix.recordEligibility.v1';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {import('./recordEligibilityRules.js').RecordEligibilityFilters} RecordEligibilityFilters
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {RecordEligibilityFilters | null | undefined} filters
|
|
15
|
+
* @returns {Record<string, unknown> | undefined}
|
|
16
|
+
*/
|
|
17
|
+
export function encodeRecordFiltersToJsonConditions(filters) {
|
|
18
|
+
if (!filters || !Array.isArray(filters.rules) || filters.rules.length === 0) return undefined;
|
|
19
|
+
const match = filters.match === 'any' ? 'any' : 'all';
|
|
20
|
+
/** @type {Record<string, unknown>[]} */
|
|
21
|
+
const rules = [];
|
|
22
|
+
for (const r of filters.rules) {
|
|
23
|
+
if (!r || typeof r !== 'object') continue;
|
|
24
|
+
const path = typeof r.path === 'string' ? r.path.trim() : '';
|
|
25
|
+
const operator = typeof r.operator === 'string' ? r.operator.trim() : '';
|
|
26
|
+
if (!path || !operator) continue;
|
|
27
|
+
/** @type {Record<string, unknown>} */
|
|
28
|
+
const rule = { path, operator };
|
|
29
|
+
if (typeof r.type === 'string' && r.type.trim()) rule.type = r.type.trim();
|
|
30
|
+
if (r.value !== undefined && r.value !== '') rule.value = r.value;
|
|
31
|
+
if (typeof r.fieldId === 'string' && r.fieldId.trim()) rule.fieldId = r.fieldId.trim();
|
|
32
|
+
if (typeof r.label === 'string' && r.label.trim()) rule.label = r.label.trim();
|
|
33
|
+
rules.push(rule);
|
|
34
|
+
}
|
|
35
|
+
if (rules.length === 0) return undefined;
|
|
36
|
+
/** @type {Record<string, unknown>} */
|
|
37
|
+
const parameters = { match, rules };
|
|
38
|
+
if (Array.isArray(filters.groups) && filters.groups.length > 0) {
|
|
39
|
+
parameters.groups = filters.groups;
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
condition: RECORD_ELIGIBILITY_JSON_CONDITION_ID,
|
|
43
|
+
parameters,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {unknown} jsonConditions
|
|
49
|
+
* @returns {RecordEligibilityFilters | undefined}
|
|
50
|
+
*/
|
|
51
|
+
export function decodeJsonConditionsToRecordFilters(jsonConditions) {
|
|
52
|
+
if (!jsonConditions || typeof jsonConditions !== 'object' || Array.isArray(jsonConditions)) return undefined;
|
|
53
|
+
const jc = /** @type {Record<string, unknown>} */ (jsonConditions);
|
|
54
|
+
if (jc.condition !== RECORD_ELIGIBILITY_JSON_CONDITION_ID) return undefined;
|
|
55
|
+
const params = jc.parameters;
|
|
56
|
+
if (!params || typeof params !== 'object' || Array.isArray(params)) return undefined;
|
|
57
|
+
const p = /** @type {Record<string, unknown>} */ (params);
|
|
58
|
+
const match = p.match === 'any' ? 'any' : 'all';
|
|
59
|
+
const rulesRaw = Array.isArray(p.rules) ? p.rules : [];
|
|
60
|
+
/** @type {import('./recordEligibilityRules.js').RecordEligibilityRule[]} */
|
|
61
|
+
const rules = [];
|
|
62
|
+
for (const r of rulesRaw) {
|
|
63
|
+
if (!r || typeof r !== 'object' || Array.isArray(r)) continue;
|
|
64
|
+
const row = /** @type {Record<string, unknown>} */ (r);
|
|
65
|
+
const path = typeof row.path === 'string' ? row.path.trim() : '';
|
|
66
|
+
const operator = typeof row.operator === 'string' ? row.operator.trim() : '';
|
|
67
|
+
if (!path || !operator) continue;
|
|
68
|
+
/** @type {import('./recordEligibilityRules.js').RecordEligibilityRule} */
|
|
69
|
+
const rule = { path, operator };
|
|
70
|
+
if (typeof row.type === 'string') rule.type = row.type;
|
|
71
|
+
if (row.value !== undefined) rule.value = row.value;
|
|
72
|
+
if (typeof row.fieldId === 'string') rule.fieldId = row.fieldId;
|
|
73
|
+
if (typeof row.label === 'string') rule.label = row.label;
|
|
74
|
+
rules.push(rule);
|
|
75
|
+
}
|
|
76
|
+
if (rules.length === 0) return undefined;
|
|
77
|
+
/** @type {RecordEligibilityFilters} */
|
|
78
|
+
const out = { match, rules };
|
|
79
|
+
if (Array.isArray(p.groups) && p.groups.length > 0) out.groups = p.groups;
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @param {unknown} conditions
|
|
85
|
+
* @returns {RecordEligibilityFilters | undefined}
|
|
86
|
+
*/
|
|
87
|
+
export function readRecordFiltersFromConditions(conditions) {
|
|
88
|
+
if (!conditions || typeof conditions !== 'object' || Array.isArray(conditions)) return undefined;
|
|
89
|
+
const c = /** @type {Record<string, unknown>} */ (conditions);
|
|
90
|
+
const fromJson = decodeJsonConditionsToRecordFilters(c.jsonConditions);
|
|
91
|
+
if (fromJson) return fromJson;
|
|
92
|
+
const df = c.dataFilters;
|
|
93
|
+
if (df && typeof df === 'object' && !Array.isArray(df)) {
|
|
94
|
+
return /** @type {RecordEligibilityFilters} */ (df);
|
|
95
|
+
}
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|