@graphrefly/graphrefly 0.9.0 → 0.10.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/dist/index.js CHANGED
@@ -143,13 +143,8 @@ import {
143
143
  nestjs_exports
144
144
  } from "./chunk-UCW3VWMN.js";
145
145
  import {
146
- JsonCodec,
147
- createDagCborCodec,
148
- createDagCborZstdCodec,
149
- graph_exports,
150
- negotiateCodec,
151
- replayWAL
152
- } from "./chunk-A2AJJOSJ.js";
146
+ core_exports
147
+ } from "./chunk-E7OH6ZAZ.js";
153
148
  import {
154
149
  cached,
155
150
  createWatermarkController,
@@ -183,12 +178,17 @@ import {
183
178
  toMessages$,
184
179
  toObservable
185
180
  } from "./chunk-YWTP2XRJ.js";
186
- import {
187
- core_exports
188
- } from "./chunk-E7OH6ZAZ.js";
189
181
  import {
190
182
  ResettableTimer
191
183
  } from "./chunk-WZ2Z2CRV.js";
184
+ import {
185
+ JsonCodec,
186
+ createDagCborCodec,
187
+ createDagCborZstdCodec,
188
+ graph_exports,
189
+ negotiateCodec,
190
+ replayWAL
191
+ } from "./chunk-A2AJJOSJ.js";
192
192
  import {
193
193
  analyzeAndMeasure,
194
194
  computeLineBreaks,
@@ -1045,6 +1045,7 @@ __export(patterns_exports, {
1045
1045
  ai: () => ai_exports,
1046
1046
  cqrs: () => cqrs_exports,
1047
1047
  demoShell: () => demo_shell_exports,
1048
+ graphspec: () => graphspec_exports,
1048
1049
  layout: () => reactive_layout_exports,
1049
1050
  memory: () => memory_exports,
1050
1051
  messaging: () => messaging_exports,
@@ -3024,92 +3025,1209 @@ function demoShell(opts) {
3024
3025
  };
3025
3026
  }
3026
3027
 
3027
- // src/patterns/messaging.ts
3028
- var messaging_exports = {};
3029
- __export(messaging_exports, {
3030
- JobFlowGraph: () => JobFlowGraph,
3031
- JobQueueGraph: () => JobQueueGraph,
3032
- SubscriptionGraph: () => SubscriptionGraph,
3033
- TopicBridgeGraph: () => TopicBridgeGraph,
3034
- TopicGraph: () => TopicGraph,
3035
- jobFlow: () => jobFlow,
3036
- jobQueue: () => jobQueue,
3037
- subscription: () => subscription,
3038
- topic: () => topic,
3039
- topicBridge: () => topicBridge
3028
+ // src/patterns/graphspec.ts
3029
+ var graphspec_exports = {};
3030
+ __export(graphspec_exports, {
3031
+ compileSpec: () => compileSpec,
3032
+ decompileGraph: () => decompileGraph,
3033
+ llmCompose: () => llmCompose,
3034
+ llmRefine: () => llmRefine,
3035
+ specDiff: () => specDiff,
3036
+ validateSpec: () => validateSpec
3040
3037
  });
3041
- var DEFAULT_MAX_PER_PUMP = 2147483647;
3042
- function requireNonNegativeInt(value, label) {
3043
- if (!Number.isFinite(value) || !Number.isInteger(value) || value < 0) {
3044
- throw new Error(`${label} must be a non-negative integer`);
3045
- }
3046
- return value;
3047
- }
3048
- function keepalive2(n) {
3049
- return n.subscribe(() => {
3050
- });
3051
- }
3052
- function messagingMeta(kind, extra) {
3038
+
3039
+ // src/patterns/reduction.ts
3040
+ var reduction_exports = {};
3041
+ __export(reduction_exports, {
3042
+ budgetGate: () => budgetGate,
3043
+ feedback: () => feedback,
3044
+ funnel: () => funnel,
3045
+ scorer: () => scorer,
3046
+ stratify: () => stratify
3047
+ });
3048
+ function baseMeta(kind, meta) {
3053
3049
  return {
3054
- messaging: true,
3055
- messaging_type: kind,
3056
- ...extra ?? {}
3050
+ reduction: true,
3051
+ reduction_type: kind,
3052
+ ...meta ?? {}
3057
3053
  };
3058
3054
  }
3059
- var TopicGraph = class extends Graph {
3060
- _log;
3061
- _keepaliveDisposers = [];
3062
- events;
3063
- latest;
3064
- constructor(name, opts = {}) {
3065
- super(name, opts.graph);
3066
- this._log = reactiveLog([], { name: "events", maxSize: opts.retainedLimit });
3067
- this.events = this._log.entries;
3068
- this.add("events", this.events);
3069
- this.latest = derived(
3070
- [this.events],
3071
- ([snapshot]) => {
3072
- const entries = snapshot.value.entries;
3073
- return entries.length === 0 ? void 0 : entries[entries.length - 1];
3074
- },
3075
- {
3076
- name: "latest",
3077
- describeKind: "derived",
3078
- meta: messagingMeta("topic_latest"),
3079
- initial: void 0
3055
+ function stratify(name, source, rules, opts) {
3056
+ const g = new Graph(name, opts);
3057
+ g.add("source", source);
3058
+ const rulesNode = state(rules, {
3059
+ meta: baseMeta("stratify_rules")
3060
+ });
3061
+ g.add("rules", rulesNode);
3062
+ for (const rule of rules) {
3063
+ _addBranch(g, source, rulesNode, rule);
3064
+ }
3065
+ return g;
3066
+ }
3067
+ function _addBranch(graph, source, rulesNode, rule) {
3068
+ const branchName = `branch/${rule.name}`;
3069
+ let pendingDirty = false;
3070
+ const filterNode = node([source, rulesNode], () => void 0, {
3071
+ describeKind: "operator",
3072
+ meta: baseMeta("stratify_branch", { branch: rule.name }),
3073
+ onMessage(msg, depIndex, actions) {
3074
+ if (depIndex !== 0) return false;
3075
+ const t = msg[0];
3076
+ if (t === DATA) {
3077
+ const value = msg[1];
3078
+ const currentRules = rulesNode.get();
3079
+ const currentRule = currentRules.find((r) => r.name === rule.name);
3080
+ if (currentRule && currentRule.classify(value)) {
3081
+ pendingDirty = false;
3082
+ actions.emit(value);
3083
+ } else {
3084
+ if (pendingDirty) {
3085
+ pendingDirty = false;
3086
+ actions.down([[DIRTY], [RESOLVED]]);
3087
+ }
3088
+ }
3089
+ return true;
3080
3090
  }
3081
- );
3082
- this.add("latest", this.latest);
3083
- this.connect("events", "latest");
3084
- this._keepaliveDisposers.push(keepalive2(this.latest));
3091
+ if (t === DIRTY) {
3092
+ pendingDirty = true;
3093
+ return true;
3094
+ }
3095
+ if (t === RESOLVED) {
3096
+ if (pendingDirty) {
3097
+ pendingDirty = false;
3098
+ actions.down([[DIRTY], [RESOLVED]]);
3099
+ } else {
3100
+ actions.down([[RESOLVED]]);
3101
+ }
3102
+ return true;
3103
+ }
3104
+ if (t === COMPLETE || t === ERROR) {
3105
+ pendingDirty = false;
3106
+ actions.down([msg]);
3107
+ return true;
3108
+ }
3109
+ return false;
3110
+ }
3111
+ });
3112
+ graph.add(branchName, filterNode);
3113
+ graph.connect("source", branchName);
3114
+ if (rule.ops) {
3115
+ const transformed = rule.ops(filterNode);
3116
+ const transformedName = `branch/${rule.name}/out`;
3117
+ graph.add(transformedName, transformed);
3118
+ graph.connect(branchName, transformedName);
3085
3119
  }
3086
- destroy() {
3087
- for (const dispose of this._keepaliveDisposers) dispose();
3088
- this._keepaliveDisposers.length = 0;
3089
- super.destroy();
3120
+ }
3121
+ function funnel(name, sources, stages, opts) {
3122
+ if (sources.length === 0) throw new RangeError("funnel requires at least one source");
3123
+ if (stages.length === 0) throw new RangeError("funnel requires at least one stage");
3124
+ const g = new Graph(name, opts);
3125
+ const merged = sources.length === 1 ? sources[0] : merge(...sources);
3126
+ g.add("merged", merged);
3127
+ let prevOutputPath = "merged";
3128
+ for (let i = 0; i < stages.length; i++) {
3129
+ const stage = stages[i];
3130
+ const sub = new Graph(stage.name);
3131
+ stage.build(sub);
3132
+ try {
3133
+ sub.resolve("input");
3134
+ } catch {
3135
+ throw new Error(`funnel stage "${stage.name}" must define an "input" node`);
3136
+ }
3137
+ try {
3138
+ sub.resolve("output");
3139
+ } catch {
3140
+ throw new Error(`funnel stage "${stage.name}" must define an "output" node`);
3141
+ }
3142
+ g.mount(stage.name, sub);
3143
+ const prevNode = g.resolve(prevOutputPath);
3144
+ const stageInputPath = `${stage.name}::input`;
3145
+ const stageInput = g.resolve(stageInputPath);
3146
+ prevNode.subscribe((msgs) => {
3147
+ for (const msg of msgs) {
3148
+ const t = msg[0];
3149
+ if (t === DATA) {
3150
+ stageInput.down([[DATA, msg[1]]]);
3151
+ } else if (t === DIRTY) {
3152
+ stageInput.down([[DIRTY]]);
3153
+ } else if (t === RESOLVED) {
3154
+ stageInput.down([[RESOLVED]]);
3155
+ } else if (t === COMPLETE || t === ERROR) {
3156
+ stageInput.down([msg]);
3157
+ }
3158
+ }
3159
+ });
3160
+ prevOutputPath = `${stage.name}::output`;
3090
3161
  }
3091
- publish(value) {
3092
- this._log.append(value);
3162
+ return g;
3163
+ }
3164
+ function feedback(graph, condition, reentry, opts) {
3165
+ const maxIter = opts?.maxIterations ?? 10;
3166
+ const counterName = `__feedback_${condition}`;
3167
+ const counter = state(0, {
3168
+ meta: baseMeta("feedback_counter", {
3169
+ maxIterations: maxIter,
3170
+ feedbackFrom: condition,
3171
+ feedbackTo: reentry
3172
+ })
3173
+ });
3174
+ graph.add(counterName, counter);
3175
+ const condNode = graph.resolve(condition);
3176
+ const reentryNode = graph.resolve(reentry);
3177
+ let tornDown = false;
3178
+ let unsubCounter = null;
3179
+ const safeUnsub = () => {
3180
+ if (tornDown) return;
3181
+ tornDown = true;
3182
+ unsub();
3183
+ unsubCounter?.();
3184
+ };
3185
+ const unsub = condNode.subscribe((msgs) => {
3186
+ for (const msg of msgs) {
3187
+ if (msg[0] === DATA) {
3188
+ const currentCount = counter.get();
3189
+ if (currentCount >= maxIter) continue;
3190
+ const condValue = msg[1];
3191
+ if (condValue == null) continue;
3192
+ counter.down([[DATA, currentCount + 1]]);
3193
+ reentryNode.down([[DATA, condValue]]);
3194
+ } else if (msg[0] === COMPLETE || msg[0] === ERROR) {
3195
+ const terminal = msg[0] === ERROR && msg.length > 1 ? [ERROR, msg[1]] : [msg[0]];
3196
+ counter.down([terminal]);
3197
+ safeUnsub();
3198
+ }
3199
+ }
3200
+ });
3201
+ unsubCounter = counter.subscribe((msgs) => {
3202
+ for (const msg of msgs) {
3203
+ if (msg[0] === COMPLETE || msg[0] === ERROR) {
3204
+ safeUnsub();
3205
+ return;
3206
+ }
3207
+ }
3208
+ });
3209
+ return graph;
3210
+ }
3211
+ function budgetGate(source, constraints, opts) {
3212
+ if (constraints.length === 0) throw new RangeError("budgetGate requires at least one constraint");
3213
+ const constraintNodes = constraints.map((c) => c.node);
3214
+ const allDeps = [source, ...constraintNodes];
3215
+ let buffer2 = [];
3216
+ let paused = false;
3217
+ let pendingResolved = false;
3218
+ const lockId = /* @__PURE__ */ Symbol("budget-gate");
3219
+ function checkBudget() {
3220
+ return constraints.every((c) => c.check(c.node.get()));
3093
3221
  }
3094
- retained() {
3095
- const snapshot = this.events.get();
3096
- return snapshot.value.entries;
3222
+ function flushBuffer(actions) {
3223
+ while (buffer2.length > 0 && checkBudget()) {
3224
+ const item = buffer2.shift();
3225
+ actions.emit(item);
3226
+ }
3227
+ if (buffer2.length === 0 && pendingResolved) {
3228
+ pendingResolved = false;
3229
+ actions.down([[RESOLVED]]);
3230
+ }
3097
3231
  }
3098
- };
3099
- var SubscriptionGraph = class extends Graph {
3100
- _keepaliveDisposers = [];
3101
- source;
3102
- cursor;
3103
- available;
3104
- constructor(name, topicGraph, opts = {}) {
3105
- super(name, opts.graph);
3106
- const initialCursor = requireNonNegativeInt(opts.cursor ?? 0, "subscription cursor");
3107
- this.mount("topic", topicGraph);
3108
- const topicEvents = topicGraph.events;
3109
- this.source = derived([topicEvents], ([snapshot]) => snapshot, {
3110
- name: "source",
3111
- describeKind: "derived",
3112
- meta: messagingMeta("subscription_source"),
3232
+ return node(allDeps, () => void 0, {
3233
+ ...opts,
3234
+ describeKind: "operator",
3235
+ meta: baseMeta("budget_gate", opts?.meta),
3236
+ onMessage(msg, depIndex, actions) {
3237
+ const t = msg[0];
3238
+ if (depIndex === 0) {
3239
+ if (t === DATA) {
3240
+ if (checkBudget() && buffer2.length === 0) {
3241
+ actions.emit(msg[1]);
3242
+ } else {
3243
+ buffer2.push(msg[1]);
3244
+ if (!paused) {
3245
+ paused = true;
3246
+ actions.up([[PAUSE, lockId]]);
3247
+ }
3248
+ }
3249
+ return true;
3250
+ }
3251
+ if (t === DIRTY) {
3252
+ actions.down([[DIRTY]]);
3253
+ return true;
3254
+ }
3255
+ if (t === RESOLVED) {
3256
+ if (buffer2.length === 0) {
3257
+ actions.down([[RESOLVED]]);
3258
+ } else {
3259
+ pendingResolved = true;
3260
+ }
3261
+ return true;
3262
+ }
3263
+ if (t === COMPLETE || t === ERROR) {
3264
+ for (const item of buffer2) {
3265
+ actions.emit(item);
3266
+ }
3267
+ buffer2 = [];
3268
+ pendingResolved = false;
3269
+ if (paused) {
3270
+ paused = false;
3271
+ actions.up([[RESUME, lockId]]);
3272
+ }
3273
+ actions.down([msg]);
3274
+ return true;
3275
+ }
3276
+ return false;
3277
+ }
3278
+ if (t === DATA || t === RESOLVED) {
3279
+ if (checkBudget() && buffer2.length > 0) {
3280
+ flushBuffer(actions);
3281
+ if (buffer2.length === 0 && paused) {
3282
+ paused = false;
3283
+ actions.up([[RESUME, lockId]]);
3284
+ }
3285
+ } else if (!checkBudget() && !paused && buffer2.length > 0) {
3286
+ paused = true;
3287
+ actions.up([[PAUSE, lockId]]);
3288
+ }
3289
+ return true;
3290
+ }
3291
+ if (t === DIRTY) {
3292
+ return true;
3293
+ }
3294
+ if (t === ERROR) {
3295
+ actions.down([msg]);
3296
+ return true;
3297
+ }
3298
+ if (t === COMPLETE) {
3299
+ return true;
3300
+ }
3301
+ return false;
3302
+ }
3303
+ });
3304
+ }
3305
+ function scorer(sources, weights, opts) {
3306
+ if (sources.length === 0) throw new RangeError("scorer requires at least one source");
3307
+ if (sources.length !== weights.length) {
3308
+ throw new RangeError("scorer requires the same number of sources and weights");
3309
+ }
3310
+ const allDeps = [...sources, ...weights];
3311
+ const n = sources.length;
3312
+ const scoreFns = opts?.scoreFns;
3313
+ return derived(
3314
+ allDeps,
3315
+ (vals) => {
3316
+ const signals = vals.slice(0, n);
3317
+ const weightValues = vals.slice(n);
3318
+ const breakdown = [];
3319
+ let totalScore = 0;
3320
+ for (let i = 0; i < n; i++) {
3321
+ const sig = signals[i] ?? 0;
3322
+ const wt = weightValues[i] ?? 0;
3323
+ const rawScore = scoreFns?.[i] ? scoreFns[i](sig) : sig;
3324
+ const weighted = rawScore * wt;
3325
+ breakdown.push(weighted);
3326
+ totalScore += weighted;
3327
+ }
3328
+ return {
3329
+ value: signals,
3330
+ score: totalScore,
3331
+ breakdown
3332
+ };
3333
+ },
3334
+ {
3335
+ ...opts,
3336
+ describeKind: "derived",
3337
+ meta: baseMeta("scorer", opts?.meta)
3338
+ }
3339
+ );
3340
+ }
3341
+
3342
+ // src/patterns/graphspec.ts
3343
+ var VALID_NODE_TYPES2 = /* @__PURE__ */ new Set([
3344
+ "state",
3345
+ "producer",
3346
+ "derived",
3347
+ "effect",
3348
+ "operator",
3349
+ "template"
3350
+ ]);
3351
+ var INNER_NODE_TYPES = /* @__PURE__ */ new Set(["state", "producer", "derived", "effect", "operator"]);
3352
+ function validateSpec(spec) {
3353
+ const errors = [];
3354
+ if (spec == null || typeof spec !== "object") {
3355
+ return { valid: false, errors: ["GraphSpec must be a non-null object"] };
3356
+ }
3357
+ const s = spec;
3358
+ if (typeof s.name !== "string" || s.name.length === 0) {
3359
+ errors.push("Missing or empty 'name' field");
3360
+ }
3361
+ if (s.nodes == null || typeof s.nodes !== "object" || Array.isArray(s.nodes)) {
3362
+ errors.push("Missing or invalid 'nodes' field (must be an object)");
3363
+ return { valid: false, errors };
3364
+ }
3365
+ const nodeNames = new Set(Object.keys(s.nodes));
3366
+ const nodeTypes = /* @__PURE__ */ new Map();
3367
+ const templateDefs = /* @__PURE__ */ new Map();
3368
+ if (s.templates != null && typeof s.templates === "object" && !Array.isArray(s.templates)) {
3369
+ for (const [tName, tRaw] of Object.entries(s.templates)) {
3370
+ if (tRaw != null && typeof tRaw === "object") {
3371
+ const t = tRaw;
3372
+ templateDefs.set(tName, {
3373
+ params: Array.isArray(t.params) ? t.params : []
3374
+ });
3375
+ }
3376
+ }
3377
+ }
3378
+ if (s.templates != null) {
3379
+ if (typeof s.templates !== "object" || Array.isArray(s.templates)) {
3380
+ errors.push("'templates' must be an object");
3381
+ } else {
3382
+ for (const [tName, tRaw] of Object.entries(s.templates)) {
3383
+ if (tRaw == null || typeof tRaw !== "object") {
3384
+ errors.push(`Template "${tName}": must be an object`);
3385
+ continue;
3386
+ }
3387
+ const t = tRaw;
3388
+ if (!Array.isArray(t.params)) {
3389
+ errors.push(`Template "${tName}": missing 'params' array`);
3390
+ }
3391
+ if (t.nodes == null || typeof t.nodes !== "object" || Array.isArray(t.nodes)) {
3392
+ errors.push(`Template "${tName}": missing or invalid 'nodes' object`);
3393
+ } else {
3394
+ const paramSet = new Set(Array.isArray(t.params) ? t.params : []);
3395
+ const innerNames = new Set(Object.keys(t.nodes));
3396
+ for (const [nName, nRaw] of Object.entries(t.nodes)) {
3397
+ if (nRaw == null || typeof nRaw !== "object") {
3398
+ errors.push(`Template "${tName}" node "${nName}": must be an object`);
3399
+ continue;
3400
+ }
3401
+ const n = nRaw;
3402
+ if (typeof n.type !== "string" || !INNER_NODE_TYPES.has(n.type)) {
3403
+ errors.push(`Template "${tName}" node "${nName}": invalid type`);
3404
+ }
3405
+ if (Array.isArray(n.deps)) {
3406
+ for (const dep of n.deps) {
3407
+ if (!innerNames.has(dep) && !paramSet.has(dep)) {
3408
+ errors.push(
3409
+ `Template "${tName}" node "${nName}": dep "${dep}" is not an inner node or param`
3410
+ );
3411
+ }
3412
+ }
3413
+ }
3414
+ }
3415
+ if (typeof t.output !== "string") {
3416
+ errors.push(`Template "${tName}": missing 'output' string`);
3417
+ } else if (!t.nodes[t.output]) {
3418
+ errors.push(`Template "${tName}": output "${t.output}" is not a declared node`);
3419
+ }
3420
+ }
3421
+ }
3422
+ }
3423
+ }
3424
+ for (const [name, raw] of Object.entries(s.nodes)) {
3425
+ if (raw == null || typeof raw !== "object") {
3426
+ errors.push(`Node "${name}": must be an object`);
3427
+ continue;
3428
+ }
3429
+ const n = raw;
3430
+ if (typeof n.type !== "string" || !VALID_NODE_TYPES2.has(n.type)) {
3431
+ errors.push(
3432
+ `Node "${name}": invalid type "${String(n.type)}" (expected: ${[...VALID_NODE_TYPES2].join(", ")})`
3433
+ );
3434
+ continue;
3435
+ }
3436
+ nodeTypes.set(name, n.type);
3437
+ if (n.type === "template") {
3438
+ if (typeof n.template !== "string" || !templateDefs.has(n.template)) {
3439
+ errors.push(`Node "${name}": template "${String(n.template)}" not found in templates`);
3440
+ } else {
3441
+ if (n.bind == null || typeof n.bind !== "object" || Array.isArray(n.bind)) {
3442
+ errors.push(`Node "${name}": template ref requires 'bind' object`);
3443
+ } else {
3444
+ const tmpl = templateDefs.get(n.template);
3445
+ const bind = n.bind;
3446
+ for (const param of tmpl.params) {
3447
+ if (!(param in bind)) {
3448
+ errors.push(
3449
+ `Node "${name}": template param "${param}" is not bound (template "${n.template}")`
3450
+ );
3451
+ }
3452
+ }
3453
+ for (const [, target] of Object.entries(bind)) {
3454
+ if (typeof target === "string" && !nodeNames.has(target)) {
3455
+ errors.push(
3456
+ `Node "${name}": bind target "${target}" does not reference an existing node`
3457
+ );
3458
+ }
3459
+ }
3460
+ }
3461
+ }
3462
+ } else {
3463
+ if (Array.isArray(n.deps)) {
3464
+ for (const dep of n.deps) {
3465
+ if (dep === name) {
3466
+ errors.push(`Node "${name}": self-referencing dep`);
3467
+ } else if (!nodeNames.has(dep)) {
3468
+ errors.push(`Node "${name}": dep "${dep}" does not reference an existing node`);
3469
+ }
3470
+ }
3471
+ }
3472
+ if ((n.type === "derived" || n.type === "effect" || n.type === "operator") && !Array.isArray(n.deps)) {
3473
+ errors.push(`Node "${name}": ${n.type} node should have a 'deps' array`);
3474
+ }
3475
+ }
3476
+ }
3477
+ if (s.feedback != null) {
3478
+ if (!Array.isArray(s.feedback)) {
3479
+ errors.push("'feedback' must be an array");
3480
+ } else {
3481
+ for (let i = 0; i < s.feedback.length; i++) {
3482
+ const edge = s.feedback[i];
3483
+ if (edge == null || typeof edge !== "object") {
3484
+ errors.push(`Feedback [${i}]: must be an object`);
3485
+ continue;
3486
+ }
3487
+ const e = edge;
3488
+ if (typeof e.from !== "string" || !nodeNames.has(e.from)) {
3489
+ errors.push(
3490
+ `Feedback [${i}]: 'from' "${String(e.from)}" does not reference an existing node`
3491
+ );
3492
+ }
3493
+ if (typeof e.from === "string" && e.from === e.to) {
3494
+ errors.push(`Feedback [${i}]: 'from' and 'to' must be different nodes`);
3495
+ }
3496
+ if (typeof e.to !== "string" || !nodeNames.has(e.to)) {
3497
+ errors.push(
3498
+ `Feedback [${i}]: 'to' "${String(e.to)}" does not reference an existing node`
3499
+ );
3500
+ } else if (typeof e.to === "string" && nodeTypes.get(e.to) !== "state") {
3501
+ errors.push(
3502
+ `Feedback [${i}]: 'to' node "${e.to}" must be a state node (got "${nodeTypes.get(e.to) ?? "unknown"}")`
3503
+ );
3504
+ }
3505
+ }
3506
+ }
3507
+ }
3508
+ return { valid: errors.length === 0, errors };
3509
+ }
3510
+ function compileSpec(spec, opts) {
3511
+ const validation = validateSpec(spec);
3512
+ if (!validation.valid) {
3513
+ throw new Error(`compileSpec: invalid GraphSpec:
3514
+ ${validation.errors.join("\n")}`);
3515
+ }
3516
+ const catalog = opts?.catalog ?? {};
3517
+ const g = new Graph(spec.name);
3518
+ const templates = spec.templates ?? {};
3519
+ const created = /* @__PURE__ */ new Map();
3520
+ const deferred = [];
3521
+ for (const [name, raw] of Object.entries(spec.nodes)) {
3522
+ if (raw.type === "template") continue;
3523
+ const n = raw;
3524
+ if (n.type === "state") {
3525
+ const nd = state(n.initial, {
3526
+ name,
3527
+ meta: n.meta ? { ...n.meta } : void 0
3528
+ });
3529
+ g.add(name, nd);
3530
+ created.set(name, nd);
3531
+ } else if (n.type === "producer") {
3532
+ const sourceFactory = n.source ? catalog.sources?.[n.source] : void 0;
3533
+ const fnFactory = n.fn ? catalog.fns?.[n.fn] : void 0;
3534
+ if (sourceFactory) {
3535
+ const nd = sourceFactory(n.config ?? {});
3536
+ g.add(name, nd);
3537
+ created.set(name, nd);
3538
+ } else if (fnFactory) {
3539
+ const nd = fnFactory([], n.config ?? {});
3540
+ g.add(name, nd);
3541
+ created.set(name, nd);
3542
+ } else {
3543
+ const nd = producer(() => {
3544
+ }, {
3545
+ name,
3546
+ meta: { ...n.meta, _specFn: n.fn, _specSource: n.source }
3547
+ });
3548
+ g.add(name, nd);
3549
+ created.set(name, nd);
3550
+ }
3551
+ } else {
3552
+ deferred.push([name, n]);
3553
+ }
3554
+ }
3555
+ let progressed = true;
3556
+ const pending = new Map(deferred);
3557
+ while (pending.size > 0 && progressed) {
3558
+ progressed = false;
3559
+ for (const [name, n] of [...pending.entries()]) {
3560
+ const deps = n.deps ?? [];
3561
+ if (!deps.every((dep) => created.has(dep))) continue;
3562
+ const resolvedDeps = deps.map((dep) => created.get(dep));
3563
+ const fnFactory = n.fn ? catalog.fns?.[n.fn] : void 0;
3564
+ let nd;
3565
+ if (fnFactory) {
3566
+ nd = fnFactory(resolvedDeps, n.config ?? {});
3567
+ } else if (n.type === "effect") {
3568
+ nd = effect(resolvedDeps, () => {
3569
+ });
3570
+ } else {
3571
+ nd = derived(resolvedDeps, (vals) => vals[0]);
3572
+ }
3573
+ g.add(name, nd);
3574
+ created.set(name, nd);
3575
+ pending.delete(name);
3576
+ progressed = true;
3577
+ }
3578
+ }
3579
+ if (pending.size > 0) {
3580
+ const unresolved = [...pending.keys()].sort().join(", ");
3581
+ throw new Error(`compileSpec: unresolvable deps for nodes: ${unresolved}`);
3582
+ }
3583
+ for (const [name, raw] of Object.entries(spec.nodes)) {
3584
+ if (raw.type !== "template") continue;
3585
+ const ref = raw;
3586
+ const tmpl = templates[ref.template];
3587
+ const sub = new Graph(name);
3588
+ const subCreated = /* @__PURE__ */ new Map();
3589
+ const subDeferred = [];
3590
+ for (const [nName, nSpec] of Object.entries(tmpl.nodes)) {
3591
+ const resolvedDeps = (nSpec.deps ?? []).map((dep) => {
3592
+ if (dep.startsWith("$") && ref.bind[dep]) {
3593
+ return ref.bind[dep];
3594
+ }
3595
+ return dep;
3596
+ });
3597
+ const specWithResolvedDeps = { ...nSpec, deps: resolvedDeps };
3598
+ if (nSpec.type === "state") {
3599
+ const nd = state(nSpec.initial, {
3600
+ name: nName,
3601
+ meta: nSpec.meta ? { ...nSpec.meta } : void 0
3602
+ });
3603
+ sub.add(nName, nd);
3604
+ subCreated.set(nName, nd);
3605
+ } else if (nSpec.type === "producer") {
3606
+ const sourceFactory = nSpec.source ? catalog.sources?.[nSpec.source] : void 0;
3607
+ const fnFactory = nSpec.fn ? catalog.fns?.[nSpec.fn] : void 0;
3608
+ if (sourceFactory) {
3609
+ const nd = sourceFactory(nSpec.config ?? {});
3610
+ sub.add(nName, nd);
3611
+ subCreated.set(nName, nd);
3612
+ } else if (fnFactory) {
3613
+ const nd = fnFactory([], nSpec.config ?? {});
3614
+ sub.add(nName, nd);
3615
+ subCreated.set(nName, nd);
3616
+ } else {
3617
+ const nd = producer(() => {
3618
+ }, {
3619
+ name: nName,
3620
+ meta: { ...nSpec.meta, _specFn: nSpec.fn, _specSource: nSpec.source }
3621
+ });
3622
+ sub.add(nName, nd);
3623
+ subCreated.set(nName, nd);
3624
+ }
3625
+ } else {
3626
+ subDeferred.push([nName, specWithResolvedDeps]);
3627
+ }
3628
+ }
3629
+ let subProgressed = true;
3630
+ const subPending = new Map(subDeferred);
3631
+ while (subPending.size > 0 && subProgressed) {
3632
+ subProgressed = false;
3633
+ for (const [nName, nSpec] of [...subPending.entries()]) {
3634
+ const deps = nSpec.deps ?? [];
3635
+ const allReady = deps.every((dep) => subCreated.has(dep) || created.has(dep));
3636
+ if (!allReady) continue;
3637
+ const resolvedDeps = deps.map((dep) => subCreated.get(dep) ?? created.get(dep));
3638
+ const fnFactory = nSpec.fn ? catalog.fns?.[nSpec.fn] : void 0;
3639
+ let nd;
3640
+ if (fnFactory) {
3641
+ nd = fnFactory(resolvedDeps, nSpec.config ?? {});
3642
+ } else if (nSpec.type === "effect") {
3643
+ nd = effect(resolvedDeps, () => {
3644
+ });
3645
+ } else {
3646
+ nd = derived(resolvedDeps, (vals) => vals[0]);
3647
+ }
3648
+ sub.add(nName, nd);
3649
+ subCreated.set(nName, nd);
3650
+ subPending.delete(nName);
3651
+ subProgressed = true;
3652
+ }
3653
+ }
3654
+ if (subPending.size > 0) {
3655
+ const unresolved = [...subPending.keys()].sort().join(", ");
3656
+ throw new Error(
3657
+ `compileSpec: template "${ref.template}" has unresolvable deps: ${unresolved}`
3658
+ );
3659
+ }
3660
+ g.mount(name, sub);
3661
+ const outputPath = `${name}::${tmpl.output}`;
3662
+ created.set(name, g.resolve(outputPath));
3663
+ try {
3664
+ const outputNode = g.resolve(outputPath);
3665
+ outputNode.meta._templateName?.down([[DATA, ref.template]]);
3666
+ outputNode.meta._templateBind?.down([[DATA, ref.bind]]);
3667
+ } catch {
3668
+ }
3669
+ }
3670
+ for (const [name, raw] of Object.entries(spec.nodes)) {
3671
+ if (raw.type === "template") continue;
3672
+ const n = raw;
3673
+ for (const dep of n.deps ?? []) {
3674
+ try {
3675
+ g.connect(dep, name);
3676
+ } catch (err) {
3677
+ const msg = err instanceof Error ? err.message : "";
3678
+ if (!msg.includes("constructor deps") && !msg.includes("already")) {
3679
+ throw err;
3680
+ }
3681
+ }
3682
+ }
3683
+ }
3684
+ for (const fb of spec.feedback ?? []) {
3685
+ feedback(g, fb.from, fb.to, {
3686
+ maxIterations: fb.maxIterations
3687
+ });
3688
+ }
3689
+ return g;
3690
+ }
3691
+ var INTERNAL_META_KEYS = /* @__PURE__ */ new Set([
3692
+ "reduction",
3693
+ "reduction_type",
3694
+ "_specFn",
3695
+ "_specSource",
3696
+ "_templateName",
3697
+ "_templateBind",
3698
+ "feedbackFrom",
3699
+ "feedbackTo"
3700
+ ]);
3701
+ function decompileGraph(graph) {
3702
+ const desc = graph.describe({ detail: "standard" });
3703
+ const nodes = {};
3704
+ const feedbackEdges = [];
3705
+ const metaSegment = `::${GRAPH_META_SEGMENT}::`;
3706
+ const feedbackCounterPattern = /^__feedback_(.+)$/;
3707
+ const feedbackConditions = /* @__PURE__ */ new Set();
3708
+ for (const path of Object.keys(desc.nodes)) {
3709
+ if (path.includes(metaSegment)) continue;
3710
+ const match = feedbackCounterPattern.exec(path);
3711
+ if (match) {
3712
+ feedbackConditions.add(match[1]);
3713
+ const meta = desc.nodes[path]?.meta;
3714
+ if (meta?.feedbackFrom && meta?.feedbackTo) {
3715
+ feedbackEdges.push({
3716
+ from: meta.feedbackFrom,
3717
+ to: meta.feedbackTo,
3718
+ ...meta.maxIterations ? { maxIterations: meta.maxIterations } : {}
3719
+ });
3720
+ }
3721
+ }
3722
+ }
3723
+ for (const [path, nodeDesc] of Object.entries(desc.nodes)) {
3724
+ if (path.includes(metaSegment)) continue;
3725
+ if (feedbackCounterPattern.test(path)) continue;
3726
+ if (path.includes("::")) continue;
3727
+ const specNode = {
3728
+ type: nodeDesc.type
3729
+ };
3730
+ if (nodeDesc.deps.length > 0) {
3731
+ specNode.deps = nodeDesc.deps.filter((d) => !d.includes("::"));
3732
+ }
3733
+ if (nodeDesc.type === "state" && nodeDesc.value !== void 0) {
3734
+ specNode.initial = nodeDesc.value;
3735
+ }
3736
+ if (nodeDesc.meta && Object.keys(nodeDesc.meta).length > 0) {
3737
+ const meta = {};
3738
+ for (const [k, v] of Object.entries(nodeDesc.meta)) {
3739
+ if (!INTERNAL_META_KEYS.has(k)) meta[k] = v;
3740
+ }
3741
+ if (Object.keys(meta).length > 0) {
3742
+ specNode.meta = meta;
3743
+ }
3744
+ }
3745
+ nodes[path] = specNode;
3746
+ }
3747
+ const templates = {};
3748
+ const templateRefs = {};
3749
+ const metaDetectedSubgraphs = /* @__PURE__ */ new Set();
3750
+ for (const subName of desc.subgraphs) {
3751
+ const prefix = `${subName}::`;
3752
+ for (const [path, nodeDesc] of Object.entries(desc.nodes)) {
3753
+ if (!path.startsWith(prefix)) continue;
3754
+ if (path.includes(metaSegment)) continue;
3755
+ const meta = nodeDesc.meta;
3756
+ if (meta?._templateName && meta?._templateBind) {
3757
+ const templateName = meta._templateName;
3758
+ const bind = meta._templateBind;
3759
+ if (!templates[templateName]) {
3760
+ const tmplNodes = {};
3761
+ const tmplInnerNames = /* @__PURE__ */ new Set();
3762
+ const tmplPrefix = `${subName}::`;
3763
+ for (const [p, nd] of Object.entries(desc.nodes)) {
3764
+ if (!p.startsWith(tmplPrefix) || p.includes(metaSegment)) continue;
3765
+ const localName = p.slice(tmplPrefix.length);
3766
+ if (localName.includes("::")) continue;
3767
+ tmplInnerNames.add(localName);
3768
+ tmplNodes[localName] = {
3769
+ type: nd.type,
3770
+ ...nd.deps.length > 0 ? {
3771
+ deps: nd.deps.map(
3772
+ (d) => d.startsWith(tmplPrefix) ? d.slice(tmplPrefix.length) : d
3773
+ )
3774
+ } : {}
3775
+ };
3776
+ }
3777
+ const tmplParams = [];
3778
+ const tmplParamMap = /* @__PURE__ */ new Map();
3779
+ for (const n of Object.values(tmplNodes)) {
3780
+ for (const dep of n.deps ?? []) {
3781
+ if (!tmplInnerNames.has(dep) && !tmplParamMap.has(dep)) {
3782
+ const param = `$${dep}`;
3783
+ tmplParams.push(param);
3784
+ tmplParamMap.set(dep, param);
3785
+ }
3786
+ }
3787
+ }
3788
+ for (const n of Object.values(tmplNodes)) {
3789
+ if (n.deps) n.deps = n.deps.map((d) => tmplParamMap.get(d) ?? d);
3790
+ }
3791
+ const depended = /* @__PURE__ */ new Set();
3792
+ for (const n of Object.values(tmplNodes)) {
3793
+ for (const dep of n.deps ?? []) {
3794
+ if (tmplInnerNames.has(dep)) depended.add(dep);
3795
+ }
3796
+ }
3797
+ const outputCandidates = [...tmplInnerNames].filter((n) => !depended.has(n));
3798
+ const tmplOutput = outputCandidates[0] ?? [...tmplInnerNames].pop();
3799
+ templates[templateName] = { params: tmplParams, nodes: tmplNodes, output: tmplOutput };
3800
+ }
3801
+ delete nodes[subName];
3802
+ templateRefs[subName] = { type: "template", template: templateName, bind };
3803
+ metaDetectedSubgraphs.add(subName);
3804
+ break;
3805
+ }
3806
+ }
3807
+ }
3808
+ const structureMap = /* @__PURE__ */ new Map();
3809
+ for (const subName of desc.subgraphs) {
3810
+ if (metaDetectedSubgraphs.has(subName)) continue;
3811
+ const subNodes = {};
3812
+ const prefix = `${subName}::`;
3813
+ for (const [path, nodeDesc] of Object.entries(desc.nodes)) {
3814
+ if (path.includes(metaSegment)) continue;
3815
+ if (!path.startsWith(prefix)) continue;
3816
+ const localName = path.slice(prefix.length);
3817
+ if (localName.includes("::")) continue;
3818
+ subNodes[localName] = {
3819
+ type: nodeDesc.type,
3820
+ ...nodeDesc.deps.length > 0 ? {
3821
+ deps: nodeDesc.deps.map((d) => d.startsWith(prefix) ? d.slice(prefix.length) : d)
3822
+ } : {}
3823
+ };
3824
+ }
3825
+ const fingerprint = JSON.stringify(
3826
+ Object.fromEntries(
3827
+ Object.entries(subNodes).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => [k, { type: v.type, deps: v.deps ?? [] }])
3828
+ )
3829
+ );
3830
+ if (!structureMap.has(fingerprint)) {
3831
+ structureMap.set(fingerprint, []);
3832
+ }
3833
+ structureMap.get(fingerprint).push({ name: subName, nodes: subNodes });
3834
+ }
3835
+ for (const [, group] of structureMap) {
3836
+ if (group.length < 2) continue;
3837
+ const templateName = `${group[0].name}_template`;
3838
+ const refNodes = group[0].nodes;
3839
+ const innerNames = new Set(Object.keys(refNodes));
3840
+ const params = [];
3841
+ const baseParamMap = /* @__PURE__ */ new Map();
3842
+ for (const n of Object.values(refNodes)) {
3843
+ for (const dep of n.deps ?? []) {
3844
+ if (!innerNames.has(dep) && !baseParamMap.has(dep)) {
3845
+ const param = `$${dep}`;
3846
+ params.push(param);
3847
+ baseParamMap.set(dep, param);
3848
+ }
3849
+ }
3850
+ }
3851
+ const depended = /* @__PURE__ */ new Set();
3852
+ for (const n of Object.values(refNodes)) {
3853
+ for (const dep of n.deps ?? []) {
3854
+ if (innerNames.has(dep)) depended.add(dep);
3855
+ }
3856
+ }
3857
+ const outputCandidates = [...innerNames].filter((n) => !depended.has(n));
3858
+ const output = outputCandidates[0] ?? [...innerNames].pop();
3859
+ const tmplNodes = {};
3860
+ for (const [nName, nSpec] of Object.entries(refNodes)) {
3861
+ tmplNodes[nName] = {
3862
+ ...nSpec,
3863
+ deps: nSpec.deps?.map((d) => baseParamMap.get(d) ?? d)
3864
+ };
3865
+ }
3866
+ templates[templateName] = { params, nodes: tmplNodes, output };
3867
+ for (const member of group) {
3868
+ delete nodes[member.name];
3869
+ const memberBind = {};
3870
+ const memberInnerNames = new Set(Object.keys(member.nodes));
3871
+ for (const n of Object.values(member.nodes)) {
3872
+ for (const dep of n.deps ?? []) {
3873
+ if (!memberInnerNames.has(dep)) {
3874
+ const param = baseParamMap.get(dep) ?? `$${dep}`;
3875
+ memberBind[param] = dep;
3876
+ }
3877
+ }
3878
+ }
3879
+ templateRefs[member.name] = {
3880
+ type: "template",
3881
+ template: templateName,
3882
+ bind: memberBind
3883
+ };
3884
+ }
3885
+ }
3886
+ const allNodes = {
3887
+ ...nodes,
3888
+ ...templateRefs
3889
+ };
3890
+ const result = { name: desc.name, nodes: allNodes };
3891
+ if (Object.keys(templates).length > 0) result.templates = templates;
3892
+ if (feedbackEdges.length > 0) result.feedback = feedbackEdges;
3893
+ return result;
3894
+ }
3895
+ function specDiff(specA, specB) {
3896
+ const entries = [];
3897
+ if (specA.name !== specB.name) {
3898
+ entries.push({
3899
+ type: "changed",
3900
+ path: "name",
3901
+ detail: `"${specA.name}" \u2192 "${specB.name}"`
3902
+ });
3903
+ }
3904
+ const nodesA = new Set(Object.keys(specA.nodes));
3905
+ const nodesB = new Set(Object.keys(specB.nodes));
3906
+ for (const name of nodesB) {
3907
+ if (!nodesA.has(name)) {
3908
+ const n = specB.nodes[name];
3909
+ entries.push({
3910
+ type: "added",
3911
+ path: `nodes.${name}`,
3912
+ detail: `type: ${n.type}`
3913
+ });
3914
+ }
3915
+ }
3916
+ for (const name of nodesA) {
3917
+ if (!nodesB.has(name)) {
3918
+ entries.push({ type: "removed", path: `nodes.${name}` });
3919
+ }
3920
+ }
3921
+ for (const name of nodesA) {
3922
+ if (!nodesB.has(name)) continue;
3923
+ const a = specA.nodes[name];
3924
+ const b = specB.nodes[name];
3925
+ if (JSON.stringify(a) !== JSON.stringify(b)) {
3926
+ const details = [];
3927
+ if (a.type !== b.type) details.push(`type: ${a.type} \u2192 ${b.type}`);
3928
+ if (JSON.stringify(a.deps) !== JSON.stringify(b.deps)) {
3929
+ details.push("deps changed");
3930
+ }
3931
+ if (a.fn !== b.fn) {
3932
+ details.push(`fn: ${a.fn} \u2192 ${b.fn}`);
3933
+ }
3934
+ if (JSON.stringify(a.config) !== JSON.stringify(b.config)) {
3935
+ details.push("config changed");
3936
+ }
3937
+ entries.push({
3938
+ type: "changed",
3939
+ path: `nodes.${name}`,
3940
+ detail: details.join("; ") || "modified"
3941
+ });
3942
+ }
3943
+ }
3944
+ const tmplA = specA.templates ?? {};
3945
+ const tmplB = specB.templates ?? {};
3946
+ const tmplNamesA = new Set(Object.keys(tmplA));
3947
+ const tmplNamesB = new Set(Object.keys(tmplB));
3948
+ for (const name of tmplNamesB) {
3949
+ if (!tmplNamesA.has(name)) {
3950
+ entries.push({ type: "added", path: `templates.${name}` });
3951
+ }
3952
+ }
3953
+ for (const name of tmplNamesA) {
3954
+ if (!tmplNamesB.has(name)) {
3955
+ entries.push({ type: "removed", path: `templates.${name}` });
3956
+ }
3957
+ }
3958
+ for (const name of tmplNamesA) {
3959
+ if (!tmplNamesB.has(name)) continue;
3960
+ if (JSON.stringify(tmplA[name]) !== JSON.stringify(tmplB[name])) {
3961
+ entries.push({
3962
+ type: "changed",
3963
+ path: `templates.${name}`,
3964
+ detail: "template definition changed"
3965
+ });
3966
+ }
3967
+ }
3968
+ const fbA = specA.feedback ?? [];
3969
+ const fbB = specB.feedback ?? [];
3970
+ const fbKeyA = new Set(fbA.map((e) => `${e.from}->${e.to}`));
3971
+ const fbKeyB = new Set(fbB.map((e) => `${e.from}->${e.to}`));
3972
+ for (const fb of fbB) {
3973
+ const key = `${fb.from}->${fb.to}`;
3974
+ if (!fbKeyA.has(key)) {
3975
+ entries.push({
3976
+ type: "added",
3977
+ path: `feedback.${key}`,
3978
+ detail: `maxIterations: ${fb.maxIterations ?? 10}`
3979
+ });
3980
+ }
3981
+ }
3982
+ for (const fb of fbA) {
3983
+ const key = `${fb.from}->${fb.to}`;
3984
+ if (!fbKeyB.has(key)) {
3985
+ entries.push({ type: "removed", path: `feedback.${key}` });
3986
+ }
3987
+ }
3988
+ for (const fb of fbA) {
3989
+ const key = `${fb.from}->${fb.to}`;
3990
+ const counterpart = fbB.find((b) => b.from === fb.from && b.to === fb.to);
3991
+ if (counterpart && JSON.stringify(fb) !== JSON.stringify(counterpart)) {
3992
+ entries.push({
3993
+ type: "changed",
3994
+ path: `feedback.${key}`,
3995
+ detail: `maxIterations: ${fb.maxIterations ?? 10} \u2192 ${counterpart.maxIterations ?? 10}`
3996
+ });
3997
+ }
3998
+ }
3999
+ const added = entries.filter((e) => e.type === "added").length;
4000
+ const removed = entries.filter((e) => e.type === "removed").length;
4001
+ const changed = entries.filter((e) => e.type === "changed").length;
4002
+ const parts = [];
4003
+ if (added) parts.push(`${added} added`);
4004
+ if (removed) parts.push(`${removed} removed`);
4005
+ if (changed) parts.push(`${changed} changed`);
4006
+ const summary = parts.length > 0 ? parts.join(", ") : "no changes";
4007
+ return { entries, summary };
4008
+ }
4009
+ var LLM_COMPOSE_SYSTEM_PROMPT = `You are a graph architect for GraphReFly, a reactive graph protocol.
4010
+
4011
+ Given a natural-language description, produce a JSON GraphSpec with this structure:
4012
+
4013
+ {
4014
+ "name": "<graph_name>",
4015
+ "nodes": {
4016
+ "<node_name>": {
4017
+ "type": "state" | "derived" | "producer" | "effect" | "operator",
4018
+ "deps": ["<dep_node_name>", ...],
4019
+ "fn": "<catalog_function_name>",
4020
+ "source": "<catalog_source_name>",
4021
+ "config": { ... },
4022
+ "initial": <value>,
4023
+ "meta": { "description": "<purpose>" }
4024
+ },
4025
+ "<template_instance>": {
4026
+ "type": "template",
4027
+ "template": "<template_name>",
4028
+ "bind": { "$param": "node_name" }
4029
+ }
4030
+ },
4031
+ "templates": {
4032
+ "<template_name>": {
4033
+ "params": ["$param1", "$param2"],
4034
+ "nodes": { ... },
4035
+ "output": "<output_node>"
4036
+ }
4037
+ },
4038
+ "feedback": [
4039
+ { "from": "<condition_node>", "to": "<state_node>", "maxIterations": 10 }
4040
+ ]
4041
+ }
4042
+
4043
+ Rules:
4044
+ - "state" nodes hold user/LLM-writable values (knobs). Use "initial" for default values.
4045
+ - "derived" nodes compute from deps using a named "fn".
4046
+ - "effect" nodes produce side effects from deps.
4047
+ - "producer" nodes generate values from a named "source".
4048
+ - Use "templates" when the same subgraph pattern repeats (e.g., per-source resilience).
4049
+ - Use "feedback" for bounded cycles where a derived value writes back to a state node.
4050
+ - meta.description is required for every node.
4051
+ - Return ONLY valid JSON, no markdown fences or commentary.`;
4052
+ function stripFences2(text) {
4053
+ const match = text.match(/^```(?:json)?\s*([\s\S]*?)\s*```[\s\S]*$/);
4054
+ return match ? match[1] : text;
4055
+ }
4056
+ async function llmCompose(problem, adapter, opts) {
4057
+ let systemPrompt = LLM_COMPOSE_SYSTEM_PROMPT;
4058
+ if (opts?.catalogDescription) {
4059
+ systemPrompt += `
4060
+
4061
+ Available catalog:
4062
+ ${opts.catalogDescription}`;
4063
+ }
4064
+ if (opts?.systemPromptExtra) {
4065
+ systemPrompt += `
4066
+
4067
+ ${opts.systemPromptExtra}`;
4068
+ }
4069
+ const messages = [
4070
+ { role: "system", content: systemPrompt },
4071
+ { role: "user", content: problem }
4072
+ ];
4073
+ const rawResult = adapter.invoke(messages, {
4074
+ model: opts?.model,
4075
+ temperature: opts?.temperature ?? 0,
4076
+ maxTokens: opts?.maxTokens
4077
+ });
4078
+ const response = await rawResult;
4079
+ let content = response.content.trim();
4080
+ if (content.startsWith("```")) {
4081
+ content = stripFences2(content);
4082
+ }
4083
+ let parsed;
4084
+ try {
4085
+ parsed = JSON.parse(content);
4086
+ } catch {
4087
+ throw new Error(`llmCompose: LLM response is not valid JSON: ${content.slice(0, 200)}`);
4088
+ }
4089
+ const validation = validateSpec(parsed);
4090
+ if (!validation.valid) {
4091
+ throw new Error(`llmCompose: invalid GraphSpec:
4092
+ ${validation.errors.join("\n")}`);
4093
+ }
4094
+ return parsed;
4095
+ }
4096
+ async function llmRefine(currentSpec, feedback2, adapter, opts) {
4097
+ let systemPrompt = LLM_COMPOSE_SYSTEM_PROMPT;
4098
+ if (opts?.catalogDescription) {
4099
+ systemPrompt += `
4100
+
4101
+ Available catalog:
4102
+ ${opts.catalogDescription}`;
4103
+ }
4104
+ if (opts?.systemPromptExtra) {
4105
+ systemPrompt += `
4106
+
4107
+ ${opts.systemPromptExtra}`;
4108
+ }
4109
+ const messages = [
4110
+ { role: "system", content: systemPrompt },
4111
+ {
4112
+ role: "user",
4113
+ content: `Current GraphSpec:
4114
+ ${JSON.stringify(currentSpec, null, 2)}
4115
+
4116
+ Modification request: ${feedback2}
4117
+
4118
+ Return the complete modified GraphSpec as JSON.`
4119
+ }
4120
+ ];
4121
+ const rawResult = adapter.invoke(messages, {
4122
+ model: opts?.model,
4123
+ temperature: opts?.temperature ?? 0,
4124
+ maxTokens: opts?.maxTokens
4125
+ });
4126
+ const response = await rawResult;
4127
+ let content = response.content.trim();
4128
+ if (content.startsWith("```")) {
4129
+ content = stripFences2(content);
4130
+ }
4131
+ let parsed;
4132
+ try {
4133
+ parsed = JSON.parse(content);
4134
+ } catch {
4135
+ throw new Error(`llmRefine: LLM response is not valid JSON: ${content.slice(0, 200)}`);
4136
+ }
4137
+ const validation = validateSpec(parsed);
4138
+ if (!validation.valid) {
4139
+ throw new Error(`llmRefine: invalid GraphSpec:
4140
+ ${validation.errors.join("\n")}`);
4141
+ }
4142
+ return parsed;
4143
+ }
4144
+
4145
+ // src/patterns/messaging.ts
4146
+ var messaging_exports = {};
4147
+ __export(messaging_exports, {
4148
+ JobFlowGraph: () => JobFlowGraph,
4149
+ JobQueueGraph: () => JobQueueGraph,
4150
+ SubscriptionGraph: () => SubscriptionGraph,
4151
+ TopicBridgeGraph: () => TopicBridgeGraph,
4152
+ TopicGraph: () => TopicGraph,
4153
+ jobFlow: () => jobFlow,
4154
+ jobQueue: () => jobQueue,
4155
+ subscription: () => subscription,
4156
+ topic: () => topic,
4157
+ topicBridge: () => topicBridge
4158
+ });
4159
+ var DEFAULT_MAX_PER_PUMP = 2147483647;
4160
+ function requireNonNegativeInt(value, label) {
4161
+ if (!Number.isFinite(value) || !Number.isInteger(value) || value < 0) {
4162
+ throw new Error(`${label} must be a non-negative integer`);
4163
+ }
4164
+ return value;
4165
+ }
4166
+ function keepalive2(n) {
4167
+ return n.subscribe(() => {
4168
+ });
4169
+ }
4170
+ function messagingMeta(kind, extra) {
4171
+ return {
4172
+ messaging: true,
4173
+ messaging_type: kind,
4174
+ ...extra ?? {}
4175
+ };
4176
+ }
4177
+ var TopicGraph = class extends Graph {
4178
+ _log;
4179
+ _keepaliveDisposers = [];
4180
+ events;
4181
+ latest;
4182
+ constructor(name, opts = {}) {
4183
+ super(name, opts.graph);
4184
+ this._log = reactiveLog([], { name: "events", maxSize: opts.retainedLimit });
4185
+ this.events = this._log.entries;
4186
+ this.add("events", this.events);
4187
+ this.latest = derived(
4188
+ [this.events],
4189
+ ([snapshot]) => {
4190
+ const entries = snapshot.value.entries;
4191
+ return entries.length === 0 ? void 0 : entries[entries.length - 1];
4192
+ },
4193
+ {
4194
+ name: "latest",
4195
+ describeKind: "derived",
4196
+ meta: messagingMeta("topic_latest"),
4197
+ initial: void 0
4198
+ }
4199
+ );
4200
+ this.add("latest", this.latest);
4201
+ this.connect("events", "latest");
4202
+ this._keepaliveDisposers.push(keepalive2(this.latest));
4203
+ }
4204
+ destroy() {
4205
+ for (const dispose of this._keepaliveDisposers) dispose();
4206
+ this._keepaliveDisposers.length = 0;
4207
+ super.destroy();
4208
+ }
4209
+ publish(value) {
4210
+ this._log.append(value);
4211
+ }
4212
+ retained() {
4213
+ const snapshot = this.events.get();
4214
+ return snapshot.value.entries;
4215
+ }
4216
+ };
4217
+ var SubscriptionGraph = class extends Graph {
4218
+ _keepaliveDisposers = [];
4219
+ source;
4220
+ cursor;
4221
+ available;
4222
+ constructor(name, topicGraph, opts = {}) {
4223
+ super(name, opts.graph);
4224
+ const initialCursor = requireNonNegativeInt(opts.cursor ?? 0, "subscription cursor");
4225
+ this.mount("topic", topicGraph);
4226
+ const topicEvents = topicGraph.events;
4227
+ this.source = derived([topicEvents], ([snapshot]) => snapshot, {
4228
+ name: "source",
4229
+ describeKind: "derived",
4230
+ meta: messagingMeta("subscription_source"),
3113
4231
  initial: topicEvents.get()
3114
4232
  });
3115
4233
  this.add("source", this.source);
@@ -3472,7 +4590,7 @@ function registerStep(graph, name, step, depPaths) {
3472
4590
  graph.connect(path, name);
3473
4591
  }
3474
4592
  }
3475
- function baseMeta(kind, meta) {
4593
+ function baseMeta2(kind, meta) {
3476
4594
  return {
3477
4595
  orchestration: true,
3478
4596
  orchestration_type: kind,
@@ -3510,7 +4628,7 @@ function task(graph, name, run, opts) {
3510
4628
  ...nodeOpts,
3511
4629
  name,
3512
4630
  describeKind: "derived",
3513
- meta: baseMeta("task", opts?.meta)
4631
+ meta: baseMeta2("task", opts?.meta)
3514
4632
  }
3515
4633
  );
3516
4634
  registerStep(
@@ -3533,7 +4651,7 @@ function branch(graph, name, source, predicate, opts) {
3533
4651
  ...opts,
3534
4652
  name,
3535
4653
  describeKind: "derived",
3536
- meta: baseMeta("branch", opts?.meta)
4654
+ meta: baseMeta2("branch", opts?.meta)
3537
4655
  }
3538
4656
  );
3539
4657
  registerStep(graph, name, step, src.path ? [src.path] : []);
@@ -3543,535 +4661,264 @@ function gate2(graph, name, source, control, opts) {
3543
4661
  const src = resolveDep(graph, source);
3544
4662
  const ctrl = resolveDep(graph, control);
3545
4663
  const step = node(
3546
- [src.node, ctrl.node],
3547
- (_deps, actions) => {
3548
- const opened = ctrl.node.get();
3549
- if (!opened) {
3550
- actions.down([[RESOLVED]]);
3551
- return void 0;
3552
- }
3553
- return src.node.get();
3554
- },
3555
- {
3556
- ...opts,
3557
- name,
3558
- describeKind: "operator",
3559
- meta: baseMeta("gate", opts?.meta)
3560
- }
3561
- );
3562
- registerStep(
3563
- graph,
3564
- name,
3565
- step,
3566
- [src.path, ctrl.path].filter((v) => typeof v === "string")
3567
- );
3568
- return step;
3569
- }
3570
- function approval(graph, name, source, approver, opts) {
3571
- const src = resolveDep(graph, source);
3572
- const ctrl = resolveDep(graph, approver);
3573
- const isApproved = opts?.isApproved ?? ((value) => Boolean(value));
3574
- const step = node(
3575
- [src.node, ctrl.node],
3576
- (_deps, actions) => {
3577
- if (!isApproved(ctrl.node.get())) {
3578
- actions.down([[RESOLVED]]);
3579
- return void 0;
3580
- }
3581
- return src.node.get();
3582
- },
3583
- {
3584
- ...opts,
3585
- name,
3586
- describeKind: "operator",
3587
- meta: baseMeta("approval", opts?.meta)
3588
- }
3589
- );
3590
- registerStep(
3591
- graph,
3592
- name,
3593
- step,
3594
- [src.path, ctrl.path].filter((v) => typeof v === "string")
3595
- );
3596
- return step;
3597
- }
3598
- function forEach2(graph, name, source, run, opts) {
3599
- const src = resolveDep(graph, source);
3600
- let terminated = false;
3601
- const step = node([src.node], () => void 0, {
3602
- ...opts,
3603
- name,
3604
- describeKind: "effect",
3605
- completeWhenDepsComplete: false,
3606
- meta: baseMeta("forEach", opts?.meta),
3607
- onMessage(msg, depIndex, actions) {
3608
- if (terminated) return true;
3609
- if (depIndex !== 0) {
3610
- actions.down([msg]);
3611
- if (msg[0] === COMPLETE || msg[0] === ERROR) terminated = true;
3612
- return true;
3613
- }
3614
- if (msg[0] === DATA) {
3615
- try {
3616
- run(msg[1], actions);
3617
- actions.down([msg]);
3618
- } catch (err) {
3619
- terminated = true;
3620
- actions.down([[ERROR, err]]);
3621
- }
3622
- return true;
3623
- }
3624
- actions.down([msg]);
3625
- if (msg[0] === COMPLETE || msg[0] === ERROR) terminated = true;
3626
- return true;
3627
- }
3628
- });
3629
- registerStep(graph, name, step, src.path ? [src.path] : []);
3630
- return step;
3631
- }
3632
- function join(graph, name, deps, opts) {
3633
- const resolved = deps.map((dep) => resolveDep(graph, dep));
3634
- const step = node(
3635
- resolved.map((d) => d.node),
3636
- (values) => values,
3637
- {
3638
- ...opts,
3639
- name,
3640
- describeKind: "derived",
3641
- meta: baseMeta("join", opts?.meta)
3642
- }
3643
- );
3644
- registerStep(
3645
- graph,
3646
- name,
3647
- step,
3648
- resolved.flatMap((d) => d.path ? [d.path] : [])
3649
- );
3650
- return step;
3651
- }
3652
- function loop(graph, name, source, iterate, opts) {
3653
- const src = resolveDep(graph, source);
3654
- const iterRef = opts?.iterations;
3655
- const iterDep = typeof iterRef === "number" || iterRef === void 0 ? void 0 : resolveDep(graph, iterRef);
3656
- const staticIterations = typeof iterRef === "number" ? iterRef : void 0;
3657
- const step = node(
3658
- iterDep ? [src.node, iterDep.node] : [src.node],
3659
- (_deps, actions) => {
3660
- let current = src.node.get();
3661
- const rawCount = staticIterations ?? iterDep?.node.get() ?? 1;
3662
- const count = coerceLoopIterations(rawCount);
3663
- for (let i = 0; i < count; i += 1) {
3664
- current = iterate(current, i, actions);
3665
- }
3666
- return current;
3667
- },
3668
- {
3669
- ...opts,
3670
- name,
3671
- describeKind: "derived",
3672
- meta: baseMeta("loop", opts?.meta)
3673
- }
3674
- );
3675
- registerStep(
3676
- graph,
3677
- name,
3678
- step,
3679
- [src.path, iterDep?.path].filter((v) => typeof v === "string")
3680
- );
3681
- return step;
3682
- }
3683
- function subPipeline(graph, name, childOrBuild, opts) {
3684
- const child = childOrBuild instanceof Graph ? childOrBuild : pipeline(name, opts);
3685
- if (typeof childOrBuild === "function") {
3686
- childOrBuild(child);
3687
- }
3688
- graph.mount(name, child);
3689
- return child;
3690
- }
3691
- function sensor(graph, name, initial, opts) {
3692
- const source = node([], () => void 0, {
3693
- ...opts,
3694
- name,
3695
- initial,
3696
- describeKind: "producer",
3697
- meta: baseMeta("sensor", opts?.meta)
3698
- });
3699
- registerStep(graph, name, source, []);
3700
- return {
3701
- node: source,
3702
- push(value) {
3703
- source.down([[DATA, value]]);
3704
- },
3705
- error(err) {
3706
- source.down([[ERROR, err]]);
4664
+ [src.node, ctrl.node],
4665
+ (_deps, actions) => {
4666
+ const opened = ctrl.node.get();
4667
+ if (!opened) {
4668
+ actions.down([[RESOLVED]]);
4669
+ return void 0;
4670
+ }
4671
+ return src.node.get();
3707
4672
  },
3708
- complete() {
3709
- source.down([[COMPLETE]]);
4673
+ {
4674
+ ...opts,
4675
+ name,
4676
+ describeKind: "operator",
4677
+ meta: baseMeta2("gate", opts?.meta)
3710
4678
  }
3711
- };
4679
+ );
4680
+ registerStep(
4681
+ graph,
4682
+ name,
4683
+ step,
4684
+ [src.path, ctrl.path].filter((v) => typeof v === "string")
4685
+ );
4686
+ return step;
3712
4687
  }
3713
- function wait(graph, name, source, ms, opts) {
4688
+ function approval(graph, name, source, approver, opts) {
3714
4689
  const src = resolveDep(graph, source);
3715
- const timers = /* @__PURE__ */ new Set();
3716
- let terminated = false;
3717
- let completed = false;
4690
+ const ctrl = resolveDep(graph, approver);
4691
+ const isApproved = opts?.isApproved ?? ((value) => Boolean(value));
3718
4692
  const step = node(
3719
- [src.node],
3720
- () => {
3721
- for (const id of timers) clearTimeout(id);
3722
- timers.clear();
3723
- return () => {
3724
- for (const id of timers) clearTimeout(id);
3725
- timers.clear();
3726
- terminated = true;
3727
- };
4693
+ [src.node, ctrl.node],
4694
+ (_deps, actions) => {
4695
+ if (!isApproved(ctrl.node.get())) {
4696
+ actions.down([[RESOLVED]]);
4697
+ return void 0;
4698
+ }
4699
+ return src.node.get();
3728
4700
  },
3729
4701
  {
3730
4702
  ...opts,
3731
4703
  name,
3732
- initial: src.node.get(),
3733
4704
  describeKind: "operator",
3734
- completeWhenDepsComplete: false,
3735
- meta: baseMeta("wait", opts?.meta),
3736
- onMessage(msg, depIndex, actions) {
3737
- if (terminated) return true;
3738
- if (depIndex !== 0) {
3739
- actions.down([msg]);
3740
- if (msg[0] === COMPLETE || msg[0] === ERROR) terminated = true;
3741
- return true;
3742
- }
3743
- if (msg[0] === DATA) {
3744
- const id = setTimeout(() => {
3745
- timers.delete(id);
3746
- actions.down([msg]);
3747
- if (completed && timers.size === 0) {
3748
- actions.down([[COMPLETE]]);
3749
- }
3750
- }, ms);
3751
- timers.add(id);
3752
- return true;
3753
- }
3754
- if (msg[0] === COMPLETE) {
3755
- terminated = true;
3756
- completed = true;
3757
- if (timers.size === 0) {
3758
- actions.down([[COMPLETE]]);
3759
- }
3760
- return true;
3761
- }
3762
- if (msg[0] === ERROR) {
3763
- terminated = true;
3764
- for (const id of timers) clearTimeout(id);
3765
- timers.clear();
3766
- actions.down([msg]);
3767
- return true;
3768
- }
3769
- actions.down([msg]);
3770
- return true;
3771
- }
4705
+ meta: baseMeta2("approval", opts?.meta)
3772
4706
  }
3773
4707
  );
3774
- registerStep(graph, name, step, src.path ? [src.path] : []);
4708
+ registerStep(
4709
+ graph,
4710
+ name,
4711
+ step,
4712
+ [src.path, ctrl.path].filter((v) => typeof v === "string")
4713
+ );
3775
4714
  return step;
3776
4715
  }
3777
- function onFailure(graph, name, source, recover, opts) {
4716
+ function forEach2(graph, name, source, run, opts) {
3778
4717
  const src = resolveDep(graph, source);
3779
4718
  let terminated = false;
3780
4719
  const step = node([src.node], () => void 0, {
3781
4720
  ...opts,
3782
4721
  name,
3783
- describeKind: "operator",
4722
+ describeKind: "effect",
3784
4723
  completeWhenDepsComplete: false,
3785
- meta: baseMeta("onFailure", opts?.meta),
3786
- onMessage(msg, _depIndex, actions) {
3787
- if (terminated) return true;
3788
- if (msg[0] === ERROR) {
3789
- try {
3790
- actions.emit(recover(msg[1], actions));
3791
- } catch (err) {
3792
- terminated = true;
3793
- actions.down([[ERROR, err]]);
3794
- }
3795
- return true;
3796
- }
3797
- actions.down([msg]);
3798
- if (msg[0] === COMPLETE) terminated = true;
3799
- return true;
3800
- }
3801
- });
3802
- registerStep(graph, name, step, src.path ? [src.path] : []);
3803
- return step;
3804
- }
3805
-
3806
- // src/patterns/reduction.ts
3807
- var reduction_exports = {};
3808
- __export(reduction_exports, {
3809
- budgetGate: () => budgetGate,
3810
- feedback: () => feedback,
3811
- funnel: () => funnel,
3812
- scorer: () => scorer,
3813
- stratify: () => stratify
3814
- });
3815
- function baseMeta2(kind, meta) {
3816
- return {
3817
- reduction: true,
3818
- reduction_type: kind,
3819
- ...meta ?? {}
3820
- };
3821
- }
3822
- function stratify(name, source, rules, opts) {
3823
- const g = new Graph(name, opts);
3824
- g.add("source", source);
3825
- const rulesNode = state(rules, {
3826
- meta: baseMeta2("stratify_rules")
3827
- });
3828
- g.add("rules", rulesNode);
3829
- for (const rule of rules) {
3830
- _addBranch(g, source, rulesNode, rule);
3831
- }
3832
- return g;
3833
- }
3834
- function _addBranch(graph, source, rulesNode, rule) {
3835
- const branchName = `branch/${rule.name}`;
3836
- let pendingDirty = false;
3837
- const filterNode = node([source, rulesNode], () => void 0, {
3838
- describeKind: "operator",
3839
- meta: baseMeta2("stratify_branch", { branch: rule.name }),
4724
+ meta: baseMeta2("forEach", opts?.meta),
3840
4725
  onMessage(msg, depIndex, actions) {
3841
- if (depIndex !== 0) return false;
3842
- const t = msg[0];
3843
- if (t === DATA) {
3844
- const value = msg[1];
3845
- const currentRules = rulesNode.get();
3846
- const currentRule = currentRules.find((r) => r.name === rule.name);
3847
- if (currentRule && currentRule.classify(value)) {
3848
- pendingDirty = false;
3849
- actions.emit(value);
3850
- } else {
3851
- if (pendingDirty) {
3852
- pendingDirty = false;
3853
- actions.down([[DIRTY], [RESOLVED]]);
3854
- }
3855
- }
3856
- return true;
3857
- }
3858
- if (t === DIRTY) {
3859
- pendingDirty = true;
3860
- return true;
3861
- }
3862
- if (t === RESOLVED) {
3863
- if (pendingDirty) {
3864
- pendingDirty = false;
3865
- actions.down([[DIRTY], [RESOLVED]]);
3866
- } else {
3867
- actions.down([[RESOLVED]]);
3868
- }
3869
- return true;
3870
- }
3871
- if (t === COMPLETE || t === ERROR) {
3872
- pendingDirty = false;
4726
+ if (terminated) return true;
4727
+ if (depIndex !== 0) {
3873
4728
  actions.down([msg]);
4729
+ if (msg[0] === COMPLETE || msg[0] === ERROR) terminated = true;
3874
4730
  return true;
3875
4731
  }
3876
- return false;
3877
- }
3878
- });
3879
- graph.add(branchName, filterNode);
3880
- graph.connect("source", branchName);
3881
- if (rule.ops) {
3882
- const transformed = rule.ops(filterNode);
3883
- const transformedName = `branch/${rule.name}/out`;
3884
- graph.add(transformedName, transformed);
3885
- graph.connect(branchName, transformedName);
3886
- }
3887
- }
3888
- function funnel(name, sources, stages, opts) {
3889
- if (sources.length === 0) throw new RangeError("funnel requires at least one source");
3890
- if (stages.length === 0) throw new RangeError("funnel requires at least one stage");
3891
- const g = new Graph(name, opts);
3892
- const merged = sources.length === 1 ? sources[0] : merge(...sources);
3893
- g.add("merged", merged);
3894
- let prevOutputPath = "merged";
3895
- for (let i = 0; i < stages.length; i++) {
3896
- const stage = stages[i];
3897
- const sub = new Graph(stage.name);
3898
- stage.build(sub);
3899
- try {
3900
- sub.resolve("input");
3901
- } catch {
3902
- throw new Error(`funnel stage "${stage.name}" must define an "input" node`);
3903
- }
3904
- try {
3905
- sub.resolve("output");
3906
- } catch {
3907
- throw new Error(`funnel stage "${stage.name}" must define an "output" node`);
3908
- }
3909
- g.mount(stage.name, sub);
3910
- const prevNode = g.resolve(prevOutputPath);
3911
- const stageInputPath = `${stage.name}::input`;
3912
- const stageInput = g.resolve(stageInputPath);
3913
- prevNode.subscribe((msgs) => {
3914
- for (const msg of msgs) {
3915
- const t = msg[0];
3916
- if (t === DATA) {
3917
- stageInput.down([[DATA, msg[1]]]);
3918
- } else if (t === DIRTY) {
3919
- stageInput.down([[DIRTY]]);
3920
- } else if (t === RESOLVED) {
3921
- stageInput.down([[RESOLVED]]);
3922
- } else if (t === COMPLETE || t === ERROR) {
3923
- stageInput.down([msg]);
3924
- }
3925
- }
3926
- });
3927
- prevOutputPath = `${stage.name}::output`;
3928
- }
3929
- return g;
3930
- }
3931
- function feedback(graph, condition, reentry, opts) {
3932
- const maxIter = opts?.maxIterations ?? 10;
3933
- const counterName = `__feedback_${condition}`;
3934
- const counter = state(0, {
3935
- meta: baseMeta2("feedback_counter", { maxIterations: maxIter })
3936
- });
3937
- graph.add(counterName, counter);
3938
- const condNode = graph.resolve(condition);
3939
- const reentryNode = graph.resolve(reentry);
3940
- condNode.subscribe((msgs) => {
3941
- for (const msg of msgs) {
3942
4732
  if (msg[0] === DATA) {
3943
- const currentCount = counter.get();
3944
- if (currentCount >= maxIter) continue;
3945
- const condValue = msg[1];
3946
- if (condValue == null) continue;
3947
- counter.down([[DATA, currentCount + 1]]);
3948
- reentryNode.down([[DATA, condValue]]);
4733
+ try {
4734
+ run(msg[1], actions);
4735
+ actions.down([msg]);
4736
+ } catch (err) {
4737
+ terminated = true;
4738
+ actions.down([[ERROR, err]]);
4739
+ }
4740
+ return true;
3949
4741
  }
4742
+ actions.down([msg]);
4743
+ if (msg[0] === COMPLETE || msg[0] === ERROR) terminated = true;
4744
+ return true;
3950
4745
  }
3951
4746
  });
3952
- return graph;
4747
+ registerStep(graph, name, step, src.path ? [src.path] : []);
4748
+ return step;
3953
4749
  }
3954
- function budgetGate(source, constraints, opts) {
3955
- if (constraints.length === 0) throw new RangeError("budgetGate requires at least one constraint");
3956
- const constraintNodes = constraints.map((c) => c.node);
3957
- const allDeps = [source, ...constraintNodes];
3958
- let buffer2 = [];
3959
- let paused = false;
3960
- const lockId = /* @__PURE__ */ Symbol("budget-gate");
3961
- function checkBudget() {
3962
- return constraints.every((c) => c.check(c.node.get()));
3963
- }
3964
- function flushBuffer(actions) {
3965
- while (buffer2.length > 0 && checkBudget()) {
3966
- const item = buffer2.shift();
3967
- actions.emit(item);
4750
+ function join(graph, name, deps, opts) {
4751
+ const resolved = deps.map((dep) => resolveDep(graph, dep));
4752
+ const step = node(
4753
+ resolved.map((d) => d.node),
4754
+ (values) => values,
4755
+ {
4756
+ ...opts,
4757
+ name,
4758
+ describeKind: "derived",
4759
+ meta: baseMeta2("join", opts?.meta)
4760
+ }
4761
+ );
4762
+ registerStep(
4763
+ graph,
4764
+ name,
4765
+ step,
4766
+ resolved.flatMap((d) => d.path ? [d.path] : [])
4767
+ );
4768
+ return step;
4769
+ }
4770
+ function loop(graph, name, source, iterate, opts) {
4771
+ const src = resolveDep(graph, source);
4772
+ const iterRef = opts?.iterations;
4773
+ const iterDep = typeof iterRef === "number" || iterRef === void 0 ? void 0 : resolveDep(graph, iterRef);
4774
+ const staticIterations = typeof iterRef === "number" ? iterRef : void 0;
4775
+ const step = node(
4776
+ iterDep ? [src.node, iterDep.node] : [src.node],
4777
+ (_deps, actions) => {
4778
+ let current = src.node.get();
4779
+ const rawCount = staticIterations ?? iterDep?.node.get() ?? 1;
4780
+ const count = coerceLoopIterations(rawCount);
4781
+ for (let i = 0; i < count; i += 1) {
4782
+ current = iterate(current, i, actions);
4783
+ }
4784
+ return current;
4785
+ },
4786
+ {
4787
+ ...opts,
4788
+ name,
4789
+ describeKind: "derived",
4790
+ meta: baseMeta2("loop", opts?.meta)
3968
4791
  }
4792
+ );
4793
+ registerStep(
4794
+ graph,
4795
+ name,
4796
+ step,
4797
+ [src.path, iterDep?.path].filter((v) => typeof v === "string")
4798
+ );
4799
+ return step;
4800
+ }
4801
+ function subPipeline(graph, name, childOrBuild, opts) {
4802
+ const child = childOrBuild instanceof Graph ? childOrBuild : pipeline(name, opts);
4803
+ if (typeof childOrBuild === "function") {
4804
+ childOrBuild(child);
3969
4805
  }
3970
- return node(allDeps, () => void 0, {
4806
+ graph.mount(name, child);
4807
+ return child;
4808
+ }
4809
+ function sensor(graph, name, initial, opts) {
4810
+ const source = node([], () => void 0, {
3971
4811
  ...opts,
3972
- describeKind: "operator",
3973
- meta: baseMeta2("budget_gate", opts?.meta),
3974
- onMessage(msg, depIndex, actions) {
3975
- const t = msg[0];
3976
- if (depIndex === 0) {
3977
- if (t === DATA) {
3978
- if (checkBudget() && buffer2.length === 0) {
3979
- actions.emit(msg[1]);
3980
- } else {
3981
- buffer2.push(msg[1]);
3982
- if (!paused) {
3983
- paused = true;
3984
- actions.up([[PAUSE, lockId]]);
3985
- }
3986
- }
4812
+ name,
4813
+ initial,
4814
+ describeKind: "producer",
4815
+ meta: baseMeta2("sensor", opts?.meta)
4816
+ });
4817
+ registerStep(graph, name, source, []);
4818
+ return {
4819
+ node: source,
4820
+ push(value) {
4821
+ source.down([[DATA, value]]);
4822
+ },
4823
+ error(err) {
4824
+ source.down([[ERROR, err]]);
4825
+ },
4826
+ complete() {
4827
+ source.down([[COMPLETE]]);
4828
+ }
4829
+ };
4830
+ }
4831
+ function wait(graph, name, source, ms, opts) {
4832
+ const src = resolveDep(graph, source);
4833
+ const timers = /* @__PURE__ */ new Set();
4834
+ let terminated = false;
4835
+ let completed = false;
4836
+ const step = node(
4837
+ [src.node],
4838
+ () => {
4839
+ for (const id of timers) clearTimeout(id);
4840
+ timers.clear();
4841
+ return () => {
4842
+ for (const id of timers) clearTimeout(id);
4843
+ timers.clear();
4844
+ terminated = true;
4845
+ };
4846
+ },
4847
+ {
4848
+ ...opts,
4849
+ name,
4850
+ initial: src.node.get(),
4851
+ describeKind: "operator",
4852
+ completeWhenDepsComplete: false,
4853
+ meta: baseMeta2("wait", opts?.meta),
4854
+ onMessage(msg, depIndex, actions) {
4855
+ if (terminated) return true;
4856
+ if (depIndex !== 0) {
4857
+ actions.down([msg]);
4858
+ if (msg[0] === COMPLETE || msg[0] === ERROR) terminated = true;
3987
4859
  return true;
3988
4860
  }
3989
- if (t === DIRTY) {
3990
- actions.down([[DIRTY]]);
4861
+ if (msg[0] === DATA) {
4862
+ const id = setTimeout(() => {
4863
+ timers.delete(id);
4864
+ actions.down([msg]);
4865
+ if (completed && timers.size === 0) {
4866
+ actions.down([[COMPLETE]]);
4867
+ }
4868
+ }, ms);
4869
+ timers.add(id);
3991
4870
  return true;
3992
4871
  }
3993
- if (t === RESOLVED) {
3994
- if (buffer2.length === 0) {
3995
- actions.down([[RESOLVED]]);
4872
+ if (msg[0] === COMPLETE) {
4873
+ terminated = true;
4874
+ completed = true;
4875
+ if (timers.size === 0) {
4876
+ actions.down([[COMPLETE]]);
3996
4877
  }
3997
4878
  return true;
3998
4879
  }
3999
- if (t === COMPLETE || t === ERROR) {
4000
- for (const item of buffer2) {
4001
- actions.emit(item);
4002
- }
4003
- buffer2 = [];
4004
- if (paused) {
4005
- paused = false;
4006
- actions.up([[RESUME, lockId]]);
4007
- }
4880
+ if (msg[0] === ERROR) {
4881
+ terminated = true;
4882
+ for (const id of timers) clearTimeout(id);
4883
+ timers.clear();
4008
4884
  actions.down([msg]);
4009
4885
  return true;
4010
4886
  }
4011
- return false;
4012
- }
4013
- if (t === DATA || t === RESOLVED) {
4014
- if (checkBudget() && buffer2.length > 0) {
4015
- flushBuffer(actions);
4016
- if (buffer2.length === 0 && paused) {
4017
- paused = false;
4018
- actions.up([[RESUME, lockId]]);
4019
- }
4020
- } else if (!checkBudget() && !paused && buffer2.length > 0) {
4021
- paused = true;
4022
- actions.up([[PAUSE, lockId]]);
4023
- }
4024
- return true;
4025
- }
4026
- if (t === DIRTY) {
4027
- return true;
4028
- }
4029
- if (t === ERROR) {
4030
4887
  actions.down([msg]);
4031
4888
  return true;
4032
4889
  }
4033
- if (t === COMPLETE) {
4034
- return true;
4035
- }
4036
- return false;
4037
4890
  }
4038
- });
4891
+ );
4892
+ registerStep(graph, name, step, src.path ? [src.path] : []);
4893
+ return step;
4039
4894
  }
4040
- function scorer(sources, weights, opts) {
4041
- if (sources.length === 0) throw new RangeError("scorer requires at least one source");
4042
- if (sources.length !== weights.length) {
4043
- throw new RangeError("scorer requires the same number of sources and weights");
4044
- }
4045
- const allDeps = [...sources, ...weights];
4046
- const n = sources.length;
4047
- const scoreFns = opts?.scoreFns;
4048
- return derived(
4049
- allDeps,
4050
- (vals) => {
4051
- const signals = vals.slice(0, n);
4052
- const weightValues = vals.slice(n);
4053
- const breakdown = [];
4054
- let totalScore = 0;
4055
- for (let i = 0; i < n; i++) {
4056
- const sig = signals[i] ?? 0;
4057
- const wt = weightValues[i] ?? 0;
4058
- const rawScore = scoreFns?.[i] ? scoreFns[i](sig) : sig;
4059
- const weighted = rawScore * wt;
4060
- breakdown.push(weighted);
4061
- totalScore += weighted;
4895
+ function onFailure(graph, name, source, recover, opts) {
4896
+ const src = resolveDep(graph, source);
4897
+ let terminated = false;
4898
+ const step = node([src.node], () => void 0, {
4899
+ ...opts,
4900
+ name,
4901
+ describeKind: "operator",
4902
+ completeWhenDepsComplete: false,
4903
+ meta: baseMeta2("onFailure", opts?.meta),
4904
+ onMessage(msg, _depIndex, actions) {
4905
+ if (terminated) return true;
4906
+ if (msg[0] === ERROR) {
4907
+ try {
4908
+ actions.emit(recover(msg[1], actions));
4909
+ } catch (err) {
4910
+ terminated = true;
4911
+ actions.down([[ERROR, err]]);
4912
+ }
4913
+ return true;
4062
4914
  }
4063
- return {
4064
- value: signals,
4065
- score: totalScore,
4066
- breakdown
4067
- };
4068
- },
4069
- {
4070
- ...opts,
4071
- describeKind: "derived",
4072
- meta: baseMeta2("scorer", opts?.meta)
4915
+ actions.down([msg]);
4916
+ if (msg[0] === COMPLETE) terminated = true;
4917
+ return true;
4073
4918
  }
4074
- );
4919
+ });
4920
+ registerStep(graph, name, step, src.path ? [src.path] : []);
4921
+ return step;
4075
4922
  }
4076
4923
 
4077
4924
  // src/index.ts
@@ -4192,6 +5039,7 @@ export {
4192
5039
  gate,
4193
5040
  globToRegExp,
4194
5041
  graph_exports as graph,
5042
+ graphspec_exports as graphspec,
4195
5043
  interval,
4196
5044
  isBatching,
4197
5045
  isKnownMessageType,