@stackables/bridge 1.0.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/LICENSE +21 -0
- package/README.md +291 -0
- package/build/ExecutionTree.d.ts +50 -0
- package/build/ExecutionTree.js +403 -0
- package/build/bridge-format.d.ts +24 -0
- package/build/bridge-format.js +1056 -0
- package/build/bridge-transform.d.ts +15 -0
- package/build/bridge-transform.js +57 -0
- package/build/index.d.ts +5 -0
- package/build/index.js +3 -0
- package/build/tools/find-object.d.ts +4 -0
- package/build/tools/find-object.js +11 -0
- package/build/tools/http-call.d.ts +34 -0
- package/build/tools/http-call.js +116 -0
- package/build/tools/index.d.ts +43 -0
- package/build/tools/index.js +43 -0
- package/build/tools/lower-case.d.ts +3 -0
- package/build/tools/lower-case.js +3 -0
- package/build/tools/pick-first.d.ts +11 -0
- package/build/tools/pick-first.js +20 -0
- package/build/tools/to-array.d.ts +8 -0
- package/build/tools/to-array.js +8 -0
- package/build/tools/upper-case.d.ts +3 -0
- package/build/tools/upper-case.js +3 -0
- package/build/types.d.ts +218 -0
- package/build/types.js +2 -0
- package/package.json +44 -0
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
import { parsePath } from "./bridge-format.js";
|
|
2
|
+
import { SELF_MODULE } from "./types.js";
|
|
3
|
+
/** Stable string key for the state map */
|
|
4
|
+
function trunkKey(ref) {
|
|
5
|
+
if (ref.element)
|
|
6
|
+
return `${ref.module}:${ref.type}:${ref.field}:*`;
|
|
7
|
+
return `${ref.module}:${ref.type}:${ref.field}${ref.instance != null ? `:${ref.instance}` : ""}`;
|
|
8
|
+
}
|
|
9
|
+
/** Match two trunks (ignoring path and element) */
|
|
10
|
+
function sameTrunk(a, b) {
|
|
11
|
+
return (a.module === b.module &&
|
|
12
|
+
a.type === b.type &&
|
|
13
|
+
a.field === b.field &&
|
|
14
|
+
(a.instance ?? undefined) === (b.instance ?? undefined));
|
|
15
|
+
}
|
|
16
|
+
/** Strict path equality */
|
|
17
|
+
function pathEquals(a, b) {
|
|
18
|
+
return a.length === b.length && a.every((v, i) => v === b[i]);
|
|
19
|
+
}
|
|
20
|
+
/** Set a value at a nested path, creating intermediate objects/arrays as needed */
|
|
21
|
+
function setNested(obj, path, value) {
|
|
22
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
23
|
+
const key = path[i];
|
|
24
|
+
const nextKey = path[i + 1];
|
|
25
|
+
if (obj[key] == null) {
|
|
26
|
+
obj[key] = /^\d+$/.test(nextKey) ? [] : {};
|
|
27
|
+
}
|
|
28
|
+
obj = obj[key];
|
|
29
|
+
}
|
|
30
|
+
if (path.length > 0) {
|
|
31
|
+
obj[path[path.length - 1]] = value;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export class ExecutionTree {
|
|
35
|
+
trunk;
|
|
36
|
+
instructions;
|
|
37
|
+
toolFns;
|
|
38
|
+
context;
|
|
39
|
+
parent;
|
|
40
|
+
state = {};
|
|
41
|
+
bridge;
|
|
42
|
+
toolDepCache = new Map();
|
|
43
|
+
toolDefCache = new Map();
|
|
44
|
+
pipeHandleMap;
|
|
45
|
+
constructor(trunk, instructions, toolFns, context, parent) {
|
|
46
|
+
this.trunk = trunk;
|
|
47
|
+
this.instructions = instructions;
|
|
48
|
+
this.toolFns = toolFns;
|
|
49
|
+
this.context = context;
|
|
50
|
+
this.parent = parent;
|
|
51
|
+
this.bridge = instructions.find((i) => i.kind === "bridge" && i.type === trunk.type && i.field === trunk.field);
|
|
52
|
+
if (this.bridge?.pipeHandles) {
|
|
53
|
+
this.pipeHandleMap = new Map(this.bridge.pipeHandles.map((ph) => [ph.key, ph]));
|
|
54
|
+
}
|
|
55
|
+
if (context) {
|
|
56
|
+
this.state[trunkKey({ module: SELF_MODULE, type: "Context", field: "context" })] = context;
|
|
57
|
+
}
|
|
58
|
+
// Collect const definitions into a single namespace object
|
|
59
|
+
const constObj = {};
|
|
60
|
+
for (const inst of instructions) {
|
|
61
|
+
if (inst.kind === "const") {
|
|
62
|
+
constObj[inst.name] = JSON.parse(inst.value);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (Object.keys(constObj).length > 0) {
|
|
66
|
+
this.state[trunkKey({ module: SELF_MODULE, type: "Const", field: "const" })] = constObj;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/** Derive tool name from a trunk */
|
|
70
|
+
getToolName(target) {
|
|
71
|
+
if (target.module === SELF_MODULE)
|
|
72
|
+
return target.field;
|
|
73
|
+
return `${target.module}.${target.field}`;
|
|
74
|
+
}
|
|
75
|
+
/** Deep-lookup a tool function by dotted name (e.g. "std.upperCase").
|
|
76
|
+
* Falls back to a flat key lookup for backward compat (e.g. "hereapi.geocode" as literal key). */
|
|
77
|
+
lookupToolFn(name) {
|
|
78
|
+
if (name.includes(".")) {
|
|
79
|
+
// Try namespace traversal first
|
|
80
|
+
const parts = name.split(".");
|
|
81
|
+
let current = this.toolFns;
|
|
82
|
+
for (const part of parts) {
|
|
83
|
+
if (current == null || typeof current !== "object") {
|
|
84
|
+
current = undefined;
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
current = current[part];
|
|
88
|
+
}
|
|
89
|
+
if (typeof current === "function")
|
|
90
|
+
return current;
|
|
91
|
+
// Fall back to flat key (e.g. "hereapi.geocode" as a literal property name)
|
|
92
|
+
const flat = this.toolFns?.[name];
|
|
93
|
+
return typeof flat === "function" ? flat : undefined;
|
|
94
|
+
}
|
|
95
|
+
// Try root level first
|
|
96
|
+
const fn = this.toolFns?.[name];
|
|
97
|
+
if (typeof fn === "function")
|
|
98
|
+
return fn;
|
|
99
|
+
// Fall back to std namespace (builtins are callable without std. prefix)
|
|
100
|
+
const stdFn = this.toolFns?.std?.[name];
|
|
101
|
+
return typeof stdFn === "function" ? stdFn : undefined;
|
|
102
|
+
}
|
|
103
|
+
/** Resolve a ToolDef by name, merging the extends chain (cached) */
|
|
104
|
+
resolveToolDefByName(name) {
|
|
105
|
+
if (this.toolDefCache.has(name))
|
|
106
|
+
return this.toolDefCache.get(name) ?? undefined;
|
|
107
|
+
const toolDefs = this.instructions.filter((i) => i.kind === "tool");
|
|
108
|
+
const base = toolDefs.find((t) => t.name === name);
|
|
109
|
+
if (!base) {
|
|
110
|
+
this.toolDefCache.set(name, null);
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
// Build extends chain: root → ... → leaf
|
|
114
|
+
const chain = [base];
|
|
115
|
+
let current = base;
|
|
116
|
+
while (current.extends) {
|
|
117
|
+
const parent = toolDefs.find((t) => t.name === current.extends);
|
|
118
|
+
if (!parent)
|
|
119
|
+
throw new Error(`Tool "${current.name}" extends unknown tool "${current.extends}"`);
|
|
120
|
+
chain.unshift(parent);
|
|
121
|
+
current = parent;
|
|
122
|
+
}
|
|
123
|
+
// Merge: root provides base, each child overrides
|
|
124
|
+
const merged = {
|
|
125
|
+
kind: "tool",
|
|
126
|
+
name,
|
|
127
|
+
fn: chain[0].fn, // fn from root ancestor
|
|
128
|
+
deps: [],
|
|
129
|
+
wires: [],
|
|
130
|
+
};
|
|
131
|
+
for (const def of chain) {
|
|
132
|
+
// Merge deps (dedupe by handle)
|
|
133
|
+
for (const dep of def.deps) {
|
|
134
|
+
if (!merged.deps.some((d) => d.handle === dep.handle)) {
|
|
135
|
+
merged.deps.push(dep);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Merge wires (child overrides parent by target; onError replaces onError)
|
|
139
|
+
for (const wire of def.wires) {
|
|
140
|
+
if (wire.kind === "onError") {
|
|
141
|
+
const idx = merged.wires.findIndex((w) => w.kind === "onError");
|
|
142
|
+
if (idx >= 0)
|
|
143
|
+
merged.wires[idx] = wire;
|
|
144
|
+
else
|
|
145
|
+
merged.wires.push(wire);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
const idx = merged.wires.findIndex((w) => "target" in w && w.target === wire.target);
|
|
149
|
+
if (idx >= 0)
|
|
150
|
+
merged.wires[idx] = wire;
|
|
151
|
+
else
|
|
152
|
+
merged.wires.push(wire);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
this.toolDefCache.set(name, merged);
|
|
157
|
+
return merged;
|
|
158
|
+
}
|
|
159
|
+
/** Resolve a tool definition's wires into a nested input object */
|
|
160
|
+
async resolveToolWires(toolDef, input) {
|
|
161
|
+
// Constants applied synchronously
|
|
162
|
+
for (const wire of toolDef.wires) {
|
|
163
|
+
if (wire.kind === "constant") {
|
|
164
|
+
setNested(input, parsePath(wire.target), wire.value);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// Pull wires resolved in parallel (independent deps shouldn't wait on each other)
|
|
168
|
+
const pullWires = toolDef.wires.filter((w) => w.kind === "pull");
|
|
169
|
+
if (pullWires.length > 0) {
|
|
170
|
+
const resolved = await Promise.all(pullWires.map(async (wire) => ({
|
|
171
|
+
target: wire.target,
|
|
172
|
+
value: await this.resolveToolSource(wire.source, toolDef),
|
|
173
|
+
})));
|
|
174
|
+
for (const { target, value } of resolved) {
|
|
175
|
+
setNested(input, parsePath(target), value);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/** Resolve a source reference from a tool wire against its dependencies */
|
|
180
|
+
async resolveToolSource(source, toolDef) {
|
|
181
|
+
const dotIdx = source.indexOf(".");
|
|
182
|
+
const handle = dotIdx === -1 ? source : source.substring(0, dotIdx);
|
|
183
|
+
const restPath = dotIdx === -1 ? [] : source.substring(dotIdx + 1).split(".");
|
|
184
|
+
const dep = toolDef.deps.find((d) => d.handle === handle);
|
|
185
|
+
if (!dep)
|
|
186
|
+
throw new Error(`Unknown source "${handle}" in tool "${toolDef.name}"`);
|
|
187
|
+
let value;
|
|
188
|
+
if (dep.kind === "context") {
|
|
189
|
+
value = this.context ?? this.parent?.context;
|
|
190
|
+
}
|
|
191
|
+
else if (dep.kind === "const") {
|
|
192
|
+
value = this.state[trunkKey({ module: SELF_MODULE, type: "Const", field: "const" })] ?? this.parent?.state[trunkKey({ module: SELF_MODULE, type: "Const", field: "const" })];
|
|
193
|
+
}
|
|
194
|
+
else if (dep.kind === "tool") {
|
|
195
|
+
value = await this.resolveToolDep(dep.tool);
|
|
196
|
+
}
|
|
197
|
+
for (const segment of restPath) {
|
|
198
|
+
value = value?.[segment];
|
|
199
|
+
}
|
|
200
|
+
return value;
|
|
201
|
+
}
|
|
202
|
+
/** Call a tool dependency (cached per request) */
|
|
203
|
+
resolveToolDep(toolName) {
|
|
204
|
+
// Check parent first (shadow trees delegate)
|
|
205
|
+
if (this.parent)
|
|
206
|
+
return this.parent.resolveToolDep(toolName);
|
|
207
|
+
if (this.toolDepCache.has(toolName))
|
|
208
|
+
return this.toolDepCache.get(toolName);
|
|
209
|
+
const promise = (async () => {
|
|
210
|
+
const toolDef = this.resolveToolDefByName(toolName);
|
|
211
|
+
if (!toolDef)
|
|
212
|
+
throw new Error(`Tool dependency "${toolName}" not found`);
|
|
213
|
+
const input = {};
|
|
214
|
+
await this.resolveToolWires(toolDef, input);
|
|
215
|
+
const fn = this.lookupToolFn(toolDef.fn);
|
|
216
|
+
if (!fn)
|
|
217
|
+
throw new Error(`Tool function "${toolDef.fn}" not registered`);
|
|
218
|
+
// on error: wrap the tool call with fallback from onError wire
|
|
219
|
+
const onErrorWire = toolDef.wires.find((w) => w.kind === "onError");
|
|
220
|
+
try {
|
|
221
|
+
return await fn(input);
|
|
222
|
+
}
|
|
223
|
+
catch (err) {
|
|
224
|
+
if (!onErrorWire)
|
|
225
|
+
throw err;
|
|
226
|
+
if ("value" in onErrorWire)
|
|
227
|
+
return JSON.parse(onErrorWire.value);
|
|
228
|
+
return this.resolveToolSource(onErrorWire.source, toolDef);
|
|
229
|
+
}
|
|
230
|
+
})();
|
|
231
|
+
this.toolDepCache.set(toolName, promise);
|
|
232
|
+
return promise;
|
|
233
|
+
}
|
|
234
|
+
schedule(target) {
|
|
235
|
+
// Delegate to parent (shadow trees don't schedule directly)
|
|
236
|
+
if (this.parent) {
|
|
237
|
+
return this.parent.schedule(target);
|
|
238
|
+
}
|
|
239
|
+
return (async () => {
|
|
240
|
+
// If this target is a pipe fork, also apply bridge wires from its base
|
|
241
|
+
// handle (non-pipe wires, e.g. `c.currency <- i.currency`) as defaults
|
|
242
|
+
// before the fork-specific pipe wires.
|
|
243
|
+
const targetKey = trunkKey(target);
|
|
244
|
+
const pipeFork = this.pipeHandleMap?.get(targetKey);
|
|
245
|
+
const baseTrunk = pipeFork?.baseTrunk;
|
|
246
|
+
const baseWires = baseTrunk
|
|
247
|
+
? (this.bridge?.wires.filter((w) => !("pipe" in w) && sameTrunk(w.to, baseTrunk)) ?? [])
|
|
248
|
+
: [];
|
|
249
|
+
// Fork-specific wires (pipe wires targeting the fork's own instance)
|
|
250
|
+
const forkWires = this.bridge?.wires.filter((w) => sameTrunk(w.to, target)) ?? [];
|
|
251
|
+
// Merge: base provides defaults, fork overrides
|
|
252
|
+
const bridgeWires = [...baseWires, ...forkWires];
|
|
253
|
+
// Look up ToolDef for this target
|
|
254
|
+
const toolName = this.getToolName(target);
|
|
255
|
+
const toolDef = this.resolveToolDefByName(toolName);
|
|
256
|
+
// Build input object: tool wires first (base), then bridge wires (override)
|
|
257
|
+
const input = {};
|
|
258
|
+
if (toolDef) {
|
|
259
|
+
await this.resolveToolWires(toolDef, input);
|
|
260
|
+
}
|
|
261
|
+
// Resolve bridge wires and apply on top
|
|
262
|
+
const resolved = await Promise.all(bridgeWires.map(async (w) => {
|
|
263
|
+
const value = "value" in w ? w.value : await this.pullSingle(w.from);
|
|
264
|
+
return [w.to.path, value];
|
|
265
|
+
}));
|
|
266
|
+
for (const [path, value] of resolved) {
|
|
267
|
+
setNested(input, path, value);
|
|
268
|
+
}
|
|
269
|
+
// Call ToolDef-backed tool function
|
|
270
|
+
if (toolDef) {
|
|
271
|
+
const fn = this.lookupToolFn(toolDef.fn);
|
|
272
|
+
if (!fn)
|
|
273
|
+
throw new Error(`Tool function "${toolDef.fn}" not registered`);
|
|
274
|
+
// on error: wrap the tool call with fallback from onError wire
|
|
275
|
+
const onErrorWire = toolDef.wires.find((w) => w.kind === "onError");
|
|
276
|
+
try {
|
|
277
|
+
return await fn(input);
|
|
278
|
+
}
|
|
279
|
+
catch (err) {
|
|
280
|
+
if (!onErrorWire)
|
|
281
|
+
throw err;
|
|
282
|
+
if ("value" in onErrorWire)
|
|
283
|
+
return JSON.parse(onErrorWire.value);
|
|
284
|
+
return this.resolveToolSource(onErrorWire.source, toolDef);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
// Direct tool function lookup by name (simple or dotted)
|
|
288
|
+
const directFn = this.lookupToolFn(toolName);
|
|
289
|
+
if (directFn) {
|
|
290
|
+
return directFn(input);
|
|
291
|
+
}
|
|
292
|
+
throw new Error(`No tool found for "${toolName}"`);
|
|
293
|
+
})();
|
|
294
|
+
}
|
|
295
|
+
shadow() {
|
|
296
|
+
return new ExecutionTree(this.trunk, this.instructions, this.toolFns, undefined, this);
|
|
297
|
+
}
|
|
298
|
+
async pullSingle(ref) {
|
|
299
|
+
const key = trunkKey(ref);
|
|
300
|
+
let value = this.state[key] ?? this.parent?.state[key];
|
|
301
|
+
if (value === undefined) {
|
|
302
|
+
this.state[key] = this.schedule(ref);
|
|
303
|
+
value = this.state[key];
|
|
304
|
+
}
|
|
305
|
+
// Always await in case the stored value is a Promise (e.g. from schedule()).
|
|
306
|
+
const resolved = await Promise.resolve(value);
|
|
307
|
+
if (!ref.path.length) {
|
|
308
|
+
return resolved;
|
|
309
|
+
}
|
|
310
|
+
let result = resolved;
|
|
311
|
+
for (const segment of ref.path) {
|
|
312
|
+
if (Array.isArray(result) && !/^\d+$/.test(segment)) {
|
|
313
|
+
console.warn(`[bridge] Accessing ".${segment}" on an array (${result.length} items) — did you mean to use pickFirst or array mapping? Source: ${trunkKey(ref)}.${ref.path.join(".")}`);
|
|
314
|
+
}
|
|
315
|
+
result = result?.[segment];
|
|
316
|
+
}
|
|
317
|
+
return result;
|
|
318
|
+
}
|
|
319
|
+
async pull(refs) {
|
|
320
|
+
return Promise.any(refs.map((ref) => this.pullSingle(ref)));
|
|
321
|
+
}
|
|
322
|
+
push(args) {
|
|
323
|
+
this.state[trunkKey(this.trunk)] = args;
|
|
324
|
+
}
|
|
325
|
+
/** Eagerly schedule tools targeted by forced (<-!) wires. */
|
|
326
|
+
executeForced() {
|
|
327
|
+
const forcedWires = this.bridge?.wires.filter((w) => "from" in w && !!w.force) ?? [];
|
|
328
|
+
const scheduled = new Set();
|
|
329
|
+
for (const wire of forcedWires) {
|
|
330
|
+
// For pipe wires the target is the fork trunk; for regular wires it's
|
|
331
|
+
// the tool trunk. In both cases scheduling the target kicks off
|
|
332
|
+
// resolution of all its input wires (including the forced source).
|
|
333
|
+
const key = trunkKey(wire.to);
|
|
334
|
+
if (scheduled.has(key) || this.state[key] !== undefined)
|
|
335
|
+
continue;
|
|
336
|
+
scheduled.add(key);
|
|
337
|
+
this.state[key] = this.schedule(wire.to);
|
|
338
|
+
// Fire-and-forget: suppress unhandled rejection for side-effect tools
|
|
339
|
+
// whose output is never consumed.
|
|
340
|
+
Promise.resolve(this.state[key]).catch(() => { });
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
/** Resolve a set of matched wires — constants win, then pull from sources.\n * If a wire has a `fallback` value and all sources reject, return the fallback. */
|
|
344
|
+
resolveWires(wires) {
|
|
345
|
+
const constant = wires.find((w) => "value" in w);
|
|
346
|
+
if (constant)
|
|
347
|
+
return Promise.resolve(constant.value);
|
|
348
|
+
const pulls = wires.filter((w) => "from" in w);
|
|
349
|
+
// Collect any fallback value from the wires (first one wins)
|
|
350
|
+
const fallbackWire = pulls.find((w) => w.fallback != null);
|
|
351
|
+
const result = this.pull(pulls.map((w) => w.from));
|
|
352
|
+
if (!fallbackWire)
|
|
353
|
+
return result;
|
|
354
|
+
return result.catch(() => {
|
|
355
|
+
try {
|
|
356
|
+
return JSON.parse(fallbackWire.fallback);
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
// Not valid JSON — return as raw string
|
|
360
|
+
return fallbackWire.fallback;
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
async response(ipath, array) {
|
|
365
|
+
// Build path segments from GraphQL resolver info
|
|
366
|
+
const pathSegments = [];
|
|
367
|
+
let index = ipath;
|
|
368
|
+
while (index.prev) {
|
|
369
|
+
pathSegments.unshift(`${index.key}`);
|
|
370
|
+
index = index.prev;
|
|
371
|
+
}
|
|
372
|
+
if (pathSegments.length === 0) {
|
|
373
|
+
// Direct output for scalar/list return types (e.g. [String!])
|
|
374
|
+
const directOutput = this.bridge?.wires.filter((w) => sameTrunk(w.to, this.trunk) &&
|
|
375
|
+
w.to.path.length === 1 &&
|
|
376
|
+
w.to.path[0] === this.trunk.field) ?? [];
|
|
377
|
+
if (directOutput.length > 0) {
|
|
378
|
+
return this.resolveWires(directOutput);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
// Strip numeric indices (array positions) from path for wire matching
|
|
382
|
+
const cleanPath = pathSegments.filter((p) => !/^\d+$/.test(p));
|
|
383
|
+
// Find wires whose target matches this trunk + path
|
|
384
|
+
const matches = this.bridge?.wires.filter((w) => !w.to.element &&
|
|
385
|
+
sameTrunk(w.to, this.trunk) &&
|
|
386
|
+
pathEquals(w.to.path, cleanPath)) ?? [];
|
|
387
|
+
if (matches.length > 0) {
|
|
388
|
+
const response = this.resolveWires(matches);
|
|
389
|
+
if (!array) {
|
|
390
|
+
return response;
|
|
391
|
+
}
|
|
392
|
+
// Array: create shadow trees for per-element resolution
|
|
393
|
+
const items = (await response);
|
|
394
|
+
return items.map((item) => {
|
|
395
|
+
const s = this.shadow();
|
|
396
|
+
s.state[trunkKey({ ...this.trunk, element: true })] = item;
|
|
397
|
+
return s;
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
// Return self to trigger downstream resolvers
|
|
401
|
+
return this;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Instruction } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Parse .bridge text format into structured instructions.
|
|
4
|
+
*
|
|
5
|
+
* The .bridge format is a human-readable representation of connection wires.
|
|
6
|
+
* Multiple blocks are separated by `---`.
|
|
7
|
+
* Tool blocks define API tools, bridge blocks define wire mappings.
|
|
8
|
+
*
|
|
9
|
+
* @param text - Bridge definition text
|
|
10
|
+
* @returns Array of instructions (Bridge, ToolDef)
|
|
11
|
+
*/
|
|
12
|
+
export declare function parseBridge(text: string): Instruction[];
|
|
13
|
+
/**
|
|
14
|
+
* Parse a dot-separated path with optional array indices.
|
|
15
|
+
*
|
|
16
|
+
* "items[0].position.lat" → ["items", "0", "position", "lat"]
|
|
17
|
+
* "properties[]" → ["properties"] ([] is stripped, signals array)
|
|
18
|
+
* "x-message-id" → ["x-message-id"]
|
|
19
|
+
*/
|
|
20
|
+
export declare function parsePath(text: string): string[];
|
|
21
|
+
/**
|
|
22
|
+
* Serialize structured instructions back to .bridge text format.
|
|
23
|
+
*/
|
|
24
|
+
export declare function serializeBridge(instructions: Instruction[]): string;
|