deepline 0.2.56 → 0.2.57

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.
@@ -160,7 +160,7 @@ export const SDK_RELEASE = {
160
160
  // 0.2.0 makes Dataset Handles uniformly async-only after 0.1.320 briefly
161
161
  // exposed storage-dependent synchronous access. This deliberate minor
162
162
  // release keeps lazy paging semantics independent of row residency.
163
- version: '0.2.56',
163
+ version: '0.2.57',
164
164
  contracts: {
165
165
  api: {
166
166
  name: 'sdk-http-api',
@@ -38,6 +38,19 @@ export type PlayDocflowNode = {
38
38
  id: string;
39
39
  label: string;
40
40
  kind: PlayDocflowNodeKind;
41
+ /**
42
+ * No statement in this play runs this box — the author said so with
43
+ * `class <id> sketch`. See {@link SKETCH_CLASS}.
44
+ *
45
+ * Deliberately NOT a `kind`. What a box IS (an action, a decision, a dataset)
46
+ * and whether code binds it are two different questions, and folding the
47
+ * second into the first was wrong in a way the tests caught immediately: a
48
+ * sketched diamond stopped being a decision, so it lost its shape on the
49
+ * canvas and the branch-label lint stopped checking its arms. Absent rather
50
+ * than `false` when bound, so the JSON a bound diagram hashes to is byte for
51
+ * byte what it was before sketches existed.
52
+ */
53
+ sketch?: true;
41
54
  };
42
55
 
43
56
  /**
@@ -225,6 +238,23 @@ const ATTRIBUTE = /([A-Za-z][\w-]*)\s*:\s*"([^"\n]*)"/y;
225
238
  const MERMAID_NODE_ATTRIBUTES = ['label', 'type', 'in', 'out', 'arm'] as const;
226
239
  const LEGACY_DOCFLOW_ATTRIBUTES = ['id', ...MERMAID_NODE_ATTRIBUTES] as const;
227
240
  const MERMAID_NODE_ID = /^[A-Za-z][\w-]*/;
241
+ /**
242
+ * `class a,b,c sketch` — the author declaring that no statement runs these boxes.
243
+ *
244
+ * Every other box in a diagram must point at a statement in this play's source,
245
+ * because that is what makes the diagram a claim about the code rather than a
246
+ * picture of it. Some boxes honestly cannot: a cascade whose legs live in a
247
+ * sibling module, a loop over a provider list, the outcome boxes hanging off a
248
+ * decision. Those boxes still belong on the canvas — they name the real route —
249
+ * and this is how the author says so out loud, so the reader is told "sketch"
250
+ * instead of being shown a box that looks misconfigured.
251
+ *
252
+ * It is mermaid's own `class` statement rather than a Deepline directive, so the
253
+ * block stays a diagram any mermaid renderer can draw. Class names other than
254
+ * `sketch` remain styling this dashboard does not apply, exactly as before.
255
+ */
256
+ const SKETCH_CLASS =
257
+ /^\s*class\s+([A-Za-z][\w-]*(?:\s*,\s*[A-Za-z][\w-]*)*)\s+sketch\s*;?\s*$/;
228
258
  /**
229
259
  * Mermaid's node shapes, longest opener FIRST.
230
260
  *
@@ -601,7 +631,14 @@ function levenshtein(left: string, right: string): number {
601
631
 
602
632
  const MERMAID_BLOCK = /\/\*\*\s*@mermaid(?:\s|\r?\n)([\s\S]*?)\*\//g;
603
633
  const LEGACY_BLOCK = /\/\*\*\s*@docflow(?:\s|\r?\n)([\s\S]*?)\*\//;
604
- const BLOCK_EXPORT_HEADER = /^[A-Za-z_$][\w$]*$/;
634
+ /**
635
+ * What a block header may name: an export name, or the play's own kebab-case
636
+ * name. Hyphens are in the set for the second — `@mermaid name-to-linkedin-url-
637
+ * waterfall` is the header worth writing, and while it was identifier-only the
638
+ * whole line silently became the diagram's first line, which surfaced as
639
+ * "Docflow must start with `flowchart`" and named nothing.
640
+ */
641
+ const BLOCK_EXPORT_HEADER = /^[A-Za-z_$][\w$-]*$/;
605
642
 
606
643
  /** Strips the JSDoc `*` gutter and trims, leaving the authored block text. */
607
644
  function cleanBlockBody(raw: string): string {
@@ -613,7 +650,8 @@ function cleanBlockBody(raw: string): string {
613
650
  }
614
651
 
615
652
  /**
616
- * Splits `/** @mermaid batch` into the export it names and the diagram itself.
653
+ * Splits `/** @mermaid contact-to-phone-waterfall` into the play it names and
654
+ * the diagram itself.
617
655
  *
618
656
  * The name is the first token after the tag, the same place `// @mermaid-node
619
657
  * <id>` puts its target, so one rule covers both halves of the grammar. It is
@@ -649,6 +687,8 @@ type ParsedDocflowBlockGraph = {
649
687
  edges: PlayDocflowEdge[];
650
688
  subgraphs: Map<string, PlayDocflowSubgraph>;
651
689
  ignoredDirectives: string[];
690
+ /** Ids a `class … sketch` line declared to run no statement. */
691
+ sketchIds: Set<string>;
652
692
  };
653
693
 
654
694
  type ParsedDocflowBlock = ParsedDocflowBlockGraph & {
@@ -701,6 +741,9 @@ function parseDocflowBlockGraph(
701
741
  // an edge on an earlier line may reference a subgraph declared later.
702
742
  const subgraphs = new Map<string, PlayDocflowSubgraph>();
703
743
  const ignoredDirectives: string[] = [];
744
+ // Boxes the author declared to be a sketch, via mermaid's own
745
+ // `class a,b sketch`. See {@link SKETCH_CLASS}.
746
+ const sketchIds = new Set<string>();
704
747
  const subgraphStack: PlayDocflowSubgraph[] = [];
705
748
  const subgraphIds = new Set<string>();
706
749
  if (syntax === 'mermaid') {
@@ -796,7 +839,15 @@ function parseDocflowBlockGraph(
796
839
  const isDirective =
797
840
  /^\s*(?:direction|classDef|class|style|linkStyle|click)\b/i.test(line);
798
841
  if (syntax === 'mermaid' && isDirective) {
799
- ignoredDirectives.push(line.trim());
842
+ const sketch = SKETCH_CLASS.exec(line);
843
+ if (sketch) {
844
+ for (const id of sketch[1]!.split(',')) {
845
+ const trimmed = id.trim();
846
+ if (trimmed) sketchIds.add(trimmed);
847
+ }
848
+ } else {
849
+ ignoredDirectives.push(line.trim());
850
+ }
800
851
  }
801
852
  if (syntax === 'mermaid' && !isDirective) {
802
853
  declaredOnThisLine = addNodes(line, line);
@@ -859,7 +910,20 @@ function parseDocflowBlockGraph(
859
910
  );
860
911
  }
861
912
 
862
- return { direction, nodes, edges, subgraphs, ignoredDirectives };
913
+ for (const id of sketchIds) {
914
+ const node = nodes.get(id);
915
+ if (!node) {
916
+ errors.push(
917
+ subgraphs.has(id)
918
+ ? `Docflow \`class ${id} sketch\` names a subgraph. A subgraph is a region, not a box, and never binds code — drop it from the class line.`
919
+ : `Docflow \`class ${id} sketch\` names "${id}", which this diagram does not draw.`,
920
+ );
921
+ continue;
922
+ }
923
+ node.sketch = true;
924
+ }
925
+
926
+ return { direction, nodes, edges, subgraphs, ignoredDirectives, sketchIds };
863
927
  }
864
928
 
865
929
  function materializeDocflow(block: ParsedDocflowBlock): PlayDocflow {
@@ -889,7 +953,11 @@ export function parsePlayDocflowFile(
889
953
  sourceCode: string,
890
954
  ): PlayDocflowFileParseResult {
891
955
  const errors: string[] = [];
892
- const bindings = parseDocflowBindings(sourceCode, errors);
956
+ // Ids whose annotation was written but refused. They are NOT unbound boxes:
957
+ // the author already has one actionable error about them, and telling them to
958
+ // "bind it" on the next line is telling them to do what they just did.
959
+ const rejectedNodeIds = new Set<string>();
960
+ const bindings = parseDocflowBindings(sourceCode, errors, rejectedNodeIds);
893
961
 
894
962
  const rawBlocks: Array<{
895
963
  syntax: NonNullable<PlayDocflow['syntax']>;
@@ -923,7 +991,13 @@ export function parsePlayDocflowFile(
923
991
  }
924
992
 
925
993
  resolveBlockExports(sourceCode, parsedBlocks, errors);
926
- attachBindingsToBlocks(sourceCode, parsedBlocks, bindings, errors);
994
+ attachBindingsToBlocks(
995
+ sourceCode,
996
+ parsedBlocks,
997
+ bindings,
998
+ errors,
999
+ rejectedNodeIds,
1000
+ );
927
1001
 
928
1002
  return {
929
1003
  blocks: parsedBlocks
@@ -1163,7 +1237,7 @@ function projectRecordedArms(
1163
1237
  // One decision, one meaning per token. Two arms both claiming `run` is the
1164
1238
  // exact defect the recorded identity exists to make impossible, so it is
1165
1239
  // refused at the source rather than resolved by precedence downstream.
1166
- const key = `${edge.from}${binding.arm}`;
1240
+ const key = `${edge.from}\u0000${binding.arm}`;
1167
1241
  const already = declaredBy.get(key);
1168
1242
  if (already !== undefined) {
1169
1243
  errors.push(
@@ -1181,6 +1255,7 @@ function attachBindingsToBlocks(
1181
1255
  blocks: ParsedDocflowBlock[],
1182
1256
  bindings: readonly PlayDocflowBinding[],
1183
1257
  errors: string[],
1258
+ rejectedNodeIds: ReadonlySet<string>,
1184
1259
  ): void {
1185
1260
  const lineStarts = sourceLineStartsForDocflow(sourceCode);
1186
1261
  const exportRanges = playExportSourceRanges(sourceCode);
@@ -1227,17 +1302,33 @@ function attachBindingsToBlocks(
1227
1302
  for (const binding of block.bindings) {
1228
1303
  const node = block.nodes.get(binding.nodeId)!;
1229
1304
  if (binding.label) node.label = binding.label;
1305
+ // A box cannot be both a sketch and a bound statement. Silently letting
1306
+ // one win renders a real, traceable step as scenery — or the reverse — and
1307
+ // the author who wrote both has no way to see which they got.
1308
+ if (block.sketchIds.has(binding.nodeId)) {
1309
+ errors.push(
1310
+ `Docflow box "${binding.nodeId}" is declared \`class ${binding.nodeId} sketch\` and also bound by \`// @mermaid-node ${binding.nodeId}\` on line ${binding.line}. It is one or the other: drop it from the class line, or drop the annotation.`,
1311
+ );
1312
+ continue;
1313
+ }
1230
1314
  if (binding.kind) node.kind = binding.kind;
1231
1315
  }
1232
1316
  projectRecordedArms(block, errors);
1233
- if (block.syntax !== 'docflow') continue;
1234
1317
  const bound = new Set(block.bindings.map((binding) => binding.nodeId));
1235
1318
  for (const node of block.nodes.values()) {
1236
- if (node.kind !== 'conceptual' && !bound.has(node.id)) {
1237
- errors.push(
1238
- `Docflow node "${node.id}" has no code binding; mark it type:"conceptual" or bind it.`,
1239
- );
1240
- }
1319
+ if (
1320
+ node.sketch ||
1321
+ node.kind === 'conceptual' ||
1322
+ bound.has(node.id) ||
1323
+ rejectedNodeIds.has(node.id)
1324
+ )
1325
+ continue;
1326
+ errors.push(
1327
+ block.syntax === 'docflow'
1328
+ ? `Docflow node "${node.id}" has no code binding; mark it type:"conceptual" or bind it.`
1329
+ : `Docflow box "${node.id}" in \`${blockLabel(block)}\` points at nothing, so the canvas can only draw it and say nothing about it. ` +
1330
+ `Either put \`// @mermaid-node ${node.id}\` above the statement it runs, or — if this play has no such statement, because the work happens in another module or inside a loop — add it to a \`class ${node.id} sketch\` line in the block to declare it a sketch.`,
1331
+ );
1241
1332
  }
1242
1333
  }
1243
1334
  }
@@ -1292,6 +1383,7 @@ export function parsePlayDocflow(
1292
1383
  function parseDocflowBindings(
1293
1384
  sourceCode: string,
1294
1385
  errors: string[],
1386
+ rejectedNodeIds: Set<string>,
1295
1387
  ): PlayDocflowBinding[] {
1296
1388
  const lines = sourceCode.split(/\r?\n/);
1297
1389
  const bindings: PlayDocflowBinding[] = [];
@@ -1302,6 +1394,11 @@ function parseDocflowBindings(
1302
1394
  const legacyMatch = PUT.exec(lines[index]!);
1303
1395
  const mermaidMatch = MERMAID_NODE.exec(lines[index]!);
1304
1396
  if (!legacyMatch && !mermaidMatch) continue;
1397
+ // Whatever this annotation names, the author has now written it down. Every
1398
+ // `continue` below is a rejection, and a rejected id must not come back as
1399
+ // an unbound box — see `rejectedNodeIds` in `parsePlayDocflowFile`.
1400
+ const annotatedId = mermaidMatch?.[1]?.trim();
1401
+ if (annotatedId) rejectedNodeIds.add(annotatedId);
1305
1402
  const attributes = parseAttributes(
1306
1403
  mermaidMatch ? (mermaidMatch[2] ?? '') : legacyMatch![1]!,
1307
1404
  {
@@ -1311,11 +1408,12 @@ function parseDocflowBindings(
1311
1408
  },
1312
1409
  );
1313
1410
  if (!attributes) continue;
1314
- const id = (mermaidMatch ? mermaidMatch[1] : attributes.id)?.trim();
1411
+ const id = (annotatedId ?? attributes.id)?.trim();
1315
1412
  if (!id) {
1316
1413
  errors.push(`Docflow annotation on line ${index + 1} requires id:"…".`);
1317
1414
  continue;
1318
1415
  }
1416
+ rejectedNodeIds.add(id);
1319
1417
  const kind = nodeKind(attributes.type);
1320
1418
  if (attributes.type && !kind) {
1321
1419
  errors.push(
@@ -1370,6 +1468,7 @@ function parseDocflowBindings(
1370
1468
  ioConfidence: 'explicit' as const,
1371
1469
  }
1372
1470
  : inferBindingIo(lines[nextLine]!);
1471
+ rejectedNodeIds.delete(id);
1373
1472
  bindings.push({
1374
1473
  nodeId: id,
1375
1474
  line: nextLine + 1,
@@ -34,6 +34,16 @@ export type PlayFileExport = {
34
34
  name: string;
35
35
  /** Other export names that resolve to the same `definePlay` call. */
36
36
  aliases: string[];
37
+ /**
38
+ * The play's own name — `definePlay('<this>', …)` — when it is a literal.
39
+ *
40
+ * An export name is a module detail (`scalar`, `batch`); the play name is what
41
+ * the product, the registry and the customer call this thing. So it is a legal
42
+ * way to address the play, which is why an `@mermaid <name>` header may write
43
+ * either. Null when the first argument is not a string literal, which is only
44
+ * possible for a play the registry could not name either.
45
+ */
46
+ playName: string | null;
37
47
  };
38
48
 
39
49
  function getIdentifierName(node: unknown): string | null {
@@ -83,6 +93,16 @@ export function isDefinePlayCall(node: AstNode | null): boolean {
83
93
  return false;
84
94
  }
85
95
 
96
+ /** The literal name a `definePlay('…', …)` call declares, when it is one. */
97
+ function definePlayName(node: AstNode | null): string | null {
98
+ const expression = unwrapStaticExpression(node);
99
+ if (!expression || expression.type !== 'CallExpression') return null;
100
+ const first = astArray(expression.arguments)[0] ?? null;
101
+ return first?.type === 'Literal' && typeof first.value === 'string'
102
+ ? first.value
103
+ : null;
104
+ }
105
+
86
106
  /**
87
107
  * The plays `sourceCode` exports, default first, then named exports in source
88
108
  * order. `null` means acorn could not parse the file — the caller abstains and
@@ -163,13 +183,26 @@ export function listPlayFileExports(
163
183
  .map(([exported]) => exported)
164
184
  : [];
165
185
  if (defaultLocalName) aliasedLocals.add(defaultLocalName);
166
- exports.push({ name: PLAY_DEFAULT_EXPORT, aliases });
186
+ exports.push({
187
+ name: PLAY_DEFAULT_EXPORT,
188
+ aliases,
189
+ playName: definePlayName(
190
+ defaultLocalName
191
+ ? (declarations.get(defaultLocalName) ?? null)
192
+ : defaultExpression,
193
+ ),
194
+ });
167
195
  }
168
196
  for (const [exportedName, localName] of namedExports) {
169
197
  if (exportedName === PLAY_DEFAULT_EXPORT) continue;
170
198
  if (aliasedLocals.has(localName)) continue;
171
- if (!isDefinePlayCall(declarations.get(localName) ?? null)) continue;
172
- exports.push({ name: exportedName, aliases: [] });
199
+ const declaration = declarations.get(localName) ?? null;
200
+ if (!isDefinePlayCall(declaration)) continue;
201
+ exports.push({
202
+ name: exportedName,
203
+ aliases: [],
204
+ playName: definePlayName(declaration),
205
+ });
173
206
  }
174
207
 
175
208
  return exports;
@@ -180,6 +213,12 @@ export function listPlayFileExports(
180
213
  * canonical name. `scalar` in `export default scalar` resolves to `default`;
181
214
  * an unknown name resolves to `null` so the caller can fail loudly with the
182
215
  * available set rather than silently binding nothing.
216
+ *
217
+ * The play's own name resolves too, and is the better thing to write: a diagram
218
+ * headed `@mermaid scalar` names a module-local binding, while one headed
219
+ * `@mermaid name-and-domain-to-email-waterfall` names the play the reader came
220
+ * here for. Export names keep working because every diagram authored before this
221
+ * used one.
183
222
  */
184
223
  export function canonicalPlayExportName(
185
224
  exportName: string | null | undefined,
@@ -191,6 +230,12 @@ export function canonicalPlayExportName(
191
230
  return entry.name;
192
231
  }
193
232
  }
233
+ // Second pass, so an export literally named after another play's play name
234
+ // can never lose to it. Ambiguity between two plays' names is impossible:
235
+ // the registry rejects a duplicate play name before this ever runs.
236
+ for (const entry of exports) {
237
+ if (entry.playName === requested) return entry.name;
238
+ }
194
239
  return null;
195
240
  }
196
241
 
@@ -198,5 +243,9 @@ export function canonicalPlayExportName(
198
243
  export function playExportNamesForMessage(
199
244
  exports: readonly PlayFileExport[],
200
245
  ): string[] {
201
- return exports.flatMap((entry) => [entry.name, ...entry.aliases]);
246
+ return exports.flatMap((entry) => [
247
+ ...(entry.playName ? [entry.playName] : []),
248
+ entry.name,
249
+ ...entry.aliases,
250
+ ]);
202
251
  }
package/dist/cli/index.js CHANGED
@@ -1044,7 +1044,7 @@ var SDK_RELEASE = {
1044
1044
  // 0.2.0 makes Dataset Handles uniformly async-only after 0.1.320 briefly
1045
1045
  // exposed storage-dependent synchronous access. This deliberate minor
1046
1046
  // release keeps lazy paging semantics independent of row residency.
1047
- version: "0.2.56",
1047
+ version: "0.2.57",
1048
1048
  contracts: {
1049
1049
  api: {
1050
1050
  name: "sdk-http-api",
@@ -16645,6 +16645,12 @@ function isDefinePlayCall(node) {
16645
16645
  }
16646
16646
  return false;
16647
16647
  }
16648
+ function definePlayName(node) {
16649
+ const expression = unwrapStaticExpression(node);
16650
+ if (!expression || expression.type !== "CallExpression") return null;
16651
+ const first = astArray(expression.arguments)[0] ?? null;
16652
+ return first?.type === "Literal" && typeof first.value === "string" ? first.value : null;
16653
+ }
16648
16654
  function listPlayFileExports(sourceCode) {
16649
16655
  const ast = parsePlaySourceForAnalysis(sourceCode);
16650
16656
  if (!ast) return null;
@@ -16694,13 +16700,24 @@ function listPlayFileExports(sourceCode) {
16694
16700
  if (defaultIsPlay) {
16695
16701
  const aliases = defaultLocalName ? [...namedExports.entries()].filter(([, local]) => local === defaultLocalName).map(([exported]) => exported) : [];
16696
16702
  if (defaultLocalName) aliasedLocals.add(defaultLocalName);
16697
- exports2.push({ name: PLAY_DEFAULT_EXPORT, aliases });
16703
+ exports2.push({
16704
+ name: PLAY_DEFAULT_EXPORT,
16705
+ aliases,
16706
+ playName: definePlayName(
16707
+ defaultLocalName ? declarations.get(defaultLocalName) ?? null : defaultExpression
16708
+ )
16709
+ });
16698
16710
  }
16699
16711
  for (const [exportedName, localName] of namedExports) {
16700
16712
  if (exportedName === PLAY_DEFAULT_EXPORT) continue;
16701
16713
  if (aliasedLocals.has(localName)) continue;
16702
- if (!isDefinePlayCall(declarations.get(localName) ?? null)) continue;
16703
- exports2.push({ name: exportedName, aliases: [] });
16714
+ const declaration = declarations.get(localName) ?? null;
16715
+ if (!isDefinePlayCall(declaration)) continue;
16716
+ exports2.push({
16717
+ name: exportedName,
16718
+ aliases: [],
16719
+ playName: definePlayName(declaration)
16720
+ });
16704
16721
  }
16705
16722
  return exports2;
16706
16723
  }
@@ -1030,7 +1030,7 @@ var SDK_RELEASE = {
1030
1030
  // 0.2.0 makes Dataset Handles uniformly async-only after 0.1.320 briefly
1031
1031
  // exposed storage-dependent synchronous access. This deliberate minor
1032
1032
  // release keeps lazy paging semantics independent of row residency.
1033
- version: "0.2.56",
1033
+ version: "0.2.57",
1034
1034
  contracts: {
1035
1035
  api: {
1036
1036
  name: "sdk-http-api",
@@ -16690,6 +16690,12 @@ function isDefinePlayCall(node) {
16690
16690
  }
16691
16691
  return false;
16692
16692
  }
16693
+ function definePlayName(node) {
16694
+ const expression = unwrapStaticExpression(node);
16695
+ if (!expression || expression.type !== "CallExpression") return null;
16696
+ const first = astArray(expression.arguments)[0] ?? null;
16697
+ return first?.type === "Literal" && typeof first.value === "string" ? first.value : null;
16698
+ }
16693
16699
  function listPlayFileExports(sourceCode) {
16694
16700
  const ast = parsePlaySourceForAnalysis(sourceCode);
16695
16701
  if (!ast) return null;
@@ -16739,13 +16745,24 @@ function listPlayFileExports(sourceCode) {
16739
16745
  if (defaultIsPlay) {
16740
16746
  const aliases = defaultLocalName ? [...namedExports.entries()].filter(([, local]) => local === defaultLocalName).map(([exported]) => exported) : [];
16741
16747
  if (defaultLocalName) aliasedLocals.add(defaultLocalName);
16742
- exports.push({ name: PLAY_DEFAULT_EXPORT, aliases });
16748
+ exports.push({
16749
+ name: PLAY_DEFAULT_EXPORT,
16750
+ aliases,
16751
+ playName: definePlayName(
16752
+ defaultLocalName ? declarations.get(defaultLocalName) ?? null : defaultExpression
16753
+ )
16754
+ });
16743
16755
  }
16744
16756
  for (const [exportedName, localName] of namedExports) {
16745
16757
  if (exportedName === PLAY_DEFAULT_EXPORT) continue;
16746
16758
  if (aliasedLocals.has(localName)) continue;
16747
- if (!isDefinePlayCall(declarations.get(localName) ?? null)) continue;
16748
- exports.push({ name: exportedName, aliases: [] });
16759
+ const declaration = declarations.get(localName) ?? null;
16760
+ if (!isDefinePlayCall(declaration)) continue;
16761
+ exports.push({
16762
+ name: exportedName,
16763
+ aliases: [],
16764
+ playName: definePlayName(declaration)
16765
+ });
16749
16766
  }
16750
16767
  return exports;
16751
16768
  }
@@ -21,6 +21,19 @@ type PlayDocflowNode = {
21
21
  id: string;
22
22
  label: string;
23
23
  kind: PlayDocflowNodeKind;
24
+ /**
25
+ * No statement in this play runs this box — the author said so with
26
+ * `class <id> sketch`. See {@link SKETCH_CLASS}.
27
+ *
28
+ * Deliberately NOT a `kind`. What a box IS (an action, a decision, a dataset)
29
+ * and whether code binds it are two different questions, and folding the
30
+ * second into the first was wrong in a way the tests caught immediately: a
31
+ * sketched diamond stopped being a decision, so it lost its shape on the
32
+ * canvas and the branch-label lint stopped checking its arms. Absent rather
33
+ * than `false` when bound, so the JSON a bound diagram hashes to is byte for
34
+ * byte what it was before sketches existed.
35
+ */
36
+ sketch?: true;
24
37
  };
25
38
  /**
26
39
  * Which arm of a conditional a drawn decision edge IS.
@@ -21,6 +21,19 @@ type PlayDocflowNode = {
21
21
  id: string;
22
22
  label: string;
23
23
  kind: PlayDocflowNodeKind;
24
+ /**
25
+ * No statement in this play runs this box — the author said so with
26
+ * `class <id> sketch`. See {@link SKETCH_CLASS}.
27
+ *
28
+ * Deliberately NOT a `kind`. What a box IS (an action, a decision, a dataset)
29
+ * and whether code binds it are two different questions, and folding the
30
+ * second into the first was wrong in a way the tests caught immediately: a
31
+ * sketched diamond stopped being a decision, so it lost its shape on the
32
+ * canvas and the branch-label lint stopped checking its arms. Absent rather
33
+ * than `false` when bound, so the JSON a bound diagram hashes to is byte for
34
+ * byte what it was before sketches existed.
35
+ */
36
+ sketch?: true;
24
37
  };
25
38
  /**
26
39
  * Which arm of a conditional a drawn decision edge IS.
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { e as PlayRuntimeBackendId, c as PlayCompilerManifest, D as DeeplineError, f as ToolExecutionError, g as ToolExecutionErrorOptions, h as PlayAuthoringColumnMap, i as PlayAuthoringColumnResolver, j as PlayAuthoringRuntimeContext, k as PlayAuthoringConditionalStepResolver, l as PlayAuthoringCsvInput, m as PlayAuthoringCsvOptions, n as PlayAuthoringDatasetBuilder, o as PlayAuthoringDatasetColumnDefinition, p as PlayAuthoringDatasetColumnRunInput, q as ToolExecuteResult, r as PlayAuthoringReferenceLike, s as PlayReturnObject$1, t as PlayAuthoringDefineConfig, u as PlayAuthoringDefinedPlay, v as PlayAuthoringFetchOptions, w as PlayAuthoringFileInput, x as PlayAuthoringBindings, y as PlayAuthoringCallExecution, z as PlayAuthoringCallOptions, A as PlayAuthoringFetchResponse, B as PlayAuthoringInputContract, C as PlayAuthoringStepProgramStep, E as PlayAuthoringRuntimeStepOptions, F as PlaySqlListenerDeclaration, G as PlaySqlListenerEvent, H as PlaySqlListenerOperation, I as PlaySqlQuery, J as PlayAuthoringStepOptions, K as PlayAuthoringStepProgram, L as PlayAuthoringStepProgramResolver, M as PlayAuthoringStepResolver, N as PlayToolExecutionRequest } from './compiler-manifest-Bl8kmLx9.mjs';
2
- export { O as DEEPLINE_EXTRACTOR_TARGETS, Q as DEEPLINE_EXTRACTOR_TARGET_DEFINITIONS, R as DeeplineEmailStatusGetterValue, S as DeeplineExtractorTarget, U as DeeplineGetterValue, V as DeeplineGetterValueMap, W as JOB_CHANGE_STATUS_VALUES, X as JobChangeStatus, Y as PHONE_STATUS_VALUES, Z as PhoneStatus, _ as PlayDataset, $ as PlayDatasetInput, a0 as PreviousCell, a1 as ProviderTransientError, a2 as ProviderTransientErrorCategory, a3 as ToolExecutionErrorCategory, a4 as ToolExecutionErrorOrigin, a5 as ToolExecutionFailureV1, a6 as ToolExecutionNetworkKind, a7 as ToolExecutionNetworkScope, a8 as isDeeplineExtractorTarget } from './compiler-manifest-Bl8kmLx9.mjs';
1
+ import { e as PlayRuntimeBackendId, c as PlayCompilerManifest, D as DeeplineError, f as ToolExecutionError, g as ToolExecutionErrorOptions, h as PlayAuthoringColumnMap, i as PlayAuthoringColumnResolver, j as PlayAuthoringRuntimeContext, k as PlayAuthoringConditionalStepResolver, l as PlayAuthoringCsvInput, m as PlayAuthoringCsvOptions, n as PlayAuthoringDatasetBuilder, o as PlayAuthoringDatasetColumnDefinition, p as PlayAuthoringDatasetColumnRunInput, q as ToolExecuteResult, r as PlayAuthoringReferenceLike, s as PlayReturnObject$1, t as PlayAuthoringDefineConfig, u as PlayAuthoringDefinedPlay, v as PlayAuthoringFetchOptions, w as PlayAuthoringFileInput, x as PlayAuthoringBindings, y as PlayAuthoringCallExecution, z as PlayAuthoringCallOptions, A as PlayAuthoringFetchResponse, B as PlayAuthoringInputContract, C as PlayAuthoringStepProgramStep, E as PlayAuthoringRuntimeStepOptions, F as PlaySqlListenerDeclaration, G as PlaySqlListenerEvent, H as PlaySqlListenerOperation, I as PlaySqlQuery, J as PlayAuthoringStepOptions, K as PlayAuthoringStepProgram, L as PlayAuthoringStepProgramResolver, M as PlayAuthoringStepResolver, N as PlayToolExecutionRequest } from './compiler-manifest-TgaC4DeD.mjs';
2
+ export { O as DEEPLINE_EXTRACTOR_TARGETS, Q as DEEPLINE_EXTRACTOR_TARGET_DEFINITIONS, R as DeeplineEmailStatusGetterValue, S as DeeplineExtractorTarget, U as DeeplineGetterValue, V as DeeplineGetterValueMap, W as JOB_CHANGE_STATUS_VALUES, X as JobChangeStatus, Y as PHONE_STATUS_VALUES, Z as PhoneStatus, _ as PlayDataset, $ as PlayDatasetInput, a0 as PreviousCell, a1 as ProviderTransientError, a2 as ProviderTransientErrorCategory, a3 as ToolExecutionErrorCategory, a4 as ToolExecutionErrorOrigin, a5 as ToolExecutionFailureV1, a6 as ToolExecutionNetworkKind, a7 as ToolExecutionNetworkScope, a8 as isDeeplineExtractorTarget } from './compiler-manifest-TgaC4DeD.mjs';
3
3
  import '@sinclair/typebox';
4
4
 
5
5
  declare const FIXTURE_BEHAVIOR_VERSION: 1;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { e as PlayRuntimeBackendId, c as PlayCompilerManifest, D as DeeplineError, f as ToolExecutionError, g as ToolExecutionErrorOptions, h as PlayAuthoringColumnMap, i as PlayAuthoringColumnResolver, j as PlayAuthoringRuntimeContext, k as PlayAuthoringConditionalStepResolver, l as PlayAuthoringCsvInput, m as PlayAuthoringCsvOptions, n as PlayAuthoringDatasetBuilder, o as PlayAuthoringDatasetColumnDefinition, p as PlayAuthoringDatasetColumnRunInput, q as ToolExecuteResult, r as PlayAuthoringReferenceLike, s as PlayReturnObject$1, t as PlayAuthoringDefineConfig, u as PlayAuthoringDefinedPlay, v as PlayAuthoringFetchOptions, w as PlayAuthoringFileInput, x as PlayAuthoringBindings, y as PlayAuthoringCallExecution, z as PlayAuthoringCallOptions, A as PlayAuthoringFetchResponse, B as PlayAuthoringInputContract, C as PlayAuthoringStepProgramStep, E as PlayAuthoringRuntimeStepOptions, F as PlaySqlListenerDeclaration, G as PlaySqlListenerEvent, H as PlaySqlListenerOperation, I as PlaySqlQuery, J as PlayAuthoringStepOptions, K as PlayAuthoringStepProgram, L as PlayAuthoringStepProgramResolver, M as PlayAuthoringStepResolver, N as PlayToolExecutionRequest } from './compiler-manifest-Bl8kmLx9.js';
2
- export { O as DEEPLINE_EXTRACTOR_TARGETS, Q as DEEPLINE_EXTRACTOR_TARGET_DEFINITIONS, R as DeeplineEmailStatusGetterValue, S as DeeplineExtractorTarget, U as DeeplineGetterValue, V as DeeplineGetterValueMap, W as JOB_CHANGE_STATUS_VALUES, X as JobChangeStatus, Y as PHONE_STATUS_VALUES, Z as PhoneStatus, _ as PlayDataset, $ as PlayDatasetInput, a0 as PreviousCell, a1 as ProviderTransientError, a2 as ProviderTransientErrorCategory, a3 as ToolExecutionErrorCategory, a4 as ToolExecutionErrorOrigin, a5 as ToolExecutionFailureV1, a6 as ToolExecutionNetworkKind, a7 as ToolExecutionNetworkScope, a8 as isDeeplineExtractorTarget } from './compiler-manifest-Bl8kmLx9.js';
1
+ import { e as PlayRuntimeBackendId, c as PlayCompilerManifest, D as DeeplineError, f as ToolExecutionError, g as ToolExecutionErrorOptions, h as PlayAuthoringColumnMap, i as PlayAuthoringColumnResolver, j as PlayAuthoringRuntimeContext, k as PlayAuthoringConditionalStepResolver, l as PlayAuthoringCsvInput, m as PlayAuthoringCsvOptions, n as PlayAuthoringDatasetBuilder, o as PlayAuthoringDatasetColumnDefinition, p as PlayAuthoringDatasetColumnRunInput, q as ToolExecuteResult, r as PlayAuthoringReferenceLike, s as PlayReturnObject$1, t as PlayAuthoringDefineConfig, u as PlayAuthoringDefinedPlay, v as PlayAuthoringFetchOptions, w as PlayAuthoringFileInput, x as PlayAuthoringBindings, y as PlayAuthoringCallExecution, z as PlayAuthoringCallOptions, A as PlayAuthoringFetchResponse, B as PlayAuthoringInputContract, C as PlayAuthoringStepProgramStep, E as PlayAuthoringRuntimeStepOptions, F as PlaySqlListenerDeclaration, G as PlaySqlListenerEvent, H as PlaySqlListenerOperation, I as PlaySqlQuery, J as PlayAuthoringStepOptions, K as PlayAuthoringStepProgram, L as PlayAuthoringStepProgramResolver, M as PlayAuthoringStepResolver, N as PlayToolExecutionRequest } from './compiler-manifest-TgaC4DeD.js';
2
+ export { O as DEEPLINE_EXTRACTOR_TARGETS, Q as DEEPLINE_EXTRACTOR_TARGET_DEFINITIONS, R as DeeplineEmailStatusGetterValue, S as DeeplineExtractorTarget, U as DeeplineGetterValue, V as DeeplineGetterValueMap, W as JOB_CHANGE_STATUS_VALUES, X as JobChangeStatus, Y as PHONE_STATUS_VALUES, Z as PhoneStatus, _ as PlayDataset, $ as PlayDatasetInput, a0 as PreviousCell, a1 as ProviderTransientError, a2 as ProviderTransientErrorCategory, a3 as ToolExecutionErrorCategory, a4 as ToolExecutionErrorOrigin, a5 as ToolExecutionFailureV1, a6 as ToolExecutionNetworkKind, a7 as ToolExecutionNetworkScope, a8 as isDeeplineExtractorTarget } from './compiler-manifest-TgaC4DeD.js';
3
3
  import '@sinclair/typebox';
4
4
 
5
5
  declare const FIXTURE_BEHAVIOR_VERSION: 1;
package/dist/index.js CHANGED
@@ -763,7 +763,7 @@ var SDK_RELEASE = {
763
763
  // 0.2.0 makes Dataset Handles uniformly async-only after 0.1.320 briefly
764
764
  // exposed storage-dependent synchronous access. This deliberate minor
765
765
  // release keeps lazy paging semantics independent of row residency.
766
- version: "0.2.56",
766
+ version: "0.2.57",
767
767
  contracts: {
768
768
  api: {
769
769
  name: "sdk-http-api",
package/dist/index.mjs CHANGED
@@ -689,7 +689,7 @@ var SDK_RELEASE = {
689
689
  // 0.2.0 makes Dataset Handles uniformly async-only after 0.1.320 briefly
690
690
  // exposed storage-dependent synchronous access. This deliberate minor
691
691
  // release keeps lazy paging semantics independent of row residency.
692
- version: "0.2.56",
692
+ version: "0.2.57",
693
693
  contracts: {
694
694
  api: {
695
695
  name: "sdk-http-api",
@@ -225,8 +225,8 @@
225
225
  "dist/cli/index.d.ts",
226
226
  "dist/cli/index.js",
227
227
  "dist/cli/index.mjs",
228
- "dist/compiler-manifest-Bl8kmLx9.d.mts",
229
- "dist/compiler-manifest-Bl8kmLx9.d.ts",
228
+ "dist/compiler-manifest-TgaC4DeD.d.mts",
229
+ "dist/compiler-manifest-TgaC4DeD.d.ts",
230
230
  "dist/helpers.d.mts",
231
231
  "dist/helpers.d.ts",
232
232
  "dist/helpers.js",
@@ -1,5 +1,5 @@
1
- import { T as ToolExecutionErrorSchemaVersion, P as PlayAuthoringContractEdition, a as PlayArtifactKind$1, b as PlaySandboxRuntimeDeclaration, c as PlayCompilerManifest } from '../compiler-manifest-Bl8kmLx9.mjs';
2
- export { d as PLAY_ARTIFACT_KINDS } from '../compiler-manifest-Bl8kmLx9.mjs';
1
+ import { T as ToolExecutionErrorSchemaVersion, P as PlayAuthoringContractEdition, a as PlayArtifactKind$1, b as PlaySandboxRuntimeDeclaration, c as PlayCompilerManifest } from '../compiler-manifest-TgaC4DeD.mjs';
2
+ export { d as PLAY_ARTIFACT_KINDS } from '../compiler-manifest-TgaC4DeD.mjs';
3
3
  import '@sinclair/typebox';
4
4
 
5
5
  type PlayPackageImport = {
@@ -1,5 +1,5 @@
1
- import { T as ToolExecutionErrorSchemaVersion, P as PlayAuthoringContractEdition, a as PlayArtifactKind$1, b as PlaySandboxRuntimeDeclaration, c as PlayCompilerManifest } from '../compiler-manifest-Bl8kmLx9.js';
2
- export { d as PLAY_ARTIFACT_KINDS } from '../compiler-manifest-Bl8kmLx9.js';
1
+ import { T as ToolExecutionErrorSchemaVersion, P as PlayAuthoringContractEdition, a as PlayArtifactKind$1, b as PlaySandboxRuntimeDeclaration, c as PlayCompilerManifest } from '../compiler-manifest-TgaC4DeD.js';
2
+ export { d as PLAY_ARTIFACT_KINDS } from '../compiler-manifest-TgaC4DeD.js';
3
3
  import '@sinclair/typebox';
4
4
 
5
5
  type PlayPackageImport = {
@@ -4059,6 +4059,12 @@ function isDefinePlayCall(node) {
4059
4059
  }
4060
4060
  return false;
4061
4061
  }
4062
+ function definePlayName(node) {
4063
+ const expression = unwrapStaticExpression(node);
4064
+ if (!expression || expression.type !== "CallExpression") return null;
4065
+ const first = astArray(expression.arguments)[0] ?? null;
4066
+ return first?.type === "Literal" && typeof first.value === "string" ? first.value : null;
4067
+ }
4062
4068
  function listPlayFileExports(sourceCode) {
4063
4069
  const ast = parsePlaySourceForAnalysis(sourceCode);
4064
4070
  if (!ast) return null;
@@ -4108,13 +4114,24 @@ function listPlayFileExports(sourceCode) {
4108
4114
  if (defaultIsPlay) {
4109
4115
  const aliases = defaultLocalName ? [...namedExports.entries()].filter(([, local]) => local === defaultLocalName).map(([exported]) => exported) : [];
4110
4116
  if (defaultLocalName) aliasedLocals.add(defaultLocalName);
4111
- exports.push({ name: PLAY_DEFAULT_EXPORT, aliases });
4117
+ exports.push({
4118
+ name: PLAY_DEFAULT_EXPORT,
4119
+ aliases,
4120
+ playName: definePlayName(
4121
+ defaultLocalName ? declarations.get(defaultLocalName) ?? null : defaultExpression
4122
+ )
4123
+ });
4112
4124
  }
4113
4125
  for (const [exportedName, localName] of namedExports) {
4114
4126
  if (exportedName === PLAY_DEFAULT_EXPORT) continue;
4115
4127
  if (aliasedLocals.has(localName)) continue;
4116
- if (!isDefinePlayCall(declarations.get(localName) ?? null)) continue;
4117
- exports.push({ name: exportedName, aliases: [] });
4128
+ const declaration = declarations.get(localName) ?? null;
4129
+ if (!isDefinePlayCall(declaration)) continue;
4130
+ exports.push({
4131
+ name: exportedName,
4132
+ aliases: [],
4133
+ playName: definePlayName(declaration)
4134
+ });
4118
4135
  }
4119
4136
  return exports;
4120
4137
  }
@@ -4125,10 +4142,17 @@ function canonicalPlayExportName(exportName, exports) {
4125
4142
  return entry.name;
4126
4143
  }
4127
4144
  }
4145
+ for (const entry of exports) {
4146
+ if (entry.playName === requested) return entry.name;
4147
+ }
4128
4148
  return null;
4129
4149
  }
4130
4150
  function playExportNamesForMessage(exports) {
4131
- return exports.flatMap((entry) => [entry.name, ...entry.aliases]);
4151
+ return exports.flatMap((entry) => [
4152
+ ...entry.playName ? [entry.playName] : [],
4153
+ entry.name,
4154
+ ...entry.aliases
4155
+ ]);
4132
4156
  }
4133
4157
 
4134
4158
  // ../shared_libs/plays/docflow.ts
@@ -4150,6 +4174,7 @@ var ATTRIBUTE = /([A-Za-z][\w-]*)\s*:\s*"([^"\n]*)"/y;
4150
4174
  var MERMAID_NODE_ATTRIBUTES = ["label", "type", "in", "out", "arm"];
4151
4175
  var LEGACY_DOCFLOW_ATTRIBUTES = ["id", ...MERMAID_NODE_ATTRIBUTES];
4152
4176
  var MERMAID_NODE_ID = /^[A-Za-z][\w-]*/;
4177
+ var SKETCH_CLASS = /^\s*class\s+([A-Za-z][\w-]*(?:\s*,\s*[A-Za-z][\w-]*)*)\s+sketch\s*;?\s*$/;
4153
4178
  var MERMAID_NODE_SHAPES = [
4154
4179
  ["[[", "]]"],
4155
4180
  ["[(", ")]"],
@@ -4375,7 +4400,7 @@ function inferBindingIo(statement) {
4375
4400
  }
4376
4401
  var MERMAID_BLOCK = /\/\*\*\s*@mermaid(?:\s|\r?\n)([\s\S]*?)\*\//g;
4377
4402
  var LEGACY_BLOCK = /\/\*\*\s*@docflow(?:\s|\r?\n)([\s\S]*?)\*\//;
4378
- var BLOCK_EXPORT_HEADER = /^[A-Za-z_$][\w$]*$/;
4403
+ var BLOCK_EXPORT_HEADER = /^[A-Za-z_$][\w$-]*$/;
4379
4404
  function cleanBlockBody(raw) {
4380
4405
  return raw.split(/\r?\n/).map((line) => /^\s*\*/.test(line) ? line.replace(/^\s*\* ?/, "") : line).join("\n").trim();
4381
4406
  }
@@ -4407,6 +4432,7 @@ function parseDocflowBlockGraph(mermaidSource, syntax, errors) {
4407
4432
  const edges = [];
4408
4433
  const subgraphs = /* @__PURE__ */ new Map();
4409
4434
  const ignoredDirectives = [];
4435
+ const sketchIds = /* @__PURE__ */ new Set();
4410
4436
  const subgraphStack = [];
4411
4437
  const subgraphIds = /* @__PURE__ */ new Set();
4412
4438
  if (syntax === "mermaid") {
@@ -4483,7 +4509,15 @@ function parseDocflowBlockGraph(mermaidSource, syntax, errors) {
4483
4509
  let declaredOnThisLine = false;
4484
4510
  const isDirective = /^\s*(?:direction|classDef|class|style|linkStyle|click)\b/i.test(line);
4485
4511
  if (syntax === "mermaid" && isDirective) {
4486
- ignoredDirectives.push(line.trim());
4512
+ const sketch = SKETCH_CLASS.exec(line);
4513
+ if (sketch) {
4514
+ for (const id of sketch[1].split(",")) {
4515
+ const trimmed = id.trim();
4516
+ if (trimmed) sketchIds.add(trimmed);
4517
+ }
4518
+ } else {
4519
+ ignoredDirectives.push(line.trim());
4520
+ }
4487
4521
  }
4488
4522
  if (syntax === "mermaid" && !isDirective) {
4489
4523
  declaredOnThisLine = addNodes(line, line);
@@ -4532,7 +4566,17 @@ function parseDocflowBlockGraph(mermaidSource, syntax, errors) {
4532
4566
  `Docflow node "${node.id}" is referenced by an edge but never declared with a shape and label, so the canvas would show its id. Declare it once, e.g. \`${node.id}["What this step does"]\`.`
4533
4567
  );
4534
4568
  }
4535
- return { direction, nodes, edges, subgraphs, ignoredDirectives };
4569
+ for (const id of sketchIds) {
4570
+ const node = nodes.get(id);
4571
+ if (!node) {
4572
+ errors.push(
4573
+ subgraphs.has(id) ? `Docflow \`class ${id} sketch\` names a subgraph. A subgraph is a region, not a box, and never binds code \u2014 drop it from the class line.` : `Docflow \`class ${id} sketch\` names "${id}", which this diagram does not draw.`
4574
+ );
4575
+ continue;
4576
+ }
4577
+ node.sketch = true;
4578
+ }
4579
+ return { direction, nodes, edges, subgraphs, ignoredDirectives, sketchIds };
4536
4580
  }
4537
4581
  function materializeDocflow(block) {
4538
4582
  return {
@@ -4548,7 +4592,8 @@ function materializeDocflow(block) {
4548
4592
  }
4549
4593
  function parsePlayDocflowFile(sourceCode) {
4550
4594
  const errors = [];
4551
- const bindings = parseDocflowBindings(sourceCode, errors);
4595
+ const rejectedNodeIds = /* @__PURE__ */ new Set();
4596
+ const bindings = parseDocflowBindings(sourceCode, errors, rejectedNodeIds);
4552
4597
  const rawBlocks = [];
4553
4598
  MERMAID_BLOCK.lastIndex = 0;
4554
4599
  for (const match of sourceCode.matchAll(MERMAID_BLOCK)) {
@@ -4576,7 +4621,13 @@ function parsePlayDocflowFile(sourceCode) {
4576
4621
  });
4577
4622
  }
4578
4623
  resolveBlockExports(sourceCode, parsedBlocks, errors);
4579
- attachBindingsToBlocks(sourceCode, parsedBlocks, bindings, errors);
4624
+ attachBindingsToBlocks(
4625
+ sourceCode,
4626
+ parsedBlocks,
4627
+ bindings,
4628
+ errors,
4629
+ rejectedNodeIds
4630
+ );
4580
4631
  return {
4581
4632
  blocks: parsedBlocks.filter((block) => block.exportName !== null).map((block) => ({
4582
4633
  exportName: block.exportName,
@@ -4723,7 +4774,7 @@ function projectRecordedArms(block, errors) {
4723
4774
  edge.arm = binding.arm;
4724
4775
  }
4725
4776
  }
4726
- function attachBindingsToBlocks(sourceCode, blocks, bindings, errors) {
4777
+ function attachBindingsToBlocks(sourceCode, blocks, bindings, errors, rejectedNodeIds) {
4727
4778
  const lineStarts = sourceLineStartsForDocflow(sourceCode);
4728
4779
  const exportRanges = playExportSourceRanges(sourceCode);
4729
4780
  for (const binding of bindings) {
@@ -4764,17 +4815,22 @@ function attachBindingsToBlocks(sourceCode, blocks, bindings, errors) {
4764
4815
  for (const binding of block.bindings) {
4765
4816
  const node = block.nodes.get(binding.nodeId);
4766
4817
  if (binding.label) node.label = binding.label;
4818
+ if (block.sketchIds.has(binding.nodeId)) {
4819
+ errors.push(
4820
+ `Docflow box "${binding.nodeId}" is declared \`class ${binding.nodeId} sketch\` and also bound by \`// @mermaid-node ${binding.nodeId}\` on line ${binding.line}. It is one or the other: drop it from the class line, or drop the annotation.`
4821
+ );
4822
+ continue;
4823
+ }
4767
4824
  if (binding.kind) node.kind = binding.kind;
4768
4825
  }
4769
4826
  projectRecordedArms(block, errors);
4770
- if (block.syntax !== "docflow") continue;
4771
4827
  const bound = new Set(block.bindings.map((binding) => binding.nodeId));
4772
4828
  for (const node of block.nodes.values()) {
4773
- if (node.kind !== "conceptual" && !bound.has(node.id)) {
4774
- errors.push(
4775
- `Docflow node "${node.id}" has no code binding; mark it type:"conceptual" or bind it.`
4776
- );
4777
- }
4829
+ if (node.sketch || node.kind === "conceptual" || bound.has(node.id) || rejectedNodeIds.has(node.id))
4830
+ continue;
4831
+ errors.push(
4832
+ block.syntax === "docflow" ? `Docflow node "${node.id}" has no code binding; mark it type:"conceptual" or bind it.` : `Docflow box "${node.id}" in \`${blockLabel(block)}\` points at nothing, so the canvas can only draw it and say nothing about it. Either put \`// @mermaid-node ${node.id}\` above the statement it runs, or \u2014 if this play has no such statement, because the work happens in another module or inside a loop \u2014 add it to a \`class ${node.id} sketch\` line in the block to declare it a sketch.`
4833
+ );
4778
4834
  }
4779
4835
  }
4780
4836
  }
@@ -4785,7 +4841,7 @@ function sourceLineStartsForDocflow(sourceCode) {
4785
4841
  }
4786
4842
  return starts;
4787
4843
  }
4788
- function parseDocflowBindings(sourceCode, errors) {
4844
+ function parseDocflowBindings(sourceCode, errors, rejectedNodeIds) {
4789
4845
  const lines = sourceCode.split(/\r?\n/);
4790
4846
  const bindings = [];
4791
4847
  for (let index = 0; index < lines.length; index += 1) {
@@ -4794,6 +4850,8 @@ function parseDocflowBindings(sourceCode, errors) {
4794
4850
  const legacyMatch = PUT.exec(lines[index]);
4795
4851
  const mermaidMatch = MERMAID_NODE.exec(lines[index]);
4796
4852
  if (!legacyMatch && !mermaidMatch) continue;
4853
+ const annotatedId = mermaidMatch?.[1]?.trim();
4854
+ if (annotatedId) rejectedNodeIds.add(annotatedId);
4797
4855
  const attributes = parseAttributes(
4798
4856
  mermaidMatch ? mermaidMatch[2] ?? "" : legacyMatch[1],
4799
4857
  {
@@ -4803,11 +4861,12 @@ function parseDocflowBindings(sourceCode, errors) {
4803
4861
  }
4804
4862
  );
4805
4863
  if (!attributes) continue;
4806
- const id = (mermaidMatch ? mermaidMatch[1] : attributes.id)?.trim();
4864
+ const id = (annotatedId ?? attributes.id)?.trim();
4807
4865
  if (!id) {
4808
4866
  errors.push(`Docflow annotation on line ${index + 1} requires id:"\u2026".`);
4809
4867
  continue;
4810
4868
  }
4869
+ rejectedNodeIds.add(id);
4811
4870
  const kind = nodeKind(attributes.type);
4812
4871
  if (attributes.type && !kind) {
4813
4872
  errors.push(
@@ -4853,6 +4912,7 @@ function parseDocflowBindings(sourceCode, errors) {
4853
4912
  ...outputs ? { outputs } : {},
4854
4913
  ioConfidence: "explicit"
4855
4914
  } : inferBindingIo(lines[nextLine]);
4915
+ rejectedNodeIds.delete(id);
4856
4916
  bindings.push({
4857
4917
  nodeId: id,
4858
4918
  line: nextLine + 1,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepline",
3
- "version": "0.2.56",
3
+ "version": "0.2.57",
4
4
  "description": "Deepline SDK + CLI — B2B data enrichment powered by durable cloud execution",
5
5
  "license": "MIT",
6
6
  "repository": {