deepline 0.3.195 → 0.3.196

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.
Files changed (28) hide show
  1. package/dist/bundling-sources/sdk/src/client.ts +13 -0
  2. package/dist/bundling-sources/sdk/src/release.ts +1 -1
  3. package/dist/bundling-sources/sdk/src/types.ts +18 -0
  4. package/dist/bundling-sources/shared_libs/observability/scheduled-jobs.json +18 -0
  5. package/dist/bundling-sources/shared_libs/observability/scheduled-work-manual.ts +1 -0
  6. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +10 -2
  7. package/dist/bundling-sources/shared_libs/plays/docflow-binding-owner.ts +138 -14
  8. package/dist/bundling-sources/shared_libs/plays/docflow.ts +57 -3
  9. package/dist/bundling-sources/shared_libs/plays/share/static-pipeline-summary.ts +3 -0
  10. package/dist/bundling-sources/shared_libs/plays/static-pipeline.ts +21 -0
  11. package/dist/bundling-sources/shared_libs/plays/workspace-bundle.ts +651 -0
  12. package/dist/cli/index.js +48 -2
  13. package/dist/cli/index.mjs +48 -2
  14. package/dist/{compiler-manifest-4bYusxBr.d.mts → compiler-manifest-Dpd2hEet.d.mts} +28 -0
  15. package/dist/{compiler-manifest-4bYusxBr.d.ts → compiler-manifest-Dpd2hEet.d.ts} +28 -0
  16. package/dist/index.d.mts +27 -2
  17. package/dist/index.d.ts +27 -2
  18. package/dist/index.js +7 -1
  19. package/dist/index.mjs +7 -1
  20. package/dist/install-integrity.json +3 -2
  21. package/dist/plays/bundle-play-file.d.mts +2 -2
  22. package/dist/plays/bundle-play-file.d.ts +2 -2
  23. package/dist/plays/bundle-play-file.mjs +24 -3
  24. package/dist/release.d.mts +1 -1
  25. package/dist/release.d.ts +1 -1
  26. package/dist/release.js +1 -1
  27. package/dist/release.mjs +1 -1
  28. package/package.json +1 -1
