deepline 0.3.6 → 0.3.8

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.
@@ -75,6 +75,13 @@ export function redactSecretLikeString(value: string): string {
75
75
 
76
76
  export type SecretRedactionContext = {
77
77
  register(value: string): void;
78
+ containsRegisteredSecret(
79
+ value: string,
80
+ options?: { includeEncoded?: boolean; minimumLength?: number },
81
+ ): boolean;
82
+ matchesRegisteredSecret(value: string): boolean;
83
+ /** Redacts only values registered by the secret resolver, preserving safe diagnostics. */
84
+ redactRegisteredSecrets(value: string): string;
78
85
  redactString(value: string): string;
79
86
  redactKnownSecrets<T>(value: T): T;
80
87
  redact<T>(value: T): T;
@@ -86,12 +93,39 @@ export function createSecretRedactionContext(
86
93
  const exactSecrets = new Set<string>();
87
94
 
88
95
  function register(value: string): void {
89
- if (value.length >= 4) exactSecrets.add(value);
96
+ if (value.length > 0) exactSecrets.add(value);
90
97
  }
91
98
 
92
99
  for (const value of initialValues) register(value);
93
100
 
94
- function redactString(value: string): string {
101
+ function containsRegisteredSecret(
102
+ value: string,
103
+ {
104
+ includeEncoded = false,
105
+ minimumLength = 1,
106
+ }: { includeEncoded?: boolean; minimumLength?: number } = {},
107
+ ): boolean {
108
+ for (const secret of exactSecrets) {
109
+ if (secret.length < minimumLength) continue;
110
+ if (value.includes(secret)) return true;
111
+ if (includeEncoded) {
112
+ try {
113
+ const encoded = encodeURIComponent(secret);
114
+ if (encoded !== secret && value.includes(encoded)) return true;
115
+ } catch {
116
+ // Malformed surrogate pairs cannot be encoded. The raw comparison is
117
+ // still safe and handles every other secret value.
118
+ }
119
+ }
120
+ }
121
+ return false;
122
+ }
123
+
124
+ function matchesRegisteredSecret(value: string): boolean {
125
+ return exactSecrets.has(value);
126
+ }
127
+
128
+ function redactRegisteredSecrets(value: string): string {
95
129
  let output = value;
96
130
  for (const secret of exactSecrets) {
97
131
  output = output.replace(
@@ -111,6 +145,11 @@ export function createSecretRedactionContext(
111
145
  // redaction above still applies in that rare case.
112
146
  }
113
147
  }
148
+ return output;
149
+ }
150
+
151
+ function redactString(value: string): string {
152
+ const output = redactRegisteredSecrets(value);
114
153
  return redactSecretLikeString(output);
115
154
  }
116
155
 
@@ -148,6 +187,9 @@ export function createSecretRedactionContext(
148
187
 
149
188
  return {
150
189
  register,
190
+ containsRegisteredSecret,
191
+ matchesRegisteredSecret,
192
+ redactRegisteredSecrets,
151
193
  redactString,
152
194
  redactKnownSecrets,
153
195
  redact,
@@ -7,9 +7,9 @@ import type { ToolExecutionErrorSchemaVersion } from '../tool-execution-error';
7
7
  import type { PlayDataset, PlayDatasetInput, PlayDatasetRow } from './dataset';
8
8
 
9
9
  export const LEGACY_PLAY_AUTHORING_CONTRACT_EDITION = 1 as const;
10
- export const PLAY_AUTHORING_CONTRACT_EDITION = 3 as const;
10
+ export const PLAY_AUTHORING_CONTRACT_EDITION = 4 as const;
11
11
  export const PLAY_AUTHORING_INPUT_SCHEMA_SNAPSHOT_EDITION = 3 as const;
12
- export const SUPPORTED_PLAY_AUTHORING_CONTRACT_EDITIONS = [1, 2, 3] as const;
12
+ export const SUPPORTED_PLAY_AUTHORING_CONTRACT_EDITIONS = [1, 2, 3, 4] as const;
13
13
 
14
14
  export type PlayAuthoringContractEdition =
15
15
  (typeof SUPPORTED_PLAY_AUTHORING_CONTRACT_EDITIONS)[number];
@@ -41,6 +41,14 @@ export const PLAY_AUTHORING_CONTRACT_CHANGELOG = [
41
41
  newWritesEnd: '2026-08-19',
42
42
  readerRemoval: null,
43
43
  },
44
+ {
45
+ edition: 4,
46
+ changed:
47
+ 'ctx.secrets.get(name) resolves an allowed secret to a plaintext string for ordinary Play code. Editions 1–3 retain opaque secret handles.',
48
+ compatibilityOwner: 'Plays Runtime',
49
+ newWritesEnd: null,
50
+ readerRemoval: null,
51
+ },
44
52
  ] as const;
45
53
 
46
54
  export const PLAY_AUTHORING_CONTRACT_ISSUE_CODES = [
@@ -58,6 +66,7 @@ export const PLAY_AUTHORING_CONTRACT_ISSUE_CODES = [
58
66
  'play_authoring_webhook_hmac_invalid',
59
67
  'play_authoring_standard_webhooks_invalid',
60
68
  'play_authoring_secret_invalid',
69
+ 'play_authoring_secret_undeclared',
61
70
  'play_authoring_cron_timezone_invalid',
62
71
  'play_authoring_tool_request_invalid',
63
72
  'play_authoring_durable_policy_invalid',
@@ -74,6 +83,7 @@ export const PLAY_AUTHORING_CONTRACT_ISSUE_CODES = [
74
83
  'tool_response_raw_access',
75
84
  'check_error',
76
85
  'docflow_compile_error',
86
+ 'docflow_visualization_missing',
77
87
  'docflow_binding_drift',
78
88
  'docflow_branch_labels_required',
79
89
  'docflow_branch_requires_decision',
@@ -390,8 +400,10 @@ export type PlaySqlQuery = {
390
400
  };
391
401
 
392
402
  declare const PLAY_SECRET_HANDLE_BRAND: unique symbol;
403
+ declare const PLAY_SECRET_PROMISE_BRAND: unique symbol;
393
404
  /**
394
- * An opaque reference to a workspace secret, returned by `ctx.secrets.get`. The handle never carries the value into play code: it stringifies to `[secret:NAME]` and throws on `JSON.stringify`, so a secret cannot reach a play variable, a log line, a dataset cell, or a replay artifact even by accident. Only the runtime resolves it, at the moment it attaches the request header.
405
+ * An opaque reference to a workspace secret used by legacy authoring-contract
406
+ * editions. New Plays receive plaintext strings from `ctx.secrets.get`.
395
407
  *
396
408
  * @sdkReference runtime 176 SecretHandle
397
409
  */
@@ -404,6 +416,12 @@ export type PlaySecretHandle = {
404
416
  /** Always throws. A secret handle is deliberately not serializable. */
405
417
  toJSON(): never;
406
418
  };
419
+ /** A secret-reading promise returned only by `ctx.secrets.get`. */
420
+ export type PlaySecretPromise = Promise<string> & {
421
+ readonly [PLAY_SECRET_PROMISE_BRAND]: never;
422
+ };
423
+ /** An opaque legacy secret handle retained for earlier Play artifacts. */
424
+ export type PlaySecretValue = PlaySecretHandle;
407
425
  /**
408
426
  * One resolved authentication scheme, built by `ctx.secrets.bearer` or `ctx.secrets.header` and attached to a request through `init.auth`.
409
427
  *
@@ -412,8 +430,8 @@ export type PlaySecretHandle = {
412
430
  export type PlaySecretAuth = {
413
431
  /** `bearer` sends `Authorization: Bearer <value>`; `header` sends a named header. */
414
432
  readonly kind: 'bearer' | 'header';
415
- /** The handle whose value the runtime attaches. */
416
- readonly secret: PlaySecretHandle;
433
+ /** The value whose bytes the runtime attaches. */
434
+ readonly secret: string | PlaySecretPromise | PlaySecretValue;
417
435
  /** Header name, set only when `kind` is `header`. */
418
436
  readonly header?: string;
419
437
  };
@@ -425,10 +443,10 @@ export type PlaySecretAuthInput = PlaySecretAuth | readonly PlaySecretAuth[];
425
443
  * @sdkReference runtime 174 SecretAwareRequestInit
426
444
  */
427
445
  export type PlaySecretAwareRequestInit = Omit<RequestInit, 'headers'> & {
428
- /** Ordinary request headers, recorded in the durable receipt. Never interpolate a secret value here use `auth`. */
446
+ /** Ordinary request headers, recorded in the durable receipt with any resolved Play secret value redacted. Prefer `auth` for credentials: it enforces HTTPS and keeps the auth header out of the receipt. */
429
447
  headers?: HeadersInit;
430
448
  /**
431
- * One or more secret-backed authentication headers for this request. Pass a single `ctx.secrets` auth for the common case, or an array when an API requires multiple credentialed headers — for example, Supabase with both `apikey` and `Authorization`. Every secret is resolved only while the request is attached, never stored in the durable receipt. Each auth entry must target a distinct header.
449
+ * One or more credentialed headers for this request. Pass a single `ctx.secrets` auth for the common case, or an array when an API requires multiple credentialed headers — for example, Supabase with both `apikey` and `Authorization`. Auth-helper requests require HTTPS and omit the credential from the durable receipt. Each auth entry must target a distinct header.
432
450
  */
433
451
  auth?: PlaySecretAuthInput;
434
452
  };
@@ -997,23 +1015,31 @@ export interface PlayAuthoringRuntimeContext {
997
1015
 
998
1016
  secrets: {
999
1017
  /**
1000
- * Reference a workspace secret by name without reading its value. This is the only supported way to authenticate an outbound request from a Play: `process.env.X` is rejected at check time because an env read puts the raw value in a play variable, where it can reach a log line or a replay artifact. A handle cannot — pass it to `bearer` or `header` and the runtime resolves it only while attaching the header. Names are uppercased; manage stored values with `deepline secrets set` / `deepline secrets list`.
1018
+ * Read an allowed workspace secret inside the running Play; do not log or return it. Declare uppercase names in top-level `secrets`.
1001
1019
  *
1002
1020
  * @sdkReference runtime 171 ctx.secrets.get(name)
1003
1021
  */
1004
- get(name: string): PlaySecretHandle;
1022
+ get(name: string): PlaySecretPromise;
1005
1023
  /**
1006
- * Send the secret as `Authorization: Bearer <value>`.
1024
+ * Send a credential as `Authorization: Bearer <value>`. Await `get` first;
1025
+ * its direct promise remains accepted for source compatibility, while other
1026
+ * promises are rejected.
1007
1027
  *
1008
1028
  * @sdkReference runtime 172 ctx.secrets.bearer(secret)
1009
1029
  */
1010
- bearer(secret: PlaySecretHandle): PlaySecretAuth;
1030
+ bearer(
1031
+ secret: string | PlaySecretPromise | PlaySecretHandle,
1032
+ ): PlaySecretAuth;
1011
1033
  /**
1012
- * Send the secret as a named header, for APIs that do not use bearer tokens — `x-api-key`, `apikey`, `private-token`, and similar.
1034
+ * Send a credential as a named header, for APIs that do not use bearer
1035
+ * tokens — `x-api-key`, `apikey`, `private-token`, and similar.
1013
1036
  *
1014
1037
  * @sdkReference runtime 173 ctx.secrets.header(header, secret)
1015
1038
  */
1016
- header(header: string, secret: PlaySecretHandle): PlaySecretAuth;
1039
+ header(
1040
+ header: string,
1041
+ secret: string | PlaySecretPromise | PlaySecretHandle,
1042
+ ): PlaySecretAuth;
1017
1043
  };
1018
1044
 
1019
1045
  /**
@@ -2472,8 +2498,11 @@ export const PLAY_AUTHORING_CLOUD_TYPE_DECLARATIONS = [
2472
2498
  ` secrets?: readonly ${cloudReferenceType('bindings.secrets[]')}[];`,
2473
2499
  '};',
2474
2500
  'declare const SECRET_HANDLE_BRAND: unique symbol;',
2501
+ 'declare const SECRET_PROMISE_BRAND: unique symbol;',
2475
2502
  'export type SecretHandle = { readonly [SECRET_HANDLE_BRAND]: never; readonly name: string; toString(): string; toJSON(): never };',
2476
- "export type SecretAuth = { readonly kind: 'bearer' | 'header'; readonly secret: SecretHandle; readonly header?: string };",
2503
+ 'export type SecretPromise = Promise<string> & { readonly [SECRET_PROMISE_BRAND]: never };',
2504
+ 'export type SecretValue = SecretHandle;',
2505
+ "export type SecretAuth = { readonly kind: 'bearer' | 'header'; readonly secret: string | SecretPromise | SecretHandle; readonly header?: string };",
2477
2506
  'export type SecretAuthInput = SecretAuth | readonly SecretAuth[];',
2478
2507
  'export type PlayInputContract<TInput> = { readonly schema: Record<string, unknown>; readonly __inputType?: TInput };',
2479
2508
  'export type PlayReturnObject = Record<string, unknown> & { readonly _metadata?: never };',
@@ -2524,7 +2553,7 @@ export const PLAY_AUTHORING_CLOUD_TYPE_DECLARATIONS = [
2524
2553
  ` tool<K extends string>(key: ${cloudReferenceType('ctx.tool.key')}, toolId: K, input: ${cloudReferenceType('ctx.tool.input')}, options?: { description?: ${cloudReferenceType('ctx.tool.options.description')} }): Promise<ToolExecutionOutput<K>>;`,
2525
2554
  ' step<T>(id: string, run: () => T | Promise<T>, options?: RuntimeStepOptions): Promise<T>;',
2526
2555
  " fetch(key: string, url: string | URL, init?: RequestInit & { headers?: HeadersInit & { 'Idempotency-Key'?: string; 'X-Idempotency-Key'?: string }; auth?: SecretAuthInput }, options?: FetchOptions): Promise<PlayFetchResponse>;",
2527
- ' secrets: { get(name: string): SecretHandle; bearer(secret: SecretHandle): SecretAuth; header(header: string, secret: SecretHandle): SecretAuth };',
2556
+ ' secrets: { get(name: string): SecretPromise; bearer(secret: string | SecretPromise | SecretHandle): SecretAuth; header(header: string, secret: string | SecretPromise | SecretHandle): SecretAuth };',
2528
2557
  ` runPlay<TOutput = unknown>(key: string, playRef: ${cloudReferenceType('ctx.runPlay.playRef')}, input: ${cloudReferenceType('ctx.runPlay.input')}, options: PlayCallOptions): Promise<TOutput>;`,
2529
2558
  ' log(message: string): void;',
2530
2559
  ` sleep(ms: ${cloudReferenceType('ctx.sleep.ms')}): Promise<void>;`,
@@ -128,13 +128,13 @@ export type PlayDocflow = {
128
128
  nodes: PlayDocflowNode[];
129
129
  edges: PlayDocflowEdge[];
130
130
  bindings: PlayDocflowBinding[];
131
- /** Authoring syntax used by the source file. Absent on older persisted graphs. */
132
- syntax?: 'docflow' | 'mermaid';
131
+ /** Authoring syntax used by the source file. */
132
+ syntax?: 'mermaid';
133
133
  /** Normalized Mermaid text, ready to pass directly to a Mermaid renderer. */
134
134
  mermaidSource?: string;
135
135
  /**
136
- * Mermaid `subgraph` loop regions. Optional so older persisted graphs stay
137
- * valid. Only populated for the Mermaid syntax; legacy `docflow` has none.
136
+ * Mermaid `subgraph` loop regions. Optional for persisted graphs that have
137
+ * no authored regions.
138
138
  */
139
139
  subgraphs?: PlayDocflowSubgraph[];
140
140
  /** Mermaid directives accepted by the parser but not applied by React Flow. */
@@ -232,11 +232,9 @@ export function docflowLabelCountFragment(label: string): string | null {
232
232
  }
233
233
 
234
234
  const FLOW_START = /^\s*(?:flowchart|graph)\s+(LR|RL|TB|TD|BT)\s*$/i;
235
- const PUT = /^\s*\/\/\s*put\s+(.+)$/;
236
235
  const MERMAID_NODE = /^\s*\/\/\s*@mermaid-node\s+([A-Za-z][\w-]*)(?:\s+(.*))?$/;
237
236
  const ATTRIBUTE = /([A-Za-z][\w-]*)\s*:\s*"([^"\n]*)"/y;
238
237
  const MERMAID_NODE_ATTRIBUTES = ['label', 'type', 'in', 'out', 'arm'] as const;
239
- const LEGACY_DOCFLOW_ATTRIBUTES = ['id', ...MERMAID_NODE_ATTRIBUTES] as const;
240
238
  const MERMAID_NODE_ID = /^[A-Za-z][\w-]*/;
241
239
  /**
242
240
  * `class a,b,c sketch` — the author declaring that no statement runs these boxes.
@@ -292,7 +290,6 @@ const MERMAID_NODE_SHAPES = [
292
290
  // label bracket is optional; when present the quotes are stripped by the caller.
293
291
  const SUBGRAPH_OPEN = /^\s*subgraph\s+([A-Za-z][\w-]*)\s*(?:\[(.*)\])?\s*$/i;
294
292
  const SUBGRAPH_END = /^\s*end\s*$/i;
295
- const DOCFLOW_EDGE_CONNECTOR = /^\s*-->(?:\|([^|]+)\|)?/;
296
293
  const MERMAID_EDGE_CONNECTOR = /^\s*(?:-->|-\.->|==>|---)(?:\|([^|]+)\|)?/;
297
294
  const DOCFLOW_PATH = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/;
298
295
  const IDENTIFIER = /[A-Za-z_$][\w$]*/g;
@@ -314,17 +311,11 @@ const JS_WORDS = new Set([
314
311
 
315
312
  function parseAttributes(
316
313
  source: string,
317
- input: {
318
- line: number;
319
- annotation: '@mermaid-node' | 'put';
320
- errors: string[];
321
- },
314
+ line: number,
315
+ errors: string[],
322
316
  ): Record<string, string> | null {
323
317
  const attributes: Record<string, string> = {};
324
- const validAttributes =
325
- input.annotation === '@mermaid-node'
326
- ? MERMAID_NODE_ATTRIBUTES
327
- : LEGACY_DOCFLOW_ATTRIBUTES;
318
+ const validAttributes = MERMAID_NODE_ATTRIBUTES;
328
319
  let cursor = 0;
329
320
  while (cursor < source.length) {
330
321
  cursor += /^\s*/.exec(source.slice(cursor))?.[0].length ?? 0;
@@ -334,21 +325,21 @@ function parseAttributes(
334
325
  if (!match) {
335
326
  const fragment =
336
327
  source.slice(cursor).split(/\s+/)[0] ?? source.slice(cursor);
337
- input.errors.push(
338
- `Docflow annotation on line ${input.line} has malformed attribute ${JSON.stringify(fragment)}. Use key:"value" pairs. Valid attributes: ${validAttributes.map((name) => `"${name}"`).join(', ')}.`,
328
+ errors.push(
329
+ `Docflow \`// @mermaid-node\` annotation on line ${line} has malformed attribute ${JSON.stringify(fragment)}. Use key:"value" pairs. Valid attributes: ${validAttributes.map((name) => `"${name}"`).join(', ')}.`,
339
330
  );
340
331
  return null;
341
332
  }
342
333
  const name = match[1]!;
343
334
  if (!(validAttributes as readonly string[]).includes(name)) {
344
- input.errors.push(
345
- `Docflow annotation on line ${input.line} has unknown attribute "${name}". Valid attributes: ${validAttributes.map((option) => `"${option}"`).join(', ')}.`,
335
+ errors.push(
336
+ `Docflow \`// @mermaid-node\` annotation on line ${line} has unknown attribute "${name}". Valid attributes: ${validAttributes.map((option) => `"${option}"`).join(', ')}.`,
346
337
  );
347
338
  return null;
348
339
  }
349
340
  if (Object.prototype.hasOwnProperty.call(attributes, name)) {
350
- input.errors.push(
351
- `Docflow annotation on line ${input.line} repeats attribute "${name}". Write each attribute once.`,
341
+ errors.push(
342
+ `Docflow \`// @mermaid-node\` annotation on line ${line} repeats attribute "${name}". Write each attribute once.`,
352
343
  );
353
344
  return null;
354
345
  }
@@ -508,7 +499,6 @@ function parseShapedMermaidNodes(fragment: string): ParsedMermaidNode[] {
508
499
 
509
500
  function parseEdgeChain(
510
501
  line: string,
511
- syntax: 'docflow' | 'mermaid',
512
502
  ): { nodes: ParsedEdgeNode[]; labels: Array<string | undefined> } | null {
513
503
  let remaining = line;
514
504
  const first = parseMermaidNodeAtStart(remaining);
@@ -517,9 +507,7 @@ function parseEdgeChain(
517
507
  const labels: Array<string | undefined> = [];
518
508
  remaining = remaining.slice(first.length);
519
509
  while (remaining.trim()) {
520
- const connector = (
521
- syntax === 'mermaid' ? MERMAID_EDGE_CONNECTOR : DOCFLOW_EDGE_CONNECTOR
522
- ).exec(remaining);
510
+ const connector = MERMAID_EDGE_CONNECTOR.exec(remaining);
523
511
  if (!connector) return null;
524
512
  remaining = remaining.slice(connector[0].length);
525
513
  const next = parseMermaidNodeAtStart(remaining);
@@ -630,7 +618,6 @@ function levenshtein(left: string, right: string): number {
630
618
  }
631
619
 
632
620
  const MERMAID_BLOCK = /\/\*\*\s*@mermaid(?:\s|\r?\n)([\s\S]*?)\*\//g;
633
- const LEGACY_BLOCK = /\/\*\*\s*@docflow(?:\s|\r?\n)([\s\S]*?)\*\//;
634
621
  /**
635
622
  * What a block header may name: an export name, or the play's own kebab-case
636
623
  * name. Hyphens are in the set for the second — `@mermaid name-to-linkedin-url-
@@ -702,12 +689,10 @@ type ParsedDocflowBlock = ParsedDocflowBlockGraph & {
702
689
  };
703
690
 
704
691
  /** How a block is named in a diagnostic. */
705
- function blockLabel(block: {
706
- authoredExportName: string | null;
707
- syntax: string;
708
- }): string {
709
- const tag = block.syntax === 'mermaid' ? '@mermaid' : '@docflow';
710
- return block.authoredExportName ? `${tag} ${block.authoredExportName}` : tag;
692
+ function blockLabel(block: { authoredExportName: string | null }): string {
693
+ return block.authoredExportName
694
+ ? `@mermaid ${block.authoredExportName}`
695
+ : '@mermaid';
711
696
  }
712
697
 
713
698
  /**
@@ -717,7 +702,6 @@ function blockLabel(block: {
717
702
  */
718
703
  function parseDocflowBlockGraph(
719
704
  mermaidSource: string,
720
- syntax: NonNullable<PlayDocflow['syntax']>,
721
705
  errors: string[],
722
706
  ): ParsedDocflowBlockGraph | null {
723
707
  const docLines = mermaidSource
@@ -746,11 +730,9 @@ function parseDocflowBlockGraph(
746
730
  const sketchIds = new Set<string>();
747
731
  const subgraphStack: PlayDocflowSubgraph[] = [];
748
732
  const subgraphIds = new Set<string>();
749
- if (syntax === 'mermaid') {
750
- for (const line of docLines.slice(1)) {
751
- const opened = SUBGRAPH_OPEN.exec(line);
752
- if (opened) subgraphIds.add(opened[1]!);
753
- }
733
+ for (const line of docLines.slice(1)) {
734
+ const opened = SUBGRAPH_OPEN.exec(line);
735
+ if (opened) subgraphIds.add(opened[1]!);
754
736
  }
755
737
  // Ids that were DECLARED with a shape somewhere, as opposed to merely named
756
738
  // as an edge endpoint. A node nothing ever declares reaches the canvas
@@ -805,40 +787,38 @@ function parseDocflowBlockGraph(
805
787
  return sawDeclaration;
806
788
  };
807
789
  for (const line of docLines.slice(1)) {
808
- if (syntax === 'mermaid') {
809
- const opened = SUBGRAPH_OPEN.exec(line);
810
- if (opened) {
811
- const id = opened[1]!;
812
- const rawLabel = opened[2]?.trim() ?? '';
813
- const label =
814
- rawLabel.length >= 2 &&
815
- ((rawLabel.startsWith('"') && rawLabel.endsWith('"')) ||
816
- (rawLabel.startsWith("'") && rawLabel.endsWith("'")))
817
- ? rawLabel.slice(1, -1)
818
- : rawLabel;
819
- const subgraph: PlayDocflowSubgraph = {
820
- id,
821
- label: label || id,
822
- memberIds: [],
823
- };
824
- subgraphs.set(id, subgraph);
825
- subgraphStack.push(subgraph);
826
- continue;
827
- }
828
- if (SUBGRAPH_END.test(line)) {
829
- if (subgraphStack.length === 0) {
830
- errors.push(
831
- 'Docflow has an `end` with no open `subgraph` above it. Every `end` closes exactly one `subgraph`.',
832
- );
833
- }
834
- subgraphStack.pop();
835
- continue;
790
+ const opened = SUBGRAPH_OPEN.exec(line);
791
+ if (opened) {
792
+ const id = opened[1]!;
793
+ const rawLabel = opened[2]?.trim() ?? '';
794
+ const label =
795
+ rawLabel.length >= 2 &&
796
+ ((rawLabel.startsWith('"') && rawLabel.endsWith('"')) ||
797
+ (rawLabel.startsWith("'") && rawLabel.endsWith("'")))
798
+ ? rawLabel.slice(1, -1)
799
+ : rawLabel;
800
+ const subgraph: PlayDocflowSubgraph = {
801
+ id,
802
+ label: label || id,
803
+ memberIds: [],
804
+ };
805
+ subgraphs.set(id, subgraph);
806
+ subgraphStack.push(subgraph);
807
+ continue;
808
+ }
809
+ if (SUBGRAPH_END.test(line)) {
810
+ if (subgraphStack.length === 0) {
811
+ errors.push(
812
+ 'Docflow has an `end` with no open `subgraph` above it. Every `end` closes exactly one `subgraph`.',
813
+ );
836
814
  }
815
+ subgraphStack.pop();
816
+ continue;
837
817
  }
838
818
  let declaredOnThisLine = false;
839
819
  const isDirective =
840
820
  /^\s*(?:direction|classDef|class|style|linkStyle|click)\b/i.test(line);
841
- if (syntax === 'mermaid' && isDirective) {
821
+ if (isDirective) {
842
822
  const sketch = SKETCH_CLASS.exec(line);
843
823
  if (sketch) {
844
824
  for (const id of sketch[1]!.split(',')) {
@@ -849,14 +829,12 @@ function parseDocflowBlockGraph(
849
829
  ignoredDirectives.push(line.trim());
850
830
  }
851
831
  }
852
- if (syntax === 'mermaid' && !isDirective) {
832
+ if (!isDirective) {
853
833
  declaredOnThisLine = addNodes(line, line);
854
834
  }
855
- const chain = parseEdgeChain(line, syntax);
835
+ const chain = parseEdgeChain(line);
856
836
  if (!chain) {
857
- if (syntax === 'docflow') {
858
- errors.push(`Unsupported docflow line: ${line.trim()}`);
859
- } else if (!isDirective && !declaredOnThisLine) {
837
+ if (!isDirective && !declaredOnThisLine) {
860
838
  // Mermaid the parser could not consume at all: no edge, no node
861
839
  // declaration, not a directive it knowingly ignores. It used to be
862
840
  // skipped in silence, so a typo'd arrow or a stray token simply removed
@@ -867,7 +845,7 @@ function parseDocflowBlockGraph(
867
845
  }
868
846
  continue;
869
847
  }
870
- if (syntax === 'docflow') addNodes(line, line);
848
+ addNodes(line, line);
871
849
  for (const node of chain.nodes) {
872
850
  // An edge endpoint may name a subgraph id; keep the edge but do not
873
851
  // materialize a regular node for the region.
@@ -953,12 +931,6 @@ export function parsePlayDocflowFile(
953
931
  sourceCode: string,
954
932
  ): PlayDocflowFileParseResult {
955
933
  const errors: string[] = [];
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);
961
-
962
934
  const rawBlocks: Array<{
963
935
  syntax: NonNullable<PlayDocflow['syntax']>;
964
936
  body: string;
@@ -967,18 +939,18 @@ export function parsePlayDocflowFile(
967
939
  for (const match of sourceCode.matchAll(MERMAID_BLOCK)) {
968
940
  rawBlocks.push({ syntax: 'mermaid', body: cleanBlockBody(match[1]!) });
969
941
  }
970
- if (rawBlocks.length === 0) {
971
- const legacy = LEGACY_BLOCK.exec(sourceCode);
972
- if (legacy) {
973
- rawBlocks.push({ syntax: 'docflow', body: cleanBlockBody(legacy[1]!) });
974
- }
975
- }
976
- if (rawBlocks.length === 0) return { blocks: [], bindings, errors };
942
+ if (rawBlocks.length === 0) return { blocks: [], bindings: [], errors };
943
+
944
+ // Ids whose annotation was written but refused. They are NOT unbound boxes:
945
+ // the author already has one actionable error about them, and telling them to
946
+ // "bind it" on the next line is telling them to do what they just did.
947
+ const rejectedNodeIds = new Set<string>();
948
+ const bindings = parseDocflowBindings(sourceCode, errors, rejectedNodeIds);
977
949
 
978
950
  const parsedBlocks: ParsedDocflowBlock[] = [];
979
951
  for (const raw of rawBlocks) {
980
952
  const { exportName, diagram } = splitBlockExportHeader(raw.body);
981
- const graph = parseDocflowBlockGraph(diagram, raw.syntax, errors);
953
+ const graph = parseDocflowBlockGraph(diagram, errors);
982
954
  if (!graph) continue;
983
955
  parsedBlocks.push({
984
956
  ...graph,
@@ -1324,10 +1296,8 @@ function attachBindingsToBlocks(
1324
1296
  )
1325
1297
  continue;
1326
1298
  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.`,
1299
+ `Docflow box "${node.id}" in \`${blockLabel(block)}\` points at nothing, so the canvas can only draw it and say nothing about it. ` +
1300
+ `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
1301
  );
1332
1302
  }
1333
1303
  }
@@ -1389,23 +1359,18 @@ function parseDocflowBindings(
1389
1359
  const bindings: PlayDocflowBinding[] = [];
1390
1360
 
1391
1361
  for (let index = 0; index < lines.length; index += 1) {
1392
- PUT.lastIndex = 0;
1393
1362
  MERMAID_NODE.lastIndex = 0;
1394
- const legacyMatch = PUT.exec(lines[index]!);
1395
- const mermaidMatch = MERMAID_NODE.exec(lines[index]!);
1396
- if (!legacyMatch && !mermaidMatch) continue;
1363
+ const mermaidMatch = MERMAID_NODE.exec(lines[index]!) ?? null;
1364
+ if (!mermaidMatch) continue;
1397
1365
  // Whatever this annotation names, the author has now written it down. Every
1398
1366
  // `continue` below is a rejection, and a rejected id must not come back as
1399
1367
  // an unbound box — see `rejectedNodeIds` in `parsePlayDocflowFile`.
1400
- const annotatedId = mermaidMatch?.[1]?.trim();
1368
+ const annotatedId = mermaidMatch[1]?.trim();
1401
1369
  if (annotatedId) rejectedNodeIds.add(annotatedId);
1402
1370
  const attributes = parseAttributes(
1403
- mermaidMatch ? (mermaidMatch[2] ?? '') : legacyMatch![1]!,
1404
- {
1405
- line: index + 1,
1406
- annotation: mermaidMatch ? '@mermaid-node' : 'put',
1407
- errors,
1408
- },
1371
+ mermaidMatch[2] ?? '',
1372
+ index + 1,
1373
+ errors,
1409
1374
  );
1410
1375
  if (!attributes) continue;
1411
1376
  const id = (annotatedId ?? attributes.id)?.trim();