@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.cjs CHANGED
@@ -180,6 +180,7 @@ __export(index_exports, {
180
180
  gate: () => gate,
181
181
  globToRegExp: () => globToRegExp,
182
182
  graph: () => graph_exports,
183
+ graphspec: () => graphspec_exports,
183
184
  interval: () => interval,
184
185
  isBatching: () => isBatching,
185
186
  isKnownMessageType: () => isKnownMessageType,
@@ -12862,6 +12863,7 @@ __export(patterns_exports, {
12862
12863
  ai: () => ai_exports,
12863
12864
  cqrs: () => cqrs_exports,
12864
12865
  demoShell: () => demo_shell_exports,
12866
+ graphspec: () => graphspec_exports,
12865
12867
  layout: () => reactive_layout_exports,
12866
12868
  memory: () => memory_exports,
12867
12869
  messaging: () => messaging_exports,
@@ -15417,1544 +15419,2390 @@ function demoShell(opts) {
15417
15419
  };
15418
15420
  }
15419
15421
 
15420
- // src/patterns/messaging.ts
15421
- var messaging_exports = {};
15422
- __export(messaging_exports, {
15423
- JobFlowGraph: () => JobFlowGraph,
15424
- JobQueueGraph: () => JobQueueGraph,
15425
- SubscriptionGraph: () => SubscriptionGraph,
15426
- TopicBridgeGraph: () => TopicBridgeGraph,
15427
- TopicGraph: () => TopicGraph,
15428
- jobFlow: () => jobFlow,
15429
- jobQueue: () => jobQueue,
15430
- subscription: () => subscription,
15431
- topic: () => topic,
15432
- topicBridge: () => topicBridge
15422
+ // src/patterns/graphspec.ts
15423
+ var graphspec_exports = {};
15424
+ __export(graphspec_exports, {
15425
+ compileSpec: () => compileSpec,
15426
+ decompileGraph: () => decompileGraph,
15427
+ llmCompose: () => llmCompose,
15428
+ llmRefine: () => llmRefine,
15429
+ specDiff: () => specDiff,
15430
+ validateSpec: () => validateSpec
15433
15431
  });
15434
- var DEFAULT_MAX_PER_PUMP = 2147483647;
15435
- function requireNonNegativeInt(value, label) {
15436
- if (!Number.isFinite(value) || !Number.isInteger(value) || value < 0) {
15437
- throw new Error(`${label} must be a non-negative integer`);
15438
- }
15439
- return value;
15440
- }
15441
- function keepalive4(n) {
15442
- return n.subscribe(() => {
15443
- });
15444
- }
15445
- function messagingMeta(kind, extra) {
15432
+
15433
+ // src/patterns/reduction.ts
15434
+ var reduction_exports = {};
15435
+ __export(reduction_exports, {
15436
+ budgetGate: () => budgetGate,
15437
+ feedback: () => feedback,
15438
+ funnel: () => funnel,
15439
+ scorer: () => scorer,
15440
+ stratify: () => stratify
15441
+ });
15442
+ function baseMeta(kind, meta) {
15446
15443
  return {
15447
- messaging: true,
15448
- messaging_type: kind,
15449
- ...extra ?? {}
15444
+ reduction: true,
15445
+ reduction_type: kind,
15446
+ ...meta ?? {}
15450
15447
  };
15451
15448
  }
15452
- var TopicGraph = class extends Graph {
15453
- _log;
15454
- _keepaliveDisposers = [];
15455
- events;
15456
- latest;
15457
- constructor(name, opts = {}) {
15458
- super(name, opts.graph);
15459
- this._log = reactiveLog([], { name: "events", maxSize: opts.retainedLimit });
15460
- this.events = this._log.entries;
15461
- this.add("events", this.events);
15462
- this.latest = derived(
15463
- [this.events],
15464
- ([snapshot]) => {
15465
- const entries = snapshot.value.entries;
15466
- return entries.length === 0 ? void 0 : entries[entries.length - 1];
15467
- },
15468
- {
15469
- name: "latest",
15470
- describeKind: "derived",
15471
- meta: messagingMeta("topic_latest"),
15472
- initial: void 0
15473
- }
15474
- );
15475
- this.add("latest", this.latest);
15476
- this.connect("events", "latest");
15477
- this._keepaliveDisposers.push(keepalive4(this.latest));
15478
- }
15479
- destroy() {
15480
- for (const dispose of this._keepaliveDisposers) dispose();
15481
- this._keepaliveDisposers.length = 0;
15482
- super.destroy();
15483
- }
15484
- publish(value) {
15485
- this._log.append(value);
15486
- }
15487
- retained() {
15488
- const snapshot = this.events.get();
15489
- return snapshot.value.entries;
15449
+ function stratify(name, source, rules, opts) {
15450
+ const g = new Graph(name, opts);
15451
+ g.add("source", source);
15452
+ const rulesNode = state(rules, {
15453
+ meta: baseMeta("stratify_rules")
15454
+ });
15455
+ g.add("rules", rulesNode);
15456
+ for (const rule of rules) {
15457
+ _addBranch(g, source, rulesNode, rule);
15490
15458
  }
15491
- };
15492
- var SubscriptionGraph = class extends Graph {
15493
- _keepaliveDisposers = [];
15494
- source;
15495
- cursor;
15496
- available;
15497
- constructor(name, topicGraph, opts = {}) {
15498
- super(name, opts.graph);
15499
- const initialCursor = requireNonNegativeInt(opts.cursor ?? 0, "subscription cursor");
15500
- this.mount("topic", topicGraph);
15501
- const topicEvents = topicGraph.events;
15502
- this.source = derived([topicEvents], ([snapshot]) => snapshot, {
15503
- name: "source",
15504
- describeKind: "derived",
15505
- meta: messagingMeta("subscription_source"),
15506
- initial: topicEvents.get()
15507
- });
15508
- this.add("source", this.source);
15509
- this.cursor = state(initialCursor, {
15510
- name: "cursor",
15511
- describeKind: "state",
15512
- meta: messagingMeta("subscription_cursor")
15513
- });
15514
- this.add("cursor", this.cursor);
15515
- this.available = derived(
15516
- [this.source, this.cursor],
15517
- ([sourceSnapshot, cursor]) => {
15518
- const entries = sourceSnapshot.value.entries;
15519
- const start = Math.max(0, Math.trunc(cursor ?? 0));
15520
- return entries.slice(start);
15521
- },
15522
- {
15523
- name: "available",
15524
- describeKind: "derived",
15525
- meta: messagingMeta("subscription_available"),
15526
- initial: []
15459
+ return g;
15460
+ }
15461
+ function _addBranch(graph, source, rulesNode, rule) {
15462
+ const branchName = `branch/${rule.name}`;
15463
+ let pendingDirty = false;
15464
+ const filterNode = node([source, rulesNode], () => void 0, {
15465
+ describeKind: "operator",
15466
+ meta: baseMeta("stratify_branch", { branch: rule.name }),
15467
+ onMessage(msg, depIndex, actions) {
15468
+ if (depIndex !== 0) return false;
15469
+ const t = msg[0];
15470
+ if (t === DATA) {
15471
+ const value = msg[1];
15472
+ const currentRules = rulesNode.get();
15473
+ const currentRule = currentRules.find((r) => r.name === rule.name);
15474
+ if (currentRule && currentRule.classify(value)) {
15475
+ pendingDirty = false;
15476
+ actions.emit(value);
15477
+ } else {
15478
+ if (pendingDirty) {
15479
+ pendingDirty = false;
15480
+ actions.down([[DIRTY], [RESOLVED]]);
15481
+ }
15482
+ }
15483
+ return true;
15527
15484
  }
15528
- );
15529
- this.add("available", this.available);
15530
- this.connect("topic::events", "source");
15531
- this.connect("source", "available");
15532
- this.connect("cursor", "available");
15533
- this._keepaliveDisposers.push(keepalive4(this.source));
15534
- this._keepaliveDisposers.push(keepalive4(this.available));
15535
- }
15536
- destroy() {
15537
- for (const dispose of this._keepaliveDisposers) dispose();
15538
- this._keepaliveDisposers.length = 0;
15539
- super.destroy();
15540
- }
15541
- ack(count) {
15542
- const available = this.available.get();
15543
- const requested = count === void 0 ? available.length : requireNonNegativeInt(count, "subscription ack count");
15544
- const step = Math.min(requested, available.length);
15545
- if (step <= 0) return this.cursor.get();
15546
- const next = this.cursor.get() + step;
15547
- this.cursor.down([[DATA, next]]);
15548
- return next;
15549
- }
15550
- pull(limit, opts = {}) {
15551
- const available = this.available.get();
15552
- const max = limit === void 0 ? available.length : requireNonNegativeInt(limit, "subscription pull limit");
15553
- const out = available.slice(0, max);
15554
- if (opts.ack && out.length > 0) this.ack(out.length);
15555
- return out;
15556
- }
15557
- };
15558
- var JobQueueGraph = class extends Graph {
15559
- _pending;
15560
- _jobs;
15561
- _keepaliveDisposers = [];
15562
- _seq = 0;
15563
- pending;
15564
- jobs;
15565
- depth;
15566
- constructor(name, opts = {}) {
15567
- super(name, opts.graph);
15568
- this._pending = reactiveList([], { name: "pending" });
15569
- this._jobs = reactiveMap({ name: "jobs" });
15570
- this.pending = this._pending.items;
15571
- this.jobs = this._jobs.node;
15572
- this.add("pending", this.pending);
15573
- this.add("jobs", this.jobs);
15574
- this.depth = derived(
15575
- [this.pending],
15576
- ([snapshot]) => snapshot.value.items.length,
15577
- {
15578
- name: "depth",
15579
- describeKind: "derived",
15580
- meta: messagingMeta("queue_depth"),
15581
- initial: 0
15485
+ if (t === DIRTY) {
15486
+ pendingDirty = true;
15487
+ return true;
15582
15488
  }
15583
- );
15584
- this.add("depth", this.depth);
15585
- this.connect("pending", "depth");
15586
- this._keepaliveDisposers.push(keepalive4(this.depth));
15587
- }
15588
- destroy() {
15589
- for (const dispose of this._keepaliveDisposers) dispose();
15590
- this._keepaliveDisposers.length = 0;
15591
- super.destroy();
15592
- }
15593
- enqueue(payload, opts = {}) {
15594
- const id = opts.id ?? `${this.name}-${++this._seq}`;
15595
- if (this._jobs.get(id) !== void 0) {
15596
- throw new Error(`jobQueue("${this.name}"): duplicate job id "${id}"`);
15489
+ if (t === RESOLVED) {
15490
+ if (pendingDirty) {
15491
+ pendingDirty = false;
15492
+ actions.down([[DIRTY], [RESOLVED]]);
15493
+ } else {
15494
+ actions.down([[RESOLVED]]);
15495
+ }
15496
+ return true;
15497
+ }
15498
+ if (t === COMPLETE || t === ERROR) {
15499
+ pendingDirty = false;
15500
+ actions.down([msg]);
15501
+ return true;
15502
+ }
15503
+ return false;
15597
15504
  }
15598
- const job = {
15599
- id,
15600
- payload,
15601
- attempts: 0,
15602
- metadata: Object.freeze({ ...opts.metadata ?? {} }),
15603
- state: "queued"
15604
- };
15605
- this._jobs.set(id, job);
15606
- this._pending.append(id);
15607
- return id;
15505
+ });
15506
+ graph.add(branchName, filterNode);
15507
+ graph.connect("source", branchName);
15508
+ if (rule.ops) {
15509
+ const transformed = rule.ops(filterNode);
15510
+ const transformedName = `branch/${rule.name}/out`;
15511
+ graph.add(transformedName, transformed);
15512
+ graph.connect(branchName, transformedName);
15608
15513
  }
15609
- claim(limit = 1) {
15610
- const max = requireNonNegativeInt(limit, "job queue claim limit");
15611
- if (max === 0) return [];
15612
- const out = [];
15613
- while (out.length < max) {
15614
- const snapshot = this.pending.get();
15615
- const ids = snapshot.value.items;
15616
- if (ids.length === 0) break;
15617
- const id = this._pending.pop(0);
15618
- const job = this._jobs.get(id);
15619
- if (!job || job.state !== "queued") continue;
15620
- const inflight = {
15621
- ...job,
15622
- state: "inflight",
15623
- attempts: job.attempts + 1
15624
- };
15625
- this._jobs.set(id, inflight);
15626
- out.push(inflight);
15514
+ }
15515
+ function funnel(name, sources, stages, opts) {
15516
+ if (sources.length === 0) throw new RangeError("funnel requires at least one source");
15517
+ if (stages.length === 0) throw new RangeError("funnel requires at least one stage");
15518
+ const g = new Graph(name, opts);
15519
+ const merged = sources.length === 1 ? sources[0] : merge(...sources);
15520
+ g.add("merged", merged);
15521
+ let prevOutputPath = "merged";
15522
+ for (let i = 0; i < stages.length; i++) {
15523
+ const stage = stages[i];
15524
+ const sub = new Graph(stage.name);
15525
+ stage.build(sub);
15526
+ try {
15527
+ sub.resolve("input");
15528
+ } catch {
15529
+ throw new Error(`funnel stage "${stage.name}" must define an "input" node`);
15627
15530
  }
15628
- return out;
15629
- }
15630
- ack(id) {
15631
- const job = this._jobs.get(id);
15632
- if (!job || job.state !== "inflight") return false;
15633
- this._jobs.delete(id);
15634
- return true;
15635
- }
15636
- nack(id, opts = {}) {
15637
- const job = this._jobs.get(id);
15638
- if (!job || job.state !== "inflight") return false;
15639
- if (opts.requeue ?? true) {
15640
- this._jobs.set(id, { ...job, state: "queued" });
15641
- this._pending.append(id);
15642
- return true;
15531
+ try {
15532
+ sub.resolve("output");
15533
+ } catch {
15534
+ throw new Error(`funnel stage "${stage.name}" must define an "output" node`);
15643
15535
  }
15644
- this._jobs.delete(id);
15645
- return true;
15536
+ g.mount(stage.name, sub);
15537
+ const prevNode = g.resolve(prevOutputPath);
15538
+ const stageInputPath = `${stage.name}::input`;
15539
+ const stageInput = g.resolve(stageInputPath);
15540
+ prevNode.subscribe((msgs) => {
15541
+ for (const msg of msgs) {
15542
+ const t = msg[0];
15543
+ if (t === DATA) {
15544
+ stageInput.down([[DATA, msg[1]]]);
15545
+ } else if (t === DIRTY) {
15546
+ stageInput.down([[DIRTY]]);
15547
+ } else if (t === RESOLVED) {
15548
+ stageInput.down([[RESOLVED]]);
15549
+ } else if (t === COMPLETE || t === ERROR) {
15550
+ stageInput.down([msg]);
15551
+ }
15552
+ }
15553
+ });
15554
+ prevOutputPath = `${stage.name}::output`;
15646
15555
  }
15647
- };
15648
- var JobFlowGraph = class extends Graph {
15649
- _stageNames;
15650
- _queues = /* @__PURE__ */ new Map();
15651
- _keepaliveDisposers = [];
15652
- _completed;
15653
- completed;
15654
- completedCount;
15655
- constructor(name, opts = {}) {
15656
- super(name, opts.graph);
15657
- const stages = (opts.stages ?? ["incoming", "processing", "done"]).map((v) => v.trim());
15658
- if (stages.length < 2) {
15659
- throw new Error(`jobFlow("${name}"): requires at least 2 stages`);
15556
+ return g;
15557
+ }
15558
+ function feedback(graph, condition, reentry, opts) {
15559
+ const maxIter = opts?.maxIterations ?? 10;
15560
+ const counterName = `__feedback_${condition}`;
15561
+ const counter = state(0, {
15562
+ meta: baseMeta("feedback_counter", {
15563
+ maxIterations: maxIter,
15564
+ feedbackFrom: condition,
15565
+ feedbackTo: reentry
15566
+ })
15567
+ });
15568
+ graph.add(counterName, counter);
15569
+ const condNode = graph.resolve(condition);
15570
+ const reentryNode = graph.resolve(reentry);
15571
+ let tornDown = false;
15572
+ let unsubCounter = null;
15573
+ const safeUnsub = () => {
15574
+ if (tornDown) return;
15575
+ tornDown = true;
15576
+ unsub();
15577
+ unsubCounter?.();
15578
+ };
15579
+ const unsub = condNode.subscribe((msgs) => {
15580
+ for (const msg of msgs) {
15581
+ if (msg[0] === DATA) {
15582
+ const currentCount = counter.get();
15583
+ if (currentCount >= maxIter) continue;
15584
+ const condValue = msg[1];
15585
+ if (condValue == null) continue;
15586
+ counter.down([[DATA, currentCount + 1]]);
15587
+ reentryNode.down([[DATA, condValue]]);
15588
+ } else if (msg[0] === COMPLETE || msg[0] === ERROR) {
15589
+ const terminal = msg[0] === ERROR && msg.length > 1 ? [ERROR, msg[1]] : [msg[0]];
15590
+ counter.down([terminal]);
15591
+ safeUnsub();
15592
+ }
15660
15593
  }
15661
- const unique = new Set(stages);
15662
- if (unique.size !== stages.length) {
15663
- throw new Error(`jobFlow("${name}"): stage names must be unique`);
15594
+ });
15595
+ unsubCounter = counter.subscribe((msgs) => {
15596
+ for (const msg of msgs) {
15597
+ if (msg[0] === COMPLETE || msg[0] === ERROR) {
15598
+ safeUnsub();
15599
+ return;
15600
+ }
15664
15601
  }
15665
- this._stageNames = Object.freeze([...stages]);
15666
- for (const stage of this._stageNames) {
15667
- const q = jobQueue(`${name}-${stage}`);
15668
- this._queues.set(stage, q);
15669
- this.mount(stage, q);
15602
+ });
15603
+ return graph;
15604
+ }
15605
+ function budgetGate(source, constraints, opts) {
15606
+ if (constraints.length === 0) throw new RangeError("budgetGate requires at least one constraint");
15607
+ const constraintNodes = constraints.map((c) => c.node);
15608
+ const allDeps = [source, ...constraintNodes];
15609
+ let buffer2 = [];
15610
+ let paused = false;
15611
+ let pendingResolved = false;
15612
+ const lockId = /* @__PURE__ */ Symbol("budget-gate");
15613
+ function checkBudget() {
15614
+ return constraints.every((c) => c.check(c.node.get()));
15615
+ }
15616
+ function flushBuffer(actions) {
15617
+ while (buffer2.length > 0 && checkBudget()) {
15618
+ const item = buffer2.shift();
15619
+ actions.emit(item);
15670
15620
  }
15671
- this._completed = reactiveLog([], { name: "completed" });
15672
- this.completed = this._completed.entries;
15673
- this.add("completed", this.completed);
15674
- this.completedCount = derived(
15675
- [this.completed],
15676
- ([snapshot]) => snapshot.value.entries.length,
15677
- {
15678
- name: "completedCount",
15679
- describeKind: "derived",
15680
- meta: messagingMeta("job_flow_completed_count"),
15681
- initial: 0
15682
- }
15683
- );
15684
- this.add("completedCount", this.completedCount);
15685
- this.connect("completed", "completedCount");
15686
- this._keepaliveDisposers.push(keepalive4(this.completedCount));
15687
- const maxPerPump = Math.max(
15688
- 1,
15689
- requireNonNegativeInt(opts.maxPerPump ?? DEFAULT_MAX_PER_PUMP, "job flow maxPerPump")
15690
- );
15691
- for (let i = 0; i < this._stageNames.length; i += 1) {
15692
- const stage = this._stageNames[i];
15693
- const current = this.queue(stage);
15694
- const next = i + 1 < this._stageNames.length ? this.queue(this._stageNames[i + 1]) : null;
15695
- const pump = node(
15696
- [current.pending],
15697
- () => {
15698
- let moved = 0;
15699
- while (moved < maxPerPump) {
15700
- const claim = current.claim(1);
15701
- if (claim.length === 0) break;
15702
- const job = claim[0];
15703
- if (!job) break;
15704
- if (next) {
15705
- next.enqueue(job.payload, {
15706
- metadata: {
15707
- ...job.metadata,
15708
- job_flow_from: stage
15709
- }
15710
- });
15711
- } else {
15712
- this._completed.append(job);
15621
+ if (buffer2.length === 0 && pendingResolved) {
15622
+ pendingResolved = false;
15623
+ actions.down([[RESOLVED]]);
15624
+ }
15625
+ }
15626
+ return node(allDeps, () => void 0, {
15627
+ ...opts,
15628
+ describeKind: "operator",
15629
+ meta: baseMeta("budget_gate", opts?.meta),
15630
+ onMessage(msg, depIndex, actions) {
15631
+ const t = msg[0];
15632
+ if (depIndex === 0) {
15633
+ if (t === DATA) {
15634
+ if (checkBudget() && buffer2.length === 0) {
15635
+ actions.emit(msg[1]);
15636
+ } else {
15637
+ buffer2.push(msg[1]);
15638
+ if (!paused) {
15639
+ paused = true;
15640
+ actions.up([[PAUSE, lockId]]);
15713
15641
  }
15714
- current.ack(job.id);
15715
- moved += 1;
15716
15642
  }
15717
- },
15718
- {
15719
- name: `pump_${stage}`,
15720
- describeKind: "effect",
15721
- meta: messagingMeta("job_flow_pump")
15643
+ return true;
15722
15644
  }
15723
- );
15724
- this.add(`pump_${stage}`, pump);
15725
- this.connect(`${stage}::pending`, `pump_${stage}`);
15726
- this._keepaliveDisposers.push(keepalive4(pump));
15645
+ if (t === DIRTY) {
15646
+ actions.down([[DIRTY]]);
15647
+ return true;
15648
+ }
15649
+ if (t === RESOLVED) {
15650
+ if (buffer2.length === 0) {
15651
+ actions.down([[RESOLVED]]);
15652
+ } else {
15653
+ pendingResolved = true;
15654
+ }
15655
+ return true;
15656
+ }
15657
+ if (t === COMPLETE || t === ERROR) {
15658
+ for (const item of buffer2) {
15659
+ actions.emit(item);
15660
+ }
15661
+ buffer2 = [];
15662
+ pendingResolved = false;
15663
+ if (paused) {
15664
+ paused = false;
15665
+ actions.up([[RESUME, lockId]]);
15666
+ }
15667
+ actions.down([msg]);
15668
+ return true;
15669
+ }
15670
+ return false;
15671
+ }
15672
+ if (t === DATA || t === RESOLVED) {
15673
+ if (checkBudget() && buffer2.length > 0) {
15674
+ flushBuffer(actions);
15675
+ if (buffer2.length === 0 && paused) {
15676
+ paused = false;
15677
+ actions.up([[RESUME, lockId]]);
15678
+ }
15679
+ } else if (!checkBudget() && !paused && buffer2.length > 0) {
15680
+ paused = true;
15681
+ actions.up([[PAUSE, lockId]]);
15682
+ }
15683
+ return true;
15684
+ }
15685
+ if (t === DIRTY) {
15686
+ return true;
15687
+ }
15688
+ if (t === ERROR) {
15689
+ actions.down([msg]);
15690
+ return true;
15691
+ }
15692
+ if (t === COMPLETE) {
15693
+ return true;
15694
+ }
15695
+ return false;
15727
15696
  }
15697
+ });
15698
+ }
15699
+ function scorer(sources, weights, opts) {
15700
+ if (sources.length === 0) throw new RangeError("scorer requires at least one source");
15701
+ if (sources.length !== weights.length) {
15702
+ throw new RangeError("scorer requires the same number of sources and weights");
15728
15703
  }
15729
- destroy() {
15730
- for (const dispose of this._keepaliveDisposers) dispose();
15731
- this._keepaliveDisposers.length = 0;
15732
- super.destroy();
15733
- }
15734
- stages() {
15735
- return this._stageNames;
15704
+ const allDeps = [...sources, ...weights];
15705
+ const n = sources.length;
15706
+ const scoreFns = opts?.scoreFns;
15707
+ return derived(
15708
+ allDeps,
15709
+ (vals) => {
15710
+ const signals = vals.slice(0, n);
15711
+ const weightValues = vals.slice(n);
15712
+ const breakdown = [];
15713
+ let totalScore = 0;
15714
+ for (let i = 0; i < n; i++) {
15715
+ const sig = signals[i] ?? 0;
15716
+ const wt = weightValues[i] ?? 0;
15717
+ const rawScore = scoreFns?.[i] ? scoreFns[i](sig) : sig;
15718
+ const weighted = rawScore * wt;
15719
+ breakdown.push(weighted);
15720
+ totalScore += weighted;
15721
+ }
15722
+ return {
15723
+ value: signals,
15724
+ score: totalScore,
15725
+ breakdown
15726
+ };
15727
+ },
15728
+ {
15729
+ ...opts,
15730
+ describeKind: "derived",
15731
+ meta: baseMeta("scorer", opts?.meta)
15732
+ }
15733
+ );
15734
+ }
15735
+
15736
+ // src/patterns/graphspec.ts
15737
+ var VALID_NODE_TYPES2 = /* @__PURE__ */ new Set([
15738
+ "state",
15739
+ "producer",
15740
+ "derived",
15741
+ "effect",
15742
+ "operator",
15743
+ "template"
15744
+ ]);
15745
+ var INNER_NODE_TYPES = /* @__PURE__ */ new Set(["state", "producer", "derived", "effect", "operator"]);
15746
+ function validateSpec(spec) {
15747
+ const errors = [];
15748
+ if (spec == null || typeof spec !== "object") {
15749
+ return { valid: false, errors: ["GraphSpec must be a non-null object"] };
15736
15750
  }
15737
- queue(stage) {
15738
- const q = this._queues.get(stage);
15739
- if (!q) throw new Error(`jobFlow("${this.name}"): unknown stage "${stage}"`);
15740
- return q;
15751
+ const s = spec;
15752
+ if (typeof s.name !== "string" || s.name.length === 0) {
15753
+ errors.push("Missing or empty 'name' field");
15741
15754
  }
15742
- enqueue(payload, opts = {}) {
15743
- return this.queue(this._stageNames[0]).enqueue(payload, opts);
15755
+ if (s.nodes == null || typeof s.nodes !== "object" || Array.isArray(s.nodes)) {
15756
+ errors.push("Missing or invalid 'nodes' field (must be an object)");
15757
+ return { valid: false, errors };
15744
15758
  }
15745
- retainedCompleted() {
15746
- const snapshot = this.completed.get();
15747
- return snapshot.value.entries;
15759
+ const nodeNames = new Set(Object.keys(s.nodes));
15760
+ const nodeTypes = /* @__PURE__ */ new Map();
15761
+ const templateDefs = /* @__PURE__ */ new Map();
15762
+ if (s.templates != null && typeof s.templates === "object" && !Array.isArray(s.templates)) {
15763
+ for (const [tName, tRaw] of Object.entries(s.templates)) {
15764
+ if (tRaw != null && typeof tRaw === "object") {
15765
+ const t = tRaw;
15766
+ templateDefs.set(tName, {
15767
+ params: Array.isArray(t.params) ? t.params : []
15768
+ });
15769
+ }
15770
+ }
15748
15771
  }
15749
- };
15750
- var TopicBridgeGraph = class extends Graph {
15751
- _sourceSub;
15752
- _target;
15753
- _keepaliveDisposers = [];
15754
- bridgedCount;
15755
- constructor(name, sourceTopic, targetTopic, opts = {}) {
15756
- super(name, opts.graph);
15757
- this._sourceSub = subscription(`${name}-subscription`, sourceTopic, {
15758
- cursor: opts.cursor
15759
- });
15760
- this._target = targetTopic;
15761
- this.mount("subscription", this._sourceSub);
15762
- this.bridgedCount = state(0, {
15763
- name: "bridgedCount",
15764
- describeKind: "state",
15765
- meta: messagingMeta("topic_bridge_count")
15766
- });
15767
- this.add("bridgedCount", this.bridgedCount);
15768
- const maxPerPump = Math.max(
15769
- 1,
15770
- requireNonNegativeInt(opts.maxPerPump ?? DEFAULT_MAX_PER_PUMP, "topic bridge maxPerPump")
15771
- );
15772
- const mapValue = opts.map ?? ((value) => value);
15773
- const pump = node(
15774
- [this._sourceSub.available],
15775
- () => {
15776
- const available = this._sourceSub.pull(maxPerPump, { ack: true });
15777
- if (available.length === 0) return;
15778
- let bridged = 0;
15779
- for (const value of available) {
15780
- const mapped = mapValue(value);
15781
- if (mapped === void 0) continue;
15782
- this._target.publish(mapped);
15783
- bridged += 1;
15772
+ if (s.templates != null) {
15773
+ if (typeof s.templates !== "object" || Array.isArray(s.templates)) {
15774
+ errors.push("'templates' must be an object");
15775
+ } else {
15776
+ for (const [tName, tRaw] of Object.entries(s.templates)) {
15777
+ if (tRaw == null || typeof tRaw !== "object") {
15778
+ errors.push(`Template "${tName}": must be an object`);
15779
+ continue;
15784
15780
  }
15785
- if (bridged > 0) {
15786
- const current = this.bridgedCount.get();
15787
- this.bridgedCount.down([[DATA, current + bridged]]);
15781
+ const t = tRaw;
15782
+ if (!Array.isArray(t.params)) {
15783
+ errors.push(`Template "${tName}": missing 'params' array`);
15784
+ }
15785
+ if (t.nodes == null || typeof t.nodes !== "object" || Array.isArray(t.nodes)) {
15786
+ errors.push(`Template "${tName}": missing or invalid 'nodes' object`);
15787
+ } else {
15788
+ const paramSet = new Set(Array.isArray(t.params) ? t.params : []);
15789
+ const innerNames = new Set(Object.keys(t.nodes));
15790
+ for (const [nName, nRaw] of Object.entries(t.nodes)) {
15791
+ if (nRaw == null || typeof nRaw !== "object") {
15792
+ errors.push(`Template "${tName}" node "${nName}": must be an object`);
15793
+ continue;
15794
+ }
15795
+ const n = nRaw;
15796
+ if (typeof n.type !== "string" || !INNER_NODE_TYPES.has(n.type)) {
15797
+ errors.push(`Template "${tName}" node "${nName}": invalid type`);
15798
+ }
15799
+ if (Array.isArray(n.deps)) {
15800
+ for (const dep of n.deps) {
15801
+ if (!innerNames.has(dep) && !paramSet.has(dep)) {
15802
+ errors.push(
15803
+ `Template "${tName}" node "${nName}": dep "${dep}" is not an inner node or param`
15804
+ );
15805
+ }
15806
+ }
15807
+ }
15808
+ }
15809
+ if (typeof t.output !== "string") {
15810
+ errors.push(`Template "${tName}": missing 'output' string`);
15811
+ } else if (!t.nodes[t.output]) {
15812
+ errors.push(`Template "${tName}": output "${t.output}" is not a declared node`);
15813
+ }
15788
15814
  }
15789
- },
15790
- {
15791
- name: "pump",
15792
- describeKind: "effect",
15793
- meta: messagingMeta("topic_bridge_pump")
15794
15815
  }
15795
- );
15796
- this.add("pump", pump);
15797
- this.connect("subscription::available", "pump");
15798
- this._keepaliveDisposers.push(keepalive4(pump));
15799
- }
15800
- destroy() {
15801
- for (const dispose of this._keepaliveDisposers) dispose();
15802
- this._keepaliveDisposers.length = 0;
15803
- super.destroy();
15816
+ }
15804
15817
  }
15805
- };
15806
- function topic(name, opts) {
15807
- return new TopicGraph(name, opts);
15808
- }
15809
- function subscription(name, topicGraph, opts) {
15810
- return new SubscriptionGraph(name, topicGraph, opts);
15811
- }
15812
- function jobQueue(name, opts) {
15813
- return new JobQueueGraph(name, opts);
15814
- }
15815
- function jobFlow(name, opts) {
15816
- return new JobFlowGraph(name, opts);
15817
- }
15818
- function topicBridge(name, sourceTopic, targetTopic, opts) {
15819
- return new TopicBridgeGraph(name, sourceTopic, targetTopic, opts);
15820
- }
15821
-
15822
- // src/patterns/orchestration.ts
15823
- var orchestration_exports = {};
15824
- __export(orchestration_exports, {
15825
- approval: () => approval,
15826
- branch: () => branch,
15827
- forEach: () => forEach2,
15828
- gate: () => gate2,
15829
- join: () => join2,
15830
- loop: () => loop,
15831
- onFailure: () => onFailure,
15832
- pipeline: () => pipeline,
15833
- sensor: () => sensor,
15834
- subPipeline: () => subPipeline,
15835
- task: () => task,
15836
- wait: () => wait
15837
- });
15838
- function resolveDep(graph, dep) {
15839
- if (typeof dep === "string") {
15840
- return { node: graph.resolve(dep), path: dep };
15818
+ for (const [name, raw] of Object.entries(s.nodes)) {
15819
+ if (raw == null || typeof raw !== "object") {
15820
+ errors.push(`Node "${name}": must be an object`);
15821
+ continue;
15822
+ }
15823
+ const n = raw;
15824
+ if (typeof n.type !== "string" || !VALID_NODE_TYPES2.has(n.type)) {
15825
+ errors.push(
15826
+ `Node "${name}": invalid type "${String(n.type)}" (expected: ${[...VALID_NODE_TYPES2].join(", ")})`
15827
+ );
15828
+ continue;
15829
+ }
15830
+ nodeTypes.set(name, n.type);
15831
+ if (n.type === "template") {
15832
+ if (typeof n.template !== "string" || !templateDefs.has(n.template)) {
15833
+ errors.push(`Node "${name}": template "${String(n.template)}" not found in templates`);
15834
+ } else {
15835
+ if (n.bind == null || typeof n.bind !== "object" || Array.isArray(n.bind)) {
15836
+ errors.push(`Node "${name}": template ref requires 'bind' object`);
15837
+ } else {
15838
+ const tmpl = templateDefs.get(n.template);
15839
+ const bind = n.bind;
15840
+ for (const param of tmpl.params) {
15841
+ if (!(param in bind)) {
15842
+ errors.push(
15843
+ `Node "${name}": template param "${param}" is not bound (template "${n.template}")`
15844
+ );
15845
+ }
15846
+ }
15847
+ for (const [, target] of Object.entries(bind)) {
15848
+ if (typeof target === "string" && !nodeNames.has(target)) {
15849
+ errors.push(
15850
+ `Node "${name}": bind target "${target}" does not reference an existing node`
15851
+ );
15852
+ }
15853
+ }
15854
+ }
15855
+ }
15856
+ } else {
15857
+ if (Array.isArray(n.deps)) {
15858
+ for (const dep of n.deps) {
15859
+ if (dep === name) {
15860
+ errors.push(`Node "${name}": self-referencing dep`);
15861
+ } else if (!nodeNames.has(dep)) {
15862
+ errors.push(`Node "${name}": dep "${dep}" does not reference an existing node`);
15863
+ }
15864
+ }
15865
+ }
15866
+ if ((n.type === "derived" || n.type === "effect" || n.type === "operator") && !Array.isArray(n.deps)) {
15867
+ errors.push(`Node "${name}": ${n.type} node should have a 'deps' array`);
15868
+ }
15869
+ }
15841
15870
  }
15842
- const path = findRegisteredNodePath(graph, dep);
15843
- if (!path) {
15844
- throw new Error(
15845
- "orchestration dep node must already be registered in the graph so explicit edges can be recorded; pass a string path or register the node first"
15846
- );
15871
+ if (s.feedback != null) {
15872
+ if (!Array.isArray(s.feedback)) {
15873
+ errors.push("'feedback' must be an array");
15874
+ } else {
15875
+ for (let i = 0; i < s.feedback.length; i++) {
15876
+ const edge = s.feedback[i];
15877
+ if (edge == null || typeof edge !== "object") {
15878
+ errors.push(`Feedback [${i}]: must be an object`);
15879
+ continue;
15880
+ }
15881
+ const e = edge;
15882
+ if (typeof e.from !== "string" || !nodeNames.has(e.from)) {
15883
+ errors.push(
15884
+ `Feedback [${i}]: 'from' "${String(e.from)}" does not reference an existing node`
15885
+ );
15886
+ }
15887
+ if (typeof e.from === "string" && e.from === e.to) {
15888
+ errors.push(`Feedback [${i}]: 'from' and 'to' must be different nodes`);
15889
+ }
15890
+ if (typeof e.to !== "string" || !nodeNames.has(e.to)) {
15891
+ errors.push(
15892
+ `Feedback [${i}]: 'to' "${String(e.to)}" does not reference an existing node`
15893
+ );
15894
+ } else if (typeof e.to === "string" && nodeTypes.get(e.to) !== "state") {
15895
+ errors.push(
15896
+ `Feedback [${i}]: 'to' node "${e.to}" must be a state node (got "${nodeTypes.get(e.to) ?? "unknown"}")`
15897
+ );
15898
+ }
15899
+ }
15900
+ }
15847
15901
  }
15848
- return { node: dep, path };
15902
+ return { valid: errors.length === 0, errors };
15849
15903
  }
15850
- function findRegisteredNodePath(graph, target) {
15851
- const described = graph.describe();
15852
- const metaSegment = `::${GRAPH_META_SEGMENT}::`;
15853
- for (const path of Object.keys(described.nodes).sort()) {
15854
- if (path.includes(metaSegment)) continue;
15904
+ function compileSpec(spec, opts) {
15905
+ const validation = validateSpec(spec);
15906
+ if (!validation.valid) {
15907
+ throw new Error(`compileSpec: invalid GraphSpec:
15908
+ ${validation.errors.join("\n")}`);
15909
+ }
15910
+ const catalog = opts?.catalog ?? {};
15911
+ const g = new Graph(spec.name);
15912
+ const templates = spec.templates ?? {};
15913
+ const created = /* @__PURE__ */ new Map();
15914
+ const deferred = [];
15915
+ for (const [name, raw] of Object.entries(spec.nodes)) {
15916
+ if (raw.type === "template") continue;
15917
+ const n = raw;
15918
+ if (n.type === "state") {
15919
+ const nd = state(n.initial, {
15920
+ name,
15921
+ meta: n.meta ? { ...n.meta } : void 0
15922
+ });
15923
+ g.add(name, nd);
15924
+ created.set(name, nd);
15925
+ } else if (n.type === "producer") {
15926
+ const sourceFactory = n.source ? catalog.sources?.[n.source] : void 0;
15927
+ const fnFactory = n.fn ? catalog.fns?.[n.fn] : void 0;
15928
+ if (sourceFactory) {
15929
+ const nd = sourceFactory(n.config ?? {});
15930
+ g.add(name, nd);
15931
+ created.set(name, nd);
15932
+ } else if (fnFactory) {
15933
+ const nd = fnFactory([], n.config ?? {});
15934
+ g.add(name, nd);
15935
+ created.set(name, nd);
15936
+ } else {
15937
+ const nd = producer(() => {
15938
+ }, {
15939
+ name,
15940
+ meta: { ...n.meta, _specFn: n.fn, _specSource: n.source }
15941
+ });
15942
+ g.add(name, nd);
15943
+ created.set(name, nd);
15944
+ }
15945
+ } else {
15946
+ deferred.push([name, n]);
15947
+ }
15948
+ }
15949
+ let progressed = true;
15950
+ const pending = new Map(deferred);
15951
+ while (pending.size > 0 && progressed) {
15952
+ progressed = false;
15953
+ for (const [name, n] of [...pending.entries()]) {
15954
+ const deps = n.deps ?? [];
15955
+ if (!deps.every((dep) => created.has(dep))) continue;
15956
+ const resolvedDeps = deps.map((dep) => created.get(dep));
15957
+ const fnFactory = n.fn ? catalog.fns?.[n.fn] : void 0;
15958
+ let nd;
15959
+ if (fnFactory) {
15960
+ nd = fnFactory(resolvedDeps, n.config ?? {});
15961
+ } else if (n.type === "effect") {
15962
+ nd = effect(resolvedDeps, () => {
15963
+ });
15964
+ } else {
15965
+ nd = derived(resolvedDeps, (vals) => vals[0]);
15966
+ }
15967
+ g.add(name, nd);
15968
+ created.set(name, nd);
15969
+ pending.delete(name);
15970
+ progressed = true;
15971
+ }
15972
+ }
15973
+ if (pending.size > 0) {
15974
+ const unresolved = [...pending.keys()].sort().join(", ");
15975
+ throw new Error(`compileSpec: unresolvable deps for nodes: ${unresolved}`);
15976
+ }
15977
+ for (const [name, raw] of Object.entries(spec.nodes)) {
15978
+ if (raw.type !== "template") continue;
15979
+ const ref = raw;
15980
+ const tmpl = templates[ref.template];
15981
+ const sub = new Graph(name);
15982
+ const subCreated = /* @__PURE__ */ new Map();
15983
+ const subDeferred = [];
15984
+ for (const [nName, nSpec] of Object.entries(tmpl.nodes)) {
15985
+ const resolvedDeps = (nSpec.deps ?? []).map((dep) => {
15986
+ if (dep.startsWith("$") && ref.bind[dep]) {
15987
+ return ref.bind[dep];
15988
+ }
15989
+ return dep;
15990
+ });
15991
+ const specWithResolvedDeps = { ...nSpec, deps: resolvedDeps };
15992
+ if (nSpec.type === "state") {
15993
+ const nd = state(nSpec.initial, {
15994
+ name: nName,
15995
+ meta: nSpec.meta ? { ...nSpec.meta } : void 0
15996
+ });
15997
+ sub.add(nName, nd);
15998
+ subCreated.set(nName, nd);
15999
+ } else if (nSpec.type === "producer") {
16000
+ const sourceFactory = nSpec.source ? catalog.sources?.[nSpec.source] : void 0;
16001
+ const fnFactory = nSpec.fn ? catalog.fns?.[nSpec.fn] : void 0;
16002
+ if (sourceFactory) {
16003
+ const nd = sourceFactory(nSpec.config ?? {});
16004
+ sub.add(nName, nd);
16005
+ subCreated.set(nName, nd);
16006
+ } else if (fnFactory) {
16007
+ const nd = fnFactory([], nSpec.config ?? {});
16008
+ sub.add(nName, nd);
16009
+ subCreated.set(nName, nd);
16010
+ } else {
16011
+ const nd = producer(() => {
16012
+ }, {
16013
+ name: nName,
16014
+ meta: { ...nSpec.meta, _specFn: nSpec.fn, _specSource: nSpec.source }
16015
+ });
16016
+ sub.add(nName, nd);
16017
+ subCreated.set(nName, nd);
16018
+ }
16019
+ } else {
16020
+ subDeferred.push([nName, specWithResolvedDeps]);
16021
+ }
16022
+ }
16023
+ let subProgressed = true;
16024
+ const subPending = new Map(subDeferred);
16025
+ while (subPending.size > 0 && subProgressed) {
16026
+ subProgressed = false;
16027
+ for (const [nName, nSpec] of [...subPending.entries()]) {
16028
+ const deps = nSpec.deps ?? [];
16029
+ const allReady = deps.every((dep) => subCreated.has(dep) || created.has(dep));
16030
+ if (!allReady) continue;
16031
+ const resolvedDeps = deps.map((dep) => subCreated.get(dep) ?? created.get(dep));
16032
+ const fnFactory = nSpec.fn ? catalog.fns?.[nSpec.fn] : void 0;
16033
+ let nd;
16034
+ if (fnFactory) {
16035
+ nd = fnFactory(resolvedDeps, nSpec.config ?? {});
16036
+ } else if (nSpec.type === "effect") {
16037
+ nd = effect(resolvedDeps, () => {
16038
+ });
16039
+ } else {
16040
+ nd = derived(resolvedDeps, (vals) => vals[0]);
16041
+ }
16042
+ sub.add(nName, nd);
16043
+ subCreated.set(nName, nd);
16044
+ subPending.delete(nName);
16045
+ subProgressed = true;
16046
+ }
16047
+ }
16048
+ if (subPending.size > 0) {
16049
+ const unresolved = [...subPending.keys()].sort().join(", ");
16050
+ throw new Error(
16051
+ `compileSpec: template "${ref.template}" has unresolvable deps: ${unresolved}`
16052
+ );
16053
+ }
16054
+ g.mount(name, sub);
16055
+ const outputPath = `${name}::${tmpl.output}`;
16056
+ created.set(name, g.resolve(outputPath));
15855
16057
  try {
15856
- if (graph.resolve(path) === target) return path;
16058
+ const outputNode = g.resolve(outputPath);
16059
+ outputNode.meta._templateName?.down([[DATA, ref.template]]);
16060
+ outputNode.meta._templateBind?.down([[DATA, ref.bind]]);
15857
16061
  } catch {
15858
16062
  }
15859
16063
  }
15860
- return void 0;
15861
- }
15862
- function registerStep(graph, name, step, depPaths) {
15863
- graph.add(name, step);
15864
- for (const path of depPaths) {
15865
- graph.connect(path, name);
16064
+ for (const [name, raw] of Object.entries(spec.nodes)) {
16065
+ if (raw.type === "template") continue;
16066
+ const n = raw;
16067
+ for (const dep of n.deps ?? []) {
16068
+ try {
16069
+ g.connect(dep, name);
16070
+ } catch (err) {
16071
+ const msg = err instanceof Error ? err.message : "";
16072
+ if (!msg.includes("constructor deps") && !msg.includes("already")) {
16073
+ throw err;
16074
+ }
16075
+ }
16076
+ }
15866
16077
  }
16078
+ for (const fb of spec.feedback ?? []) {
16079
+ feedback(g, fb.from, fb.to, {
16080
+ maxIterations: fb.maxIterations
16081
+ });
16082
+ }
16083
+ return g;
15867
16084
  }
15868
- function baseMeta(kind, meta) {
15869
- return {
15870
- orchestration: true,
15871
- orchestration_type: kind,
15872
- ...meta ?? {}
15873
- };
15874
- }
15875
- function coerceLoopIterations(raw) {
15876
- const parseString = (value) => {
15877
- const trimmed = value.trim();
15878
- if (trimmed.length === 0) return 0;
15879
- return Number(trimmed);
15880
- };
15881
- let parsed;
15882
- if (typeof raw === "string") {
15883
- parsed = parseString(raw);
15884
- } else if (raw === null) {
15885
- parsed = 0;
15886
- } else {
15887
- parsed = Number(raw);
16085
+ var INTERNAL_META_KEYS = /* @__PURE__ */ new Set([
16086
+ "reduction",
16087
+ "reduction_type",
16088
+ "_specFn",
16089
+ "_specSource",
16090
+ "_templateName",
16091
+ "_templateBind",
16092
+ "feedbackFrom",
16093
+ "feedbackTo"
16094
+ ]);
16095
+ function decompileGraph(graph) {
16096
+ const desc = graph.describe({ detail: "standard" });
16097
+ const nodes = {};
16098
+ const feedbackEdges = [];
16099
+ const metaSegment = `::${GRAPH_META_SEGMENT}::`;
16100
+ const feedbackCounterPattern = /^__feedback_(.+)$/;
16101
+ const feedbackConditions = /* @__PURE__ */ new Set();
16102
+ for (const path of Object.keys(desc.nodes)) {
16103
+ if (path.includes(metaSegment)) continue;
16104
+ const match = feedbackCounterPattern.exec(path);
16105
+ if (match) {
16106
+ feedbackConditions.add(match[1]);
16107
+ const meta = desc.nodes[path]?.meta;
16108
+ if (meta?.feedbackFrom && meta?.feedbackTo) {
16109
+ feedbackEdges.push({
16110
+ from: meta.feedbackFrom,
16111
+ to: meta.feedbackTo,
16112
+ ...meta.maxIterations ? { maxIterations: meta.maxIterations } : {}
16113
+ });
16114
+ }
16115
+ }
15888
16116
  }
15889
- if (!Number.isFinite(parsed)) return 1;
15890
- return Math.max(0, Math.trunc(parsed));
15891
- }
15892
- function pipeline(name, opts) {
15893
- return new Graph(name, opts);
15894
- }
15895
- function task(graph, name, run, opts) {
15896
- const depRefs = opts?.deps ?? [];
15897
- const deps = depRefs.map((dep) => resolveDep(graph, dep));
15898
- const { deps: _deps, ...nodeOpts } = opts ?? {};
15899
- const step = node(
15900
- deps.map((d) => d.node),
15901
- run,
15902
- {
15903
- ...nodeOpts,
15904
- name,
15905
- describeKind: "derived",
15906
- meta: baseMeta("task", opts?.meta)
16117
+ for (const [path, nodeDesc] of Object.entries(desc.nodes)) {
16118
+ if (path.includes(metaSegment)) continue;
16119
+ if (feedbackCounterPattern.test(path)) continue;
16120
+ if (path.includes("::")) continue;
16121
+ const specNode = {
16122
+ type: nodeDesc.type
16123
+ };
16124
+ if (nodeDesc.deps.length > 0) {
16125
+ specNode.deps = nodeDesc.deps.filter((d) => !d.includes("::"));
16126
+ }
16127
+ if (nodeDesc.type === "state" && nodeDesc.value !== void 0) {
16128
+ specNode.initial = nodeDesc.value;
16129
+ }
16130
+ if (nodeDesc.meta && Object.keys(nodeDesc.meta).length > 0) {
16131
+ const meta = {};
16132
+ for (const [k, v] of Object.entries(nodeDesc.meta)) {
16133
+ if (!INTERNAL_META_KEYS.has(k)) meta[k] = v;
16134
+ }
16135
+ if (Object.keys(meta).length > 0) {
16136
+ specNode.meta = meta;
16137
+ }
16138
+ }
16139
+ nodes[path] = specNode;
16140
+ }
16141
+ const templates = {};
16142
+ const templateRefs = {};
16143
+ const metaDetectedSubgraphs = /* @__PURE__ */ new Set();
16144
+ for (const subName of desc.subgraphs) {
16145
+ const prefix = `${subName}::`;
16146
+ for (const [path, nodeDesc] of Object.entries(desc.nodes)) {
16147
+ if (!path.startsWith(prefix)) continue;
16148
+ if (path.includes(metaSegment)) continue;
16149
+ const meta = nodeDesc.meta;
16150
+ if (meta?._templateName && meta?._templateBind) {
16151
+ const templateName = meta._templateName;
16152
+ const bind = meta._templateBind;
16153
+ if (!templates[templateName]) {
16154
+ const tmplNodes = {};
16155
+ const tmplInnerNames = /* @__PURE__ */ new Set();
16156
+ const tmplPrefix = `${subName}::`;
16157
+ for (const [p, nd] of Object.entries(desc.nodes)) {
16158
+ if (!p.startsWith(tmplPrefix) || p.includes(metaSegment)) continue;
16159
+ const localName = p.slice(tmplPrefix.length);
16160
+ if (localName.includes("::")) continue;
16161
+ tmplInnerNames.add(localName);
16162
+ tmplNodes[localName] = {
16163
+ type: nd.type,
16164
+ ...nd.deps.length > 0 ? {
16165
+ deps: nd.deps.map(
16166
+ (d) => d.startsWith(tmplPrefix) ? d.slice(tmplPrefix.length) : d
16167
+ )
16168
+ } : {}
16169
+ };
16170
+ }
16171
+ const tmplParams = [];
16172
+ const tmplParamMap = /* @__PURE__ */ new Map();
16173
+ for (const n of Object.values(tmplNodes)) {
16174
+ for (const dep of n.deps ?? []) {
16175
+ if (!tmplInnerNames.has(dep) && !tmplParamMap.has(dep)) {
16176
+ const param = `$${dep}`;
16177
+ tmplParams.push(param);
16178
+ tmplParamMap.set(dep, param);
16179
+ }
16180
+ }
16181
+ }
16182
+ for (const n of Object.values(tmplNodes)) {
16183
+ if (n.deps) n.deps = n.deps.map((d) => tmplParamMap.get(d) ?? d);
16184
+ }
16185
+ const depended = /* @__PURE__ */ new Set();
16186
+ for (const n of Object.values(tmplNodes)) {
16187
+ for (const dep of n.deps ?? []) {
16188
+ if (tmplInnerNames.has(dep)) depended.add(dep);
16189
+ }
16190
+ }
16191
+ const outputCandidates = [...tmplInnerNames].filter((n) => !depended.has(n));
16192
+ const tmplOutput = outputCandidates[0] ?? [...tmplInnerNames].pop();
16193
+ templates[templateName] = { params: tmplParams, nodes: tmplNodes, output: tmplOutput };
16194
+ }
16195
+ delete nodes[subName];
16196
+ templateRefs[subName] = { type: "template", template: templateName, bind };
16197
+ metaDetectedSubgraphs.add(subName);
16198
+ break;
16199
+ }
15907
16200
  }
15908
- );
15909
- registerStep(
15910
- graph,
15911
- name,
15912
- step,
15913
- deps.flatMap((d) => d.path ? [d.path] : [])
15914
- );
15915
- return step;
15916
- }
15917
- function branch(graph, name, source, predicate, opts) {
15918
- const src = resolveDep(graph, source);
15919
- const step = node(
15920
- [src.node],
15921
- ([value]) => ({
15922
- branch: predicate(value) ? "then" : "else",
15923
- value
15924
- }),
15925
- {
15926
- ...opts,
15927
- name,
15928
- describeKind: "derived",
15929
- meta: baseMeta("branch", opts?.meta)
16201
+ }
16202
+ const structureMap = /* @__PURE__ */ new Map();
16203
+ for (const subName of desc.subgraphs) {
16204
+ if (metaDetectedSubgraphs.has(subName)) continue;
16205
+ const subNodes = {};
16206
+ const prefix = `${subName}::`;
16207
+ for (const [path, nodeDesc] of Object.entries(desc.nodes)) {
16208
+ if (path.includes(metaSegment)) continue;
16209
+ if (!path.startsWith(prefix)) continue;
16210
+ const localName = path.slice(prefix.length);
16211
+ if (localName.includes("::")) continue;
16212
+ subNodes[localName] = {
16213
+ type: nodeDesc.type,
16214
+ ...nodeDesc.deps.length > 0 ? {
16215
+ deps: nodeDesc.deps.map((d) => d.startsWith(prefix) ? d.slice(prefix.length) : d)
16216
+ } : {}
16217
+ };
15930
16218
  }
15931
- );
15932
- registerStep(graph, name, step, src.path ? [src.path] : []);
15933
- return step;
15934
- }
15935
- function gate2(graph, name, source, control, opts) {
15936
- const src = resolveDep(graph, source);
15937
- const ctrl = resolveDep(graph, control);
15938
- const step = node(
15939
- [src.node, ctrl.node],
15940
- (_deps, actions) => {
15941
- const opened = ctrl.node.get();
15942
- if (!opened) {
15943
- actions.down([[RESOLVED]]);
15944
- return void 0;
15945
- }
15946
- return src.node.get();
15947
- },
15948
- {
15949
- ...opts,
15950
- name,
15951
- describeKind: "operator",
15952
- meta: baseMeta("gate", opts?.meta)
16219
+ const fingerprint = JSON.stringify(
16220
+ Object.fromEntries(
16221
+ Object.entries(subNodes).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => [k, { type: v.type, deps: v.deps ?? [] }])
16222
+ )
16223
+ );
16224
+ if (!structureMap.has(fingerprint)) {
16225
+ structureMap.set(fingerprint, []);
16226
+ }
16227
+ structureMap.get(fingerprint).push({ name: subName, nodes: subNodes });
16228
+ }
16229
+ for (const [, group] of structureMap) {
16230
+ if (group.length < 2) continue;
16231
+ const templateName = `${group[0].name}_template`;
16232
+ const refNodes = group[0].nodes;
16233
+ const innerNames = new Set(Object.keys(refNodes));
16234
+ const params = [];
16235
+ const baseParamMap = /* @__PURE__ */ new Map();
16236
+ for (const n of Object.values(refNodes)) {
16237
+ for (const dep of n.deps ?? []) {
16238
+ if (!innerNames.has(dep) && !baseParamMap.has(dep)) {
16239
+ const param = `$${dep}`;
16240
+ params.push(param);
16241
+ baseParamMap.set(dep, param);
16242
+ }
16243
+ }
16244
+ }
16245
+ const depended = /* @__PURE__ */ new Set();
16246
+ for (const n of Object.values(refNodes)) {
16247
+ for (const dep of n.deps ?? []) {
16248
+ if (innerNames.has(dep)) depended.add(dep);
16249
+ }
16250
+ }
16251
+ const outputCandidates = [...innerNames].filter((n) => !depended.has(n));
16252
+ const output = outputCandidates[0] ?? [...innerNames].pop();
16253
+ const tmplNodes = {};
16254
+ for (const [nName, nSpec] of Object.entries(refNodes)) {
16255
+ tmplNodes[nName] = {
16256
+ ...nSpec,
16257
+ deps: nSpec.deps?.map((d) => baseParamMap.get(d) ?? d)
16258
+ };
15953
16259
  }
15954
- );
15955
- registerStep(
15956
- graph,
15957
- name,
15958
- step,
15959
- [src.path, ctrl.path].filter((v) => typeof v === "string")
15960
- );
15961
- return step;
15962
- }
15963
- function approval(graph, name, source, approver, opts) {
15964
- const src = resolveDep(graph, source);
15965
- const ctrl = resolveDep(graph, approver);
15966
- const isApproved = opts?.isApproved ?? ((value) => Boolean(value));
15967
- const step = node(
15968
- [src.node, ctrl.node],
15969
- (_deps, actions) => {
15970
- if (!isApproved(ctrl.node.get())) {
15971
- actions.down([[RESOLVED]]);
15972
- return void 0;
16260
+ templates[templateName] = { params, nodes: tmplNodes, output };
16261
+ for (const member of group) {
16262
+ delete nodes[member.name];
16263
+ const memberBind = {};
16264
+ const memberInnerNames = new Set(Object.keys(member.nodes));
16265
+ for (const n of Object.values(member.nodes)) {
16266
+ for (const dep of n.deps ?? []) {
16267
+ if (!memberInnerNames.has(dep)) {
16268
+ const param = baseParamMap.get(dep) ?? `$${dep}`;
16269
+ memberBind[param] = dep;
16270
+ }
16271
+ }
15973
16272
  }
15974
- return src.node.get();
15975
- },
15976
- {
15977
- ...opts,
15978
- name,
15979
- describeKind: "operator",
15980
- meta: baseMeta("approval", opts?.meta)
16273
+ templateRefs[member.name] = {
16274
+ type: "template",
16275
+ template: templateName,
16276
+ bind: memberBind
16277
+ };
15981
16278
  }
15982
- );
15983
- registerStep(
15984
- graph,
15985
- name,
15986
- step,
15987
- [src.path, ctrl.path].filter((v) => typeof v === "string")
15988
- );
15989
- return step;
16279
+ }
16280
+ const allNodes = {
16281
+ ...nodes,
16282
+ ...templateRefs
16283
+ };
16284
+ const result = { name: desc.name, nodes: allNodes };
16285
+ if (Object.keys(templates).length > 0) result.templates = templates;
16286
+ if (feedbackEdges.length > 0) result.feedback = feedbackEdges;
16287
+ return result;
15990
16288
  }
15991
- function forEach2(graph, name, source, run, opts) {
15992
- const src = resolveDep(graph, source);
15993
- let terminated = false;
15994
- const step = node([src.node], () => void 0, {
15995
- ...opts,
15996
- name,
15997
- describeKind: "effect",
15998
- completeWhenDepsComplete: false,
15999
- meta: baseMeta("forEach", opts?.meta),
16000
- onMessage(msg, depIndex, actions) {
16001
- if (terminated) return true;
16002
- if (depIndex !== 0) {
16003
- actions.down([msg]);
16004
- if (msg[0] === COMPLETE || msg[0] === ERROR) terminated = true;
16005
- return true;
16289
+ function specDiff(specA, specB) {
16290
+ const entries = [];
16291
+ if (specA.name !== specB.name) {
16292
+ entries.push({
16293
+ type: "changed",
16294
+ path: "name",
16295
+ detail: `"${specA.name}" \u2192 "${specB.name}"`
16296
+ });
16297
+ }
16298
+ const nodesA = new Set(Object.keys(specA.nodes));
16299
+ const nodesB = new Set(Object.keys(specB.nodes));
16300
+ for (const name of nodesB) {
16301
+ if (!nodesA.has(name)) {
16302
+ const n = specB.nodes[name];
16303
+ entries.push({
16304
+ type: "added",
16305
+ path: `nodes.${name}`,
16306
+ detail: `type: ${n.type}`
16307
+ });
16308
+ }
16309
+ }
16310
+ for (const name of nodesA) {
16311
+ if (!nodesB.has(name)) {
16312
+ entries.push({ type: "removed", path: `nodes.${name}` });
16313
+ }
16314
+ }
16315
+ for (const name of nodesA) {
16316
+ if (!nodesB.has(name)) continue;
16317
+ const a = specA.nodes[name];
16318
+ const b = specB.nodes[name];
16319
+ if (JSON.stringify(a) !== JSON.stringify(b)) {
16320
+ const details = [];
16321
+ if (a.type !== b.type) details.push(`type: ${a.type} \u2192 ${b.type}`);
16322
+ if (JSON.stringify(a.deps) !== JSON.stringify(b.deps)) {
16323
+ details.push("deps changed");
16006
16324
  }
16007
- if (msg[0] === DATA) {
16008
- try {
16009
- run(msg[1], actions);
16010
- actions.down([msg]);
16011
- } catch (err) {
16012
- terminated = true;
16013
- actions.down([[ERROR, err]]);
16014
- }
16015
- return true;
16325
+ if (a.fn !== b.fn) {
16326
+ details.push(`fn: ${a.fn} \u2192 ${b.fn}`);
16016
16327
  }
16017
- actions.down([msg]);
16018
- if (msg[0] === COMPLETE || msg[0] === ERROR) terminated = true;
16019
- return true;
16328
+ if (JSON.stringify(a.config) !== JSON.stringify(b.config)) {
16329
+ details.push("config changed");
16330
+ }
16331
+ entries.push({
16332
+ type: "changed",
16333
+ path: `nodes.${name}`,
16334
+ detail: details.join("; ") || "modified"
16335
+ });
16020
16336
  }
16021
- });
16022
- registerStep(graph, name, step, src.path ? [src.path] : []);
16023
- return step;
16024
- }
16025
- function join2(graph, name, deps, opts) {
16026
- const resolved = deps.map((dep) => resolveDep(graph, dep));
16027
- const step = node(
16028
- resolved.map((d) => d.node),
16029
- (values) => values,
16030
- {
16031
- ...opts,
16032
- name,
16033
- describeKind: "derived",
16034
- meta: baseMeta("join", opts?.meta)
16337
+ }
16338
+ const tmplA = specA.templates ?? {};
16339
+ const tmplB = specB.templates ?? {};
16340
+ const tmplNamesA = new Set(Object.keys(tmplA));
16341
+ const tmplNamesB = new Set(Object.keys(tmplB));
16342
+ for (const name of tmplNamesB) {
16343
+ if (!tmplNamesA.has(name)) {
16344
+ entries.push({ type: "added", path: `templates.${name}` });
16035
16345
  }
16036
- );
16037
- registerStep(
16038
- graph,
16039
- name,
16040
- step,
16041
- resolved.flatMap((d) => d.path ? [d.path] : [])
16042
- );
16043
- return step;
16346
+ }
16347
+ for (const name of tmplNamesA) {
16348
+ if (!tmplNamesB.has(name)) {
16349
+ entries.push({ type: "removed", path: `templates.${name}` });
16350
+ }
16351
+ }
16352
+ for (const name of tmplNamesA) {
16353
+ if (!tmplNamesB.has(name)) continue;
16354
+ if (JSON.stringify(tmplA[name]) !== JSON.stringify(tmplB[name])) {
16355
+ entries.push({
16356
+ type: "changed",
16357
+ path: `templates.${name}`,
16358
+ detail: "template definition changed"
16359
+ });
16360
+ }
16361
+ }
16362
+ const fbA = specA.feedback ?? [];
16363
+ const fbB = specB.feedback ?? [];
16364
+ const fbKeyA = new Set(fbA.map((e) => `${e.from}->${e.to}`));
16365
+ const fbKeyB = new Set(fbB.map((e) => `${e.from}->${e.to}`));
16366
+ for (const fb of fbB) {
16367
+ const key = `${fb.from}->${fb.to}`;
16368
+ if (!fbKeyA.has(key)) {
16369
+ entries.push({
16370
+ type: "added",
16371
+ path: `feedback.${key}`,
16372
+ detail: `maxIterations: ${fb.maxIterations ?? 10}`
16373
+ });
16374
+ }
16375
+ }
16376
+ for (const fb of fbA) {
16377
+ const key = `${fb.from}->${fb.to}`;
16378
+ if (!fbKeyB.has(key)) {
16379
+ entries.push({ type: "removed", path: `feedback.${key}` });
16380
+ }
16381
+ }
16382
+ for (const fb of fbA) {
16383
+ const key = `${fb.from}->${fb.to}`;
16384
+ const counterpart = fbB.find((b) => b.from === fb.from && b.to === fb.to);
16385
+ if (counterpart && JSON.stringify(fb) !== JSON.stringify(counterpart)) {
16386
+ entries.push({
16387
+ type: "changed",
16388
+ path: `feedback.${key}`,
16389
+ detail: `maxIterations: ${fb.maxIterations ?? 10} \u2192 ${counterpart.maxIterations ?? 10}`
16390
+ });
16391
+ }
16392
+ }
16393
+ const added = entries.filter((e) => e.type === "added").length;
16394
+ const removed = entries.filter((e) => e.type === "removed").length;
16395
+ const changed = entries.filter((e) => e.type === "changed").length;
16396
+ const parts = [];
16397
+ if (added) parts.push(`${added} added`);
16398
+ if (removed) parts.push(`${removed} removed`);
16399
+ if (changed) parts.push(`${changed} changed`);
16400
+ const summary = parts.length > 0 ? parts.join(", ") : "no changes";
16401
+ return { entries, summary };
16044
16402
  }
16045
- function loop(graph, name, source, iterate, opts) {
16046
- const src = resolveDep(graph, source);
16047
- const iterRef = opts?.iterations;
16048
- const iterDep = typeof iterRef === "number" || iterRef === void 0 ? void 0 : resolveDep(graph, iterRef);
16049
- const staticIterations = typeof iterRef === "number" ? iterRef : void 0;
16050
- const step = node(
16051
- iterDep ? [src.node, iterDep.node] : [src.node],
16052
- (_deps, actions) => {
16053
- let current = src.node.get();
16054
- const rawCount = staticIterations ?? iterDep?.node.get() ?? 1;
16055
- const count = coerceLoopIterations(rawCount);
16056
- for (let i = 0; i < count; i += 1) {
16057
- current = iterate(current, i, actions);
16058
- }
16059
- return current;
16403
+ var LLM_COMPOSE_SYSTEM_PROMPT = `You are a graph architect for GraphReFly, a reactive graph protocol.
16404
+
16405
+ Given a natural-language description, produce a JSON GraphSpec with this structure:
16406
+
16407
+ {
16408
+ "name": "<graph_name>",
16409
+ "nodes": {
16410
+ "<node_name>": {
16411
+ "type": "state" | "derived" | "producer" | "effect" | "operator",
16412
+ "deps": ["<dep_node_name>", ...],
16413
+ "fn": "<catalog_function_name>",
16414
+ "source": "<catalog_source_name>",
16415
+ "config": { ... },
16416
+ "initial": <value>,
16417
+ "meta": { "description": "<purpose>" }
16060
16418
  },
16061
- {
16062
- ...opts,
16063
- name,
16064
- describeKind: "derived",
16065
- meta: baseMeta("loop", opts?.meta)
16419
+ "<template_instance>": {
16420
+ "type": "template",
16421
+ "template": "<template_name>",
16422
+ "bind": { "$param": "node_name" }
16066
16423
  }
16067
- );
16068
- registerStep(
16069
- graph,
16070
- name,
16071
- step,
16072
- [src.path, iterDep?.path].filter((v) => typeof v === "string")
16073
- );
16074
- return step;
16424
+ },
16425
+ "templates": {
16426
+ "<template_name>": {
16427
+ "params": ["$param1", "$param2"],
16428
+ "nodes": { ... },
16429
+ "output": "<output_node>"
16430
+ }
16431
+ },
16432
+ "feedback": [
16433
+ { "from": "<condition_node>", "to": "<state_node>", "maxIterations": 10 }
16434
+ ]
16075
16435
  }
16076
- function subPipeline(graph, name, childOrBuild, opts) {
16077
- const child = childOrBuild instanceof Graph ? childOrBuild : pipeline(name, opts);
16078
- if (typeof childOrBuild === "function") {
16079
- childOrBuild(child);
16080
- }
16081
- graph.mount(name, child);
16082
- return child;
16436
+
16437
+ Rules:
16438
+ - "state" nodes hold user/LLM-writable values (knobs). Use "initial" for default values.
16439
+ - "derived" nodes compute from deps using a named "fn".
16440
+ - "effect" nodes produce side effects from deps.
16441
+ - "producer" nodes generate values from a named "source".
16442
+ - Use "templates" when the same subgraph pattern repeats (e.g., per-source resilience).
16443
+ - Use "feedback" for bounded cycles where a derived value writes back to a state node.
16444
+ - meta.description is required for every node.
16445
+ - Return ONLY valid JSON, no markdown fences or commentary.`;
16446
+ function stripFences2(text) {
16447
+ const match = text.match(/^```(?:json)?\s*([\s\S]*?)\s*```[\s\S]*$/);
16448
+ return match ? match[1] : text;
16083
16449
  }
16084
- function sensor(graph, name, initial, opts) {
16085
- const source = node([], () => void 0, {
16086
- ...opts,
16087
- name,
16088
- initial,
16089
- describeKind: "producer",
16090
- meta: baseMeta("sensor", opts?.meta)
16450
+ async function llmCompose(problem, adapter, opts) {
16451
+ let systemPrompt = LLM_COMPOSE_SYSTEM_PROMPT;
16452
+ if (opts?.catalogDescription) {
16453
+ systemPrompt += `
16454
+
16455
+ Available catalog:
16456
+ ${opts.catalogDescription}`;
16457
+ }
16458
+ if (opts?.systemPromptExtra) {
16459
+ systemPrompt += `
16460
+
16461
+ ${opts.systemPromptExtra}`;
16462
+ }
16463
+ const messages = [
16464
+ { role: "system", content: systemPrompt },
16465
+ { role: "user", content: problem }
16466
+ ];
16467
+ const rawResult = adapter.invoke(messages, {
16468
+ model: opts?.model,
16469
+ temperature: opts?.temperature ?? 0,
16470
+ maxTokens: opts?.maxTokens
16091
16471
  });
16092
- registerStep(graph, name, source, []);
16093
- return {
16094
- node: source,
16095
- push(value) {
16096
- source.down([[DATA, value]]);
16097
- },
16098
- error(err) {
16099
- source.down([[ERROR, err]]);
16100
- },
16101
- complete() {
16102
- source.down([[COMPLETE]]);
16103
- }
16104
- };
16472
+ const response = await rawResult;
16473
+ let content = response.content.trim();
16474
+ if (content.startsWith("```")) {
16475
+ content = stripFences2(content);
16476
+ }
16477
+ let parsed;
16478
+ try {
16479
+ parsed = JSON.parse(content);
16480
+ } catch {
16481
+ throw new Error(`llmCompose: LLM response is not valid JSON: ${content.slice(0, 200)}`);
16482
+ }
16483
+ const validation = validateSpec(parsed);
16484
+ if (!validation.valid) {
16485
+ throw new Error(`llmCompose: invalid GraphSpec:
16486
+ ${validation.errors.join("\n")}`);
16487
+ }
16488
+ return parsed;
16105
16489
  }
16106
- function wait(graph, name, source, ms, opts) {
16107
- const src = resolveDep(graph, source);
16108
- const timers = /* @__PURE__ */ new Set();
16109
- let terminated = false;
16110
- let completed = false;
16111
- const step = node(
16112
- [src.node],
16113
- () => {
16114
- for (const id of timers) clearTimeout(id);
16115
- timers.clear();
16116
- return () => {
16117
- for (const id of timers) clearTimeout(id);
16118
- timers.clear();
16119
- terminated = true;
16120
- };
16121
- },
16490
+ async function llmRefine(currentSpec, feedback2, adapter, opts) {
16491
+ let systemPrompt = LLM_COMPOSE_SYSTEM_PROMPT;
16492
+ if (opts?.catalogDescription) {
16493
+ systemPrompt += `
16494
+
16495
+ Available catalog:
16496
+ ${opts.catalogDescription}`;
16497
+ }
16498
+ if (opts?.systemPromptExtra) {
16499
+ systemPrompt += `
16500
+
16501
+ ${opts.systemPromptExtra}`;
16502
+ }
16503
+ const messages = [
16504
+ { role: "system", content: systemPrompt },
16122
16505
  {
16123
- ...opts,
16124
- name,
16125
- initial: src.node.get(),
16126
- describeKind: "operator",
16127
- completeWhenDepsComplete: false,
16128
- meta: baseMeta("wait", opts?.meta),
16129
- onMessage(msg, depIndex, actions) {
16130
- if (terminated) return true;
16131
- if (depIndex !== 0) {
16132
- actions.down([msg]);
16133
- if (msg[0] === COMPLETE || msg[0] === ERROR) terminated = true;
16134
- return true;
16135
- }
16136
- if (msg[0] === DATA) {
16137
- const id = setTimeout(() => {
16138
- timers.delete(id);
16139
- actions.down([msg]);
16140
- if (completed && timers.size === 0) {
16141
- actions.down([[COMPLETE]]);
16142
- }
16143
- }, ms);
16144
- timers.add(id);
16145
- return true;
16146
- }
16147
- if (msg[0] === COMPLETE) {
16148
- terminated = true;
16149
- completed = true;
16150
- if (timers.size === 0) {
16151
- actions.down([[COMPLETE]]);
16152
- }
16153
- return true;
16154
- }
16155
- if (msg[0] === ERROR) {
16156
- terminated = true;
16157
- for (const id of timers) clearTimeout(id);
16158
- timers.clear();
16159
- actions.down([msg]);
16160
- return true;
16161
- }
16162
- actions.down([msg]);
16163
- return true;
16164
- }
16165
- }
16166
- );
16167
- registerStep(graph, name, step, src.path ? [src.path] : []);
16168
- return step;
16169
- }
16170
- function onFailure(graph, name, source, recover, opts) {
16171
- const src = resolveDep(graph, source);
16172
- let terminated = false;
16173
- const step = node([src.node], () => void 0, {
16174
- ...opts,
16175
- name,
16176
- describeKind: "operator",
16177
- completeWhenDepsComplete: false,
16178
- meta: baseMeta("onFailure", opts?.meta),
16179
- onMessage(msg, _depIndex, actions) {
16180
- if (terminated) return true;
16181
- if (msg[0] === ERROR) {
16182
- try {
16183
- actions.emit(recover(msg[1], actions));
16184
- } catch (err) {
16185
- terminated = true;
16186
- actions.down([[ERROR, err]]);
16187
- }
16188
- return true;
16189
- }
16190
- actions.down([msg]);
16191
- if (msg[0] === COMPLETE) terminated = true;
16192
- return true;
16506
+ role: "user",
16507
+ content: `Current GraphSpec:
16508
+ ${JSON.stringify(currentSpec, null, 2)}
16509
+
16510
+ Modification request: ${feedback2}
16511
+
16512
+ Return the complete modified GraphSpec as JSON.`
16193
16513
  }
16514
+ ];
16515
+ const rawResult = adapter.invoke(messages, {
16516
+ model: opts?.model,
16517
+ temperature: opts?.temperature ?? 0,
16518
+ maxTokens: opts?.maxTokens
16194
16519
  });
16195
- registerStep(graph, name, step, src.path ? [src.path] : []);
16196
- return step;
16520
+ const response = await rawResult;
16521
+ let content = response.content.trim();
16522
+ if (content.startsWith("```")) {
16523
+ content = stripFences2(content);
16524
+ }
16525
+ let parsed;
16526
+ try {
16527
+ parsed = JSON.parse(content);
16528
+ } catch {
16529
+ throw new Error(`llmRefine: LLM response is not valid JSON: ${content.slice(0, 200)}`);
16530
+ }
16531
+ const validation = validateSpec(parsed);
16532
+ if (!validation.valid) {
16533
+ throw new Error(`llmRefine: invalid GraphSpec:
16534
+ ${validation.errors.join("\n")}`);
16535
+ }
16536
+ return parsed;
16197
16537
  }
16198
16538
 
16199
- // src/patterns/reactive-layout/index.ts
16200
- var reactive_layout_exports = {};
16201
- __export(reactive_layout_exports, {
16202
- CanvasMeasureAdapter: () => CanvasMeasureAdapter,
16203
- CliMeasureAdapter: () => CliMeasureAdapter,
16204
- ImageSizeAdapter: () => ImageSizeAdapter,
16205
- NodeCanvasMeasureAdapter: () => NodeCanvasMeasureAdapter,
16206
- PrecomputedAdapter: () => PrecomputedAdapter,
16207
- SvgBoundsAdapter: () => SvgBoundsAdapter,
16208
- analyzeAndMeasure: () => analyzeAndMeasure,
16209
- computeBlockFlow: () => computeBlockFlow,
16210
- computeCharPositions: () => computeCharPositions,
16211
- computeLineBreaks: () => computeLineBreaks,
16212
- computeTotalHeight: () => computeTotalHeight,
16213
- measureBlock: () => measureBlock,
16214
- measureBlocks: () => measureBlocks,
16215
- reactiveBlockLayout: () => reactiveBlockLayout,
16216
- reactiveLayout: () => reactiveLayout
16539
+ // src/patterns/messaging.ts
16540
+ var messaging_exports = {};
16541
+ __export(messaging_exports, {
16542
+ JobFlowGraph: () => JobFlowGraph,
16543
+ JobQueueGraph: () => JobQueueGraph,
16544
+ SubscriptionGraph: () => SubscriptionGraph,
16545
+ TopicBridgeGraph: () => TopicBridgeGraph,
16546
+ TopicGraph: () => TopicGraph,
16547
+ jobFlow: () => jobFlow,
16548
+ jobQueue: () => jobQueue,
16549
+ subscription: () => subscription,
16550
+ topic: () => topic,
16551
+ topicBridge: () => topicBridge
16217
16552
  });
16218
-
16219
- // src/patterns/reactive-layout/measurement-adapters.ts
16220
- function cellWidth(code) {
16221
- if (code >= 768 && code <= 879 || // Combining Diacritical Marks
16222
- code >= 1155 && code <= 1161 || // Cyrillic combining marks
16223
- code >= 1425 && code <= 1469 || // Hebrew combining marks
16224
- code >= 1552 && code <= 1562 || // Arabic combining marks
16225
- code >= 1611 && code <= 1631 || // Arabic combining marks
16226
- code >= 1648 && code === 1648 || // Arabic superscript alef
16227
- code >= 1750 && code <= 1756 || // Arabic combining marks
16228
- code >= 1759 && code <= 1764 || // Arabic combining marks
16229
- code >= 1767 && code <= 1768 || // Arabic combining marks
16230
- code >= 1770 && code <= 1773 || // Arabic combining marks
16231
- code >= 1840 && code <= 1866 || // Syriac combining marks
16232
- code >= 1958 && code <= 1968 || // Thaana combining marks
16233
- code >= 2304 && code <= 2307 || // Devanagari combining marks
16234
- code >= 2362 && code <= 2383 || // Devanagari combining marks
16235
- code >= 2385 && code <= 2391 || // Devanagari combining marks
16236
- code >= 2402 && code <= 2403 || // Devanagari combining marks
16237
- code >= 2433 && code <= 2435 || // Bengali combining marks
16238
- code >= 2492 && code <= 2509 || // Bengali combining marks
16239
- code >= 2561 && code <= 2563 || // Gurmukhi combining marks
16240
- code >= 2620 && code <= 2641 || // Gurmukhi combining marks
16241
- code >= 2672 && code <= 2673 || // Gurmukhi combining marks
16242
- code >= 2677 && code === 2677 || // Gurmukhi combining mark
16243
- code >= 3633 && code === 3633 || // Thai combining mark
16244
- code >= 3636 && code <= 3642 || // Thai combining marks
16245
- code >= 3655 && code <= 3662 || // Thai combining marks
16246
- code >= 3761 && code === 3761 || // Lao combining mark
16247
- code >= 3764 && code <= 3772 || // Lao combining marks
16248
- code >= 3784 && code <= 3790 || // Lao combining marks
16249
- code >= 7616 && code <= 7679 || // Combining Diacritical Marks Supplement
16250
- code >= 8400 && code <= 8447 || // Combining Diacritical Marks for Symbols
16251
- code >= 65024 && code <= 65039 || // Variation Selectors
16252
- code >= 65056 && code <= 65071 || // Combining Half Marks
16253
- code === 8205) {
16254
- return 0;
16553
+ var DEFAULT_MAX_PER_PUMP = 2147483647;
16554
+ function requireNonNegativeInt(value, label) {
16555
+ if (!Number.isFinite(value) || !Number.isInteger(value) || value < 0) {
16556
+ throw new Error(`${label} must be a non-negative integer`);
16255
16557
  }
16256
- if (code >= 4352 && code <= 4447 || // Hangul Jamo
16257
- code >= 8986 && code <= 8987 || // Watch, Hourglass
16258
- code >= 9001 && code <= 9002 || // Angle brackets
16259
- code >= 9193 && code <= 9203 || // Media control symbols
16260
- code >= 9208 && code <= 9210 || // Media control symbols
16261
- code >= 9725 && code <= 9726 || // Medium squares
16262
- code >= 9748 && code <= 9749 || // Umbrella, Hot Beverage
16263
- code >= 9800 && code <= 9811 || // Zodiac symbols
16264
- code === 9855 || // Wheelchair
16265
- code === 9875 || // Anchor
16266
- code === 9889 || // High Voltage
16267
- code >= 9898 && code <= 9899 || // Medium circles
16268
- code >= 9917 && code <= 9918 || // Soccer, Baseball
16269
- code >= 9924 && code <= 9925 || // Snowman, Sun behind cloud
16270
- code === 9934 || // Ophiuchus
16271
- code === 9940 || // No Entry
16272
- code === 9962 || // Church
16273
- code >= 9970 && code <= 9971 || // Fountain, Golf
16274
- code === 9973 || // Sailboat
16275
- code === 9978 || // Tent
16276
- code === 9981 || // Fuel Pump
16277
- code === 9986 || // Scissors
16278
- code === 9989 || // Check Mark
16279
- code >= 9992 && code <= 9997 || // Airplane...Writing Hand
16280
- code === 9999 || // Pencil
16281
- code >= 10067 && code <= 10069 || // Question marks
16282
- code === 10071 || // Exclamation
16283
- code >= 10133 && code <= 10135 || // Plus, Minus, Divide
16284
- code === 10160 || // Curly Loop
16285
- code === 10175 || // Double Curly Loop
16286
- code >= 10548 && code <= 10549 || // Arrows
16287
- code >= 11013 && code <= 11015 || // Arrows
16288
- code >= 11035 && code <= 11036 || // Squares
16289
- code === 11088 || // Star
16290
- code === 11093 || // Circle
16291
- code >= 11904 && code <= 12350 || // CJK Radicals, Symbols, Punctuation
16292
- code >= 12352 && code <= 12447 || // Hiragana
16293
- code >= 12448 && code <= 12543 || // Katakana
16294
- code >= 12549 && code <= 12591 || // Bopomofo
16295
- code >= 12593 && code <= 12686 || // Hangul Compatibility Jamo
16296
- code >= 12688 && code <= 12771 || // Kanbun, CJK Strokes
16297
- code >= 12784 && code <= 12830 || // Katakana Phonetic Extensions
16298
- code >= 12832 && code <= 12871 || // Enclosed CJK
16299
- code >= 12880 && code <= 19903 || // CJK Extensions + Unified block
16300
- code >= 19968 && code <= 40959 || // CJK Unified Ideographs
16301
- code >= 43360 && code <= 43388 || // Hangul Jamo Extended-A
16302
- code >= 44032 && code <= 55203 || // Hangul Syllables
16303
- code >= 63744 && code <= 64255 || // CJK Compatibility Ideographs
16304
- code >= 65040 && code <= 65049 || // Vertical forms
16305
- code >= 65072 && code <= 65131 || // CJK Compatibility Forms
16306
- code >= 65281 && code <= 65376 || // Fullwidth Forms (excl. halfwidth)
16307
- code >= 65504 && code <= 65510 || // Fullwidth Signs
16308
- code >= 126980 && code === 126980 || // Mahjong Red Dragon
16309
- code === 127183 || // Joker
16310
- code >= 127344 && code <= 127345 || // A/B buttons
16311
- code === 127358 || // O button
16312
- code === 127359 || // P button
16313
- code === 127374 || // AB button
16314
- code >= 127377 && code <= 127386 || // Squared symbols
16315
- code >= 127456 && code <= 127487 || // Regional Indicator Symbols
16316
- code >= 127488 && code <= 127490 || // Enclosed ideographic
16317
- code === 127514 || // Squared CJK
16318
- code === 127535 || // Squared CJK
16319
- code >= 127538 && code <= 127546 || // Squared CJK
16320
- code >= 127568 && code <= 127569 || // Circled ideographic
16321
- code >= 127744 && code <= 129535 || // Misc Symbols / Emoticons / Emoji
16322
- code >= 129536 && code <= 129791 || // Chess, Symbols Extended-A
16323
- code >= 129792 && code <= 130047 || // Symbols for Legacy Computing
16324
- code >= 131072 && code <= 196605 || // CJK Extension B-F (excl. nonchars)
16325
- code >= 196608 && code <= 262141) {
16326
- return 2;
16558
+ return value;
16559
+ }
16560
+ function keepalive4(n) {
16561
+ return n.subscribe(() => {
16562
+ });
16563
+ }
16564
+ function messagingMeta(kind, extra) {
16565
+ return {
16566
+ messaging: true,
16567
+ messaging_type: kind,
16568
+ ...extra ?? {}
16569
+ };
16570
+ }
16571
+ var TopicGraph = class extends Graph {
16572
+ _log;
16573
+ _keepaliveDisposers = [];
16574
+ events;
16575
+ latest;
16576
+ constructor(name, opts = {}) {
16577
+ super(name, opts.graph);
16578
+ this._log = reactiveLog([], { name: "events", maxSize: opts.retainedLimit });
16579
+ this.events = this._log.entries;
16580
+ this.add("events", this.events);
16581
+ this.latest = derived(
16582
+ [this.events],
16583
+ ([snapshot]) => {
16584
+ const entries = snapshot.value.entries;
16585
+ return entries.length === 0 ? void 0 : entries[entries.length - 1];
16586
+ },
16587
+ {
16588
+ name: "latest",
16589
+ describeKind: "derived",
16590
+ meta: messagingMeta("topic_latest"),
16591
+ initial: void 0
16592
+ }
16593
+ );
16594
+ this.add("latest", this.latest);
16595
+ this.connect("events", "latest");
16596
+ this._keepaliveDisposers.push(keepalive4(this.latest));
16597
+ }
16598
+ destroy() {
16599
+ for (const dispose of this._keepaliveDisposers) dispose();
16600
+ this._keepaliveDisposers.length = 0;
16601
+ super.destroy();
16602
+ }
16603
+ publish(value) {
16604
+ this._log.append(value);
16605
+ }
16606
+ retained() {
16607
+ const snapshot = this.events.get();
16608
+ return snapshot.value.entries;
16609
+ }
16610
+ };
16611
+ var SubscriptionGraph = class extends Graph {
16612
+ _keepaliveDisposers = [];
16613
+ source;
16614
+ cursor;
16615
+ available;
16616
+ constructor(name, topicGraph, opts = {}) {
16617
+ super(name, opts.graph);
16618
+ const initialCursor = requireNonNegativeInt(opts.cursor ?? 0, "subscription cursor");
16619
+ this.mount("topic", topicGraph);
16620
+ const topicEvents = topicGraph.events;
16621
+ this.source = derived([topicEvents], ([snapshot]) => snapshot, {
16622
+ name: "source",
16623
+ describeKind: "derived",
16624
+ meta: messagingMeta("subscription_source"),
16625
+ initial: topicEvents.get()
16626
+ });
16627
+ this.add("source", this.source);
16628
+ this.cursor = state(initialCursor, {
16629
+ name: "cursor",
16630
+ describeKind: "state",
16631
+ meta: messagingMeta("subscription_cursor")
16632
+ });
16633
+ this.add("cursor", this.cursor);
16634
+ this.available = derived(
16635
+ [this.source, this.cursor],
16636
+ ([sourceSnapshot, cursor]) => {
16637
+ const entries = sourceSnapshot.value.entries;
16638
+ const start = Math.max(0, Math.trunc(cursor ?? 0));
16639
+ return entries.slice(start);
16640
+ },
16641
+ {
16642
+ name: "available",
16643
+ describeKind: "derived",
16644
+ meta: messagingMeta("subscription_available"),
16645
+ initial: []
16646
+ }
16647
+ );
16648
+ this.add("available", this.available);
16649
+ this.connect("topic::events", "source");
16650
+ this.connect("source", "available");
16651
+ this.connect("cursor", "available");
16652
+ this._keepaliveDisposers.push(keepalive4(this.source));
16653
+ this._keepaliveDisposers.push(keepalive4(this.available));
16654
+ }
16655
+ destroy() {
16656
+ for (const dispose of this._keepaliveDisposers) dispose();
16657
+ this._keepaliveDisposers.length = 0;
16658
+ super.destroy();
16659
+ }
16660
+ ack(count) {
16661
+ const available = this.available.get();
16662
+ const requested = count === void 0 ? available.length : requireNonNegativeInt(count, "subscription ack count");
16663
+ const step = Math.min(requested, available.length);
16664
+ if (step <= 0) return this.cursor.get();
16665
+ const next = this.cursor.get() + step;
16666
+ this.cursor.down([[DATA, next]]);
16667
+ return next;
16668
+ }
16669
+ pull(limit, opts = {}) {
16670
+ const available = this.available.get();
16671
+ const max = limit === void 0 ? available.length : requireNonNegativeInt(limit, "subscription pull limit");
16672
+ const out = available.slice(0, max);
16673
+ if (opts.ack && out.length > 0) this.ack(out.length);
16674
+ return out;
16675
+ }
16676
+ };
16677
+ var JobQueueGraph = class extends Graph {
16678
+ _pending;
16679
+ _jobs;
16680
+ _keepaliveDisposers = [];
16681
+ _seq = 0;
16682
+ pending;
16683
+ jobs;
16684
+ depth;
16685
+ constructor(name, opts = {}) {
16686
+ super(name, opts.graph);
16687
+ this._pending = reactiveList([], { name: "pending" });
16688
+ this._jobs = reactiveMap({ name: "jobs" });
16689
+ this.pending = this._pending.items;
16690
+ this.jobs = this._jobs.node;
16691
+ this.add("pending", this.pending);
16692
+ this.add("jobs", this.jobs);
16693
+ this.depth = derived(
16694
+ [this.pending],
16695
+ ([snapshot]) => snapshot.value.items.length,
16696
+ {
16697
+ name: "depth",
16698
+ describeKind: "derived",
16699
+ meta: messagingMeta("queue_depth"),
16700
+ initial: 0
16701
+ }
16702
+ );
16703
+ this.add("depth", this.depth);
16704
+ this.connect("pending", "depth");
16705
+ this._keepaliveDisposers.push(keepalive4(this.depth));
16706
+ }
16707
+ destroy() {
16708
+ for (const dispose of this._keepaliveDisposers) dispose();
16709
+ this._keepaliveDisposers.length = 0;
16710
+ super.destroy();
16711
+ }
16712
+ enqueue(payload, opts = {}) {
16713
+ const id = opts.id ?? `${this.name}-${++this._seq}`;
16714
+ if (this._jobs.get(id) !== void 0) {
16715
+ throw new Error(`jobQueue("${this.name}"): duplicate job id "${id}"`);
16716
+ }
16717
+ const job = {
16718
+ id,
16719
+ payload,
16720
+ attempts: 0,
16721
+ metadata: Object.freeze({ ...opts.metadata ?? {} }),
16722
+ state: "queued"
16723
+ };
16724
+ this._jobs.set(id, job);
16725
+ this._pending.append(id);
16726
+ return id;
16327
16727
  }
16328
- return 1;
16329
- }
16330
- function countCells(text) {
16331
- let cells = 0;
16332
- for (const ch of text) {
16333
- cells += cellWidth(ch.codePointAt(0));
16728
+ claim(limit = 1) {
16729
+ const max = requireNonNegativeInt(limit, "job queue claim limit");
16730
+ if (max === 0) return [];
16731
+ const out = [];
16732
+ while (out.length < max) {
16733
+ const snapshot = this.pending.get();
16734
+ const ids = snapshot.value.items;
16735
+ if (ids.length === 0) break;
16736
+ const id = this._pending.pop(0);
16737
+ const job = this._jobs.get(id);
16738
+ if (!job || job.state !== "queued") continue;
16739
+ const inflight = {
16740
+ ...job,
16741
+ state: "inflight",
16742
+ attempts: job.attempts + 1
16743
+ };
16744
+ this._jobs.set(id, inflight);
16745
+ out.push(inflight);
16746
+ }
16747
+ return out;
16334
16748
  }
16335
- return cells;
16336
- }
16337
- var CliMeasureAdapter = class {
16338
- cellPx;
16339
- constructor(opts) {
16340
- this.cellPx = opts?.cellPx ?? 8;
16749
+ ack(id) {
16750
+ const job = this._jobs.get(id);
16751
+ if (!job || job.state !== "inflight") return false;
16752
+ this._jobs.delete(id);
16753
+ return true;
16341
16754
  }
16342
- measureSegment(text, _font) {
16343
- return { width: countCells(text) * this.cellPx };
16755
+ nack(id, opts = {}) {
16756
+ const job = this._jobs.get(id);
16757
+ if (!job || job.state !== "inflight") return false;
16758
+ if (opts.requeue ?? true) {
16759
+ this._jobs.set(id, { ...job, state: "queued" });
16760
+ this._pending.append(id);
16761
+ return true;
16762
+ }
16763
+ this._jobs.delete(id);
16764
+ return true;
16344
16765
  }
16345
16766
  };
16346
- var PrecomputedAdapterKeyError = class extends Error {
16347
- name = "KeyError";
16348
- };
16349
- var PrecomputedAdapter = class {
16350
- metrics;
16351
- fallback;
16352
- constructor(opts) {
16353
- this.metrics = opts.metrics;
16354
- const fb = opts.fallback ?? "per-char";
16355
- if (fb !== "per-char" && fb !== "error") {
16356
- throw new Error(
16357
- `fallback must be 'per-char' or 'error', got ${JSON.stringify(opts.fallback)}`
16358
- );
16767
+ var JobFlowGraph = class extends Graph {
16768
+ _stageNames;
16769
+ _queues = /* @__PURE__ */ new Map();
16770
+ _keepaliveDisposers = [];
16771
+ _completed;
16772
+ completed;
16773
+ completedCount;
16774
+ constructor(name, opts = {}) {
16775
+ super(name, opts.graph);
16776
+ const stages = (opts.stages ?? ["incoming", "processing", "done"]).map((v) => v.trim());
16777
+ if (stages.length < 2) {
16778
+ throw new Error(`jobFlow("${name}"): requires at least 2 stages`);
16359
16779
  }
16360
- this.fallback = fb;
16361
- }
16362
- measureSegment(text, font) {
16363
- const fontMap = this.metrics[font];
16364
- if (fontMap) {
16365
- const w = fontMap[text];
16366
- if (w !== void 0) return { width: w };
16780
+ const unique = new Set(stages);
16781
+ if (unique.size !== stages.length) {
16782
+ throw new Error(`jobFlow("${name}"): stage names must be unique`);
16367
16783
  }
16368
- if (this.fallback === "error") {
16369
- throw new PrecomputedAdapterKeyError(
16370
- `PrecomputedAdapter: no metrics for segment ${JSON.stringify(text)} in font ${JSON.stringify(font)}`
16371
- );
16784
+ this._stageNames = Object.freeze([...stages]);
16785
+ for (const stage of this._stageNames) {
16786
+ const q = jobQueue(`${name}-${stage}`);
16787
+ this._queues.set(stage, q);
16788
+ this.mount(stage, q);
16372
16789
  }
16373
- let total = 0;
16374
- if (fontMap) {
16375
- for (const ch of text) {
16376
- const cw = fontMap[ch];
16377
- if (cw !== void 0) {
16378
- total += cw;
16379
- }
16790
+ this._completed = reactiveLog([], { name: "completed" });
16791
+ this.completed = this._completed.entries;
16792
+ this.add("completed", this.completed);
16793
+ this.completedCount = derived(
16794
+ [this.completed],
16795
+ ([snapshot]) => snapshot.value.entries.length,
16796
+ {
16797
+ name: "completedCount",
16798
+ describeKind: "derived",
16799
+ meta: messagingMeta("job_flow_completed_count"),
16800
+ initial: 0
16380
16801
  }
16802
+ );
16803
+ this.add("completedCount", this.completedCount);
16804
+ this.connect("completed", "completedCount");
16805
+ this._keepaliveDisposers.push(keepalive4(this.completedCount));
16806
+ const maxPerPump = Math.max(
16807
+ 1,
16808
+ requireNonNegativeInt(opts.maxPerPump ?? DEFAULT_MAX_PER_PUMP, "job flow maxPerPump")
16809
+ );
16810
+ for (let i = 0; i < this._stageNames.length; i += 1) {
16811
+ const stage = this._stageNames[i];
16812
+ const current = this.queue(stage);
16813
+ const next = i + 1 < this._stageNames.length ? this.queue(this._stageNames[i + 1]) : null;
16814
+ const pump = node(
16815
+ [current.pending],
16816
+ () => {
16817
+ let moved = 0;
16818
+ while (moved < maxPerPump) {
16819
+ const claim = current.claim(1);
16820
+ if (claim.length === 0) break;
16821
+ const job = claim[0];
16822
+ if (!job) break;
16823
+ if (next) {
16824
+ next.enqueue(job.payload, {
16825
+ metadata: {
16826
+ ...job.metadata,
16827
+ job_flow_from: stage
16828
+ }
16829
+ });
16830
+ } else {
16831
+ this._completed.append(job);
16832
+ }
16833
+ current.ack(job.id);
16834
+ moved += 1;
16835
+ }
16836
+ },
16837
+ {
16838
+ name: `pump_${stage}`,
16839
+ describeKind: "effect",
16840
+ meta: messagingMeta("job_flow_pump")
16841
+ }
16842
+ );
16843
+ this.add(`pump_${stage}`, pump);
16844
+ this.connect(`${stage}::pending`, `pump_${stage}`);
16845
+ this._keepaliveDisposers.push(keepalive4(pump));
16381
16846
  }
16382
- return { width: total };
16383
16847
  }
16384
- };
16385
- var CanvasMeasureAdapter = class {
16386
- ctx = null;
16387
- currentFont = "";
16388
- emojiCorrection;
16389
- constructor(opts) {
16390
- this.emojiCorrection = opts?.emojiCorrection ?? 1;
16848
+ destroy() {
16849
+ for (const dispose of this._keepaliveDisposers) dispose();
16850
+ this._keepaliveDisposers.length = 0;
16851
+ super.destroy();
16391
16852
  }
16392
- getContext() {
16393
- if (!this.ctx) {
16394
- if (typeof OffscreenCanvas === "undefined") {
16395
- throw new Error(
16396
- "CanvasMeasureAdapter requires a browser environment with OffscreenCanvas support. Use CliMeasureAdapter or NodeCanvasMeasureAdapter for Node.js."
16397
- );
16398
- }
16399
- const canvas = new OffscreenCanvas(0, 0);
16400
- const ctx = canvas.getContext("2d");
16401
- if (!ctx) throw new Error("CanvasMeasureAdapter: failed to get 2d context");
16402
- this.ctx = ctx;
16403
- }
16404
- return this.ctx;
16853
+ stages() {
16854
+ return this._stageNames;
16405
16855
  }
16406
- measureSegment(text, font) {
16407
- const ctx = this.getContext();
16408
- if (font !== this.currentFont) {
16409
- ctx.font = font;
16410
- this.currentFont = font;
16411
- }
16412
- let width = ctx.measureText(text).width;
16413
- if (this.emojiCorrection !== 1 && /\p{Emoji_Presentation}/u.test(text)) {
16414
- width *= this.emojiCorrection;
16415
- }
16416
- return { width };
16856
+ queue(stage) {
16857
+ const q = this._queues.get(stage);
16858
+ if (!q) throw new Error(`jobFlow("${this.name}"): unknown stage "${stage}"`);
16859
+ return q;
16417
16860
  }
16418
- clearCache() {
16419
- this.currentFont = "";
16861
+ enqueue(payload, opts = {}) {
16862
+ return this.queue(this._stageNames[0]).enqueue(payload, opts);
16863
+ }
16864
+ retainedCompleted() {
16865
+ const snapshot = this.completed.get();
16866
+ return snapshot.value.entries;
16867
+ }
16868
+ };
16869
+ var TopicBridgeGraph = class extends Graph {
16870
+ _sourceSub;
16871
+ _target;
16872
+ _keepaliveDisposers = [];
16873
+ bridgedCount;
16874
+ constructor(name, sourceTopic, targetTopic, opts = {}) {
16875
+ super(name, opts.graph);
16876
+ this._sourceSub = subscription(`${name}-subscription`, sourceTopic, {
16877
+ cursor: opts.cursor
16878
+ });
16879
+ this._target = targetTopic;
16880
+ this.mount("subscription", this._sourceSub);
16881
+ this.bridgedCount = state(0, {
16882
+ name: "bridgedCount",
16883
+ describeKind: "state",
16884
+ meta: messagingMeta("topic_bridge_count")
16885
+ });
16886
+ this.add("bridgedCount", this.bridgedCount);
16887
+ const maxPerPump = Math.max(
16888
+ 1,
16889
+ requireNonNegativeInt(opts.maxPerPump ?? DEFAULT_MAX_PER_PUMP, "topic bridge maxPerPump")
16890
+ );
16891
+ const mapValue = opts.map ?? ((value) => value);
16892
+ const pump = node(
16893
+ [this._sourceSub.available],
16894
+ () => {
16895
+ const available = this._sourceSub.pull(maxPerPump, { ack: true });
16896
+ if (available.length === 0) return;
16897
+ let bridged = 0;
16898
+ for (const value of available) {
16899
+ const mapped = mapValue(value);
16900
+ if (mapped === void 0) continue;
16901
+ this._target.publish(mapped);
16902
+ bridged += 1;
16903
+ }
16904
+ if (bridged > 0) {
16905
+ const current = this.bridgedCount.get();
16906
+ this.bridgedCount.down([[DATA, current + bridged]]);
16907
+ }
16908
+ },
16909
+ {
16910
+ name: "pump",
16911
+ describeKind: "effect",
16912
+ meta: messagingMeta("topic_bridge_pump")
16913
+ }
16914
+ );
16915
+ this.add("pump", pump);
16916
+ this.connect("subscription::available", "pump");
16917
+ this._keepaliveDisposers.push(keepalive4(pump));
16918
+ }
16919
+ destroy() {
16920
+ for (const dispose of this._keepaliveDisposers) dispose();
16921
+ this._keepaliveDisposers.length = 0;
16922
+ super.destroy();
16420
16923
  }
16421
16924
  };
16422
- var NodeCanvasMeasureAdapter = class {
16423
- ctx = null;
16424
- currentFont = "";
16425
- canvasModule;
16426
- constructor(canvasModule) {
16427
- this.canvasModule = canvasModule;
16925
+ function topic(name, opts) {
16926
+ return new TopicGraph(name, opts);
16927
+ }
16928
+ function subscription(name, topicGraph, opts) {
16929
+ return new SubscriptionGraph(name, topicGraph, opts);
16930
+ }
16931
+ function jobQueue(name, opts) {
16932
+ return new JobQueueGraph(name, opts);
16933
+ }
16934
+ function jobFlow(name, opts) {
16935
+ return new JobFlowGraph(name, opts);
16936
+ }
16937
+ function topicBridge(name, sourceTopic, targetTopic, opts) {
16938
+ return new TopicBridgeGraph(name, sourceTopic, targetTopic, opts);
16939
+ }
16940
+
16941
+ // src/patterns/orchestration.ts
16942
+ var orchestration_exports = {};
16943
+ __export(orchestration_exports, {
16944
+ approval: () => approval,
16945
+ branch: () => branch,
16946
+ forEach: () => forEach2,
16947
+ gate: () => gate2,
16948
+ join: () => join2,
16949
+ loop: () => loop,
16950
+ onFailure: () => onFailure,
16951
+ pipeline: () => pipeline,
16952
+ sensor: () => sensor,
16953
+ subPipeline: () => subPipeline,
16954
+ task: () => task,
16955
+ wait: () => wait
16956
+ });
16957
+ function resolveDep(graph, dep) {
16958
+ if (typeof dep === "string") {
16959
+ return { node: graph.resolve(dep), path: dep };
16428
16960
  }
16429
- getContext() {
16430
- if (!this.ctx) {
16431
- const canvas = this.canvasModule.createCanvas(0, 0);
16432
- const ctx = canvas.getContext("2d");
16433
- if (!ctx) throw new Error("NodeCanvasMeasureAdapter: failed to get 2d context");
16434
- this.ctx = ctx;
16435
- }
16436
- return this.ctx;
16961
+ const path = findRegisteredNodePath(graph, dep);
16962
+ if (!path) {
16963
+ throw new Error(
16964
+ "orchestration dep node must already be registered in the graph so explicit edges can be recorded; pass a string path or register the node first"
16965
+ );
16437
16966
  }
16438
- measureSegment(text, font) {
16439
- const ctx = this.getContext();
16440
- if (font !== this.currentFont) {
16441
- ctx.font = font;
16442
- this.currentFont = font;
16967
+ return { node: dep, path };
16968
+ }
16969
+ function findRegisteredNodePath(graph, target) {
16970
+ const described = graph.describe();
16971
+ const metaSegment = `::${GRAPH_META_SEGMENT}::`;
16972
+ for (const path of Object.keys(described.nodes).sort()) {
16973
+ if (path.includes(metaSegment)) continue;
16974
+ try {
16975
+ if (graph.resolve(path) === target) return path;
16976
+ } catch {
16443
16977
  }
16444
- return { width: ctx.measureText(text).width };
16445
- }
16446
- clearCache() {
16447
- this.currentFont = "";
16448
16978
  }
16449
- };
16450
- var SvgBoundsAdapter = class {
16451
- measureSvg(content) {
16452
- const viewBoxMatch = content.match(/viewBox\s*=\s*["']([^"']+)["']/);
16453
- if (viewBoxMatch) {
16454
- const parts = viewBoxMatch[1].trim().split(/[\s,]+/);
16455
- if (parts.length >= 4) {
16456
- const w = Number.parseFloat(parts[2]);
16457
- const h = Number.parseFloat(parts[3]);
16458
- if (Number.isFinite(w) && Number.isFinite(h) && w > 0 && h > 0) {
16459
- return { width: w, height: h };
16460
- }
16461
- throw new Error(
16462
- "SvgBoundsAdapter: viewBox width/height are missing, non-finite, or not positive"
16463
- );
16464
- }
16465
- }
16466
- const widthMatch = content.match(/<svg[^>]*\bwidth\s*=\s*["']?([\d.]+)/);
16467
- const heightMatch = content.match(/<svg[^>]*\bheight\s*=\s*["']?([\d.]+)/);
16468
- if (widthMatch && heightMatch) {
16469
- const w = Number.parseFloat(widthMatch[1]);
16470
- const h = Number.parseFloat(heightMatch[1]);
16471
- if (Number.isFinite(w) && Number.isFinite(h) && w > 0 && h > 0) {
16472
- return { width: w, height: h };
16473
- }
16474
- throw new Error(
16475
- "SvgBoundsAdapter: svg width/height attributes are non-finite or not positive"
16476
- );
16477
- }
16478
- throw new Error(
16479
- "SvgBoundsAdapter: cannot determine dimensions \u2014 SVG has no viewBox or width/height attributes"
16480
- );
16979
+ return void 0;
16980
+ }
16981
+ function registerStep(graph, name, step, depPaths) {
16982
+ graph.add(name, step);
16983
+ for (const path of depPaths) {
16984
+ graph.connect(path, name);
16481
16985
  }
16482
- };
16483
- var ImageSizeAdapter = class {
16484
- sizes;
16485
- constructor(sizes) {
16486
- this.sizes = new Map(Object.entries(sizes));
16986
+ }
16987
+ function baseMeta2(kind, meta) {
16988
+ return {
16989
+ orchestration: true,
16990
+ orchestration_type: kind,
16991
+ ...meta ?? {}
16992
+ };
16993
+ }
16994
+ function coerceLoopIterations(raw) {
16995
+ const parseString = (value) => {
16996
+ const trimmed = value.trim();
16997
+ if (trimmed.length === 0) return 0;
16998
+ return Number(trimmed);
16999
+ };
17000
+ let parsed;
17001
+ if (typeof raw === "string") {
17002
+ parsed = parseString(raw);
17003
+ } else if (raw === null) {
17004
+ parsed = 0;
17005
+ } else {
17006
+ parsed = Number(raw);
16487
17007
  }
16488
- measureImage(src) {
16489
- const dims = this.sizes.get(src);
16490
- if (!dims) {
16491
- throw new Error(`ImageSizeAdapter: no dimensions registered for ${JSON.stringify(src)}`);
17008
+ if (!Number.isFinite(parsed)) return 1;
17009
+ return Math.max(0, Math.trunc(parsed));
17010
+ }
17011
+ function pipeline(name, opts) {
17012
+ return new Graph(name, opts);
17013
+ }
17014
+ function task(graph, name, run, opts) {
17015
+ const depRefs = opts?.deps ?? [];
17016
+ const deps = depRefs.map((dep) => resolveDep(graph, dep));
17017
+ const { deps: _deps, ...nodeOpts } = opts ?? {};
17018
+ const step = node(
17019
+ deps.map((d) => d.node),
17020
+ run,
17021
+ {
17022
+ ...nodeOpts,
17023
+ name,
17024
+ describeKind: "derived",
17025
+ meta: baseMeta2("task", opts?.meta)
16492
17026
  }
16493
- return { width: dims.width, height: dims.height };
16494
- }
16495
- };
16496
-
16497
- // src/patterns/reactive-layout/reactive-block-layout.ts
16498
- function measureBlock(block, maxWidth, adapters, measureCache, defaultFont, defaultLineHeight, index) {
16499
- switch (block.type) {
16500
- case "text": {
16501
- const font = block.font ?? defaultFont;
16502
- const lineHeight = block.lineHeight ?? defaultLineHeight;
16503
- const segments = analyzeAndMeasure(block.text, font, adapters.text, measureCache);
16504
- const lineBreaks = computeLineBreaks(segments, maxWidth, adapters.text, font, measureCache);
16505
- const charPositions = computeCharPositions(lineBreaks, segments, lineHeight);
16506
- const height = lineBreaks.lineCount * lineHeight;
16507
- let width = 0;
16508
- for (const line of lineBreaks.lines) {
16509
- if (line.width > width) width = line.width;
16510
- }
16511
- return {
16512
- index,
16513
- type: "text",
16514
- width: Math.min(width, maxWidth),
16515
- height,
16516
- textSegments: segments,
16517
- textLineBreaks: lineBreaks,
16518
- textCharPositions: charPositions
16519
- };
17027
+ );
17028
+ registerStep(
17029
+ graph,
17030
+ name,
17031
+ step,
17032
+ deps.flatMap((d) => d.path ? [d.path] : [])
17033
+ );
17034
+ return step;
17035
+ }
17036
+ function branch(graph, name, source, predicate, opts) {
17037
+ const src = resolveDep(graph, source);
17038
+ const step = node(
17039
+ [src.node],
17040
+ ([value]) => ({
17041
+ branch: predicate(value) ? "then" : "else",
17042
+ value
17043
+ }),
17044
+ {
17045
+ ...opts,
17046
+ name,
17047
+ describeKind: "derived",
17048
+ meta: baseMeta2("branch", opts?.meta)
16520
17049
  }
16521
- case "image": {
16522
- let w;
16523
- let h;
16524
- if (block.naturalWidth != null && block.naturalHeight != null) {
16525
- w = block.naturalWidth;
16526
- h = block.naturalHeight;
16527
- } else if (adapters.image) {
16528
- const dims = adapters.image.measureImage(block.src);
16529
- w = dims.width;
16530
- h = dims.height;
16531
- } else {
16532
- throw new Error(
16533
- `Image block at index ${index} has no naturalWidth/naturalHeight and no ImageMeasurer adapter`
16534
- );
17050
+ );
17051
+ registerStep(graph, name, step, src.path ? [src.path] : []);
17052
+ return step;
17053
+ }
17054
+ function gate2(graph, name, source, control, opts) {
17055
+ const src = resolveDep(graph, source);
17056
+ const ctrl = resolveDep(graph, control);
17057
+ const step = node(
17058
+ [src.node, ctrl.node],
17059
+ (_deps, actions) => {
17060
+ const opened = ctrl.node.get();
17061
+ if (!opened) {
17062
+ actions.down([[RESOLVED]]);
17063
+ return void 0;
16535
17064
  }
16536
- if (w > maxWidth) {
16537
- h = h * maxWidth / w;
16538
- w = maxWidth;
17065
+ return src.node.get();
17066
+ },
17067
+ {
17068
+ ...opts,
17069
+ name,
17070
+ describeKind: "operator",
17071
+ meta: baseMeta2("gate", opts?.meta)
17072
+ }
17073
+ );
17074
+ registerStep(
17075
+ graph,
17076
+ name,
17077
+ step,
17078
+ [src.path, ctrl.path].filter((v) => typeof v === "string")
17079
+ );
17080
+ return step;
17081
+ }
17082
+ function approval(graph, name, source, approver, opts) {
17083
+ const src = resolveDep(graph, source);
17084
+ const ctrl = resolveDep(graph, approver);
17085
+ const isApproved = opts?.isApproved ?? ((value) => Boolean(value));
17086
+ const step = node(
17087
+ [src.node, ctrl.node],
17088
+ (_deps, actions) => {
17089
+ if (!isApproved(ctrl.node.get())) {
17090
+ actions.down([[RESOLVED]]);
17091
+ return void 0;
16539
17092
  }
16540
- return { index, type: "image", width: w, height: h };
17093
+ return src.node.get();
17094
+ },
17095
+ {
17096
+ ...opts,
17097
+ name,
17098
+ describeKind: "operator",
17099
+ meta: baseMeta2("approval", opts?.meta)
16541
17100
  }
16542
- case "svg": {
16543
- let w;
16544
- let h;
16545
- if (block.viewBox) {
16546
- w = block.viewBox.width;
16547
- h = block.viewBox.height;
16548
- } else if (adapters.svg) {
16549
- const dims = adapters.svg.measureSvg(block.content);
16550
- w = dims.width;
16551
- h = dims.height;
16552
- } else {
16553
- throw new Error(`SVG block at index ${index} has no viewBox and no SvgMeasurer adapter`);
17101
+ );
17102
+ registerStep(
17103
+ graph,
17104
+ name,
17105
+ step,
17106
+ [src.path, ctrl.path].filter((v) => typeof v === "string")
17107
+ );
17108
+ return step;
17109
+ }
17110
+ function forEach2(graph, name, source, run, opts) {
17111
+ const src = resolveDep(graph, source);
17112
+ let terminated = false;
17113
+ const step = node([src.node], () => void 0, {
17114
+ ...opts,
17115
+ name,
17116
+ describeKind: "effect",
17117
+ completeWhenDepsComplete: false,
17118
+ meta: baseMeta2("forEach", opts?.meta),
17119
+ onMessage(msg, depIndex, actions) {
17120
+ if (terminated) return true;
17121
+ if (depIndex !== 0) {
17122
+ actions.down([msg]);
17123
+ if (msg[0] === COMPLETE || msg[0] === ERROR) terminated = true;
17124
+ return true;
16554
17125
  }
16555
- if (w > maxWidth) {
16556
- h = h * maxWidth / w;
16557
- w = maxWidth;
17126
+ if (msg[0] === DATA) {
17127
+ try {
17128
+ run(msg[1], actions);
17129
+ actions.down([msg]);
17130
+ } catch (err) {
17131
+ terminated = true;
17132
+ actions.down([[ERROR, err]]);
17133
+ }
17134
+ return true;
16558
17135
  }
16559
- return { index, type: "svg", width: w, height: h };
17136
+ actions.down([msg]);
17137
+ if (msg[0] === COMPLETE || msg[0] === ERROR) terminated = true;
17138
+ return true;
16560
17139
  }
16561
- }
17140
+ });
17141
+ registerStep(graph, name, step, src.path ? [src.path] : []);
17142
+ return step;
16562
17143
  }
16563
- function measureBlocks(blocks, maxWidth, adapters, measureCache, defaultFont, defaultLineHeight) {
16564
- return blocks.map(
16565
- (block, i) => measureBlock(block, maxWidth, adapters, measureCache, defaultFont, defaultLineHeight, i)
17144
+ function join2(graph, name, deps, opts) {
17145
+ const resolved = deps.map((dep) => resolveDep(graph, dep));
17146
+ const step = node(
17147
+ resolved.map((d) => d.node),
17148
+ (values) => values,
17149
+ {
17150
+ ...opts,
17151
+ name,
17152
+ describeKind: "derived",
17153
+ meta: baseMeta2("join", opts?.meta)
17154
+ }
17155
+ );
17156
+ registerStep(
17157
+ graph,
17158
+ name,
17159
+ step,
17160
+ resolved.flatMap((d) => d.path ? [d.path] : [])
16566
17161
  );
17162
+ return step;
16567
17163
  }
16568
- function computeBlockFlow(measured, gap) {
16569
- const result = [];
16570
- let y = 0;
16571
- for (let i = 0; i < measured.length; i++) {
16572
- const m = measured[i];
16573
- result.push({ ...m, x: 0, y });
16574
- y += m.height + (i < measured.length - 1 ? gap : 0);
17164
+ function loop(graph, name, source, iterate, opts) {
17165
+ const src = resolveDep(graph, source);
17166
+ const iterRef = opts?.iterations;
17167
+ const iterDep = typeof iterRef === "number" || iterRef === void 0 ? void 0 : resolveDep(graph, iterRef);
17168
+ const staticIterations = typeof iterRef === "number" ? iterRef : void 0;
17169
+ const step = node(
17170
+ iterDep ? [src.node, iterDep.node] : [src.node],
17171
+ (_deps, actions) => {
17172
+ let current = src.node.get();
17173
+ const rawCount = staticIterations ?? iterDep?.node.get() ?? 1;
17174
+ const count = coerceLoopIterations(rawCount);
17175
+ for (let i = 0; i < count; i += 1) {
17176
+ current = iterate(current, i, actions);
17177
+ }
17178
+ return current;
17179
+ },
17180
+ {
17181
+ ...opts,
17182
+ name,
17183
+ describeKind: "derived",
17184
+ meta: baseMeta2("loop", opts?.meta)
17185
+ }
17186
+ );
17187
+ registerStep(
17188
+ graph,
17189
+ name,
17190
+ step,
17191
+ [src.path, iterDep?.path].filter((v) => typeof v === "string")
17192
+ );
17193
+ return step;
17194
+ }
17195
+ function subPipeline(graph, name, childOrBuild, opts) {
17196
+ const child = childOrBuild instanceof Graph ? childOrBuild : pipeline(name, opts);
17197
+ if (typeof childOrBuild === "function") {
17198
+ childOrBuild(child);
16575
17199
  }
16576
- return result;
17200
+ graph.mount(name, child);
17201
+ return child;
16577
17202
  }
16578
- function computeTotalHeight(flow) {
16579
- if (flow.length === 0) return 0;
16580
- const last2 = flow[flow.length - 1];
16581
- return last2.y + last2.height;
17203
+ function sensor(graph, name, initial, opts) {
17204
+ const source = node([], () => void 0, {
17205
+ ...opts,
17206
+ name,
17207
+ initial,
17208
+ describeKind: "producer",
17209
+ meta: baseMeta2("sensor", opts?.meta)
17210
+ });
17211
+ registerStep(graph, name, source, []);
17212
+ return {
17213
+ node: source,
17214
+ push(value) {
17215
+ source.down([[DATA, value]]);
17216
+ },
17217
+ error(err) {
17218
+ source.down([[ERROR, err]]);
17219
+ },
17220
+ complete() {
17221
+ source.down([[COMPLETE]]);
17222
+ }
17223
+ };
16582
17224
  }
16583
- function reactiveBlockLayout(opts) {
16584
- const {
16585
- adapters,
16586
- name = "reactive-block-layout",
16587
- defaultFont = "16px sans-serif",
16588
- defaultLineHeight = 20
16589
- } = opts;
16590
- const g = new Graph(name);
16591
- const measureCache = /* @__PURE__ */ new Map();
16592
- const blocksNode = state(opts.blocks ?? [], { name: "blocks" });
16593
- const maxWidthNode = state(Math.max(0, opts.maxWidth ?? 800), { name: "max-width" });
16594
- const gapNode = state(opts.gap ?? 0, { name: "gap" });
16595
- const measuredBlocksNode = derived(
16596
- [blocksNode, maxWidthNode],
16597
- ([blocksVal, mwVal]) => {
16598
- const t0 = monotonicNs();
16599
- const result = measureBlocks(
16600
- blocksVal,
16601
- mwVal,
16602
- adapters,
16603
- measureCache,
16604
- defaultFont,
16605
- defaultLineHeight
16606
- );
16607
- const elapsed = monotonicNs() - t0;
16608
- const meta = measuredBlocksNode.meta;
16609
- if (meta) {
16610
- emitWithBatch((msgs) => meta["block-count"]?.down(msgs), [[DATA, result.length]], 3);
16611
- emitWithBatch((msgs) => meta["layout-time-ns"]?.down(msgs), [[DATA, elapsed]], 3);
16612
- }
16613
- return result;
17225
+ function wait(graph, name, source, ms, opts) {
17226
+ const src = resolveDep(graph, source);
17227
+ const timers = /* @__PURE__ */ new Set();
17228
+ let terminated = false;
17229
+ let completed = false;
17230
+ const step = node(
17231
+ [src.node],
17232
+ () => {
17233
+ for (const id of timers) clearTimeout(id);
17234
+ timers.clear();
17235
+ return () => {
17236
+ for (const id of timers) clearTimeout(id);
17237
+ timers.clear();
17238
+ terminated = true;
17239
+ };
16614
17240
  },
16615
17241
  {
16616
- name: "measured-blocks",
16617
- meta: { "block-count": 0, "layout-time-ns": 0 },
16618
- onMessage(msg, _depIndex, _actions) {
16619
- if (msg[0] === INVALIDATE || msg[0] === TEARDOWN) {
16620
- measureCache.clear();
16621
- adapters.text.clearCache?.();
17242
+ ...opts,
17243
+ name,
17244
+ initial: src.node.get(),
17245
+ describeKind: "operator",
17246
+ completeWhenDepsComplete: false,
17247
+ meta: baseMeta2("wait", opts?.meta),
17248
+ onMessage(msg, depIndex, actions) {
17249
+ if (terminated) return true;
17250
+ if (depIndex !== 0) {
17251
+ actions.down([msg]);
17252
+ if (msg[0] === COMPLETE || msg[0] === ERROR) terminated = true;
17253
+ return true;
16622
17254
  }
16623
- return false;
16624
- },
16625
- equals: (a, b) => {
16626
- const ma = a;
16627
- const mb = b;
16628
- if (ma == null || mb == null) return ma === mb;
16629
- if (ma.length !== mb.length) return false;
16630
- for (let i = 0; i < ma.length; i++) {
16631
- const ba = ma[i];
16632
- const bb = mb[i];
16633
- if (ba.type !== bb.type || ba.width !== bb.width || ba.height !== bb.height || ba.index !== bb.index)
16634
- return false;
17255
+ if (msg[0] === DATA) {
17256
+ const id = setTimeout(() => {
17257
+ timers.delete(id);
17258
+ actions.down([msg]);
17259
+ if (completed && timers.size === 0) {
17260
+ actions.down([[COMPLETE]]);
17261
+ }
17262
+ }, ms);
17263
+ timers.add(id);
17264
+ return true;
17265
+ }
17266
+ if (msg[0] === COMPLETE) {
17267
+ terminated = true;
17268
+ completed = true;
17269
+ if (timers.size === 0) {
17270
+ actions.down([[COMPLETE]]);
17271
+ }
17272
+ return true;
17273
+ }
17274
+ if (msg[0] === ERROR) {
17275
+ terminated = true;
17276
+ for (const id of timers) clearTimeout(id);
17277
+ timers.clear();
17278
+ actions.down([msg]);
17279
+ return true;
16635
17280
  }
17281
+ actions.down([msg]);
16636
17282
  return true;
16637
17283
  }
16638
17284
  }
16639
17285
  );
16640
- const blockFlowNode = derived(
16641
- [measuredBlocksNode, gapNode],
16642
- ([measured, gapVal]) => {
16643
- return computeBlockFlow(measured, gapVal);
16644
- },
16645
- {
16646
- name: "block-flow",
16647
- equals: (a, b) => {
16648
- const fa = a;
16649
- const fb = b;
16650
- if (fa == null || fb == null) return fa === fb;
16651
- if (fa.length !== fb.length) return false;
16652
- for (let i = 0; i < fa.length; i++) {
16653
- const pa = fa[i];
16654
- const pb = fb[i];
16655
- if (pa.x !== pb.x || pa.y !== pb.y || pa.width !== pb.width || pa.height !== pb.height)
16656
- return false;
17286
+ registerStep(graph, name, step, src.path ? [src.path] : []);
17287
+ return step;
17288
+ }
17289
+ function onFailure(graph, name, source, recover, opts) {
17290
+ const src = resolveDep(graph, source);
17291
+ let terminated = false;
17292
+ const step = node([src.node], () => void 0, {
17293
+ ...opts,
17294
+ name,
17295
+ describeKind: "operator",
17296
+ completeWhenDepsComplete: false,
17297
+ meta: baseMeta2("onFailure", opts?.meta),
17298
+ onMessage(msg, _depIndex, actions) {
17299
+ if (terminated) return true;
17300
+ if (msg[0] === ERROR) {
17301
+ try {
17302
+ actions.emit(recover(msg[1], actions));
17303
+ } catch (err) {
17304
+ terminated = true;
17305
+ actions.down([[ERROR, err]]);
16657
17306
  }
16658
17307
  return true;
16659
17308
  }
17309
+ actions.down([msg]);
17310
+ if (msg[0] === COMPLETE) terminated = true;
17311
+ return true;
16660
17312
  }
16661
- );
16662
- const totalHeightNode = derived(
16663
- [blockFlowNode],
16664
- ([flow]) => computeTotalHeight(flow),
16665
- { name: "total-height" }
16666
- );
16667
- g.add("blocks", blocksNode);
16668
- g.add("max-width", maxWidthNode);
16669
- g.add("gap", gapNode);
16670
- g.add("measured-blocks", measuredBlocksNode);
16671
- g.add("block-flow", blockFlowNode);
16672
- g.add("total-height", totalHeightNode);
16673
- g.connect("blocks", "measured-blocks");
16674
- g.connect("max-width", "measured-blocks");
16675
- g.connect("measured-blocks", "block-flow");
16676
- g.connect("gap", "block-flow");
16677
- g.connect("block-flow", "total-height");
16678
- return {
16679
- graph: g,
16680
- setBlocks: (blocks) => g.set("blocks", blocks),
16681
- setMaxWidth: (mw) => g.set("max-width", Math.max(0, mw)),
16682
- setGap: (gap) => g.set("gap", gap),
16683
- measuredBlocks: measuredBlocksNode,
16684
- blockFlow: blockFlowNode,
16685
- totalHeight: totalHeightNode
16686
- };
17313
+ });
17314
+ registerStep(graph, name, step, src.path ? [src.path] : []);
17315
+ return step;
16687
17316
  }
16688
17317
 
16689
- // src/patterns/reduction.ts
16690
- var reduction_exports = {};
16691
- __export(reduction_exports, {
16692
- budgetGate: () => budgetGate,
16693
- feedback: () => feedback,
16694
- funnel: () => funnel,
16695
- scorer: () => scorer,
16696
- stratify: () => stratify
17318
+ // src/patterns/reactive-layout/index.ts
17319
+ var reactive_layout_exports = {};
17320
+ __export(reactive_layout_exports, {
17321
+ CanvasMeasureAdapter: () => CanvasMeasureAdapter,
17322
+ CliMeasureAdapter: () => CliMeasureAdapter,
17323
+ ImageSizeAdapter: () => ImageSizeAdapter,
17324
+ NodeCanvasMeasureAdapter: () => NodeCanvasMeasureAdapter,
17325
+ PrecomputedAdapter: () => PrecomputedAdapter,
17326
+ SvgBoundsAdapter: () => SvgBoundsAdapter,
17327
+ analyzeAndMeasure: () => analyzeAndMeasure,
17328
+ computeBlockFlow: () => computeBlockFlow,
17329
+ computeCharPositions: () => computeCharPositions,
17330
+ computeLineBreaks: () => computeLineBreaks,
17331
+ computeTotalHeight: () => computeTotalHeight,
17332
+ measureBlock: () => measureBlock,
17333
+ measureBlocks: () => measureBlocks,
17334
+ reactiveBlockLayout: () => reactiveBlockLayout,
17335
+ reactiveLayout: () => reactiveLayout
16697
17336
  });
16698
- function baseMeta2(kind, meta) {
16699
- return {
16700
- reduction: true,
16701
- reduction_type: kind,
16702
- ...meta ?? {}
16703
- };
17337
+
17338
+ // src/patterns/reactive-layout/measurement-adapters.ts
17339
+ function cellWidth(code) {
17340
+ if (code >= 768 && code <= 879 || // Combining Diacritical Marks
17341
+ code >= 1155 && code <= 1161 || // Cyrillic combining marks
17342
+ code >= 1425 && code <= 1469 || // Hebrew combining marks
17343
+ code >= 1552 && code <= 1562 || // Arabic combining marks
17344
+ code >= 1611 && code <= 1631 || // Arabic combining marks
17345
+ code >= 1648 && code === 1648 || // Arabic superscript alef
17346
+ code >= 1750 && code <= 1756 || // Arabic combining marks
17347
+ code >= 1759 && code <= 1764 || // Arabic combining marks
17348
+ code >= 1767 && code <= 1768 || // Arabic combining marks
17349
+ code >= 1770 && code <= 1773 || // Arabic combining marks
17350
+ code >= 1840 && code <= 1866 || // Syriac combining marks
17351
+ code >= 1958 && code <= 1968 || // Thaana combining marks
17352
+ code >= 2304 && code <= 2307 || // Devanagari combining marks
17353
+ code >= 2362 && code <= 2383 || // Devanagari combining marks
17354
+ code >= 2385 && code <= 2391 || // Devanagari combining marks
17355
+ code >= 2402 && code <= 2403 || // Devanagari combining marks
17356
+ code >= 2433 && code <= 2435 || // Bengali combining marks
17357
+ code >= 2492 && code <= 2509 || // Bengali combining marks
17358
+ code >= 2561 && code <= 2563 || // Gurmukhi combining marks
17359
+ code >= 2620 && code <= 2641 || // Gurmukhi combining marks
17360
+ code >= 2672 && code <= 2673 || // Gurmukhi combining marks
17361
+ code >= 2677 && code === 2677 || // Gurmukhi combining mark
17362
+ code >= 3633 && code === 3633 || // Thai combining mark
17363
+ code >= 3636 && code <= 3642 || // Thai combining marks
17364
+ code >= 3655 && code <= 3662 || // Thai combining marks
17365
+ code >= 3761 && code === 3761 || // Lao combining mark
17366
+ code >= 3764 && code <= 3772 || // Lao combining marks
17367
+ code >= 3784 && code <= 3790 || // Lao combining marks
17368
+ code >= 7616 && code <= 7679 || // Combining Diacritical Marks Supplement
17369
+ code >= 8400 && code <= 8447 || // Combining Diacritical Marks for Symbols
17370
+ code >= 65024 && code <= 65039 || // Variation Selectors
17371
+ code >= 65056 && code <= 65071 || // Combining Half Marks
17372
+ code === 8205) {
17373
+ return 0;
17374
+ }
17375
+ if (code >= 4352 && code <= 4447 || // Hangul Jamo
17376
+ code >= 8986 && code <= 8987 || // Watch, Hourglass
17377
+ code >= 9001 && code <= 9002 || // Angle brackets
17378
+ code >= 9193 && code <= 9203 || // Media control symbols
17379
+ code >= 9208 && code <= 9210 || // Media control symbols
17380
+ code >= 9725 && code <= 9726 || // Medium squares
17381
+ code >= 9748 && code <= 9749 || // Umbrella, Hot Beverage
17382
+ code >= 9800 && code <= 9811 || // Zodiac symbols
17383
+ code === 9855 || // Wheelchair
17384
+ code === 9875 || // Anchor
17385
+ code === 9889 || // High Voltage
17386
+ code >= 9898 && code <= 9899 || // Medium circles
17387
+ code >= 9917 && code <= 9918 || // Soccer, Baseball
17388
+ code >= 9924 && code <= 9925 || // Snowman, Sun behind cloud
17389
+ code === 9934 || // Ophiuchus
17390
+ code === 9940 || // No Entry
17391
+ code === 9962 || // Church
17392
+ code >= 9970 && code <= 9971 || // Fountain, Golf
17393
+ code === 9973 || // Sailboat
17394
+ code === 9978 || // Tent
17395
+ code === 9981 || // Fuel Pump
17396
+ code === 9986 || // Scissors
17397
+ code === 9989 || // Check Mark
17398
+ code >= 9992 && code <= 9997 || // Airplane...Writing Hand
17399
+ code === 9999 || // Pencil
17400
+ code >= 10067 && code <= 10069 || // Question marks
17401
+ code === 10071 || // Exclamation
17402
+ code >= 10133 && code <= 10135 || // Plus, Minus, Divide
17403
+ code === 10160 || // Curly Loop
17404
+ code === 10175 || // Double Curly Loop
17405
+ code >= 10548 && code <= 10549 || // Arrows
17406
+ code >= 11013 && code <= 11015 || // Arrows
17407
+ code >= 11035 && code <= 11036 || // Squares
17408
+ code === 11088 || // Star
17409
+ code === 11093 || // Circle
17410
+ code >= 11904 && code <= 12350 || // CJK Radicals, Symbols, Punctuation
17411
+ code >= 12352 && code <= 12447 || // Hiragana
17412
+ code >= 12448 && code <= 12543 || // Katakana
17413
+ code >= 12549 && code <= 12591 || // Bopomofo
17414
+ code >= 12593 && code <= 12686 || // Hangul Compatibility Jamo
17415
+ code >= 12688 && code <= 12771 || // Kanbun, CJK Strokes
17416
+ code >= 12784 && code <= 12830 || // Katakana Phonetic Extensions
17417
+ code >= 12832 && code <= 12871 || // Enclosed CJK
17418
+ code >= 12880 && code <= 19903 || // CJK Extensions + Unified block
17419
+ code >= 19968 && code <= 40959 || // CJK Unified Ideographs
17420
+ code >= 43360 && code <= 43388 || // Hangul Jamo Extended-A
17421
+ code >= 44032 && code <= 55203 || // Hangul Syllables
17422
+ code >= 63744 && code <= 64255 || // CJK Compatibility Ideographs
17423
+ code >= 65040 && code <= 65049 || // Vertical forms
17424
+ code >= 65072 && code <= 65131 || // CJK Compatibility Forms
17425
+ code >= 65281 && code <= 65376 || // Fullwidth Forms (excl. halfwidth)
17426
+ code >= 65504 && code <= 65510 || // Fullwidth Signs
17427
+ code >= 126980 && code === 126980 || // Mahjong Red Dragon
17428
+ code === 127183 || // Joker
17429
+ code >= 127344 && code <= 127345 || // A/B buttons
17430
+ code === 127358 || // O button
17431
+ code === 127359 || // P button
17432
+ code === 127374 || // AB button
17433
+ code >= 127377 && code <= 127386 || // Squared symbols
17434
+ code >= 127456 && code <= 127487 || // Regional Indicator Symbols
17435
+ code >= 127488 && code <= 127490 || // Enclosed ideographic
17436
+ code === 127514 || // Squared CJK
17437
+ code === 127535 || // Squared CJK
17438
+ code >= 127538 && code <= 127546 || // Squared CJK
17439
+ code >= 127568 && code <= 127569 || // Circled ideographic
17440
+ code >= 127744 && code <= 129535 || // Misc Symbols / Emoticons / Emoji
17441
+ code >= 129536 && code <= 129791 || // Chess, Symbols Extended-A
17442
+ code >= 129792 && code <= 130047 || // Symbols for Legacy Computing
17443
+ code >= 131072 && code <= 196605 || // CJK Extension B-F (excl. nonchars)
17444
+ code >= 196608 && code <= 262141) {
17445
+ return 2;
17446
+ }
17447
+ return 1;
16704
17448
  }
16705
- function stratify(name, source, rules, opts) {
16706
- const g = new Graph(name, opts);
16707
- g.add("source", source);
16708
- const rulesNode = state(rules, {
16709
- meta: baseMeta2("stratify_rules")
16710
- });
16711
- g.add("rules", rulesNode);
16712
- for (const rule of rules) {
16713
- _addBranch(g, source, rulesNode, rule);
17449
+ function countCells(text) {
17450
+ let cells = 0;
17451
+ for (const ch of text) {
17452
+ cells += cellWidth(ch.codePointAt(0));
16714
17453
  }
16715
- return g;
17454
+ return cells;
16716
17455
  }
16717
- function _addBranch(graph, source, rulesNode, rule) {
16718
- const branchName = `branch/${rule.name}`;
16719
- let pendingDirty = false;
16720
- const filterNode = node([source, rulesNode], () => void 0, {
16721
- describeKind: "operator",
16722
- meta: baseMeta2("stratify_branch", { branch: rule.name }),
16723
- onMessage(msg, depIndex, actions) {
16724
- if (depIndex !== 0) return false;
16725
- const t = msg[0];
16726
- if (t === DATA) {
16727
- const value = msg[1];
16728
- const currentRules = rulesNode.get();
16729
- const currentRule = currentRules.find((r) => r.name === rule.name);
16730
- if (currentRule && currentRule.classify(value)) {
16731
- pendingDirty = false;
16732
- actions.emit(value);
16733
- } else {
16734
- if (pendingDirty) {
16735
- pendingDirty = false;
16736
- actions.down([[DIRTY], [RESOLVED]]);
16737
- }
17456
+ var CliMeasureAdapter = class {
17457
+ cellPx;
17458
+ constructor(opts) {
17459
+ this.cellPx = opts?.cellPx ?? 8;
17460
+ }
17461
+ measureSegment(text, _font) {
17462
+ return { width: countCells(text) * this.cellPx };
17463
+ }
17464
+ };
17465
+ var PrecomputedAdapterKeyError = class extends Error {
17466
+ name = "KeyError";
17467
+ };
17468
+ var PrecomputedAdapter = class {
17469
+ metrics;
17470
+ fallback;
17471
+ constructor(opts) {
17472
+ this.metrics = opts.metrics;
17473
+ const fb = opts.fallback ?? "per-char";
17474
+ if (fb !== "per-char" && fb !== "error") {
17475
+ throw new Error(
17476
+ `fallback must be 'per-char' or 'error', got ${JSON.stringify(opts.fallback)}`
17477
+ );
17478
+ }
17479
+ this.fallback = fb;
17480
+ }
17481
+ measureSegment(text, font) {
17482
+ const fontMap = this.metrics[font];
17483
+ if (fontMap) {
17484
+ const w = fontMap[text];
17485
+ if (w !== void 0) return { width: w };
17486
+ }
17487
+ if (this.fallback === "error") {
17488
+ throw new PrecomputedAdapterKeyError(
17489
+ `PrecomputedAdapter: no metrics for segment ${JSON.stringify(text)} in font ${JSON.stringify(font)}`
17490
+ );
17491
+ }
17492
+ let total = 0;
17493
+ if (fontMap) {
17494
+ for (const ch of text) {
17495
+ const cw = fontMap[ch];
17496
+ if (cw !== void 0) {
17497
+ total += cw;
16738
17498
  }
16739
- return true;
16740
17499
  }
16741
- if (t === DIRTY) {
16742
- pendingDirty = true;
16743
- return true;
17500
+ }
17501
+ return { width: total };
17502
+ }
17503
+ };
17504
+ var CanvasMeasureAdapter = class {
17505
+ ctx = null;
17506
+ currentFont = "";
17507
+ emojiCorrection;
17508
+ constructor(opts) {
17509
+ this.emojiCorrection = opts?.emojiCorrection ?? 1;
17510
+ }
17511
+ getContext() {
17512
+ if (!this.ctx) {
17513
+ if (typeof OffscreenCanvas === "undefined") {
17514
+ throw new Error(
17515
+ "CanvasMeasureAdapter requires a browser environment with OffscreenCanvas support. Use CliMeasureAdapter or NodeCanvasMeasureAdapter for Node.js."
17516
+ );
16744
17517
  }
16745
- if (t === RESOLVED) {
16746
- if (pendingDirty) {
16747
- pendingDirty = false;
16748
- actions.down([[DIRTY], [RESOLVED]]);
16749
- } else {
16750
- actions.down([[RESOLVED]]);
17518
+ const canvas = new OffscreenCanvas(0, 0);
17519
+ const ctx = canvas.getContext("2d");
17520
+ if (!ctx) throw new Error("CanvasMeasureAdapter: failed to get 2d context");
17521
+ this.ctx = ctx;
17522
+ }
17523
+ return this.ctx;
17524
+ }
17525
+ measureSegment(text, font) {
17526
+ const ctx = this.getContext();
17527
+ if (font !== this.currentFont) {
17528
+ ctx.font = font;
17529
+ this.currentFont = font;
17530
+ }
17531
+ let width = ctx.measureText(text).width;
17532
+ if (this.emojiCorrection !== 1 && /\p{Emoji_Presentation}/u.test(text)) {
17533
+ width *= this.emojiCorrection;
17534
+ }
17535
+ return { width };
17536
+ }
17537
+ clearCache() {
17538
+ this.currentFont = "";
17539
+ }
17540
+ };
17541
+ var NodeCanvasMeasureAdapter = class {
17542
+ ctx = null;
17543
+ currentFont = "";
17544
+ canvasModule;
17545
+ constructor(canvasModule) {
17546
+ this.canvasModule = canvasModule;
17547
+ }
17548
+ getContext() {
17549
+ if (!this.ctx) {
17550
+ const canvas = this.canvasModule.createCanvas(0, 0);
17551
+ const ctx = canvas.getContext("2d");
17552
+ if (!ctx) throw new Error("NodeCanvasMeasureAdapter: failed to get 2d context");
17553
+ this.ctx = ctx;
17554
+ }
17555
+ return this.ctx;
17556
+ }
17557
+ measureSegment(text, font) {
17558
+ const ctx = this.getContext();
17559
+ if (font !== this.currentFont) {
17560
+ ctx.font = font;
17561
+ this.currentFont = font;
17562
+ }
17563
+ return { width: ctx.measureText(text).width };
17564
+ }
17565
+ clearCache() {
17566
+ this.currentFont = "";
17567
+ }
17568
+ };
17569
+ var SvgBoundsAdapter = class {
17570
+ measureSvg(content) {
17571
+ const viewBoxMatch = content.match(/viewBox\s*=\s*["']([^"']+)["']/);
17572
+ if (viewBoxMatch) {
17573
+ const parts = viewBoxMatch[1].trim().split(/[\s,]+/);
17574
+ if (parts.length >= 4) {
17575
+ const w = Number.parseFloat(parts[2]);
17576
+ const h = Number.parseFloat(parts[3]);
17577
+ if (Number.isFinite(w) && Number.isFinite(h) && w > 0 && h > 0) {
17578
+ return { width: w, height: h };
16751
17579
  }
16752
- return true;
17580
+ throw new Error(
17581
+ "SvgBoundsAdapter: viewBox width/height are missing, non-finite, or not positive"
17582
+ );
16753
17583
  }
16754
- if (t === COMPLETE || t === ERROR) {
16755
- pendingDirty = false;
16756
- actions.down([msg]);
16757
- return true;
17584
+ }
17585
+ const widthMatch = content.match(/<svg[^>]*\bwidth\s*=\s*["']?([\d.]+)/);
17586
+ const heightMatch = content.match(/<svg[^>]*\bheight\s*=\s*["']?([\d.]+)/);
17587
+ if (widthMatch && heightMatch) {
17588
+ const w = Number.parseFloat(widthMatch[1]);
17589
+ const h = Number.parseFloat(heightMatch[1]);
17590
+ if (Number.isFinite(w) && Number.isFinite(h) && w > 0 && h > 0) {
17591
+ return { width: w, height: h };
16758
17592
  }
16759
- return false;
17593
+ throw new Error(
17594
+ "SvgBoundsAdapter: svg width/height attributes are non-finite or not positive"
17595
+ );
16760
17596
  }
16761
- });
16762
- graph.add(branchName, filterNode);
16763
- graph.connect("source", branchName);
16764
- if (rule.ops) {
16765
- const transformed = rule.ops(filterNode);
16766
- const transformedName = `branch/${rule.name}/out`;
16767
- graph.add(transformedName, transformed);
16768
- graph.connect(branchName, transformedName);
17597
+ throw new Error(
17598
+ "SvgBoundsAdapter: cannot determine dimensions \u2014 SVG has no viewBox or width/height attributes"
17599
+ );
16769
17600
  }
16770
- }
16771
- function funnel(name, sources, stages, opts) {
16772
- if (sources.length === 0) throw new RangeError("funnel requires at least one source");
16773
- if (stages.length === 0) throw new RangeError("funnel requires at least one stage");
16774
- const g = new Graph(name, opts);
16775
- const merged = sources.length === 1 ? sources[0] : merge(...sources);
16776
- g.add("merged", merged);
16777
- let prevOutputPath = "merged";
16778
- for (let i = 0; i < stages.length; i++) {
16779
- const stage = stages[i];
16780
- const sub = new Graph(stage.name);
16781
- stage.build(sub);
16782
- try {
16783
- sub.resolve("input");
16784
- } catch {
16785
- throw new Error(`funnel stage "${stage.name}" must define an "input" node`);
17601
+ };
17602
+ var ImageSizeAdapter = class {
17603
+ sizes;
17604
+ constructor(sizes) {
17605
+ this.sizes = new Map(Object.entries(sizes));
17606
+ }
17607
+ measureImage(src) {
17608
+ const dims = this.sizes.get(src);
17609
+ if (!dims) {
17610
+ throw new Error(`ImageSizeAdapter: no dimensions registered for ${JSON.stringify(src)}`);
16786
17611
  }
16787
- try {
16788
- sub.resolve("output");
16789
- } catch {
16790
- throw new Error(`funnel stage "${stage.name}" must define an "output" node`);
17612
+ return { width: dims.width, height: dims.height };
17613
+ }
17614
+ };
17615
+
17616
+ // src/patterns/reactive-layout/reactive-block-layout.ts
17617
+ function measureBlock(block, maxWidth, adapters, measureCache, defaultFont, defaultLineHeight, index) {
17618
+ switch (block.type) {
17619
+ case "text": {
17620
+ const font = block.font ?? defaultFont;
17621
+ const lineHeight = block.lineHeight ?? defaultLineHeight;
17622
+ const segments = analyzeAndMeasure(block.text, font, adapters.text, measureCache);
17623
+ const lineBreaks = computeLineBreaks(segments, maxWidth, adapters.text, font, measureCache);
17624
+ const charPositions = computeCharPositions(lineBreaks, segments, lineHeight);
17625
+ const height = lineBreaks.lineCount * lineHeight;
17626
+ let width = 0;
17627
+ for (const line of lineBreaks.lines) {
17628
+ if (line.width > width) width = line.width;
17629
+ }
17630
+ return {
17631
+ index,
17632
+ type: "text",
17633
+ width: Math.min(width, maxWidth),
17634
+ height,
17635
+ textSegments: segments,
17636
+ textLineBreaks: lineBreaks,
17637
+ textCharPositions: charPositions
17638
+ };
16791
17639
  }
16792
- g.mount(stage.name, sub);
16793
- const prevNode = g.resolve(prevOutputPath);
16794
- const stageInputPath = `${stage.name}::input`;
16795
- const stageInput = g.resolve(stageInputPath);
16796
- prevNode.subscribe((msgs) => {
16797
- for (const msg of msgs) {
16798
- const t = msg[0];
16799
- if (t === DATA) {
16800
- stageInput.down([[DATA, msg[1]]]);
16801
- } else if (t === DIRTY) {
16802
- stageInput.down([[DIRTY]]);
16803
- } else if (t === RESOLVED) {
16804
- stageInput.down([[RESOLVED]]);
16805
- } else if (t === COMPLETE || t === ERROR) {
16806
- stageInput.down([msg]);
16807
- }
17640
+ case "image": {
17641
+ let w;
17642
+ let h;
17643
+ if (block.naturalWidth != null && block.naturalHeight != null) {
17644
+ w = block.naturalWidth;
17645
+ h = block.naturalHeight;
17646
+ } else if (adapters.image) {
17647
+ const dims = adapters.image.measureImage(block.src);
17648
+ w = dims.width;
17649
+ h = dims.height;
17650
+ } else {
17651
+ throw new Error(
17652
+ `Image block at index ${index} has no naturalWidth/naturalHeight and no ImageMeasurer adapter`
17653
+ );
17654
+ }
17655
+ if (w > maxWidth) {
17656
+ h = h * maxWidth / w;
17657
+ w = maxWidth;
17658
+ }
17659
+ return { index, type: "image", width: w, height: h };
17660
+ }
17661
+ case "svg": {
17662
+ let w;
17663
+ let h;
17664
+ if (block.viewBox) {
17665
+ w = block.viewBox.width;
17666
+ h = block.viewBox.height;
17667
+ } else if (adapters.svg) {
17668
+ const dims = adapters.svg.measureSvg(block.content);
17669
+ w = dims.width;
17670
+ h = dims.height;
17671
+ } else {
17672
+ throw new Error(`SVG block at index ${index} has no viewBox and no SvgMeasurer adapter`);
16808
17673
  }
16809
- });
16810
- prevOutputPath = `${stage.name}::output`;
16811
- }
16812
- return g;
16813
- }
16814
- function feedback(graph, condition, reentry, opts) {
16815
- const maxIter = opts?.maxIterations ?? 10;
16816
- const counterName = `__feedback_${condition}`;
16817
- const counter = state(0, {
16818
- meta: baseMeta2("feedback_counter", { maxIterations: maxIter })
16819
- });
16820
- graph.add(counterName, counter);
16821
- const condNode = graph.resolve(condition);
16822
- const reentryNode = graph.resolve(reentry);
16823
- condNode.subscribe((msgs) => {
16824
- for (const msg of msgs) {
16825
- if (msg[0] === DATA) {
16826
- const currentCount = counter.get();
16827
- if (currentCount >= maxIter) continue;
16828
- const condValue = msg[1];
16829
- if (condValue == null) continue;
16830
- counter.down([[DATA, currentCount + 1]]);
16831
- reentryNode.down([[DATA, condValue]]);
17674
+ if (w > maxWidth) {
17675
+ h = h * maxWidth / w;
17676
+ w = maxWidth;
16832
17677
  }
17678
+ return { index, type: "svg", width: w, height: h };
16833
17679
  }
16834
- });
16835
- return graph;
16836
- }
16837
- function budgetGate(source, constraints, opts) {
16838
- if (constraints.length === 0) throw new RangeError("budgetGate requires at least one constraint");
16839
- const constraintNodes = constraints.map((c) => c.node);
16840
- const allDeps = [source, ...constraintNodes];
16841
- let buffer2 = [];
16842
- let paused = false;
16843
- const lockId = /* @__PURE__ */ Symbol("budget-gate");
16844
- function checkBudget() {
16845
- return constraints.every((c) => c.check(c.node.get()));
16846
17680
  }
16847
- function flushBuffer(actions) {
16848
- while (buffer2.length > 0 && checkBudget()) {
16849
- const item = buffer2.shift();
16850
- actions.emit(item);
16851
- }
17681
+ }
17682
+ function measureBlocks(blocks, maxWidth, adapters, measureCache, defaultFont, defaultLineHeight) {
17683
+ return blocks.map(
17684
+ (block, i) => measureBlock(block, maxWidth, adapters, measureCache, defaultFont, defaultLineHeight, i)
17685
+ );
17686
+ }
17687
+ function computeBlockFlow(measured, gap) {
17688
+ const result = [];
17689
+ let y = 0;
17690
+ for (let i = 0; i < measured.length; i++) {
17691
+ const m = measured[i];
17692
+ result.push({ ...m, x: 0, y });
17693
+ y += m.height + (i < measured.length - 1 ? gap : 0);
16852
17694
  }
16853
- return node(allDeps, () => void 0, {
16854
- ...opts,
16855
- describeKind: "operator",
16856
- meta: baseMeta2("budget_gate", opts?.meta),
16857
- onMessage(msg, depIndex, actions) {
16858
- const t = msg[0];
16859
- if (depIndex === 0) {
16860
- if (t === DATA) {
16861
- if (checkBudget() && buffer2.length === 0) {
16862
- actions.emit(msg[1]);
16863
- } else {
16864
- buffer2.push(msg[1]);
16865
- if (!paused) {
16866
- paused = true;
16867
- actions.up([[PAUSE, lockId]]);
16868
- }
16869
- }
16870
- return true;
16871
- }
16872
- if (t === DIRTY) {
16873
- actions.down([[DIRTY]]);
16874
- return true;
16875
- }
16876
- if (t === RESOLVED) {
16877
- if (buffer2.length === 0) {
16878
- actions.down([[RESOLVED]]);
16879
- }
16880
- return true;
16881
- }
16882
- if (t === COMPLETE || t === ERROR) {
16883
- for (const item of buffer2) {
16884
- actions.emit(item);
16885
- }
16886
- buffer2 = [];
16887
- if (paused) {
16888
- paused = false;
16889
- actions.up([[RESUME, lockId]]);
16890
- }
16891
- actions.down([msg]);
16892
- return true;
17695
+ return result;
17696
+ }
17697
+ function computeTotalHeight(flow) {
17698
+ if (flow.length === 0) return 0;
17699
+ const last2 = flow[flow.length - 1];
17700
+ return last2.y + last2.height;
17701
+ }
17702
+ function reactiveBlockLayout(opts) {
17703
+ const {
17704
+ adapters,
17705
+ name = "reactive-block-layout",
17706
+ defaultFont = "16px sans-serif",
17707
+ defaultLineHeight = 20
17708
+ } = opts;
17709
+ const g = new Graph(name);
17710
+ const measureCache = /* @__PURE__ */ new Map();
17711
+ const blocksNode = state(opts.blocks ?? [], { name: "blocks" });
17712
+ const maxWidthNode = state(Math.max(0, opts.maxWidth ?? 800), { name: "max-width" });
17713
+ const gapNode = state(opts.gap ?? 0, { name: "gap" });
17714
+ const measuredBlocksNode = derived(
17715
+ [blocksNode, maxWidthNode],
17716
+ ([blocksVal, mwVal]) => {
17717
+ const t0 = monotonicNs();
17718
+ const result = measureBlocks(
17719
+ blocksVal,
17720
+ mwVal,
17721
+ adapters,
17722
+ measureCache,
17723
+ defaultFont,
17724
+ defaultLineHeight
17725
+ );
17726
+ const elapsed = monotonicNs() - t0;
17727
+ const meta = measuredBlocksNode.meta;
17728
+ if (meta) {
17729
+ emitWithBatch((msgs) => meta["block-count"]?.down(msgs), [[DATA, result.length]], 3);
17730
+ emitWithBatch((msgs) => meta["layout-time-ns"]?.down(msgs), [[DATA, elapsed]], 3);
17731
+ }
17732
+ return result;
17733
+ },
17734
+ {
17735
+ name: "measured-blocks",
17736
+ meta: { "block-count": 0, "layout-time-ns": 0 },
17737
+ onMessage(msg, _depIndex, _actions) {
17738
+ if (msg[0] === INVALIDATE || msg[0] === TEARDOWN) {
17739
+ measureCache.clear();
17740
+ adapters.text.clearCache?.();
16893
17741
  }
16894
17742
  return false;
16895
- }
16896
- if (t === DATA || t === RESOLVED) {
16897
- if (checkBudget() && buffer2.length > 0) {
16898
- flushBuffer(actions);
16899
- if (buffer2.length === 0 && paused) {
16900
- paused = false;
16901
- actions.up([[RESUME, lockId]]);
16902
- }
16903
- } else if (!checkBudget() && !paused && buffer2.length > 0) {
16904
- paused = true;
16905
- actions.up([[PAUSE, lockId]]);
17743
+ },
17744
+ equals: (a, b) => {
17745
+ const ma = a;
17746
+ const mb = b;
17747
+ if (ma == null || mb == null) return ma === mb;
17748
+ if (ma.length !== mb.length) return false;
17749
+ for (let i = 0; i < ma.length; i++) {
17750
+ const ba = ma[i];
17751
+ const bb = mb[i];
17752
+ if (ba.type !== bb.type || ba.width !== bb.width || ba.height !== bb.height || ba.index !== bb.index)
17753
+ return false;
16906
17754
  }
16907
17755
  return true;
16908
17756
  }
16909
- if (t === DIRTY) {
16910
- return true;
16911
- }
16912
- if (t === ERROR) {
16913
- actions.down([msg]);
16914
- return true;
16915
- }
16916
- if (t === COMPLETE) {
16917
- return true;
16918
- }
16919
- return false;
16920
17757
  }
16921
- });
16922
- }
16923
- function scorer(sources, weights, opts) {
16924
- if (sources.length === 0) throw new RangeError("scorer requires at least one source");
16925
- if (sources.length !== weights.length) {
16926
- throw new RangeError("scorer requires the same number of sources and weights");
16927
- }
16928
- const allDeps = [...sources, ...weights];
16929
- const n = sources.length;
16930
- const scoreFns = opts?.scoreFns;
16931
- return derived(
16932
- allDeps,
16933
- (vals) => {
16934
- const signals = vals.slice(0, n);
16935
- const weightValues = vals.slice(n);
16936
- const breakdown = [];
16937
- let totalScore = 0;
16938
- for (let i = 0; i < n; i++) {
16939
- const sig = signals[i] ?? 0;
16940
- const wt = weightValues[i] ?? 0;
16941
- const rawScore = scoreFns?.[i] ? scoreFns[i](sig) : sig;
16942
- const weighted = rawScore * wt;
16943
- breakdown.push(weighted);
16944
- totalScore += weighted;
16945
- }
16946
- return {
16947
- value: signals,
16948
- score: totalScore,
16949
- breakdown
16950
- };
17758
+ );
17759
+ const blockFlowNode = derived(
17760
+ [measuredBlocksNode, gapNode],
17761
+ ([measured, gapVal]) => {
17762
+ return computeBlockFlow(measured, gapVal);
16951
17763
  },
16952
17764
  {
16953
- ...opts,
16954
- describeKind: "derived",
16955
- meta: baseMeta2("scorer", opts?.meta)
17765
+ name: "block-flow",
17766
+ equals: (a, b) => {
17767
+ const fa = a;
17768
+ const fb = b;
17769
+ if (fa == null || fb == null) return fa === fb;
17770
+ if (fa.length !== fb.length) return false;
17771
+ for (let i = 0; i < fa.length; i++) {
17772
+ const pa = fa[i];
17773
+ const pb = fb[i];
17774
+ if (pa.x !== pb.x || pa.y !== pb.y || pa.width !== pb.width || pa.height !== pb.height)
17775
+ return false;
17776
+ }
17777
+ return true;
17778
+ }
16956
17779
  }
16957
17780
  );
17781
+ const totalHeightNode = derived(
17782
+ [blockFlowNode],
17783
+ ([flow]) => computeTotalHeight(flow),
17784
+ { name: "total-height" }
17785
+ );
17786
+ g.add("blocks", blocksNode);
17787
+ g.add("max-width", maxWidthNode);
17788
+ g.add("gap", gapNode);
17789
+ g.add("measured-blocks", measuredBlocksNode);
17790
+ g.add("block-flow", blockFlowNode);
17791
+ g.add("total-height", totalHeightNode);
17792
+ g.connect("blocks", "measured-blocks");
17793
+ g.connect("max-width", "measured-blocks");
17794
+ g.connect("measured-blocks", "block-flow");
17795
+ g.connect("gap", "block-flow");
17796
+ g.connect("block-flow", "total-height");
17797
+ return {
17798
+ graph: g,
17799
+ setBlocks: (blocks) => g.set("blocks", blocks),
17800
+ setMaxWidth: (mw) => g.set("max-width", Math.max(0, mw)),
17801
+ setGap: (gap) => g.set("gap", gap),
17802
+ measuredBlocks: measuredBlocksNode,
17803
+ blockFlow: blockFlowNode,
17804
+ totalHeight: totalHeightNode
17805
+ };
16958
17806
  }
16959
17807
 
16960
17808
  // src/index.ts
@@ -17076,6 +17924,7 @@ var version = "0.0.0";
17076
17924
  gate,
17077
17925
  globToRegExp,
17078
17926
  graph,
17927
+ graphspec,
17079
17928
  interval,
17080
17929
  isBatching,
17081
17930
  isKnownMessageType,