@dforge-core/metadata 0.0.22 → 0.0.24

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.
@@ -0,0 +1,1426 @@
1
+ // src/dsl/lexer.ts
2
+ var PUNCT3 = ["===", "!=="];
3
+ var PUNCT2 = [
4
+ "==",
5
+ "!=",
6
+ "<=",
7
+ ">=",
8
+ "&&",
9
+ "||",
10
+ "++",
11
+ "--",
12
+ "+=",
13
+ "-=",
14
+ "*=",
15
+ "/=",
16
+ "%=",
17
+ "${",
18
+ "=>"
19
+ ];
20
+ var CONTROL_KEYWORDS = /* @__PURE__ */ new Set([
21
+ "if",
22
+ "while",
23
+ "for",
24
+ "switch",
25
+ "catch",
26
+ "with"
27
+ ]);
28
+ function tokenize(text) {
29
+ const tokens = [];
30
+ let i = 0;
31
+ let line = 0;
32
+ let lineStart = 0;
33
+ let pendingLineBreak = true;
34
+ const frames = [];
35
+ const parenIsControl = [];
36
+ let closedControlParen = false;
37
+ const braceIsBlock = [];
38
+ let closedBlockBrace = false;
39
+ const push = (kind, start, end) => {
40
+ tokens.push({
41
+ kind,
42
+ text: text.slice(start, end),
43
+ start,
44
+ end,
45
+ line,
46
+ character: start - lineStart,
47
+ startsLine: pendingLineBreak
48
+ });
49
+ pendingLineBreak = false;
50
+ };
51
+ while (i < text.length) {
52
+ const frame = frames[frames.length - 1];
53
+ if (frame?.kind === "template") {
54
+ const startLine = line;
55
+ const startLineStart = lineStart;
56
+ while (i < text.length) {
57
+ const ch = text[i];
58
+ if (ch === "\\") {
59
+ i += 2;
60
+ continue;
61
+ }
62
+ if (ch === "`" || ch === "$" && text[i + 1] === "{") break;
63
+ if (ch === "\n") {
64
+ line++;
65
+ lineStart = i + 1;
66
+ }
67
+ i++;
68
+ }
69
+ if (text[i] === "`") {
70
+ i++;
71
+ frames.pop();
72
+ } else if (i < text.length) {
73
+ i += 2;
74
+ frames.push({ kind: "interp", braces: 0 });
75
+ } else {
76
+ frames.pop();
77
+ }
78
+ const savedLine = line;
79
+ const savedLineStart = lineStart;
80
+ line = startLine;
81
+ lineStart = startLineStart;
82
+ push("template", frame.chunkStart, i);
83
+ line = savedLine;
84
+ lineStart = savedLineStart;
85
+ continue;
86
+ }
87
+ const c = text[i];
88
+ if (c === "\uFEFF" && i === 0) {
89
+ i++;
90
+ lineStart = i;
91
+ continue;
92
+ }
93
+ if (c === "\n") {
94
+ i++;
95
+ line++;
96
+ lineStart = i;
97
+ pendingLineBreak = true;
98
+ continue;
99
+ }
100
+ if (c === " " || c === " " || c === "\r") {
101
+ i++;
102
+ continue;
103
+ }
104
+ if (c === "/" && text[i + 1] === "/") {
105
+ const start = i;
106
+ while (i < text.length && text[i] !== "\n") i++;
107
+ push("comment", start, i);
108
+ continue;
109
+ }
110
+ if (c === "/" && text[i + 1] === "*") {
111
+ const start = i;
112
+ const startLine = line;
113
+ const startLineStart = lineStart;
114
+ i += 2;
115
+ while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) {
116
+ if (text[i] === "\n") {
117
+ line++;
118
+ lineStart = i + 1;
119
+ }
120
+ i++;
121
+ }
122
+ i = Math.min(i + 2, text.length);
123
+ const savedLine = line;
124
+ const savedLineStart = lineStart;
125
+ line = startLine;
126
+ lineStart = startLineStart;
127
+ push("comment", start, i);
128
+ line = savedLine;
129
+ lineStart = savedLineStart;
130
+ continue;
131
+ }
132
+ if (c === "'" || c === '"') {
133
+ const start = i;
134
+ const quote = c;
135
+ i++;
136
+ while (i < text.length && text[i] !== quote && text[i] !== "\n") {
137
+ if (text[i] === "\\") i++;
138
+ i++;
139
+ }
140
+ if (text[i] === quote) i++;
141
+ push("string", start, i);
142
+ continue;
143
+ }
144
+ if (c === "`") {
145
+ frames.push({ kind: "template", chunkStart: i });
146
+ i++;
147
+ continue;
148
+ }
149
+ if (c === "/" && regexAllowedAfter(
150
+ tokens[tokens.length - 1],
151
+ closedControlParen,
152
+ closedBlockBrace
153
+ )) {
154
+ const end = scanRegex(text, i);
155
+ if (end > 0) {
156
+ push("regex", i, end);
157
+ i = end;
158
+ continue;
159
+ }
160
+ }
161
+ if (/[0-9]/.test(c)) {
162
+ const start = i;
163
+ while (i < text.length && /[0-9._eE]/.test(text[i])) i++;
164
+ push("number", start, i);
165
+ continue;
166
+ }
167
+ if (/[A-Za-z_$]/.test(c)) {
168
+ const start = i;
169
+ while (i < text.length && /[A-Za-z0-9_$]/.test(text[i])) i++;
170
+ push("ident", start, i);
171
+ continue;
172
+ }
173
+ if (c === "(") {
174
+ const prev = tokens[tokens.length - 1];
175
+ parenIsControl.push(
176
+ prev?.kind === "ident" && CONTROL_KEYWORDS.has(prev.text)
177
+ );
178
+ } else if (c === ")") {
179
+ closedControlParen = parenIsControl.pop() ?? false;
180
+ }
181
+ if (frame?.kind === "interp") {
182
+ if (c === "}" && frame.braces === 0) {
183
+ frames.pop();
184
+ frames[frames.length - 1].chunkStart = i;
185
+ i++;
186
+ continue;
187
+ }
188
+ if (c === "{") frame.braces++;
189
+ else if (c === "}") frame.braces--;
190
+ }
191
+ if (c === "{") {
192
+ braceIsBlock.push(isBlockBrace(tokens[tokens.length - 1]));
193
+ } else if (c === "}") {
194
+ closedBlockBrace = braceIsBlock.pop() ?? false;
195
+ }
196
+ const three = text.slice(i, i + 3);
197
+ if (PUNCT3.includes(three)) {
198
+ push("punct", i, i + 3);
199
+ i += 3;
200
+ continue;
201
+ }
202
+ const two = text.slice(i, i + 2);
203
+ if (PUNCT2.includes(two)) {
204
+ push("punct", i, i + 2);
205
+ i += 2;
206
+ continue;
207
+ }
208
+ push("punct", i, i + 1);
209
+ i++;
210
+ }
211
+ return tokens;
212
+ }
213
+ function regexAllowedAfter(prev, closedControlParen, closedBlockBrace) {
214
+ if (!prev) return true;
215
+ if (prev.kind === "number" || prev.kind === "string") return false;
216
+ if (prev.kind === "template") return prev.text.endsWith("${");
217
+ if (prev.kind === "ident") return REGEX_PRECEDING_KEYWORDS.has(prev.text);
218
+ if (prev.kind === "regex") return false;
219
+ if (prev.kind === "comment") return true;
220
+ if (prev.text === ")") return closedControlParen;
221
+ if (prev.text === "}") return closedBlockBrace;
222
+ return !REGEX_BLOCKING_PUNCT.has(prev.text);
223
+ }
224
+ function isBlockBrace(prev) {
225
+ if (!prev) return true;
226
+ if (prev.kind === "ident") return BLOCK_PRECEDING_KEYWORDS.has(prev.text);
227
+ if (prev.kind !== "punct") return false;
228
+ return prev.text === ")" || prev.text === "}" || prev.text === "{" || prev.text === ";";
229
+ }
230
+ var BLOCK_PRECEDING_KEYWORDS = /* @__PURE__ */ new Set(["else", "do", "try", "finally"]);
231
+ var REGEX_PRECEDING_KEYWORDS = /* @__PURE__ */ new Set([
232
+ "return",
233
+ "typeof",
234
+ "instanceof",
235
+ "in",
236
+ "of",
237
+ "new",
238
+ "delete",
239
+ "void",
240
+ "case",
241
+ "do",
242
+ "else",
243
+ "throw",
244
+ "yield",
245
+ "await"
246
+ ]);
247
+ var REGEX_BLOCKING_PUNCT = /* @__PURE__ */ new Set(["]", "++", "--"]);
248
+ function scanRegex(text, start) {
249
+ let i = start + 1;
250
+ let inClass = false;
251
+ if (text[i] === "/" || text[i] === "*") return -1;
252
+ for (; i < text.length; i++) {
253
+ const c = text[i];
254
+ if (c === "\\") {
255
+ i++;
256
+ continue;
257
+ }
258
+ if (c === "\n") return -1;
259
+ if (c === "[") inClass = true;
260
+ else if (c === "]") inClass = false;
261
+ else if (c === "/" && !inClass) {
262
+ i++;
263
+ while (i < text.length && /[a-z]/.test(text[i])) i++;
264
+ return i;
265
+ }
266
+ }
267
+ return -1;
268
+ }
269
+ function stringValue(token) {
270
+ if (token.kind !== "string") return token.text;
271
+ const body = token.text.slice(1, token.text.endsWith(token.text[0]) && token.text.length > 1 ? -1 : void 0);
272
+ return unescape(body);
273
+ }
274
+ function templateChunkValue(token) {
275
+ if (token.kind !== "template") return token.text;
276
+ const body = token.text.slice(1);
277
+ if (body.endsWith("${")) return unescape(body.slice(0, -2));
278
+ if (body.endsWith("`")) return unescape(body.slice(0, -1));
279
+ return unescape(body);
280
+ }
281
+ function unescape(body) {
282
+ return body.replace(/\\(.)/g, (_, ch) => {
283
+ switch (ch) {
284
+ case "n":
285
+ return "\n";
286
+ case "t":
287
+ return " ";
288
+ case "r":
289
+ return "\r";
290
+ default:
291
+ return ch;
292
+ }
293
+ });
294
+ }
295
+
296
+ // src/dsl/parse.ts
297
+ var BLOCK_KINDS = [
298
+ "params",
299
+ "canExecute",
300
+ "schema",
301
+ "onBeforeStart",
302
+ "execute"
303
+ ];
304
+ var BLOCK_BY_LOWER = new Map(BLOCK_KINDS.map((k) => [k.toLowerCase(), k]));
305
+ var GLOBALS = /* @__PURE__ */ new Set(["params", "old", "records"]);
306
+ function parseDsl(text) {
307
+ const all = tokenize(text);
308
+ const tokens = all.filter((t) => t.kind !== "comment");
309
+ const blocks = findBlocks(text, tokens);
310
+ const blockAt = (offset) => blocks.find((b) => offset >= b.body.start && offset < b.body.end)?.kind ?? null;
311
+ const params = parseParams(tokens, blocks);
312
+ const fieldRefs = [];
313
+ const globalRefs = [];
314
+ const calls = [];
315
+ const recordsRefs = [];
316
+ const locals = collectLocals(tokens, params);
317
+ const recordsLoopVars = collectRecordsLoopVars(tokens);
318
+ for (let i = 0; i < tokens.length; i++) {
319
+ const t = tokens[i];
320
+ if (t.kind === "punct" && t.text === "[" && isFieldAccessStart(text, t.start)) {
321
+ const name = tokens[i + 1];
322
+ const close = tokens[i + 2];
323
+ if (name?.kind === "ident" && close?.text === "]") {
324
+ const ref = {
325
+ name: name.text,
326
+ span: { start: name.start, end: name.end },
327
+ outerSpan: { start: t.start, end: close.end },
328
+ navigation: [],
329
+ block: blockAt(t.start),
330
+ isCurrentRecord: true,
331
+ startIndex: i,
332
+ endIndex: i + 2
333
+ };
334
+ ref.endIndex = collectNavigation(tokens, i + 2, ref);
335
+ i = ref.endIndex;
336
+ fieldRefs.push(ref);
337
+ continue;
338
+ }
339
+ }
340
+ if (t.kind === "ident" && t.text === "records" && tokens[i - 1]?.text !== ".") {
341
+ recordsRefs.push({ start: t.start, end: t.end });
342
+ const field = indexedRecordField(tokens, i);
343
+ if (field) {
344
+ fieldRefs.push({
345
+ name: field.name.text,
346
+ span: { start: field.name.start, end: field.name.end },
347
+ outerSpan: { start: field.open.start, end: field.close.end },
348
+ navigation: [],
349
+ block: blockAt(t.start),
350
+ isCurrentRecord: false,
351
+ startIndex: field.openIndex,
352
+ endIndex: field.openIndex + 2
353
+ });
354
+ i = field.openIndex + 2;
355
+ continue;
356
+ }
357
+ }
358
+ if (t.kind === "ident" && recordsLoopVars.has(t.text)) {
359
+ const open = tokens[i + 1];
360
+ const name = tokens[i + 2];
361
+ const close = tokens[i + 3];
362
+ if (open?.text === "[" && name?.kind === "ident" && close?.text === "]") {
363
+ fieldRefs.push({
364
+ name: name.text,
365
+ span: { start: name.start, end: name.end },
366
+ outerSpan: { start: open.start, end: close.end },
367
+ navigation: [],
368
+ block: blockAt(t.start),
369
+ isCurrentRecord: false,
370
+ startIndex: i + 1,
371
+ endIndex: i + 3
372
+ });
373
+ i += 3;
374
+ continue;
375
+ }
376
+ }
377
+ if (t.kind === "ident" && GLOBALS.has(t.text)) {
378
+ const open = tokens[i + 1];
379
+ const prop = tokens[i + 2];
380
+ const close = tokens[i + 3];
381
+ if (open?.text === "[" && prop?.kind === "ident" && close?.text === "]") {
382
+ globalRefs.push({
383
+ global: t.text,
384
+ property: prop.text,
385
+ span: { start: prop.start, end: prop.end },
386
+ block: blockAt(t.start)
387
+ });
388
+ i += 3;
389
+ continue;
390
+ }
391
+ }
392
+ if (t.kind === "ident" && tokens[i + 1]?.text === "(") {
393
+ const prev = tokens[i - 1];
394
+ if (prev?.text === ".") continue;
395
+ const call = {
396
+ name: t.text,
397
+ nameSpan: { start: t.start, end: t.end },
398
+ block: blockAt(t.start)
399
+ };
400
+ const firstArg = tokens[i + 2];
401
+ if (firstArg?.kind === "string") {
402
+ call.firstStringArg = {
403
+ value: stringValue(firstArg),
404
+ span: { start: firstArg.start, end: firstArg.end },
405
+ interpolated: false,
406
+ endIndex: i + 2
407
+ };
408
+ } else if (firstArg?.kind === "template" && firstArg.text.startsWith("`")) {
409
+ call.firstStringArg = templateArg(tokens, i + 2);
410
+ }
411
+ calls.push(call);
412
+ }
413
+ }
414
+ return {
415
+ blocks,
416
+ params,
417
+ fieldRefs,
418
+ globalRefs,
419
+ calls,
420
+ tokens,
421
+ recordsRefs,
422
+ locals,
423
+ recordsLoopVars
424
+ };
425
+ }
426
+ function isFieldAccessStart(text, start) {
427
+ return start === 0 || !/\w/.test(text[start - 1]);
428
+ }
429
+ function templateArg(tokens, start) {
430
+ const parts = [];
431
+ let depth = 0;
432
+ let interpolated = false;
433
+ let last = tokens[start];
434
+ let endIndex = start;
435
+ for (let j = start; j < tokens.length; j++) {
436
+ const t = tokens[j];
437
+ if (t.kind !== "template") continue;
438
+ const opens = t.text.startsWith("`");
439
+ if (opens) depth++;
440
+ if (depth === 1) {
441
+ if (!opens) parts.push(" ");
442
+ parts.push(templateChunkValue(t));
443
+ last = t;
444
+ endIndex = j;
445
+ }
446
+ if (t.text.endsWith("${")) {
447
+ if (depth === 1) interpolated = true;
448
+ continue;
449
+ }
450
+ depth--;
451
+ if (depth === 0) break;
452
+ }
453
+ return {
454
+ value: parts.join(""),
455
+ span: { start: tokens[start].start, end: last.end },
456
+ interpolated,
457
+ endIndex
458
+ };
459
+ }
460
+ function indexedRecordField(tokens, i) {
461
+ const index = tokens[i + 2];
462
+ if (tokens[i + 1]?.text !== "[") return null;
463
+ if (index?.kind !== "number" || !/^\d+$/.test(index.text)) return null;
464
+ if (tokens[i + 3]?.text !== "]") return null;
465
+ const open = tokens[i + 4];
466
+ const name = tokens[i + 5];
467
+ const close = tokens[i + 6];
468
+ if (open?.text !== "[" || name?.kind !== "ident" || close?.text !== "]")
469
+ return null;
470
+ return { name, open, close, openIndex: i + 4 };
471
+ }
472
+ function collectNavigation(tokens, closeIdx, ref) {
473
+ let i = closeIdx;
474
+ for (; ; ) {
475
+ const dot = tokens[i + 1];
476
+ if (dot?.text !== ".") return i;
477
+ const next = tokens[i + 2];
478
+ if (next?.kind === "ident") {
479
+ if (tokens[i + 3]?.text === "(") return i;
480
+ ref.navigation.push({
481
+ name: next.text,
482
+ span: { start: next.start, end: next.end },
483
+ bracketed: false
484
+ });
485
+ i += 2;
486
+ continue;
487
+ }
488
+ if (next?.text === "[") {
489
+ const name = tokens[i + 3];
490
+ const close = tokens[i + 4];
491
+ if (name?.kind === "ident" && close?.text === "]") {
492
+ ref.navigation.push({
493
+ name: name.text,
494
+ span: { start: name.start, end: name.end },
495
+ bracketed: true
496
+ });
497
+ i += 4;
498
+ continue;
499
+ }
500
+ }
501
+ return i;
502
+ }
503
+ }
504
+ function collectRecordsLoopVars(tokens) {
505
+ const vars = /* @__PURE__ */ new Set();
506
+ for (let i = 0; i < tokens.length - 4; i++) {
507
+ if (tokens[i].text !== "for" || tokens[i].kind !== "ident") continue;
508
+ const name = tokens[i + 1];
509
+ if (name.kind !== "ident") continue;
510
+ if (tokens[i + 2].text !== "in" || tokens[i + 3].text !== "records") continue;
511
+ if (tokens[i + 4].text !== "{") continue;
512
+ vars.add(name.text);
513
+ }
514
+ return vars;
515
+ }
516
+ var DECLARATORS = /* @__PURE__ */ new Set(["var", "let", "const"]);
517
+ function collectLocals(tokens, params) {
518
+ const locals = new Set(params.map((p) => p.name));
519
+ collectFunctionParams(tokens, locals);
520
+ for (let i = 0; i < tokens.length; i++) {
521
+ const t = tokens[i];
522
+ if (t.kind !== "ident") continue;
523
+ if (DECLARATORS.has(t.text)) {
524
+ collectDeclarations(tokens, i + 1, locals);
525
+ continue;
526
+ }
527
+ if (t.text === "for") {
528
+ let j = i + 1;
529
+ if (tokens[j]?.text === "(") j++;
530
+ if (tokens[j]?.kind === "ident" && DECLARATORS.has(tokens[j].text)) j++;
531
+ const name = tokens[j];
532
+ if (name?.kind === "ident" && !DECLARATORS.has(name.text)) locals.add(name.text);
533
+ }
534
+ }
535
+ return locals;
536
+ }
537
+ function collectFunctionParams(tokens, locals) {
538
+ for (let i = 0; i < tokens.length; i++) {
539
+ const t = tokens[i];
540
+ if (t.kind === "ident" && t.text === "function") {
541
+ let j = i + 1;
542
+ if (tokens[j]?.kind === "ident") j++;
543
+ if (tokens[j]?.text === "(") collectParenIdents(tokens, j, locals);
544
+ continue;
545
+ }
546
+ if (t.text === "=>") {
547
+ const prev = tokens[i - 1];
548
+ if (prev?.kind === "ident") locals.add(prev.text);
549
+ else if (prev?.text === ")") {
550
+ const open = matchingOpenParen(tokens, i - 1);
551
+ if (open >= 0) collectParenIdents(tokens, open, locals);
552
+ }
553
+ continue;
554
+ }
555
+ if (t.kind === "ident" && !CONTROL_KEYWORDS.has(t.text) && tokens[i + 1]?.text === "(") {
556
+ const close = matchingCloseParen(tokens, i + 1);
557
+ if (close >= 0 && tokens[close + 1]?.text === "{")
558
+ collectParenIdents(tokens, i + 1, locals);
559
+ }
560
+ }
561
+ }
562
+ function collectParenIdents(tokens, openIndex, locals) {
563
+ let depth = 0;
564
+ for (let i = openIndex; i < tokens.length; i++) {
565
+ const t = tokens[i];
566
+ if (t.text === "(") depth++;
567
+ else if (t.text === ")") {
568
+ depth--;
569
+ if (depth === 0) return;
570
+ } else if (t.kind === "ident") locals.add(t.text);
571
+ }
572
+ }
573
+ function matchingCloseParen(tokens, openIndex) {
574
+ let depth = 0;
575
+ for (let i = openIndex; i < tokens.length; i++) {
576
+ if (tokens[i].text === "(") depth++;
577
+ else if (tokens[i].text === ")" && --depth === 0) return i;
578
+ }
579
+ return -1;
580
+ }
581
+ function matchingOpenParen(tokens, closeIndex) {
582
+ let depth = 0;
583
+ for (let i = closeIndex; i >= 0; i--) {
584
+ if (tokens[i].text === ")") depth++;
585
+ else if (tokens[i].text === "(" && --depth === 0) return i;
586
+ }
587
+ return -1;
588
+ }
589
+ function collectDeclarations(tokens, start, locals) {
590
+ let j = start;
591
+ for (; ; ) {
592
+ const head = tokens[j];
593
+ if (!head) return;
594
+ if (head.text === "{" || head.text === "[") {
595
+ j = collectPattern(tokens, j, locals);
596
+ } else if (head.kind === "ident" && !DECLARATORS.has(head.text)) {
597
+ locals.add(head.text);
598
+ j++;
599
+ } else return;
600
+ let depth = 0;
601
+ for (; j < tokens.length; j++) {
602
+ const n = tokens[j];
603
+ if (n.text === "(" || n.text === "[" || n.text === "{") depth++;
604
+ else if (n.text === ")" || n.text === "]" || n.text === "}") depth--;
605
+ if (depth < 0) return;
606
+ if (depth > 0) continue;
607
+ if (n.text === ";") return;
608
+ if (n.text === ",") break;
609
+ if (n.startsLine) return;
610
+ }
611
+ if (j >= tokens.length) return;
612
+ j++;
613
+ }
614
+ }
615
+ function collectPattern(tokens, start, locals) {
616
+ let depth = 0;
617
+ let i = start;
618
+ for (; i < tokens.length; i++) {
619
+ const t = tokens[i];
620
+ if (t.text === "{" || t.text === "[") depth++;
621
+ else if (t.text === "}" || t.text === "]") {
622
+ depth--;
623
+ if (depth === 0) return i + 1;
624
+ } else if (t.kind === "ident") locals.add(t.text);
625
+ }
626
+ return i;
627
+ }
628
+ function blockLabelAt(tokens, i) {
629
+ const t = tokens[i];
630
+ if (!t || t.kind !== "ident" || !t.startsLine) return null;
631
+ const colon = tokens[i + 1];
632
+ if (colon?.text !== ":" || colon.start !== t.end) return null;
633
+ const kind = BLOCK_BY_LOWER.get(t.text.toLowerCase());
634
+ return kind ? { kind, span: { start: t.start, end: colon.end } } : null;
635
+ }
636
+ function findBlocks(text, tokens) {
637
+ const labels = [];
638
+ for (let i = 0; i < tokens.length; i++) {
639
+ const t = tokens[i];
640
+ if (t.character !== 0) continue;
641
+ const label = blockLabelAt(tokens, i);
642
+ if (!label) continue;
643
+ labels.push(label);
644
+ if (label.kind === "execute") break;
645
+ }
646
+ return labels.map((l, idx) => ({
647
+ kind: l.kind,
648
+ label: l.span,
649
+ body: {
650
+ start: l.span.end,
651
+ end: labels[idx + 1]?.span.start ?? text.length
652
+ }
653
+ }));
654
+ }
655
+ function parseParams(tokens, blocks) {
656
+ const block = blocks.find((b) => b.kind === "params");
657
+ if (!block) return [];
658
+ const inBlock = tokens.filter(
659
+ (t) => t.start >= block.body.start && t.start < block.body.end
660
+ );
661
+ const params = [];
662
+ for (let i = 0; i < inBlock.length; i++) {
663
+ const name = inBlock[i];
664
+ if (!name.startsLine && i !== 0 || name.kind !== "ident") continue;
665
+ if (inBlock[i + 1]?.text !== ":") continue;
666
+ let j = i + 2;
667
+ let isRef = false;
668
+ if (inBlock[j]?.kind === "ident" && inBlock[j].text === "ref") {
669
+ isRef = true;
670
+ j++;
671
+ }
672
+ const type = inBlock[j];
673
+ if (!type || type.kind !== "ident") continue;
674
+ if (type.line !== name.line) continue;
675
+ let typeEnd = type.end;
676
+ let typeText = type.text;
677
+ if (isRef) {
678
+ while (inBlock[j + 1] && (inBlock[j + 1].text === "-" || inBlock[j + 1].text === ".") && inBlock[j + 2]?.kind === "ident" && inBlock[j + 1].start === typeEnd && inBlock[j + 2].start === inBlock[j + 1].end) {
679
+ typeText += inBlock[j + 1].text + inBlock[j + 2].text;
680
+ typeEnd = inBlock[j + 2].end;
681
+ j += 2;
682
+ }
683
+ }
684
+ let required = false;
685
+ let label;
686
+ for (let k = j + 1; k < inBlock.length && !inBlock[k].startsLine; k++) {
687
+ const t = inBlock[k];
688
+ if (t.kind === "ident" && t.text === "required") required = true;
689
+ if (t.kind === "string" && label === void 0) label = stringValue(t);
690
+ }
691
+ params.push({
692
+ name: name.text,
693
+ nameSpan: { start: name.start, end: name.end },
694
+ type: typeText,
695
+ typeSpan: { start: type.start, end: typeEnd },
696
+ isRef,
697
+ required,
698
+ label
699
+ });
700
+ }
701
+ return params;
702
+ }
703
+
704
+ // src/dsl/builtins.ts
705
+ var BUILTINS = [
706
+ // Data operations
707
+ {
708
+ name: "insert",
709
+ signature: "insert(entity, fields)",
710
+ returns: "object",
711
+ entityArg: 0,
712
+ doc: "Insert a new record. Returns the full row including auto-filled columns (PK, audit, number sequences)."
713
+ },
714
+ {
715
+ name: "select",
716
+ signature: "select(entity, opts?)",
717
+ returns: "object[]",
718
+ entityArg: 0,
719
+ doc: "Structured multi-row read. `opts` = `{columns, filter, orderBy, limit, offset}`. Prefer over `query()` for row-shaped reads."
720
+ },
721
+ {
722
+ name: "update",
723
+ signature: "update(entity, key, fields)",
724
+ returns: "number",
725
+ entityArg: 0,
726
+ doc: "Update the row(s) matched by `key`; returns rows affected. A value may be `{ inc: n }` to add to the column's current value \u2014 use that for every counter, balance and quantity."
727
+ },
728
+ {
729
+ name: "delete",
730
+ signature: "delete(entity, key)",
731
+ returns: "number",
732
+ entityArg: 0,
733
+ doc: "Delete the row(s) matched by `key`; returns rows affected."
734
+ },
735
+ {
736
+ name: "query",
737
+ signature: "query(sql, args?)",
738
+ returns: "object[]",
739
+ doc: "Execute parameterized SQL with `@argName` placeholders and schema-qualified table names. Escape hatch \u2014 prefer `select()` / `update()` / `delete()` when the structured shape fits."
740
+ },
741
+ {
742
+ name: "getRecord",
743
+ signature: "getRecord(entity, key)",
744
+ returns: "object",
745
+ entityArg: 0,
746
+ doc: "Fetch a single record by key. **Throws** a localized not-found error when absent."
747
+ },
748
+ {
749
+ name: "getRecordOrNull",
750
+ signature: "getRecordOrNull(entity, key)",
751
+ returns: "object | null",
752
+ entityArg: 0,
753
+ doc: "Like `getRecord`, but returns `null` instead of throwing \u2014 for an expected absence, guarded with `if (rec == null)`."
754
+ },
755
+ {
756
+ name: "preloadRef",
757
+ signature: "preloadRef(fkColumn)",
758
+ returns: "object",
759
+ doc: "Load the referenced record for a FK column. Read its fields with `ref.get('field')`."
760
+ },
761
+ // Messaging
762
+ {
763
+ name: "error",
764
+ signature: "error(message)",
765
+ returns: "never",
766
+ doc: "Abort the action and show an error. Rolls back all changes."
767
+ },
768
+ { name: "warn", signature: "warn(message)", returns: "void", doc: "Show a warning and continue." },
769
+ {
770
+ name: "info",
771
+ signature: "info(message)",
772
+ returns: "void",
773
+ doc: "Show an informational message \u2014 a receipt for work the action did, not a way to publish a computed value."
774
+ },
775
+ {
776
+ name: "exit",
777
+ signature: "exit(message?, level?)",
778
+ returns: "never",
779
+ doc: "Stop the script early **without** error, keeping changes made so far. Level `'info'` (default) or `'warn'`. This is the DSL's early return \u2014 the block compiles to a bare script, so a `return` outside a function is a syntax error wherever it sits."
780
+ },
781
+ {
782
+ name: "notify",
783
+ signature: "notify(userId, message)",
784
+ returns: "void",
785
+ doc: "Send an in-app notification to a specific user."
786
+ },
787
+ {
788
+ name: "sendEmail",
789
+ signature: "sendEmail(to, subjectOrTemplate, dataOrBody?)",
790
+ returns: "void",
791
+ doc: "Queue an outbound email. Three arguments, and the **second** picks the mode: `sendEmail(to, 'order.shipped', { ref: [code] })` sends a template, `sendEmail(to, 'Your order shipped', '<p>\u2026</p>')` sends raw HTML. A second argument with no spaces that contains `.` or `_` is read as a template code \u2014 so a raw subject like `'Shipped.'` silently becomes one; give a raw subject spaces. There is no fourth argument."
792
+ },
793
+ // External calls
794
+ {
795
+ name: "callApi",
796
+ signature: "callApi(url, method, headers?, body?)",
797
+ returns: "string",
798
+ doc: "HTTP request to an external API; returns the body as a string. Max 10 calls per script, 120s timeout, 5MB response."
799
+ },
800
+ {
801
+ name: "callService",
802
+ signature: "callService(name, args)",
803
+ returns: "any",
804
+ doc: "Invoke a registered host service."
805
+ },
806
+ {
807
+ name: "callProc",
808
+ signature: "callProc(procName, args)",
809
+ returns: "any",
810
+ doc: "Call a stored procedure with named parameters."
811
+ },
812
+ // Files, settings, extraction
813
+ {
814
+ name: "getFileBase64",
815
+ signature: "getFileBase64(fileField)",
816
+ returns: "string",
817
+ doc: "Read a file from storage as base64. Use with `callApi()` to send files to external APIs. Max 10 MB."
818
+ },
819
+ {
820
+ name: "getFileUrl",
821
+ signature: "getFileUrl(fileField)",
822
+ returns: "string",
823
+ doc: "Temporary signed download URL (relative path) for a file field \u2014 for emails and notifications, not for `callApi()`."
824
+ },
825
+ { name: "getFileInfo", signature: "getFileInfo(fileField)", returns: "object | null", doc: "File metadata for a file field." },
826
+ {
827
+ name: "download",
828
+ signature: "download(url, fileName?)",
829
+ returns: "void",
830
+ doc: "Set a download URL on the action result; the browser opens it when the action returns. It fetches nothing and stores nothing \u2014 the URL must already serve the bytes, e.g. the `/api/temp-file/{token}` a service handed back. To put a remote file INTO storage, fetch it with `callApi()` and write it to a file column."
831
+ },
832
+ {
833
+ name: "getSetting",
834
+ signature: "getSetting(settingCd)",
835
+ returns: "any",
836
+ doc: "Read a module setting, resolved through the folder chain (current folder \u2192 parents \u2192 module default)."
837
+ },
838
+ {
839
+ name: "getSecret",
840
+ signature: "getSecret(secretCd)",
841
+ returns: "string",
842
+ doc: "Retrieve a decrypted secret (API key, token). Managed in the admin UI."
843
+ },
844
+ {
845
+ name: "ocrExtract",
846
+ signature: "ocrExtract(fileField, endpointBaseUrl, schema?, opts?)",
847
+ returns: "string (v1) | object (v2)",
848
+ doc: "Run OCR extraction against a document. **The return type follows the mode, and only `opts.mode` selects it** \u2014 a third argument alone never does. Without `opts.mode` (v1) the third argument is forwarded as `hints` and the result is the raw bundle as a JSON **string**: `JSON.parse()` it, or every property read is `undefined`. With `{ mode: 'extract' }` the third argument is a schema object and the result is already parsed \u2014 `{ fields, confidence?, unmatched, raw_text, meta }`. `{ mode: 'summary' }` needs no schema and returns `{ summary, raw_text, meta }` parsed. `{ profile: 'module_cd.profile_cd' }` uses a registered extraction profile, implies mode `extract`, and cannot be combined with an inline schema. The OCR host must be on `Dsl:Limits:AllowedHosts`."
849
+ },
850
+ {
851
+ name: "detectDocument",
852
+ signature: "detectDocument(rawText)",
853
+ returns: "object | null",
854
+ doc: "Score text against reachable extraction profiles; returns the best match `{ profile, docType, score }` or `null`."
855
+ },
856
+ {
857
+ name: "applyProfile",
858
+ signature: "applyProfile(json, profile)",
859
+ returns: "object",
860
+ doc: "Map already-extracted JSON to target fields and return the mapped object \u2014 it never writes, so the `insert()` / `update()` stays yours. `json` is the parsed extractor output (`JSON.parse(ocrExtract(...))`). `profile` must be an **inline object**, `{ map: { target: { from, transform?, pick? } }, lines?: { from, map, key? } }` \u2014 passing a profile code throws, as profile-by-code is a later slice. `from` is a path into the extraction; `transform` is one of `firstValid:<basis>`, `lookupRef:<entity.matchField>>returnField>`, `matchCatalog:<same>`, `parseMoney` or `parseDate`. Header targets come back at the top level and the 1:N block under its `key` (default `lines`); a missing source path omits the target. Not the same thing as `ocrExtract`'s registered `opts.profile` code."
861
+ },
862
+ // Utilities
863
+ { name: "now", signature: "now()", returns: "datetime", doc: "Current date/time. Lowercase \u2014 this is the execute-block date function." },
864
+ { name: "addDays", signature: "addDays(date, n)", returns: "date", doc: "Add `n` days to a date." },
865
+ { name: "addMinutes", signature: "addMinutes(date, n)", returns: "datetime", doc: "Add `n` minutes." },
866
+ { name: "addSeconds", signature: "addSeconds(date, n)", returns: "datetime", doc: "Add `n` seconds." },
867
+ { name: "currentUserId", signature: "currentUserId()", returns: "long", doc: "The acting user's tenant user ID." },
868
+ { name: "flush", signature: "flush()", returns: "void", doc: "Flush pending writes so a following read sees them." },
869
+ { name: "tryParseJson", signature: "tryParseJson(value)", returns: "any | null", doc: "Parse JSON, returning `null` on failure instead of throwing." },
870
+ {
871
+ name: "nextNumber",
872
+ signature: "nextNumber(entity)",
873
+ returns: "string",
874
+ entityArg: 0,
875
+ doc: "Generate the next value from a number sequence. Usually unnecessary \u2014 the platform auto-fills on `insert()`."
876
+ },
877
+ {
878
+ name: "entityLink",
879
+ signature: "entityLink(entityCd, record, description?)",
880
+ returns: "link",
881
+ entityArg: 0,
882
+ doc: "Build a clickable link to a record, for an `entitylink` column. Always pass a **qualified** code \u2014 `entity_cd` is only unique per module."
883
+ }
884
+ ];
885
+ var BUILTIN_BY_NAME = new Map(BUILTINS.map((b) => [b.name, b]));
886
+ var BUILTIN_VALUES = {
887
+ userId: "The acting user's tenant user ID. A bare identifier \u2014 `userId()` is a compile error.",
888
+ records: "The selected records, in a `batch` action. Iterate with `for x in records { \u2026 }`.",
889
+ params: "Declared action parameters \u2014 read with `params[name]`.",
890
+ old: "Pre-change values, in a trigger-invoked action \u2014 read with `old[field]`. Read-only."
891
+ };
892
+
893
+ // src/dsl/check.ts
894
+ function checkDsl(text, ctx = {}) {
895
+ const parsed = parseDsl(text);
896
+ const out = [];
897
+ const positionAt = lineIndexer(text);
898
+ const add = (span, message, severity, rule) => {
899
+ const { line, column } = positionAt(span.start);
900
+ out.push({
901
+ rule,
902
+ severity,
903
+ message,
904
+ start: span.start,
905
+ end: span.end,
906
+ line,
907
+ column
908
+ });
909
+ };
910
+ const action = ctx.action;
911
+ const entity = ctx.currentEntity ?? null;
912
+ const isBatch = (action?.executionMode ?? "single") === "batch";
913
+ const hasColumn = columnMatcher(entity?.columns);
914
+ if (text.trim() === "") {
915
+ add(
916
+ { start: 0, end: text.length },
917
+ "A DSL body needs at least an execute: block \u2014 an empty file installs an action that does nothing.",
918
+ "error",
919
+ "dsl/empty-script"
920
+ );
921
+ return out;
922
+ }
923
+ const firstToken = parsed.tokens[0];
924
+ const firstLabel = parsed.blocks[0]?.label ?? (firstToken ? { start: firstToken.start, end: firstToken.end } : { start: 0, end: text.length });
925
+ if (!parsed.blocks.some((b) => b.kind === "execute")) {
926
+ add(
927
+ firstLabel,
928
+ "No execute: block \u2014 it is the only required block, and the compiler runs nothing without it.",
929
+ "error",
930
+ "dsl/missing-execute"
931
+ );
932
+ }
933
+ const executeBlock = parsed.blocks.find((b) => b.kind === "execute");
934
+ if (executeBlock) {
935
+ for (let i = 0; i < parsed.tokens.length; i++) {
936
+ const token = parsed.tokens[i];
937
+ if (token.start <= executeBlock.label.start) continue;
938
+ if (token.character !== 0) continue;
939
+ const label = blockLabelAt(parsed.tokens, i);
940
+ if (!label) continue;
941
+ add(
942
+ label.span,
943
+ label.kind === "execute" ? `"execute:" is declared more than once \u2014 the first one's body runs to the end of the file, so this header and everything under it are swallowed into it and parsed as JavaScript. Merge the two into one execute: block.` : `"${label.kind}:" is below execute:, whose body runs to the end of the file \u2014 the header and everything under it are swallowed into the execute: script and parsed as JavaScript. Move it above execute:.`,
944
+ "error",
945
+ "dsl/block-after-execute"
946
+ );
947
+ }
948
+ }
949
+ const seenBlocks = /* @__PURE__ */ new Set();
950
+ for (let i = 0; i < parsed.blocks.length; i++) {
951
+ const block = parsed.blocks[i];
952
+ if (seenBlocks.has(block.kind)) {
953
+ add(
954
+ block.label,
955
+ `"${block.kind}:" is declared more than once \u2014 the compiler keeps one and silently drops the other.`,
956
+ "error",
957
+ "dsl/duplicate-block"
958
+ );
959
+ continue;
960
+ }
961
+ seenBlocks.add(block.kind);
962
+ const prev = parsed.blocks[i - 1];
963
+ if (prev && prev.kind !== "execute" && !BLOCK_STOPS[prev.kind].has(block.kind)) {
964
+ add(
965
+ block.label,
966
+ `"${block.kind}:" must come before "${prev.kind}:" \u2014 the compiler reads ${prev.kind}: up to the next header it recognises, and ${block.kind}: is not one of them, so this header and its body end up inside the ${prev.kind}: body.`,
967
+ "error",
968
+ "dsl/block-order"
969
+ );
970
+ }
971
+ }
972
+ for (const ref of parsed.fieldRefs) {
973
+ if (ref.block === "schema" || ref.block === "params") continue;
974
+ const isDslBlock = ref.block === "execute" || ref.block === "onBeforeStart";
975
+ const secondRef = ref.navigation.findIndex((h, idx) => idx > 0 && h.bracketed);
976
+ if (isDslBlock && secondRef > 0) {
977
+ const first = ref.navigation[secondRef];
978
+ const last = ref.navigation[ref.navigation.length - 1];
979
+ add(
980
+ { start: first.span.start, end: last.span.end },
981
+ `Ref navigation is single-hop in the DSL. Use getRecord() or select() to reach past "${ref.navigation[0].name}".`,
982
+ "error",
983
+ "dsl/multi-hop-nav"
984
+ );
985
+ }
986
+ if (isBatch && ref.block === "execute" && LITERAL_NAMES.has(ref.name))
987
+ continue;
988
+ if (!entity) continue;
989
+ if (entity.columns.size === 0) continue;
990
+ if (hasColumn(ref.name)) continue;
991
+ if (ref.isCurrentRecord && parsed.locals.has(ref.name)) continue;
992
+ add(
993
+ ref.span,
994
+ `"${ref.name}" is not a column on ${entity.qualified}. [field] names are checked at pack time \u2014 this is a compile error, not an empty read.`,
995
+ "error",
996
+ "dsl/unknown-column"
997
+ );
998
+ }
999
+ if (entity && entity.columns.size > 0) {
1000
+ for (const ref of parsed.globalRefs) {
1001
+ if (ref.global !== "old") continue;
1002
+ if (ref.block !== "execute" && ref.block !== "onBeforeStart") continue;
1003
+ if (hasColumn(ref.property)) continue;
1004
+ add(
1005
+ ref.span,
1006
+ `"${ref.property}" is not a column on ${entity.qualified}. old[field] reads the pre-change value of a column, and the name is checked at pack time.`,
1007
+ "error",
1008
+ "dsl/unknown-column"
1009
+ );
1010
+ }
1011
+ }
1012
+ const declared = new Set(parsed.params.map((p) => p.name.toLowerCase()));
1013
+ const hasParamsBlock = parsed.blocks.some((b) => b.kind === "params");
1014
+ for (const ref of parsed.globalRefs) {
1015
+ if (ref.global !== "params") continue;
1016
+ if (!hasParamsBlock || declared.has(ref.property.toLowerCase())) continue;
1017
+ add(
1018
+ ref.span,
1019
+ `"${ref.property}" is not declared in the params: block.`,
1020
+ "error",
1021
+ "dsl/unknown-param"
1022
+ );
1023
+ }
1024
+ if (action) {
1025
+ const mode = action.executionMode ?? "single";
1026
+ const recordsSpan = parsed.locals.has("records") ? void 0 : parsed.recordsRefs[0];
1027
+ const bareFields = parsed.fieldRefs.filter(
1028
+ (r) => r.block === "execute" && r.isCurrentRecord && !parsed.locals.has(r.name) && !LITERAL_NAMES.has(r.name)
1029
+ );
1030
+ if (mode === "batch") {
1031
+ for (const field of bareFields) {
1032
+ add(
1033
+ field.outerSpan,
1034
+ `Action "${action.code}" runs in batch mode, which has no current record. Iterate instead: for x in records { x[${field.name}] }.`,
1035
+ "error",
1036
+ "dsl/batch-bare-field"
1037
+ );
1038
+ }
1039
+ }
1040
+ if (mode !== "batch" && recordsSpan) {
1041
+ add(
1042
+ recordsSpan,
1043
+ `"records" only exists in batch mode; action "${action.code}" runs in "${mode}" mode.`,
1044
+ "warning",
1045
+ "dsl/records-outside-batch"
1046
+ );
1047
+ }
1048
+ }
1049
+ for (const token of parsed.tokens) {
1050
+ if (blockOf(parsed, token.start) !== "canExecute") continue;
1051
+ if (token.text !== "&&" && token.text !== "||") continue;
1052
+ add(
1053
+ { start: token.start, end: token.end },
1054
+ `canExecute is a formula, not JavaScript \u2014 write ${token.text === "&&" ? "AND" : "OR"}.`,
1055
+ "warning",
1056
+ "dsl/js-in-formula"
1057
+ );
1058
+ }
1059
+ for (const call of parsed.calls) {
1060
+ if (call.name === "userId") {
1061
+ add(
1062
+ call.nameSpan,
1063
+ "'userId' is a value, not a function \u2014 write userId without parentheses, or currentUserId(). The compiler's bare-identifier rewrite fires either way, so userId() reaches Jint as a call against a number.",
1064
+ "error",
1065
+ "dsl/user-id-call"
1066
+ );
1067
+ continue;
1068
+ }
1069
+ const builtin = BUILTIN_BY_NAME.get(call.name);
1070
+ if (!builtin || builtin.entityArg === void 0) continue;
1071
+ const arg = call.firstStringArg;
1072
+ if (!arg) continue;
1073
+ if (arg.interpolated) continue;
1074
+ if (arg.value.includes(".") || ctx.moduleCode?.includes("-")) continue;
1075
+ add(
1076
+ arg.span,
1077
+ `Qualify the entity code as "${ctx.moduleCode ? `${ctx.moduleCode}.` : "<module>."}${arg.value}" \u2014 entity_cd is only unique per module, so a bare code can resolve to another module's entity.`,
1078
+ "info",
1079
+ "dsl/unqualified-entity"
1080
+ );
1081
+ }
1082
+ let depth = 0;
1083
+ const functionDepths = [];
1084
+ const parenStack = [];
1085
+ let matchingParenOpen = -1;
1086
+ for (let i = 0; i < parsed.tokens.length; i++) {
1087
+ const token = parsed.tokens[i];
1088
+ if (token.text === "(") parenStack.push(i);
1089
+ else if (token.text === ")") matchingParenOpen = parenStack.pop() ?? -1;
1090
+ if (token.text === "{") {
1091
+ depth++;
1092
+ if (opensFunctionBody(parsed.tokens, i, matchingParenOpen))
1093
+ functionDepths.push(depth);
1094
+ } else if (token.text === "}") {
1095
+ if (functionDepths[functionDepths.length - 1] === depth)
1096
+ functionDepths.pop();
1097
+ depth = Math.max(0, depth - 1);
1098
+ }
1099
+ if (depth === 0 && token.character > 0) {
1100
+ const label = blockLabelAt(parsed.tokens, i);
1101
+ const isParamDecl = parsed.params.some(
1102
+ (p) => p.nameSpan.start === token.start
1103
+ );
1104
+ if (label && !isParamDecl) {
1105
+ add(
1106
+ label.span,
1107
+ `Block headers start at column 0. Indented, "${label.kind}:" is body text \u2014 the compiler never opens the block, and it runs as if empty.`,
1108
+ "error",
1109
+ "dsl/indented-block-header"
1110
+ );
1111
+ }
1112
+ }
1113
+ if (token.kind !== "ident" || token.text !== "return") continue;
1114
+ if (parsed.tokens[i - 1]?.text === ".") continue;
1115
+ if (parsed.tokens[i + 1]?.text === ":") continue;
1116
+ if (isMethodName(parsed.tokens, i)) continue;
1117
+ if (functionDepths.length > 0) continue;
1118
+ const block = blockOf(parsed, token.start);
1119
+ if (block !== "execute" && block !== "onBeforeStart") continue;
1120
+ add(
1121
+ { start: token.start, end: token.end },
1122
+ `A return outside a function is a syntax error \u2014 ${block}: compiles to a bare script. Use exit(message, level) to stop early and keep the changes made so far.`,
1123
+ "error",
1124
+ "dsl/top-level-return"
1125
+ );
1126
+ }
1127
+ for (const call of parsed.calls) {
1128
+ const replacement = FORMULA_ONLY.get(call.name);
1129
+ if (!replacement) continue;
1130
+ if (call.block !== "execute" && call.block !== "onBeforeStart") continue;
1131
+ add(
1132
+ call.nameSpan,
1133
+ `${call.name}() is formula-only \u2014 in ${call.block}: it is undefined and install fails with "'${call.name}' is not defined". Write ${replacement}.`,
1134
+ "error",
1135
+ "dsl/formula-only-function"
1136
+ );
1137
+ }
1138
+ const tokenAt = new Map(parsed.tokens.map((t, i) => [t.start, i]));
1139
+ for (const call of parsed.calls) {
1140
+ if (call.name !== "query" && call.name !== "callProc") continue;
1141
+ const arg = call.firstStringArg;
1142
+ if (!arg) continue;
1143
+ const bad = /(^|[^:\w]):([a-z][a-z0-9_]*)/i.exec(stripSqlNoise(arg.value));
1144
+ if (bad) {
1145
+ const name = bad[2];
1146
+ add(
1147
+ arg.span,
1148
+ `SQL binds @name, not :name \u2014 rewrite ':${name}' as '@${name}' and pass { ${name}: value } as the params argument.`,
1149
+ "error",
1150
+ "dsl/sql-placeholder"
1151
+ );
1152
+ }
1153
+ if (arg.interpolated || parsed.tokens[arg.endIndex + 1]?.text === "+") {
1154
+ const built = arg.interpolated ? "${\u2026} interpolation" : "concatenation";
1155
+ add(
1156
+ arg.span,
1157
+ // callProc's first argument is the procedure name, and an
1158
+ // identifier cannot be bound — the @placeholder advice cannot
1159
+ // be followed there.
1160
+ call.name === "callProc" ? `Procedure name built by ${built} \u2014 a name is an identifier, so it cannot be bound as a @placeholder. Choose between fixed names in the script rather than splicing a value into one.` : `SQL built by ${built} \u2014 pass @placeholders and a params object instead, so values are bound rather than spliced into the statement.`,
1161
+ "warning",
1162
+ "dsl/sql-concat"
1163
+ );
1164
+ }
1165
+ }
1166
+ const declaredFunctions = /* @__PURE__ */ new Set();
1167
+ for (let i = 0; i < parsed.tokens.length; i++) {
1168
+ if (parsed.tokens[i].text !== "function") continue;
1169
+ const name = parsed.tokens[i + 1];
1170
+ if (name?.kind === "ident") declaredFunctions.add(name.text);
1171
+ }
1172
+ const reportedUnknown = /* @__PURE__ */ new Set();
1173
+ for (const call of parsed.calls) {
1174
+ if (call.block !== "execute" && call.block !== "onBeforeStart") continue;
1175
+ const name = call.name;
1176
+ const idx = tokenAt.get(call.nameSpan.start);
1177
+ if (idx !== void 0) {
1178
+ const prev = parsed.tokens[idx - 1]?.text;
1179
+ if (prev === "function" || prev === "new") continue;
1180
+ if (isMethodName(parsed.tokens, idx)) continue;
1181
+ }
1182
+ if (BUILTIN_BY_NAME.has(name) || CONTROL_KEYWORDS.has(name) || KEYWORDS_BEFORE_PAREN.has(name) || JS_GLOBALS.has(name) || FORMULA_ONLY.has(name) || // Its own rule reports this one, with better wording.
1183
+ name === "userId" || declaredFunctions.has(name) || parsed.locals.has(name) || reportedUnknown.has(name)) {
1184
+ continue;
1185
+ }
1186
+ reportedUnknown.add(name);
1187
+ add(
1188
+ call.nameSpan,
1189
+ `"${name}()" is not a DSL host function, and nothing in this script declares it. Check it against the built-in catalog.`,
1190
+ "warning",
1191
+ "dsl/unknown-builtin"
1192
+ );
1193
+ }
1194
+ if (ctx.action?.viaJob) {
1195
+ const inScript = (block) => block === "execute" || block === "onBeforeStart";
1196
+ const bound = [];
1197
+ for (const ref of parsed.fieldRefs) {
1198
+ if (!inScript(ref.block)) continue;
1199
+ if (isBatch && ref.block === "execute" && LITERAL_NAMES.has(ref.name))
1200
+ continue;
1201
+ if (ref.isCurrentRecord && parsed.locals.has(ref.name)) continue;
1202
+ bound.push({ span: ref.outerSpan, read: `[${ref.name}]` });
1203
+ }
1204
+ for (const ref of parsed.globalRefs) {
1205
+ if (ref.global !== "old" || !inScript(ref.block)) continue;
1206
+ bound.push({ span: ref.span, read: `old[${ref.property}]` });
1207
+ }
1208
+ for (const span of parsed.recordsRefs) {
1209
+ if (!inScript(blockOf(parsed, span.start))) continue;
1210
+ bound.push({ span, read: "records" });
1211
+ }
1212
+ const first = bound.sort((a, b) => a.span.start - b.span.start)[0];
1213
+ if (first) {
1214
+ add(
1215
+ first.span,
1216
+ `Action "${ctx.action.code}" is invoked by a scheduled job, which runs as the system user with no record bound \u2014 ${first.read} has nothing to read. Fetch rows with select() or query() instead.`,
1217
+ "error",
1218
+ "dsl/job-record-context"
1219
+ );
1220
+ }
1221
+ }
1222
+ out.push(...inlineAssignmentIssues(parsed, positionAt));
1223
+ return out;
1224
+ }
1225
+ function columnMatcher(columns) {
1226
+ if (!columns) return () => true;
1227
+ if (typeof columns.keys !== "function")
1228
+ return (name) => columns.has(name) || columns.has(name.toLowerCase());
1229
+ const folded = /* @__PURE__ */ new Set();
1230
+ for (const key of columns.keys()) folded.add(key.toLowerCase());
1231
+ return (name) => folded.has(name.toLowerCase());
1232
+ }
1233
+ function isMethodName(tokens, i) {
1234
+ if (tokens[i + 1]?.text !== "(") return false;
1235
+ let depth = 0;
1236
+ for (let j = i + 1; j < tokens.length; j++) {
1237
+ const text = tokens[j].text;
1238
+ if (text === "(") depth++;
1239
+ else if (text === ")") {
1240
+ depth--;
1241
+ if (depth === 0) return tokens[j + 1]?.text === "{";
1242
+ }
1243
+ }
1244
+ return false;
1245
+ }
1246
+ function opensFunctionBody(tokens, i, matchingParenOpen) {
1247
+ const prev = tokens[i - 1];
1248
+ if (prev?.text === "=>") return true;
1249
+ if (prev?.text !== ")" || matchingParenOpen < 0) return false;
1250
+ const head = tokens[matchingParenOpen - 1];
1251
+ return head?.kind === "ident" && !CONTROL_KEYWORDS.has(head.text);
1252
+ }
1253
+ var BLOCK_STOPS = {
1254
+ params: /* @__PURE__ */ new Set(["schema", "canExecute", "onBeforeStart", "execute"]),
1255
+ canExecute: /* @__PURE__ */ new Set(["schema", "onBeforeStart", "execute"]),
1256
+ schema: /* @__PURE__ */ new Set(["params", "canExecute", "onBeforeStart", "execute"]),
1257
+ onBeforeStart: /* @__PURE__ */ new Set(["schema", "execute"]),
1258
+ execute: /* @__PURE__ */ new Set()
1259
+ };
1260
+ var FORMULA_ONLY = /* @__PURE__ */ new Map([
1261
+ ["TODAY", "now()"],
1262
+ ["NOW", "now()"],
1263
+ ["CURRENT_USER_ID", "currentUserId()"]
1264
+ ]);
1265
+ var KEYWORDS_BEFORE_PAREN = /* @__PURE__ */ new Set([
1266
+ "function",
1267
+ "return",
1268
+ "typeof",
1269
+ "instanceof",
1270
+ "new",
1271
+ "delete",
1272
+ "void",
1273
+ "in",
1274
+ "of",
1275
+ "do",
1276
+ "else",
1277
+ "try",
1278
+ "throw",
1279
+ "yield",
1280
+ "await"
1281
+ ]);
1282
+ var JS_GLOBALS = /* @__PURE__ */ new Set([
1283
+ "parseInt",
1284
+ "parseFloat",
1285
+ "isNaN",
1286
+ "isFinite",
1287
+ "encodeURI",
1288
+ "decodeURI",
1289
+ "encodeURIComponent",
1290
+ "decodeURIComponent",
1291
+ "String",
1292
+ "Number",
1293
+ "Boolean",
1294
+ "Array",
1295
+ "Object",
1296
+ "Date",
1297
+ "Math",
1298
+ "JSON",
1299
+ "RegExp",
1300
+ "Error",
1301
+ "Set",
1302
+ "Map",
1303
+ "WeakSet",
1304
+ "WeakMap",
1305
+ "Promise",
1306
+ "Symbol",
1307
+ "BigInt"
1308
+ ]);
1309
+ var LITERAL_NAMES = /* @__PURE__ */ new Set([
1310
+ "true",
1311
+ "false",
1312
+ "null",
1313
+ "TRUE",
1314
+ "FALSE",
1315
+ "NULL"
1316
+ ]);
1317
+ function stripSqlNoise(sql) {
1318
+ let out = "";
1319
+ let i = 0;
1320
+ while (i < sql.length) {
1321
+ const c = sql[i];
1322
+ if (c === "'" || c === '"') {
1323
+ const quote = c;
1324
+ out += " ";
1325
+ i++;
1326
+ while (i < sql.length) {
1327
+ if (sql[i] === quote && sql[i + 1] === quote) {
1328
+ out += " ";
1329
+ i += 2;
1330
+ continue;
1331
+ }
1332
+ if (sql[i] === quote) break;
1333
+ out += sql[i] === "\n" ? "\n" : " ";
1334
+ i++;
1335
+ }
1336
+ if (i < sql.length) {
1337
+ out += " ";
1338
+ i++;
1339
+ }
1340
+ continue;
1341
+ }
1342
+ if (c === "-" && sql[i + 1] === "-") {
1343
+ while (i < sql.length && sql[i] !== "\n") {
1344
+ out += " ";
1345
+ i++;
1346
+ }
1347
+ continue;
1348
+ }
1349
+ if (c === "/" && sql[i + 1] === "*") {
1350
+ out += " ";
1351
+ i += 2;
1352
+ while (i < sql.length && !(sql[i] === "*" && sql[i + 1] === "/")) {
1353
+ out += sql[i] === "\n" ? "\n" : " ";
1354
+ i++;
1355
+ }
1356
+ if (i < sql.length) {
1357
+ out += " ";
1358
+ i += 2;
1359
+ }
1360
+ continue;
1361
+ }
1362
+ out += c;
1363
+ i++;
1364
+ }
1365
+ return out;
1366
+ }
1367
+ function blockOf(parsed, offset) {
1368
+ return parsed.blocks.find((b) => offset >= b.body.start && offset < b.body.end)?.kind ?? null;
1369
+ }
1370
+ function inlineAssignmentIssues(parsed, positionAt) {
1371
+ const out = [];
1372
+ for (const ref of parsed.fieldRefs) {
1373
+ const assign = parsed.tokens[ref.endIndex + 1];
1374
+ if (!assign || assign.kind !== "punct" || assign.text !== "=") continue;
1375
+ const line = parsed.tokens[ref.startIndex].line;
1376
+ let openedHere = false;
1377
+ for (let i = ref.startIndex - 1; i >= 0; i--) {
1378
+ const t = parsed.tokens[i];
1379
+ if (t.line !== line) break;
1380
+ if (t.text === "{") {
1381
+ openedHere = true;
1382
+ break;
1383
+ }
1384
+ }
1385
+ if (!openedHere) continue;
1386
+ const pos = positionAt(ref.outerSpan.start);
1387
+ out.push({
1388
+ rule: "dsl/inline-assignment",
1389
+ severity: "warning",
1390
+ message: "An assignment to a record field on the same line as the brace that opens its block mis-compiles. Put the assignment on its own line.",
1391
+ start: ref.outerSpan.start,
1392
+ end: ref.outerSpan.end,
1393
+ line: pos.line,
1394
+ column: pos.column
1395
+ });
1396
+ }
1397
+ return out;
1398
+ }
1399
+ function lineIndexer(text) {
1400
+ const starts = [0];
1401
+ for (let i = 0; i < text.length; i++) {
1402
+ if (text[i] === "\n") starts.push(i + 1);
1403
+ }
1404
+ return (offset) => {
1405
+ let lo = 0;
1406
+ let hi = starts.length - 1;
1407
+ while (lo < hi) {
1408
+ const mid = lo + hi + 1 >> 1;
1409
+ if (starts[mid] <= offset) lo = mid;
1410
+ else hi = mid - 1;
1411
+ }
1412
+ return { line: lo + 1, column: offset - starts[lo] + 1 };
1413
+ };
1414
+ }
1415
+ export {
1416
+ BLOCK_KINDS,
1417
+ BUILTINS,
1418
+ BUILTIN_BY_NAME,
1419
+ BUILTIN_VALUES,
1420
+ checkDsl,
1421
+ parseDsl,
1422
+ stringValue,
1423
+ templateChunkValue,
1424
+ tokenize
1425
+ };
1426
+ //# sourceMappingURL=index.js.map