@@ -793,6 +793,13 @@ export type PlaysListOptions = {
793
793
  */
794
794
  grepMode?: 'all' | 'any' | 'phrase';
795
795
  categories?: string | string[];
796
+ /**
797
+ * Keep only plays carrying one of these tags — the job a play does, not the
798
+ * tools it calls. Deepline's closed vocabulary today: `smb`,
799
+ * `find-prospects`, `qualify`, `contact-info`, `signals`. Several tags widen
800
+ * the result rather than narrowing it, because a play carries exactly one.
801
+ */
802
+ tags?: string | string[];
796
803
  includeToolCategories?: boolean;
797
804
  includeArchived?: boolean;
798
805
  includeCostEstimates?: boolean;
@@ -5420,6 +5427,12 @@ export class DeeplineClient {
5420
5427
  if (options?.categories || options?.includeToolCategories) {
5421
5428
  params.set('include_tool_categories', '1');
5422
5429
  }
5430
+ if (options?.tags) {
5431
+ params.set(
5432
+ 'tags',
5433
+ Array.isArray(options.tags) ? options.tags.join(',') : options.tags,
5434
+ );
5435
+ }
5423
5436
  if (options?.includeArchived) params.set('include_archived', '1');
5424
5437
  if (options?.includeCostEstimates) {
5425
5438
  params.set('include_cost_estimates', '1');
@@ -200,7 +200,7 @@ export const SDK_RELEASE = {
200
200
  // getters keep their established compatibility behavior.
201
201
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
202
202
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
203
- version: '0.3.195',
203
+ version: '0.3.196',
204
204
  updateSummary:
205
205
  'Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.',
206
206
  packageCapabilities: {
@@ -1378,6 +1378,14 @@ export interface PlayListItem {
1378
1378
  description?: string | null;
1379
1379
  pinned?: boolean;
1380
1380
  toolCategories?: string[];
1381
+ /**
1382
+ * The job the play does, from Deepline's closed prebuilt tag vocabulary
1383
+ * (`smb`, `find-prospects`, `qualify`, `contact-info`, `signals`) — a
1384
+ * different question from `toolCategories`, which reports the tools it
1385
+ * calls. Empty for org-owned plays; absent from a server too old to report
1386
+ * it.
1387
+ */
1388
+ tags?: string[];
1381
1389
  origin?: 'prebuilt' | 'owned';
1382
1390
  ownerType?: 'deepline' | 'org';
1383
1391
  ownerSlug?: string;
@@ -1469,6 +1477,16 @@ export interface PlayDescription {
1469
1477
  description?: string | null;
1470
1478
  pinned?: boolean;
1471
1479
  toolCategories?: string[];
1480
+ /**
1481
+ * What job this play does, as a closed vocabulary — `smb`,
1482
+ * `find-prospects`, `qualify`, `contact-info`, `signals`.
1483
+ *
1484
+ * Distinct from `toolCategories`, which is derived from the tool ids a play
1485
+ * calls and so describes its MACHINERY. A play that calls an email tool is
1486
+ * not thereby an email play; every SMB play calls one and none of them is.
1487
+ * Empty for org-owned plays, which carry no registry tag.
1488
+ */
1489
+ tags?: string[];
1472
1490
  origin?: 'prebuilt' | 'owned';
1473
1491
  ownerType?: 'deepline' | 'org';
1474
1492
  canEdit?: boolean;
@@ -885,6 +885,24 @@
885
885
  "failureAlert": "convex_action_wrapper",
886
886
  "domainReportRequired": true
887
887
  },
888
+ {
889
+ "id": "convex.cleanup-expired-api-key-claims",
890
+ "name": "Cleanup expired API key claims",
891
+ "scheduler": "convex_cron",
892
+ "cadence": "7 4 * * *",
893
+ "timezone": "UTC",
894
+ "source": "packages/convex/crons.ts",
895
+ "locator": "cleanup expired API key claims",
896
+ "owner": "auth",
897
+ "criticality": "maintenance",
898
+ "purpose": "Delete expired CLI claim tokens from apiKeyClaims, which had no reaper while a settings page view minted one durable claim row per unclaimed active key.",
899
+ "successDefinition": "The sweep advances its monotone createdAt watermark and deletes every expired claim it examines; zero expired claims is a valid no-op.",
900
+ "failureDefinition": "The sweep fails or stops advancing its watermark, leaving expired claim credentials retained indefinitely.",
901
+ "startGraceMs": 120000,
902
+ "runTimeoutMs": 1800000,
903
+ "failureAlert": "convex_action_wrapper",
904
+ "domainReportRequired": true
905
+ },
888
906
  {
889
907
  "id": "convex.cleanup-expired-analytics-ip2company-cache",
890
908
  "name": "Cleanup expired Analytics IP2Company cache entries",
@@ -11,6 +11,7 @@ export const MANUALLY_RUNNABLE_CONVEX_CRON_JOB_IDS = [
11
11
  'convex.fold-usage-rollups',
12
12
  'convex.record-neon-managed-project-usage',
13
13
  'convex.cleanup-expired-mcp-oauth-codes',
14
+ 'convex.cleanup-expired-api-key-claims',
14
15
  'convex.cleanup-expired-analytics-ip2company-cache',
15
16
  'convex.cleanup-expired-domain-health-cache',
16
17
  'convex.salesforce-oauth-refresh-maintenance',
@@ -72,7 +72,6 @@ import {
72
72
  } from '../runtime-api';
73
73
  import { PLAY_RUNTIME_ATTEMPT_EXECUTOR_TOKEN_PATH } from '../runtime-api-paths';
74
74
  import { vercelProtectionBypassHeaders } from '../vercel-protection';
75
- import { installPgRuntimePoolDriver } from '../runtime-pg-driver-pg';
76
75
  import {
77
76
  isManagedSandboxRuntimeBackend,
78
77
  normalizePlayRuntimeBackend,
@@ -302,7 +301,16 @@ class AttemptExecutorTokenCapacityError extends RuntimeSchedulerCapacityError {
302
301
  this.name = 'AttemptExecutorTokenCapacityError';
303
302
  }
304
303
  }
305
- installPgRuntimePoolDriver();
304
+
305
+ // No `installPgRuntimePoolDriver()` here: the runtime Postgres driver belongs to
306
+ // the process that owns this module's boot — `apps/absurd-runtime-worker` and
307
+ // `apps/absurd-release-scaler` install it at their entrypoints. `apps/deepline-api`
308
+ // imports the lane-count helpers below from a request path
309
+ // (`src/lib/plays/runtime-lanes.ts`), and the Next.js dev server re-evaluates that
310
+ // module graph on every recompile while `registerRuntimePoolFactory`'s registry
311
+ // lives on `globalThis` under a `Symbol.for` key and survives. A module-scope
312
+ // install therefore handed the registry a fresh factory object after each
313
+ // recompile and tripped its one-driver-per-process assertion.
306
314
 
307
315
  /**
308
316
  * A slot supervises one remote Daytona sandbox; it is not a local compute
@@ -91,6 +91,21 @@ export function flattenSubsteps(
91
91
  return flat;
92
92
  }
93
93
 
94
+ /**
95
+ * The names a substep's `field` answers to.
96
+ *
97
+ * A tool substep's field is a PATH, not a name: a step inside a `steps()`
98
+ * program is `steps.<step name>`, and a column's producer is
99
+ * `<column>.<step name>`. An `out:` contract names the step or column itself,
100
+ * so matching the whole path would never hit — `out:"contactout_personal"`
101
+ * against `steps.contactout_personal`. Both ends of the path are offered: the
102
+ * full path, and the last segment, which is the step/column the author named.
103
+ */
104
+ function substepFieldSymbols(field: string): Set<string> {
105
+ const segments = field.split('.');
106
+ return new Set([field, segments[segments.length - 1]!]);
107
+ }
108
+
94
109
  /** How a binding reached its substep — see the module header for the rules. */
95
110
  export type DocflowJoinVia = 'symbol' | 'extent';
96
111
 
@@ -115,6 +130,10 @@ export type DocflowLineExtent = { startLine: number; endLine: number };
115
130
  export function substepSourceExtent(
116
131
  substep: PlayStaticSubstep,
117
132
  ): DocflowLineExtent | null {
133
+ // Resolved through a call: the substep and everything under it live in the
134
+ // callee, so the only interval an annotation here can contain is the call.
135
+ const viaCall = callSiteExtent(substep);
136
+ if (viaCall) return viaCall;
118
137
  const ranges: PlayStaticSourceRange[] = [];
119
138
  const collect = (candidate: PlayStaticSubstep) => {
120
139
  if (candidate.sourceRange) ranges.push(candidate.sourceRange);
@@ -194,30 +213,73 @@ export type DocflowExtentResolver = (
194
213
  export function substepOwnSourceExtent(
195
214
  substep: PlayStaticSubstep,
196
215
  ): DocflowLineExtent | null {
216
+ const viaCall = callSiteExtent(substep);
217
+ if (viaCall) return viaCall;
197
218
  const range = substep.sourceRange;
198
219
  return range ? { startLine: range.startLine, endLine: range.endLine } : null;
199
220
  }
200
221
 
222
+ /**
223
+ * The extent of the call a substep was reached through, when the extractor
224
+ * followed TypeScript symbol resolution into another function to find it.
225
+ *
226
+ * This is what lets a box annotated on `const x = await helper(ctx, …)` own the
227
+ * provider calls inside `helper` — including when `helper` is imported from
228
+ * another module, where the callee's own range is not even comparable to a line
229
+ * number in this file.
230
+ */
231
+ function callSiteExtent(
232
+ substep: PlayStaticSubstep,
233
+ ): DocflowLineExtent | null {
234
+ const range = substep.callSiteRange;
235
+ return range ? { startLine: range.startLine, endLine: range.endLine } : null;
236
+ }
237
+
238
+ /**
239
+ * The extent a binding's line may be compared against.
240
+ *
241
+ * An annotation written in the ENTRY file keeps the existing behavior exactly:
242
+ * whatever the resolver prefers, no path check. An annotation written in an
243
+ * imported module is only comparable to a range from that same module — two
244
+ * files both have a line 42 — so it picks the substep's own range or its call
245
+ * site, whichever was recorded in the annotation's file, and matches nothing
246
+ * when neither was.
247
+ */
248
+ function extentForBinding(
249
+ substep: PlayStaticSubstep,
250
+ binding: PlayDocflowBinding,
251
+ extentOf: DocflowExtentResolver,
252
+ ): DocflowLineExtent | null {
253
+ if (!binding.sourcePath) return extentOf(substep);
254
+ for (const range of [substep.sourceRange, substep.callSiteRange]) {
255
+ if (range?.sourcePath !== binding.sourcePath) continue;
256
+ return { startLine: range.startLine, endLine: range.endLine };
257
+ }
258
+ return null;
259
+ }
260
+
201
261
  function extentContainsLine(
202
262
  substep: PlayStaticSubstep,
203
- line: number,
263
+ binding: PlayDocflowBinding,
204
264
  extentOf: DocflowExtentResolver,
205
265
  ): DocflowLineExtent | null {
206
- const extent = extentOf(substep);
266
+ const extent = extentForBinding(substep, binding, extentOf);
207
267
  if (!extent) return null;
208
- return line >= extent.startLine && line <= extent.endLine ? extent : null;
268
+ return binding.line >= extent.startLine && binding.line <= extent.endLine
269
+ ? extent
270
+ : null;
209
271
  }
210
272
 
211
273
  /** Smallest containing extent wins, so a nested construct beats its parent. */
212
274
  function smallestContaining<TSubstep extends PlayStaticSubstep>(
213
275
  candidates: readonly TSubstep[],
214
- line: number,
276
+ binding: PlayDocflowBinding,
215
277
  extentOf: DocflowExtentResolver = substepSourceExtent,
216
278
  ): TSubstep | null {
217
279
  let best: TSubstep | null = null;
218
280
  let bestSpan = Number.POSITIVE_INFINITY;
219
281
  for (const candidate of candidates) {
220
- const extent = extentContainsLine(candidate, line, extentOf);
282
+ const extent = extentContainsLine(candidate, binding, extentOf);
221
283
  if (!extent) continue;
222
284
  const span = extent.endLine - extent.startLine;
223
285
  if (span < bestSpan) {
@@ -269,12 +331,12 @@ function joinBySymbolThenExtent<TSubstep extends PlayStaticSubstep>(
269
331
  // Two constructs compute the same column name. The one whose body the
270
332
  // annotation actually sits in decides; otherwise this is drift, and a
271
333
  // guess would attribute a member's rows to the wrong table.
272
- const narrowed = smallestContaining(matches, binding.line, extentOf);
334
+ const narrowed = smallestContaining(matches, binding, extentOf);
273
335
  if (narrowed) return { substep: narrowed, via: 'symbol' };
274
336
  return { substep: null, reason: 'ambiguous' };
275
337
  }
276
338
  }
277
- const positional = smallestContaining(candidates, binding.line, extentOf);
339
+ const positional = smallestContaining(candidates, binding, extentOf);
278
340
  if (positional) return { substep: positional, via: 'extent' };
279
341
  return { substep: null, reason: 'no-owner' };
280
342
  }
@@ -326,13 +388,53 @@ export function resolveDocflowBoundTool(
326
388
  const tools = substeps.filter(
327
389
  (substep): substep is DocflowToolSubstep => substep.type === 'tool',
328
390
  );
329
- return joinBySymbolThenExtent(
330
- binding,
331
- tools,
332
- (substep) => new Set([substep.field]),
391
+ return joinBySymbolThenExtent(binding, tools, (substep) =>
392
+ substepFieldSymbols(substep.field),
333
393
  );
334
394
  }
335
395
 
396
+ /**
397
+ * EVERY tool a binding names, not just the one it unambiguously resolves to.
398
+ *
399
+ * {@link resolveDocflowBoundTool} answers "which single provider is this box",
400
+ * and correctly declines when a step calls two — a finder plus a validator, the
401
+ * shape every waterfall leg has. That decline is right for a headline tool and
402
+ * wrong for the Config surface, which exists to list a step's provider calls
403
+ * and their params. So this returns the whole set, in source order.
404
+ *
405
+ * Symbol match only: a step's tools are named by its `out:` contract. Extent is
406
+ * deliberately not a fallback here, because "every tool whose lines happen to
407
+ * sit inside this construct" would sweep up a sibling leg's calls.
408
+ */
409
+ export function docflowBoundToolSubsteps(
410
+ binding: PlayDocflowBinding,
411
+ substeps: readonly PlayStaticSubstep[],
412
+ ): DocflowToolSubstep[] {
413
+ const roots = docflowOutputRoots(binding);
414
+ if (roots.length === 0) return [];
415
+ const matched: DocflowToolSubstep[] = [];
416
+ const seen = new Set<string>();
417
+ // Callers hand over whatever shape they hold — the config model passes the
418
+ // top-level stages, whose tools sit several levels down inside step suites
419
+ // and control flow. Flatten here so the answer never depends on that.
420
+ for (const substep of flattenSubsteps(substeps)) {
421
+ if (substep.type !== 'tool') continue;
422
+ const symbols = substepFieldSymbols(substep.field);
423
+ if (!roots.some((root) => symbols.has(root))) continue;
424
+ if (binding.sourcePath) {
425
+ const paths = [substep.sourceRange, substep.callSiteRange].map(
426
+ (range) => range?.sourcePath,
427
+ );
428
+ if (!paths.includes(binding.sourcePath)) continue;
429
+ }
430
+ const identity = `${substep.toolId}::${substep.paramsSource ?? ''}`;
431
+ if (seen.has(identity)) continue;
432
+ seen.add(identity);
433
+ matched.push(substep);
434
+ }
435
+ return matched;
436
+ }
437
+
336
438
  /**
337
439
  * The `ctx.runPlay` or `ctx.runPlayAsync` call a `type:"play"` docflow node
338
440
  * names. Same join as every other binding — symbol first, extent as the fallback
@@ -386,6 +488,13 @@ function legNameFromField(suiteField: string, legField: string): string {
386
488
  * field, and carrying only decidable facts about the text — never a claim about
387
489
  * whether this run reached it.
388
490
  */
491
+ /** One provider call inside a leg, with the source of its input. */
492
+ export type DocflowLegCall = {
493
+ toolId: string;
494
+ /** The authored expression passed as `input`, verbatim. Absent if unparsed. */
495
+ paramsSource?: string;
496
+ };
497
+
389
498
  export type DocflowStepSuiteLeg = {
390
499
  /** The leg's durable step name — the string the author passed `.step(…)`. */
391
500
  name: string;
@@ -395,6 +504,14 @@ export type DocflowStepSuiteLeg = {
395
504
  toolId: string | null;
396
505
  /** Every provider call the leg makes, in order. */
397
506
  toolIds: string[];
507
+ /**
508
+ * The same calls, each with the source text of the input it is given.
509
+ *
510
+ * `toolIds` answers "which provider"; a reader looking at one attempt of a
511
+ * nine-attempt cascade also needs "with what" — the pane used to name the
512
+ * provider twice and never once say what it was called with.
513
+ */
514
+ calls: DocflowLegCall[];
398
515
  /** The leg is `runIf`-guarded, so whether it runs depends on earlier legs. */
399
516
  conditional: boolean;
400
517
  /** Every substep of the leg is statically off (`runIf: () => false`). */
@@ -415,12 +532,18 @@ export function stepSuiteLegs(
415
532
  for (const child of suite.steps ?? []) {
416
533
  const field = (child as { field?: string }).field ?? '';
417
534
  if (!field) continue;
418
- const toolIds = flattenSubsteps([child])
419
- .filter((step): step is DocflowToolSubstep => step.type === 'tool')
420
- .map((step) => step.toolId);
535
+ const toolSteps = flattenSubsteps([child]).filter(
536
+ (step): step is DocflowToolSubstep => step.type === 'tool',
537
+ );
538
+ const toolIds = toolSteps.map((step) => step.toolId);
539
+ const calls: DocflowLegCall[] = toolSteps.map((step) => ({
540
+ toolId: step.toolId,
541
+ ...(step.paramsSource ? { paramsSource: step.paramsSource } : {}),
542
+ }));
421
543
  const existing = byField.get(field);
422
544
  if (existing) {
423
545
  existing.toolIds.push(...toolIds);
546
+ existing.calls.push(...calls);
424
547
  existing.toolId ??= toolIds[0] ?? null;
425
548
  existing.conditional ||= child.conditional === true;
426
549
  existing.disabled &&= child.disabled === true;
@@ -432,6 +555,7 @@ export function stepSuiteLegs(
432
555
  field,
433
556
  toolId: toolIds[0] ?? null,
434
557
  toolIds: [...toolIds],
558
+ calls: [...calls],
435
559
  conditional: child.conditional === true,
436
560
  disabled: child.disabled === true,
437
561
  nested: child.type === 'step_suite',
@@ -102,6 +102,16 @@ export type PlayDocflowSubgraph = {
102
102
  export type PlayDocflowBinding = {
103
103
  nodeId: string;
104
104
  line: number;
105
+ /**
106
+ * The file this annotation was written in, when it did not come from the play
107
+ * entry file. A play whose step program lives in a `-core` module has no
108
+ * statement in its `.play.ts` to annotate, so its boxes could only ever be
109
+ * sketches; annotations are read from the entry's import closure to fix that.
110
+ *
111
+ * `line` is only meaningful together with this: two files both have a line
112
+ * 42, so any positional comparison must agree on the path first.
113
+ */
114
+ sourcePath?: string;
105
115
  label?: string;
106
116
  kind?: PlayDocflowNodeKind;
107
117
  /** Symbolic values read by this business node. Never an arbitrary JS expression. */
@@ -153,6 +163,12 @@ export type ParsePlayDocflowOptions = {
153
163
  * both `scalar` and `default`.
154
164
  */
155
165
  exportName?: string | null;
166
+ /**
167
+ * Source of every module the entry imports, keyed by path. Annotations found
168
+ * here bind to the entry's diagrams exactly like the entry's own — routing is
169
+ * by node id, which never depended on which file the annotation sat in.
170
+ */
171
+ moduleSources?: ReadonlyMap<string, string> | null;
156
172
  };
157
173
 
158
174
  /** One `@mermaid` block, already bound to the export it describes. */
@@ -233,6 +249,8 @@ export function docflowLabelCountFragment(label: string): string | null {
233
249
 
234
250
  const FLOW_START = /^\s*(?:flowchart|graph)\s+(LR|RL|TB|TD|BT)\s*$/i;
235
251
  const MERMAID_NODE = /^\s*\/\/\s*@mermaid-node\s+([A-Za-z][\w-]*)(?:\s+(.*))?$/;
252
+ /** Whole-file pre-filter for {@link MERMAID_NODE}; skips splitting clean files. */
253
+ const MERMAID_NODE_HINT = /@mermaid-node/;
236
254
  const ATTRIBUTE = /([A-Za-z][\w-]*)\s*:\s*"([^"\n]*)"/y;
237
255
  const MERMAID_NODE_ATTRIBUTES = ['label', 'type', 'in', 'out', 'arm'] as const;
238
256
  const MERMAID_NODE_ID = /^[A-Za-z][\w-]*/;
@@ -929,6 +947,7 @@ function materializeDocflow(block: ParsedDocflowBlock): PlayDocflow {
929
947
  */
930
948
  export function parsePlayDocflowFile(
931
949
  sourceCode: string,
950
+ moduleSources?: ReadonlyMap<string, string> | null,
932
951
  ): PlayDocflowFileParseResult {
933
952
  const errors: string[] = [];
934
953
  const rawBlocks: Array<{
@@ -948,7 +967,19 @@ export function parsePlayDocflowFile(
948
967
  // the author already has one actionable error about them, and telling them to
949
968
  // "bind it" on the next line is telling them to do what they just did.
950
969
  const rejectedNodeIds = new Set<string>();
951
- const bindings = parseDocflowBindings(sourceCode, errors, rejectedNodeIds);
970
+ // The entry first, then the modules it imports. Only files that actually
971
+ // contain an annotation are split into lines: a play's closure is mostly
972
+ // helpers and types, and this runs on every publish.
973
+ const annotationSources: DocflowAnnotationSource[] = [{ sourceCode }];
974
+ for (const [sourcePath, moduleCode] of moduleSources ?? []) {
975
+ if (!MERMAID_NODE_HINT.test(moduleCode)) continue;
976
+ annotationSources.push({ sourceCode: moduleCode, sourcePath });
977
+ }
978
+ const bindings = parseDocflowBindings(
979
+ annotationSources,
980
+ errors,
981
+ rejectedNodeIds,
982
+ );
952
983
 
953
984
  const parsedBlocks: ParsedDocflowBlock[] = [];
954
985
  for (const raw of rawBlocks) {
@@ -1326,7 +1357,7 @@ export function parsePlayDocflow(
1326
1357
  sourceCode: string,
1327
1358
  options: ParsePlayDocflowOptions = {},
1328
1359
  ): PlayDocflowParseResult {
1329
- const parsed = parsePlayDocflowFile(sourceCode);
1360
+ const parsed = parsePlayDocflowFile(sourceCode, options.moduleSources);
1330
1361
  const requested = options.exportName?.trim() || PLAY_DEFAULT_EXPORT;
1331
1362
  const selected =
1332
1363
  parsed.blocks.find((block) => block.exportName === requested) ??
@@ -1354,10 +1385,32 @@ export function parsePlayDocflow(
1354
1385
  * to themselves afterwards; the annotations themselves are file-scoped text.
1355
1386
  */
1356
1387
  function parseDocflowBindings(
1357
- sourceCode: string,
1388
+ sources: readonly DocflowAnnotationSource[],
1389
+ errors: string[],
1390
+ rejectedNodeIds: Set<string>,
1391
+ ): PlayDocflowBinding[] {
1392
+ const bindings: PlayDocflowBinding[] = [];
1393
+ for (const source of sources) {
1394
+ bindings.push(
1395
+ ...parseDocflowBindingsInSource(source, errors, rejectedNodeIds),
1396
+ );
1397
+ }
1398
+ return bindings;
1399
+ }
1400
+
1401
+ /** One file that may carry `// @mermaid-node` annotations. */
1402
+ type DocflowAnnotationSource = {
1403
+ sourceCode: string;
1404
+ /** Omitted for the entry file, whose paths readers already know. */
1405
+ sourcePath?: string;
1406
+ };
1407
+
1408
+ function parseDocflowBindingsInSource(
1409
+ source: DocflowAnnotationSource,
1358
1410
  errors: string[],
1359
1411
  rejectedNodeIds: Set<string>,
1360
1412
  ): PlayDocflowBinding[] {
1413
+ const { sourceCode, sourcePath } = source;
1361
1414
  const lines = sourceCode.split(/\r?\n/);
1362
1415
  const bindings: PlayDocflowBinding[] = [];
1363
1416
 
@@ -1440,6 +1493,7 @@ function parseDocflowBindings(
1440
1493
  bindings.push({
1441
1494
  nodeId: id,
1442
1495
  line: nextLine + 1,
1496
+ ...(sourcePath ? { sourcePath } : {}),
1443
1497
  ...(attributes.label ? { label: attributes.label } : {}),
1444
1498
  ...(kind ? { kind } : {}),
1445
1499
  // Spread, never `arm: arm ?? undefined` — see `PlayDocflowEdge.arm`. An
@@ -438,6 +438,9 @@ export function buildStaticPipelineSummary(
438
438
  // param sources never need to cross the public boundary.
439
439
  const SUBSTEP_STRIP_KEYS = new Set([
440
440
  'sourceRange',
441
+ // Same reason as `sourceRange`, and the same shape: it carries the absolute
442
+ // path of the file the call was made from, which is an author's machine.
443
+ 'callSiteRange',
441
444
  'sourcePath',
442
445
  'callPath',
443
446
  'callDepth',
@@ -757,6 +757,9 @@ function storageSafeNodeValue(
757
757
  if (asStaticRecord(value.sourceRange)) {
758
758
  out.sourceRange = { ...(value.sourceRange as Record<string, unknown>) };
759
759
  }
760
+ if (asStaticRecord(value.callSiteRange)) {
761
+ out.callSiteRange = { ...(value.callSiteRange as Record<string, unknown>) };
762
+ }
760
763
  if (asStaticRecord(value.sheetContract)) {
761
764
  out.sheetContract = cloneStorageSafeSheetContract(
762
765
  value.sheetContract as PlaySheetContract,
@@ -1224,6 +1227,24 @@ export interface PlayStaticSourceRange {
1224
1227
  type PlayStaticSubstepMetadata = {
1225
1228
  conditional?: boolean;
1226
1229
  disabled?: boolean;
1230
+ /**
1231
+ * Where this substep was REACHED FROM, when the extractor resolved it by
1232
+ * following a call into another function — the range of the call expression
1233
+ * itself, in the file that made the call.
1234
+ *
1235
+ * The extractor hops through `resolveLocalCallable`, which is TypeScript
1236
+ * symbol resolution (`getSymbol()` / `getAliasedSymbol()`), so the callee may
1237
+ * live anywhere in the import closure. Its `sourceRange` then points at the
1238
+ * helper's body, which no annotation in the CALLING file can ever contain.
1239
+ * That is why a binding on `const identity = await resolveCompanyIdentity(…)`
1240
+ * could not reach the `ctx.tools.execute` inside it.
1241
+ *
1242
+ * Recorded as the OUTERMOST hop: unwinding stamps each level, so a tool three
1243
+ * helpers deep ends up carrying the call site in the annotated function
1244
+ * rather than an intermediate one. Absent whenever the substep was found
1245
+ * directly in the function being analyzed.
1246
+ */
1247
+ callSiteRange?: PlayStaticSourceRange;
1227
1248
  };
1228
1249
 
1229
1250
  /**