deepline 0.3.7 → 0.3.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bundling-sources/sdk/src/client.ts +1 -0
- package/dist/bundling-sources/sdk/src/play.ts +4 -0
- package/dist/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +144 -37
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-lifecycle.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/secret-capability.ts +163 -14
- package/dist/bundling-sources/shared_libs/play-runtime/secret-redaction.ts +44 -2
- package/dist/bundling-sources/shared_libs/plays/authoring-contract.ts +43 -15
- package/dist/bundling-sources/shared_libs/plays/docflow.ts +68 -103
- package/dist/bundling-sources/shared_libs/plays/secret-guardrails.ts +137 -1
- package/dist/bundling-sources/shared_libs/security/safe-fetch.ts +25 -7
- package/dist/cli/index.js +153 -9
- package/dist/cli/index.mjs +153 -9
- package/dist/{compiler-manifest-CdgJeOr2.d.mts → compiler-manifest-YIjoJo8y.d.mts} +27 -16
- package/dist/{compiler-manifest-CdgJeOr2.d.ts → compiler-manifest-YIjoJo8y.d.ts} +27 -16
- package/dist/index.d.mts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +30 -3
- package/dist/index.mjs +30 -3
- package/dist/install-integrity.json +2 -2
- package/dist/plays/bundle-play-file.d.mts +2 -2
- package/dist/plays/bundle-play-file.d.ts +2 -2
- package/dist/plays/bundle-play-file.mjs +169 -78
- package/package.json +1 -1
|
@@ -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
|
|
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
|
|
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 =
|
|
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 = [
|
|
@@ -75,6 +83,7 @@ export const PLAY_AUTHORING_CONTRACT_ISSUE_CODES = [
|
|
|
75
83
|
'tool_response_raw_access',
|
|
76
84
|
'check_error',
|
|
77
85
|
'docflow_compile_error',
|
|
86
|
+
'docflow_visualization_missing',
|
|
78
87
|
'docflow_binding_drift',
|
|
79
88
|
'docflow_branch_labels_required',
|
|
80
89
|
'docflow_branch_requires_decision',
|
|
@@ -391,8 +400,10 @@ export type PlaySqlQuery = {
|
|
|
391
400
|
};
|
|
392
401
|
|
|
393
402
|
declare const PLAY_SECRET_HANDLE_BRAND: unique symbol;
|
|
403
|
+
declare const PLAY_SECRET_PROMISE_BRAND: unique symbol;
|
|
394
404
|
/**
|
|
395
|
-
* An opaque reference to a workspace secret
|
|
405
|
+
* An opaque reference to a workspace secret used by legacy authoring-contract
|
|
406
|
+
* editions. New Plays receive plaintext strings from `ctx.secrets.get`.
|
|
396
407
|
*
|
|
397
408
|
* @sdkReference runtime 176 SecretHandle
|
|
398
409
|
*/
|
|
@@ -405,6 +416,12 @@ export type PlaySecretHandle = {
|
|
|
405
416
|
/** Always throws. A secret handle is deliberately not serializable. */
|
|
406
417
|
toJSON(): never;
|
|
407
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;
|
|
408
425
|
/**
|
|
409
426
|
* One resolved authentication scheme, built by `ctx.secrets.bearer` or `ctx.secrets.header` and attached to a request through `init.auth`.
|
|
410
427
|
*
|
|
@@ -413,8 +430,8 @@ export type PlaySecretHandle = {
|
|
|
413
430
|
export type PlaySecretAuth = {
|
|
414
431
|
/** `bearer` sends `Authorization: Bearer <value>`; `header` sends a named header. */
|
|
415
432
|
readonly kind: 'bearer' | 'header';
|
|
416
|
-
/** The
|
|
417
|
-
readonly secret:
|
|
433
|
+
/** The value whose bytes the runtime attaches. */
|
|
434
|
+
readonly secret: string | PlaySecretPromise | PlaySecretValue;
|
|
418
435
|
/** Header name, set only when `kind` is `header`. */
|
|
419
436
|
readonly header?: string;
|
|
420
437
|
};
|
|
@@ -426,10 +443,10 @@ export type PlaySecretAuthInput = PlaySecretAuth | readonly PlaySecretAuth[];
|
|
|
426
443
|
* @sdkReference runtime 174 SecretAwareRequestInit
|
|
427
444
|
*/
|
|
428
445
|
export type PlaySecretAwareRequestInit = Omit<RequestInit, 'headers'> & {
|
|
429
|
-
/** Ordinary request headers, recorded in the durable receipt
|
|
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. */
|
|
430
447
|
headers?: HeadersInit;
|
|
431
448
|
/**
|
|
432
|
-
* One or more
|
|
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.
|
|
433
450
|
*/
|
|
434
451
|
auth?: PlaySecretAuthInput;
|
|
435
452
|
};
|
|
@@ -998,23 +1015,31 @@ export interface PlayAuthoringRuntimeContext {
|
|
|
998
1015
|
|
|
999
1016
|
secrets: {
|
|
1000
1017
|
/**
|
|
1001
|
-
*
|
|
1018
|
+
* Read an allowed workspace secret inside the running Play; do not log or return it. Declare uppercase names in top-level `secrets`.
|
|
1002
1019
|
*
|
|
1003
1020
|
* @sdkReference runtime 171 ctx.secrets.get(name)
|
|
1004
1021
|
*/
|
|
1005
|
-
get(name: string):
|
|
1022
|
+
get(name: string): PlaySecretPromise;
|
|
1006
1023
|
/**
|
|
1007
|
-
* Send
|
|
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.
|
|
1008
1027
|
*
|
|
1009
1028
|
* @sdkReference runtime 172 ctx.secrets.bearer(secret)
|
|
1010
1029
|
*/
|
|
1011
|
-
bearer(
|
|
1030
|
+
bearer(
|
|
1031
|
+
secret: string | PlaySecretPromise | PlaySecretHandle,
|
|
1032
|
+
): PlaySecretAuth;
|
|
1012
1033
|
/**
|
|
1013
|
-
* Send
|
|
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.
|
|
1014
1036
|
*
|
|
1015
1037
|
* @sdkReference runtime 173 ctx.secrets.header(header, secret)
|
|
1016
1038
|
*/
|
|
1017
|
-
header(
|
|
1039
|
+
header(
|
|
1040
|
+
header: string,
|
|
1041
|
+
secret: string | PlaySecretPromise | PlaySecretHandle,
|
|
1042
|
+
): PlaySecretAuth;
|
|
1018
1043
|
};
|
|
1019
1044
|
|
|
1020
1045
|
/**
|
|
@@ -2473,8 +2498,11 @@ export const PLAY_AUTHORING_CLOUD_TYPE_DECLARATIONS = [
|
|
|
2473
2498
|
` secrets?: readonly ${cloudReferenceType('bindings.secrets[]')}[];`,
|
|
2474
2499
|
'};',
|
|
2475
2500
|
'declare const SECRET_HANDLE_BRAND: unique symbol;',
|
|
2501
|
+
'declare const SECRET_PROMISE_BRAND: unique symbol;',
|
|
2476
2502
|
'export type SecretHandle = { readonly [SECRET_HANDLE_BRAND]: never; readonly name: string; toString(): string; toJSON(): never };',
|
|
2477
|
-
|
|
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 };",
|
|
2478
2506
|
'export type SecretAuthInput = SecretAuth | readonly SecretAuth[];',
|
|
2479
2507
|
'export type PlayInputContract<TInput> = { readonly schema: Record<string, unknown>; readonly __inputType?: TInput };',
|
|
2480
2508
|
'export type PlayReturnObject = Record<string, unknown> & { readonly _metadata?: never };',
|
|
@@ -2525,7 +2553,7 @@ export const PLAY_AUTHORING_CLOUD_TYPE_DECLARATIONS = [
|
|
|
2525
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>>;`,
|
|
2526
2554
|
' step<T>(id: string, run: () => T | Promise<T>, options?: RuntimeStepOptions): Promise<T>;',
|
|
2527
2555
|
" fetch(key: string, url: string | URL, init?: RequestInit & { headers?: HeadersInit & { 'Idempotency-Key'?: string; 'X-Idempotency-Key'?: string }; auth?: SecretAuthInput }, options?: FetchOptions): Promise<PlayFetchResponse>;",
|
|
2528
|
-
' secrets: { get(name: string):
|
|
2556
|
+
' secrets: { get(name: string): SecretPromise; bearer(secret: string | SecretPromise | SecretHandle): SecretAuth; header(header: string, secret: string | SecretPromise | SecretHandle): SecretAuth };',
|
|
2529
2557
|
` runPlay<TOutput = unknown>(key: string, playRef: ${cloudReferenceType('ctx.runPlay.playRef')}, input: ${cloudReferenceType('ctx.runPlay.input')}, options: PlayCallOptions): Promise<TOutput>;`,
|
|
2530
2558
|
' log(message: string): void;',
|
|
2531
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.
|
|
132
|
-
syntax?: '
|
|
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
|
|
137
|
-
*
|
|
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
|
-
|
|
318
|
-
|
|
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
|
-
|
|
338
|
-
`Docflow annotation on line ${
|
|
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
|
-
|
|
345
|
-
`Docflow annotation on line ${
|
|
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
|
-
|
|
351
|
-
`Docflow annotation on line ${
|
|
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
|
|
707
|
-
|
|
708
|
-
|
|
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
|
-
|
|
750
|
-
|
|
751
|
-
|
|
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
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
(
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
if (
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
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 (
|
|
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 (
|
|
832
|
+
if (!isDirective) {
|
|
853
833
|
declaredOnThisLine = addNodes(line, line);
|
|
854
834
|
}
|
|
855
|
-
const chain = parseEdgeChain(line
|
|
835
|
+
const chain = parseEdgeChain(line);
|
|
856
836
|
if (!chain) {
|
|
857
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
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,
|
|
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.
|
|
1328
|
-
|
|
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
|
|
1395
|
-
|
|
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
|
|
1368
|
+
const annotatedId = mermaidMatch[1]?.trim();
|
|
1401
1369
|
if (annotatedId) rejectedNodeIds.add(annotatedId);
|
|
1402
1370
|
const attributes = parseAttributes(
|
|
1403
|
-
mermaidMatch
|
|
1404
|
-
|
|
1405
|
-
|
|
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();
|