deepline 0.2.53 → 0.2.55
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/bundling-sources/sdk/src/client.ts +17 -1
- package/dist/bundling-sources/sdk/src/plays/bundle-play-file.ts +1 -1
- package/dist/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/sdk/src/types.ts +43 -1
- package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +30 -1
- package/dist/bundling-sources/shared_libs/play-runtime/cell-provenance.ts +231 -0
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +1094 -128
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +178 -9
- package/dist/bundling-sources/shared_libs/play-runtime/docflow-node-io.ts +634 -0
- package/dist/bundling-sources/shared_libs/play-runtime/docflow-observation.ts +64 -0
- package/dist/bundling-sources/shared_libs/play-runtime/dynamic-worker-version.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/execution-capabilities.ts +18 -0
- package/dist/bundling-sources/shared_libs/play-runtime/live-state-contract.ts +33 -0
- package/dist/bundling-sources/shared_libs/play-runtime/log-provenance.ts +251 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-node-scope.ts +160 -0
- package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +6 -0
- package/dist/bundling-sources/shared_libs/play-runtime/run-failure.ts +27 -0
- package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +43 -5
- package/dist/bundling-sources/shared_libs/play-runtime/run-snapshot-stream.ts +12 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/local-process.ts +26 -4
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-actions.ts +6 -1
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +83 -0
- package/dist/bundling-sources/shared_libs/play-runtime/worker-api-types.ts +3 -0
- package/dist/bundling-sources/shared_libs/plays/authoring-contract.ts +49 -1
- package/dist/bundling-sources/shared_libs/plays/bundling/index.ts +375 -29
- package/dist/bundling-sources/shared_libs/plays/docflow-binding-owner.ts +636 -0
- package/dist/bundling-sources/shared_libs/plays/docflow-binding.ts +598 -0
- package/dist/bundling-sources/shared_libs/plays/docflow.ts +1645 -0
- package/dist/bundling-sources/shared_libs/plays/play-exports.ts +202 -0
- package/dist/bundling-sources/shared_libs/plays/static-pipeline.ts +16 -1
- package/dist/bundling-sources/shared_libs/plays/ts-ast.ts +48 -0
- package/dist/cli/index.js +994 -312
- package/dist/cli/index.mjs +994 -312
- package/dist/{compiler-manifest-Cj3--4ZJ.d.mts → compiler-manifest-Bl8kmLx9.d.mts} +118 -0
- package/dist/{compiler-manifest-Cj3--4ZJ.d.ts → compiler-manifest-Bl8kmLx9.d.ts} +118 -0
- package/dist/index.d.mts +47 -2
- package/dist/index.d.ts +47 -2
- package/dist/index.js +419 -59
- package/dist/index.mjs +419 -59
- package/dist/install-integrity.json +12 -2
- package/dist/plays/bundle-play-file.d.mts +2 -2
- package/dist/plays/bundle-play-file.d.ts +2 -2
- package/dist/plays/bundle-play-file.mjs +1361 -45
- package/package.json +1 -1
|
@@ -0,0 +1,636 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One join, one rule: which compiled substep does a docflow binding belong to?
|
|
3
|
+
*
|
|
4
|
+
* The dashboard used to answer this six separate times with the same
|
|
5
|
+
* expression — `binding.line >= substep.sourceRange.startLine && binding.line
|
|
6
|
+
* <= substep.sourceRange.endLine`. That compares two DIFFERENT constructs by
|
|
7
|
+
* containment and is wrong on every chained play: static analysis sets a
|
|
8
|
+
* dataset stage's `sourceRange` to `ctx.dataset(key, rows)` alone (the head of
|
|
9
|
+
* the chain), while the members that belong to it annotate `.withColumn(...)`
|
|
10
|
+
* calls further down. On `find-ctos` the stage spans lines 54–55 and its four
|
|
11
|
+
* members bind at 57, 79, 84 and 89 — the join returned nothing, so loop
|
|
12
|
+
* members got no coverage chip and no provider logo.
|
|
13
|
+
*
|
|
14
|
+
* The docflow language already carries a structural key, and lint already
|
|
15
|
+
* enforces it, so the join does not need a coordinate:
|
|
16
|
+
*
|
|
17
|
+
* - A **subgraph member**'s `out:` names per-row COLUMNS of the dataset it
|
|
18
|
+
* annotates. `docflow_loop_incomplete` validates those names against the
|
|
19
|
+
* dataset's real computed columns.
|
|
20
|
+
* - Any **other** binding's `out:` names the ASSIGNED VARIABLE of the annotated
|
|
21
|
+
* statement. `docflow_output_not_found` is a hard `plays check` error when it
|
|
22
|
+
* does not.
|
|
23
|
+
*
|
|
24
|
+
* So: symbol first. A binding whose `out:` root names a column a dataset
|
|
25
|
+
* computes belongs to that dataset, wherever either one sits in the file. That
|
|
26
|
+
* key survives reformatting, an inserted comment, and a change in how the AST
|
|
27
|
+
* walk shapes ranges — the failures that produced this bug.
|
|
28
|
+
*
|
|
29
|
+
* Position remains the fallback for exactly the case where the language gives
|
|
30
|
+
* no symbol to join on: an assignment-named node (`out:"searchResult"` on a
|
|
31
|
+
* `ctx.tools.execute` statement, `out:"companies"` on the dataset statement
|
|
32
|
+
* itself), where the compiler's own name for the substep (`company_search`,
|
|
33
|
+
* `cto_targets`) is by design NOT the author's variable. This mirrors ADR 0016
|
|
34
|
+
* rule 1, which resolves a binding by symbol and keeps position as the fallback
|
|
35
|
+
* when the symbol abstains.
|
|
36
|
+
*
|
|
37
|
+
* The fallback compares like with like, which the old join did not: a
|
|
38
|
+
* substep's extent is its own range UNIONED with every nested step's range —
|
|
39
|
+
* the whole construct the substep owns, not its head call. Among containing
|
|
40
|
+
* candidates the smallest extent wins, so a dataset nested inside another
|
|
41
|
+
* dataset's per-row body resolves to the inner one.
|
|
42
|
+
*
|
|
43
|
+
* Failure is a state, not a default. When no substep owns a binding the
|
|
44
|
+
* resolution says so (`reason`), and callers render nothing rather than
|
|
45
|
+
* borrowing a neighbour's numbers or logo. The loud gate lives upstream where
|
|
46
|
+
* it belongs: `plays check` already fails a binding whose `out:` names neither
|
|
47
|
+
* a real column nor the assigned variable.
|
|
48
|
+
*/
|
|
49
|
+
import type { PlayDocflow, PlayDocflowBinding } from './docflow';
|
|
50
|
+
import type {
|
|
51
|
+
PlayStaticSourceRange,
|
|
52
|
+
PlayStaticSubstep,
|
|
53
|
+
} from './static-pipeline';
|
|
54
|
+
|
|
55
|
+
export type DocflowDatasetSubstep = Extract<
|
|
56
|
+
PlayStaticSubstep,
|
|
57
|
+
{ type: 'dataset' }
|
|
58
|
+
>;
|
|
59
|
+
export type DocflowOwnerSubstep = Extract<
|
|
60
|
+
PlayStaticSubstep,
|
|
61
|
+
{ type: 'dataset' | 'csv' }
|
|
62
|
+
>;
|
|
63
|
+
export type DocflowToolSubstep = Extract<PlayStaticSubstep, { type: 'tool' }>;
|
|
64
|
+
export type DocflowPlayCallSubstep = Extract<
|
|
65
|
+
PlayStaticSubstep,
|
|
66
|
+
{ type: 'play_call' }
|
|
67
|
+
>;
|
|
68
|
+
export type DocflowStepSuiteSubstep = Extract<
|
|
69
|
+
PlayStaticSubstep,
|
|
70
|
+
{ type: 'step_suite' }
|
|
71
|
+
>;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Every substep in a pipeline tree, parents before children. Top-level substeps
|
|
75
|
+
* are not enough for child plays: measured on the shipped prebuilts, the only
|
|
76
|
+
* real `ctx.runPlay` call site (`people-search-to-email`) is a `play_call`
|
|
77
|
+
* nested inside a dataset's per-row body, so a top-level-only scan finds none.
|
|
78
|
+
*/
|
|
79
|
+
export function flattenSubsteps(
|
|
80
|
+
substeps: readonly PlayStaticSubstep[],
|
|
81
|
+
): PlayStaticSubstep[] {
|
|
82
|
+
const flat: PlayStaticSubstep[] = [];
|
|
83
|
+
const visit = (candidate: PlayStaticSubstep) => {
|
|
84
|
+
flat.push(candidate);
|
|
85
|
+
const nested = (candidate as { steps?: PlayStaticSubstep[] }).steps;
|
|
86
|
+
if (Array.isArray(nested)) for (const step of nested) visit(step);
|
|
87
|
+
};
|
|
88
|
+
for (const substep of substeps) visit(substep);
|
|
89
|
+
return flat;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** How a binding reached its substep — see the module header for the rules. */
|
|
93
|
+
export type DocflowJoinVia = 'symbol' | 'extent';
|
|
94
|
+
|
|
95
|
+
export type DocflowJoinFailure =
|
|
96
|
+
/** The binding names no joinable symbol and no substep extent contains it. */
|
|
97
|
+
| 'no-owner'
|
|
98
|
+
/** Two or more substeps claim the symbol and none contains the line. */
|
|
99
|
+
| 'ambiguous';
|
|
100
|
+
|
|
101
|
+
export type DocflowBindingJoin<TSubstep> =
|
|
102
|
+
| { substep: TSubstep; via: DocflowJoinVia; reason?: undefined }
|
|
103
|
+
| { substep: null; via?: undefined; reason: DocflowJoinFailure };
|
|
104
|
+
|
|
105
|
+
export type DocflowLineExtent = { startLine: number; endLine: number };
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The line span of the whole construct a substep owns: its own range unioned
|
|
109
|
+
* with every nested step's range. A `ctx.dataset(...)` chain's own range covers
|
|
110
|
+
* only the head call, so a dataset's per-row body is only visible once the
|
|
111
|
+
* `.withColumn(...)` steps are folded in.
|
|
112
|
+
*/
|
|
113
|
+
export function substepSourceExtent(
|
|
114
|
+
substep: PlayStaticSubstep,
|
|
115
|
+
): DocflowLineExtent | null {
|
|
116
|
+
const ranges: PlayStaticSourceRange[] = [];
|
|
117
|
+
const collect = (candidate: PlayStaticSubstep) => {
|
|
118
|
+
if (candidate.sourceRange) ranges.push(candidate.sourceRange);
|
|
119
|
+
const nested = (candidate as { steps?: PlayStaticSubstep[] }).steps;
|
|
120
|
+
if (Array.isArray(nested)) for (const step of nested) collect(step);
|
|
121
|
+
};
|
|
122
|
+
collect(substep);
|
|
123
|
+
if (ranges.length === 0) return null;
|
|
124
|
+
return {
|
|
125
|
+
startLine: Math.min(...ranges.map((range) => range.startLine)),
|
|
126
|
+
endLine: Math.max(...ranges.map((range) => range.endLine)),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The distinct first path segments of a binding's `out:` contract, dropping
|
|
132
|
+
* `$output` (a return value names no substep symbol).
|
|
133
|
+
*/
|
|
134
|
+
export function docflowOutputRoots(binding: PlayDocflowBinding): string[] {
|
|
135
|
+
return [
|
|
136
|
+
...new Set(
|
|
137
|
+
(binding.outputs ?? [])
|
|
138
|
+
.map((path) => path.split('.')[0] ?? '')
|
|
139
|
+
.filter((root) => root.length > 0 && root !== '$output'),
|
|
140
|
+
),
|
|
141
|
+
];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The names of the per-row columns a dataset computes — the exact set a loop
|
|
146
|
+
* member's `out:` is validated against by `docflow_loop_incomplete`. Seed/input
|
|
147
|
+
* columns are excluded on purpose: a member claims work the dataset DOES, not a
|
|
148
|
+
* column it merely carries.
|
|
149
|
+
*/
|
|
150
|
+
export function datasetComputedColumnNames(
|
|
151
|
+
substep: DocflowDatasetSubstep,
|
|
152
|
+
): Set<string> {
|
|
153
|
+
const names = new Set<string>();
|
|
154
|
+
const add = (name: string | undefined | null) => {
|
|
155
|
+
if (name && name !== 'row_number') names.add(name);
|
|
156
|
+
};
|
|
157
|
+
for (const step of substep.steps ?? []) {
|
|
158
|
+
if ('field' in step) add(step.field);
|
|
159
|
+
}
|
|
160
|
+
for (const column of substep.columns ?? []) {
|
|
161
|
+
if (column.source !== 'datasetColumn') continue;
|
|
162
|
+
add(column.sqlName ?? column.id);
|
|
163
|
+
add(column.id);
|
|
164
|
+
for (const producer of column.producers ?? []) {
|
|
165
|
+
add(producer.field.split('.')[0]);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
for (const column of substep.sheetContract?.columns ?? []) {
|
|
169
|
+
if (column.source !== 'datasetColumn') continue;
|
|
170
|
+
add(column.field ?? column.id);
|
|
171
|
+
add(column.sqlName);
|
|
172
|
+
}
|
|
173
|
+
return names;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* The extent a positional fallback measures a binding against. Defaults to the
|
|
178
|
+
* whole construct a substep owns ({@link substepSourceExtent}); a caller whose
|
|
179
|
+
* construct is a single CALL passes {@link substepOwnSourceExtent} instead.
|
|
180
|
+
*/
|
|
181
|
+
export type DocflowExtentResolver = (
|
|
182
|
+
substep: PlayStaticSubstep,
|
|
183
|
+
) => DocflowLineExtent | null;
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* A substep's OWN call range, ignoring nested steps. The right extent for a
|
|
187
|
+
* construct whose annotation sits directly above one call — `ctx.runSteps(...)`
|
|
188
|
+
* — where the union would instead span every leg the builder declares, and for
|
|
189
|
+
* an imported builder those legs live in ANOTHER FILE, so the union is not even
|
|
190
|
+
* a coherent interval.
|
|
191
|
+
*/
|
|
192
|
+
export function substepOwnSourceExtent(
|
|
193
|
+
substep: PlayStaticSubstep,
|
|
194
|
+
): DocflowLineExtent | null {
|
|
195
|
+
const range = substep.sourceRange;
|
|
196
|
+
return range ? { startLine: range.startLine, endLine: range.endLine } : null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function extentContainsLine(
|
|
200
|
+
substep: PlayStaticSubstep,
|
|
201
|
+
line: number,
|
|
202
|
+
extentOf: DocflowExtentResolver,
|
|
203
|
+
): DocflowLineExtent | null {
|
|
204
|
+
const extent = extentOf(substep);
|
|
205
|
+
if (!extent) return null;
|
|
206
|
+
return line >= extent.startLine && line <= extent.endLine ? extent : null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Smallest containing extent wins, so a nested construct beats its parent. */
|
|
210
|
+
function smallestContaining<TSubstep extends PlayStaticSubstep>(
|
|
211
|
+
candidates: readonly TSubstep[],
|
|
212
|
+
line: number,
|
|
213
|
+
extentOf: DocflowExtentResolver = substepSourceExtent,
|
|
214
|
+
): TSubstep | null {
|
|
215
|
+
let best: TSubstep | null = null;
|
|
216
|
+
let bestSpan = Number.POSITIVE_INFINITY;
|
|
217
|
+
for (const candidate of candidates) {
|
|
218
|
+
const extent = extentContainsLine(candidate, line, extentOf);
|
|
219
|
+
if (!extent) continue;
|
|
220
|
+
const span = extent.endLine - extent.startLine;
|
|
221
|
+
if (span < bestSpan) {
|
|
222
|
+
best = candidate;
|
|
223
|
+
bestSpan = span;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return best;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* One call site, identified by what it IS. A pipeline exposes the same construct
|
|
231
|
+
* through more than one tree — the stages tree and the flattened compiled
|
|
232
|
+
* substeps each carry their own copy of a nested `play_call` — so a naive match
|
|
233
|
+
* count sees N copies of one statement and calls it ambiguous. Ambiguity has to
|
|
234
|
+
* mean "two DIFFERENT statements compute this symbol", which is the only case a
|
|
235
|
+
* guess could get wrong.
|
|
236
|
+
*/
|
|
237
|
+
function substepIdentity(substep: PlayStaticSubstep): string {
|
|
238
|
+
const range = substep.sourceRange;
|
|
239
|
+
const position = range
|
|
240
|
+
? `${range.sourcePath ?? ''}:${range.startLine}:${range.startColumn ?? ''}-${range.endLine}:${range.endColumn ?? ''}`
|
|
241
|
+
: 'no-range';
|
|
242
|
+
const name =
|
|
243
|
+
(substep as { field?: string }).field ??
|
|
244
|
+
(substep as { alias?: string }).alias ??
|
|
245
|
+
'';
|
|
246
|
+
return `${substep.type}::${name}::${position}`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function joinBySymbolThenExtent<TSubstep extends PlayStaticSubstep>(
|
|
250
|
+
binding: PlayDocflowBinding,
|
|
251
|
+
candidates: readonly TSubstep[],
|
|
252
|
+
symbolsOf: (substep: TSubstep) => Set<string>,
|
|
253
|
+
extentOf: DocflowExtentResolver = substepSourceExtent,
|
|
254
|
+
): DocflowBindingJoin<TSubstep> {
|
|
255
|
+
const roots = docflowOutputRoots(binding);
|
|
256
|
+
if (roots.length > 0) {
|
|
257
|
+
const byIdentity = new Map<string, TSubstep>();
|
|
258
|
+
for (const candidate of candidates) {
|
|
259
|
+
const symbols = symbolsOf(candidate);
|
|
260
|
+
if (!roots.some((root) => symbols.has(root))) continue;
|
|
261
|
+
const identity = substepIdentity(candidate);
|
|
262
|
+
if (!byIdentity.has(identity)) byIdentity.set(identity, candidate);
|
|
263
|
+
}
|
|
264
|
+
const matches = [...byIdentity.values()];
|
|
265
|
+
if (matches.length === 1) return { substep: matches[0]!, via: 'symbol' };
|
|
266
|
+
if (matches.length > 1) {
|
|
267
|
+
// Two constructs compute the same column name. The one whose body the
|
|
268
|
+
// annotation actually sits in decides; otherwise this is drift, and a
|
|
269
|
+
// guess would attribute a member's rows to the wrong table.
|
|
270
|
+
const narrowed = smallestContaining(matches, binding.line, extentOf);
|
|
271
|
+
if (narrowed) return { substep: narrowed, via: 'symbol' };
|
|
272
|
+
return { substep: null, reason: 'ambiguous' };
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
const positional = smallestContaining(candidates, binding.line, extentOf);
|
|
276
|
+
if (positional) return { substep: positional, via: 'extent' };
|
|
277
|
+
return { substep: null, reason: 'no-owner' };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* The dataset (or CSV) substep a docflow binding belongs to. Datasets carry
|
|
282
|
+
* computed-column symbols; a CSV substep has rows but no per-row column work,
|
|
283
|
+
* so it can only be reached by extent.
|
|
284
|
+
*/
|
|
285
|
+
export function resolveDocflowOwnerSubstep(
|
|
286
|
+
binding: PlayDocflowBinding,
|
|
287
|
+
substeps: readonly PlayStaticSubstep[],
|
|
288
|
+
): DocflowBindingJoin<DocflowOwnerSubstep> {
|
|
289
|
+
const owners = substeps.filter(
|
|
290
|
+
(substep): substep is DocflowOwnerSubstep =>
|
|
291
|
+
substep.type === 'dataset' || substep.type === 'csv',
|
|
292
|
+
);
|
|
293
|
+
return joinBySymbolThenExtent(binding, owners, (substep) =>
|
|
294
|
+
substep.type === 'dataset'
|
|
295
|
+
? datasetComputedColumnNames(substep)
|
|
296
|
+
: new Set<string>(),
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** Same join, narrowed to dataset substeps (CSV has no per-row columns). */
|
|
301
|
+
export function resolveDocflowOwnerDataset(
|
|
302
|
+
binding: PlayDocflowBinding,
|
|
303
|
+
substeps: readonly PlayStaticSubstep[],
|
|
304
|
+
): DocflowBindingJoin<DocflowDatasetSubstep> {
|
|
305
|
+
const resolution = resolveDocflowOwnerSubstep(binding, substeps);
|
|
306
|
+
if (resolution.substep?.type === 'dataset') {
|
|
307
|
+
return { substep: resolution.substep, via: resolution.via! };
|
|
308
|
+
}
|
|
309
|
+
return {
|
|
310
|
+
substep: null,
|
|
311
|
+
reason: resolution.substep ? 'no-owner' : resolution.reason,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* The top-level tool substep a docflow binding names. A tool substep's `field`
|
|
317
|
+
* is the column it produces, so a member's `out:"col"` joins by symbol; an
|
|
318
|
+
* assignment-named action node (`out:"searchResult"`) falls back to extent.
|
|
319
|
+
*/
|
|
320
|
+
export function resolveDocflowBoundTool(
|
|
321
|
+
binding: PlayDocflowBinding,
|
|
322
|
+
substeps: readonly PlayStaticSubstep[],
|
|
323
|
+
): DocflowBindingJoin<DocflowToolSubstep> {
|
|
324
|
+
const tools = substeps.filter(
|
|
325
|
+
(substep): substep is DocflowToolSubstep => substep.type === 'tool',
|
|
326
|
+
);
|
|
327
|
+
return joinBySymbolThenExtent(
|
|
328
|
+
binding,
|
|
329
|
+
tools,
|
|
330
|
+
(substep) => new Set([substep.field]),
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* The `ctx.runPlay` call a `type:"play"` docflow node names. Same join as every
|
|
336
|
+
* other binding — symbol first, extent as the fallback — over the FLATTENED
|
|
337
|
+
* substep tree, because a child call inside a `.withColumn(...)` body is nested
|
|
338
|
+
* under its dataset. A `play_call`'s `field` is the column or variable the child
|
|
339
|
+
* result lands in, which is exactly what the node's `out:` names.
|
|
340
|
+
*/
|
|
341
|
+
export function resolveDocflowBoundPlayCall(
|
|
342
|
+
binding: PlayDocflowBinding,
|
|
343
|
+
substeps: readonly PlayStaticSubstep[],
|
|
344
|
+
): DocflowBindingJoin<DocflowPlayCallSubstep> {
|
|
345
|
+
const playCalls = flattenSubsteps(substeps).filter(
|
|
346
|
+
(substep): substep is DocflowPlayCallSubstep =>
|
|
347
|
+
substep.type === 'play_call',
|
|
348
|
+
);
|
|
349
|
+
return joinBySymbolThenExtent(
|
|
350
|
+
binding,
|
|
351
|
+
playCalls,
|
|
352
|
+
(substep) => new Set(substep.field.split('.')),
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ── waterfalls: the step suite and its legs ─────────────────────────────────
|
|
357
|
+
//
|
|
358
|
+
// `steps().step('hunter_email', …).step('leadmagic_email', …).return(…)` compiles
|
|
359
|
+
// to ONE `step_suite` substep whose children are the legs. Six shipped prebuilts
|
|
360
|
+
// run one of these as their whole scalar body, 4–16 legs deep, and until this
|
|
361
|
+
// join existed an authored node over the waterfall resolved to nothing: the
|
|
362
|
+
// suite is not a `tool`, so `resolveDocflowBoundTool` skipped it and the node
|
|
363
|
+
// rendered with no provider, no legs, and no order — strictly less than the
|
|
364
|
+
// undiagrammed `StepSuiteCard` beside it.
|
|
365
|
+
//
|
|
366
|
+
// A leg is NOT one substep. The extractor emits one substep per tool CALL inside
|
|
367
|
+
// the leg's resolver, so a leg that finds a phone and then validates it emits
|
|
368
|
+
// two, both carrying the leg's own `field`. `contact-to-phone-waterfall` compiles
|
|
369
|
+
// 21 substeps for 11 legs that way. The leg is the `field`; its substeps are how
|
|
370
|
+
// it works.
|
|
371
|
+
|
|
372
|
+
/** The dotted tail of a leg's field relative to its suite (`steps.a` -> `a`). */
|
|
373
|
+
function legNameFromField(suiteField: string, legField: string): string {
|
|
374
|
+
return legField.startsWith(`${suiteField}.`)
|
|
375
|
+
? legField.slice(suiteField.length + 1)
|
|
376
|
+
: legField;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* One leg of a waterfall, as the source declares it. Ordered, deduplicated by
|
|
381
|
+
* field, and carrying only decidable facts about the text — never a claim about
|
|
382
|
+
* whether this run reached it.
|
|
383
|
+
*/
|
|
384
|
+
export type DocflowStepSuiteLeg = {
|
|
385
|
+
/** The leg's durable step name — the string the author passed `.step(…)`. */
|
|
386
|
+
name: string;
|
|
387
|
+
/** The suite-qualified field, i.e. the compiled substep key. */
|
|
388
|
+
field: string;
|
|
389
|
+
/** The first provider call the leg makes, when it makes one. */
|
|
390
|
+
toolId: string | null;
|
|
391
|
+
/** Every provider call the leg makes, in order. */
|
|
392
|
+
toolIds: string[];
|
|
393
|
+
/** The leg is `runIf`-guarded, so whether it runs depends on earlier legs. */
|
|
394
|
+
conditional: boolean;
|
|
395
|
+
/** Every substep of the leg is statically off (`runIf: () => false`). */
|
|
396
|
+
disabled: boolean;
|
|
397
|
+
/** The leg is itself a nested `steps()` program. */
|
|
398
|
+
nested: boolean;
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* The legs of a suite, in declaration order. Deduplicated by field so a
|
|
403
|
+
* find-then-validate leg reads as one attempt; `conditional` is true when ANY of
|
|
404
|
+
* a leg's substeps is guarded, `disabled` only when EVERY one is off.
|
|
405
|
+
*/
|
|
406
|
+
export function stepSuiteLegs(
|
|
407
|
+
suite: DocflowStepSuiteSubstep,
|
|
408
|
+
): DocflowStepSuiteLeg[] {
|
|
409
|
+
const byField = new Map<string, DocflowStepSuiteLeg>();
|
|
410
|
+
for (const child of suite.steps ?? []) {
|
|
411
|
+
const field = (child as { field?: string }).field ?? '';
|
|
412
|
+
if (!field) continue;
|
|
413
|
+
const toolIds = flattenSubsteps([child])
|
|
414
|
+
.filter((step): step is DocflowToolSubstep => step.type === 'tool')
|
|
415
|
+
.map((step) => step.toolId);
|
|
416
|
+
const existing = byField.get(field);
|
|
417
|
+
if (existing) {
|
|
418
|
+
existing.toolIds.push(...toolIds);
|
|
419
|
+
existing.toolId ??= toolIds[0] ?? null;
|
|
420
|
+
existing.conditional ||= child.conditional === true;
|
|
421
|
+
existing.disabled &&= child.disabled === true;
|
|
422
|
+
existing.nested ||= child.type === 'step_suite';
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
byField.set(field, {
|
|
426
|
+
name: legNameFromField(suite.field, field),
|
|
427
|
+
field,
|
|
428
|
+
toolId: toolIds[0] ?? null,
|
|
429
|
+
toolIds: [...toolIds],
|
|
430
|
+
conditional: child.conditional === true,
|
|
431
|
+
disabled: child.disabled === true,
|
|
432
|
+
nested: child.type === 'step_suite',
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
return [...byField.values()];
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* The `step_suite` a docflow node names — the whole waterfall as one step.
|
|
440
|
+
*
|
|
441
|
+
* Symbol first, like every other join. Inside a dataset the suite's `field` IS
|
|
442
|
+
* the column it fills (`.withColumn('email_result', personToEmailSteps())`
|
|
443
|
+
* compiles to `step_suite` field `email_result`), so `out:"email_result"` binds
|
|
444
|
+
* by name. A scalar `ctx.runSteps(personalEmailSteps(), input)` has no such
|
|
445
|
+
* name — the extractor calls it `steps` because the argument is a call, not a
|
|
446
|
+
* named program — so `out:"result"` falls through to position.
|
|
447
|
+
*
|
|
448
|
+
* That fallback measures the suite's OWN call range, never the union with its
|
|
449
|
+
* legs: an imported builder's legs live in another file, and unioning line
|
|
450
|
+
* numbers across files produces an interval that means nothing. The annotation
|
|
451
|
+
* sits directly above the `ctx.runSteps(...)` call, which is exactly what the
|
|
452
|
+
* own range covers.
|
|
453
|
+
*/
|
|
454
|
+
export function resolveDocflowBoundStepSuite(
|
|
455
|
+
binding: PlayDocflowBinding,
|
|
456
|
+
substeps: readonly PlayStaticSubstep[],
|
|
457
|
+
): DocflowBindingJoin<DocflowStepSuiteSubstep> {
|
|
458
|
+
const suites = flattenSubsteps(substeps).filter(
|
|
459
|
+
(substep): substep is DocflowStepSuiteSubstep =>
|
|
460
|
+
substep.type === 'step_suite',
|
|
461
|
+
);
|
|
462
|
+
return joinBySymbolThenExtent(
|
|
463
|
+
binding,
|
|
464
|
+
suites,
|
|
465
|
+
(substep) => new Set([substep.field, substep.field.split('.').pop()!]),
|
|
466
|
+
substepOwnSourceExtent,
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** One leg, with the suite it belongs to. */
|
|
471
|
+
export type DocflowBoundStepLeg = {
|
|
472
|
+
suite: DocflowStepSuiteSubstep;
|
|
473
|
+
leg: DocflowStepSuiteLeg;
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* The waterfall LEG a docflow node names, for a member drawn inside a waterfall
|
|
478
|
+
* region. Symbol only — a leg name is always available (the author wrote it as a
|
|
479
|
+
* string literal in `.step('<name>', …)`), so there is nothing position could
|
|
480
|
+
* add, and the legs of an imported builder have no line coordinate this file
|
|
481
|
+
* could compare against anyway.
|
|
482
|
+
*
|
|
483
|
+
* Ambiguity is real and reported: two suites in one play may both declare a leg
|
|
484
|
+
* called `validate`, and picking one would attribute a provider to the wrong
|
|
485
|
+
* cascade.
|
|
486
|
+
*/
|
|
487
|
+
export function resolveDocflowBoundStepLeg(
|
|
488
|
+
binding: PlayDocflowBinding,
|
|
489
|
+
substeps: readonly PlayStaticSubstep[],
|
|
490
|
+
): DocflowBindingJoin<DocflowBoundStepLeg> {
|
|
491
|
+
const roots = docflowOutputRoots(binding);
|
|
492
|
+
if (roots.length === 0) return { substep: null, reason: 'no-owner' };
|
|
493
|
+
const seen = new Set<string>();
|
|
494
|
+
const matches: DocflowBoundStepLeg[] = [];
|
|
495
|
+
for (const suite of flattenSubsteps(substeps)) {
|
|
496
|
+
if (suite.type !== 'step_suite') continue;
|
|
497
|
+
for (const leg of stepSuiteLegs(suite)) {
|
|
498
|
+
if (!roots.includes(leg.name) && !roots.includes(leg.field)) continue;
|
|
499
|
+
// The same suite reaches this walk through both the stages tree and the
|
|
500
|
+
// flattened compiled substeps, so identity is the leg's field, not the
|
|
501
|
+
// object.
|
|
502
|
+
if (seen.has(leg.field)) continue;
|
|
503
|
+
seen.add(leg.field);
|
|
504
|
+
matches.push({ suite, leg });
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
if (matches.length === 1) return { substep: matches[0]!, via: 'symbol' };
|
|
508
|
+
return {
|
|
509
|
+
substep: null,
|
|
510
|
+
reason: matches.length > 1 ? 'ambiguous' : 'no-owner',
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// ── the waterfall region ────────────────────────────────────────────────────
|
|
515
|
+
|
|
516
|
+
export type DocflowWaterfallRegionMember = {
|
|
517
|
+
nodeId: string;
|
|
518
|
+
leg: DocflowStepSuiteLeg;
|
|
519
|
+
/** 1-based position of the leg in the cascade the suite declares. */
|
|
520
|
+
position: number;
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* A `subgraph` whose members are the legs of one cascade.
|
|
525
|
+
*
|
|
526
|
+
* Ownership needs no edge and no coordinate: a member's `out:` names a leg, a leg
|
|
527
|
+
* belongs to exactly one suite, so the members THEMSELVES say which cascade the
|
|
528
|
+
* region is. That is strictly better than the dataset loop region, which has to
|
|
529
|
+
* fall back to "which dataset does an edge touch" and then disambiguate by line.
|
|
530
|
+
*/
|
|
531
|
+
export type DocflowWaterfallRegion = {
|
|
532
|
+
subgraphId: string;
|
|
533
|
+
suite: DocflowStepSuiteSubstep;
|
|
534
|
+
/** Drawn legs, in the order the SUITE declares them, not the order drawn. */
|
|
535
|
+
members: DocflowWaterfallRegionMember[];
|
|
536
|
+
/**
|
|
537
|
+
* Members that carry an `out:` and still name no leg of this suite. A claim
|
|
538
|
+
* the region cannot back — surfaced, never rendered as if fine.
|
|
539
|
+
*/
|
|
540
|
+
foreignMemberIds: string[];
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
export type DocflowWaterfallRegionIndex = {
|
|
544
|
+
regions: DocflowWaterfallRegion[];
|
|
545
|
+
/**
|
|
546
|
+
* Subgraphs whose members name legs of MORE than one cascade. One region
|
|
547
|
+
* cannot be two waterfalls, and picking one would attribute providers to the
|
|
548
|
+
* wrong cascade.
|
|
549
|
+
*/
|
|
550
|
+
splitRegions: Array<{ subgraphId: string; suiteFields: string[] }>;
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Every waterfall region in a diagram. A subgraph with no leg-bound member is
|
|
555
|
+
* not one (it is a dataset loop, or pure presentation) and is absent from both
|
|
556
|
+
* lists, so this is safe to run over any diagram.
|
|
557
|
+
*/
|
|
558
|
+
export function resolveDocflowWaterfallRegions(
|
|
559
|
+
docflow: PlayDocflow | null | undefined,
|
|
560
|
+
substeps: readonly PlayStaticSubstep[],
|
|
561
|
+
): DocflowWaterfallRegionIndex {
|
|
562
|
+
const regions: DocflowWaterfallRegion[] = [];
|
|
563
|
+
const splitRegions: Array<{ subgraphId: string; suiteFields: string[] }> = [];
|
|
564
|
+
if (!docflow?.subgraphs?.length) return { regions, splitRegions };
|
|
565
|
+
const bindingByNodeId = new Map(
|
|
566
|
+
docflow.bindings.map((binding) => [binding.nodeId, binding]),
|
|
567
|
+
);
|
|
568
|
+
for (const subgraph of docflow.subgraphs) {
|
|
569
|
+
const resolved: Array<{
|
|
570
|
+
nodeId: string;
|
|
571
|
+
suite: DocflowStepSuiteSubstep;
|
|
572
|
+
leg: DocflowStepSuiteLeg;
|
|
573
|
+
}> = [];
|
|
574
|
+
const foreignMemberIds: string[] = [];
|
|
575
|
+
for (const memberId of subgraph.memberIds) {
|
|
576
|
+
const binding = bindingByNodeId.get(memberId);
|
|
577
|
+
// An unbound member, or one that names nothing, is presentation — a note
|
|
578
|
+
// drawn inside the region. Only a member making a claim can be wrong.
|
|
579
|
+
if (!binding || docflowOutputRoots(binding).length === 0) continue;
|
|
580
|
+
const join = resolveDocflowBoundStepLeg(binding, substeps);
|
|
581
|
+
if (join.substep) {
|
|
582
|
+
resolved.push({ nodeId: memberId, ...join.substep });
|
|
583
|
+
} else {
|
|
584
|
+
foreignMemberIds.push(memberId);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
if (resolved.length === 0) continue;
|
|
588
|
+
const suiteFields = [
|
|
589
|
+
...new Set(resolved.map((entry) => entry.suite.field)),
|
|
590
|
+
];
|
|
591
|
+
if (suiteFields.length > 1) {
|
|
592
|
+
splitRegions.push({ subgraphId: subgraph.id, suiteFields });
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
const suite = resolved[0]!.suite;
|
|
596
|
+
const order = stepSuiteLegs(suite).map((leg) => leg.field);
|
|
597
|
+
regions.push({
|
|
598
|
+
subgraphId: subgraph.id,
|
|
599
|
+
suite,
|
|
600
|
+
members: resolved
|
|
601
|
+
.map((entry) => ({
|
|
602
|
+
nodeId: entry.nodeId,
|
|
603
|
+
leg: entry.leg,
|
|
604
|
+
position: order.indexOf(entry.leg.field) + 1,
|
|
605
|
+
}))
|
|
606
|
+
.sort((left, right) => left.position - right.position),
|
|
607
|
+
// Only a member that claimed a leg of THIS suite and missed is foreign.
|
|
608
|
+
// A member naming a leg of another suite already made the region split.
|
|
609
|
+
foreignMemberIds,
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
return { regions, splitRegions };
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
export type DocflowOwnerIndex = {
|
|
616
|
+
/** Owning dataset/CSV substep per bound docflow node id. */
|
|
617
|
+
ownerByNodeId: Map<string, DocflowOwnerSubstep>;
|
|
618
|
+
/** Bindings no substep owns, with why — a state to surface, not to default. */
|
|
619
|
+
unresolved: Array<{ nodeId: string; reason: DocflowJoinFailure }>;
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
/** Resolves every binding in a docflow once, for callers that need the map. */
|
|
623
|
+
export function resolveDocflowOwnerIndex(
|
|
624
|
+
docflow: PlayDocflow | null | undefined,
|
|
625
|
+
substeps: readonly PlayStaticSubstep[],
|
|
626
|
+
): DocflowOwnerIndex {
|
|
627
|
+
const ownerByNodeId = new Map<string, DocflowOwnerSubstep>();
|
|
628
|
+
const unresolved: Array<{ nodeId: string; reason: DocflowJoinFailure }> = [];
|
|
629
|
+
for (const binding of docflow?.bindings ?? []) {
|
|
630
|
+
const resolution = resolveDocflowOwnerSubstep(binding, substeps);
|
|
631
|
+
if (resolution.substep)
|
|
632
|
+
ownerByNodeId.set(binding.nodeId, resolution.substep);
|
|
633
|
+
else unresolved.push({ nodeId: binding.nodeId, reason: resolution.reason });
|
|
634
|
+
}
|
|
635
|
+
return { ownerByNodeId, unresolved };
|
|
636
|
+
}
|