@xemahq/dsl 0.8.0 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/payload-codec/lib/blob-store.js +0 -0
  2. package/dist/payload-codec/lib/blob-store.js.map +1 -1
  3. package/package.json +3 -4
  4. package/src/deliverable-spec/index.ts +0 -19
  5. package/src/deliverable-spec/lib/schema.ts +0 -270
  6. package/src/deliverable-spec/lib/types.ts +0 -26
  7. package/src/payload-codec/index.ts +0 -44
  8. package/src/payload-codec/lib/blob-store.ts +0 -0
  9. package/src/payload-codec/lib/codec-context.ts +0 -38
  10. package/src/payload-codec/lib/codec.ts +0 -605
  11. package/src/payload-codec/lib/enums.ts +0 -58
  12. package/src/payload-codec/lib/errors.ts +0 -54
  13. package/src/payload-codec/lib/http-blob-store.ts +0 -267
  14. package/src/payload-codec/lib/lru-cache.ts +0 -81
  15. package/src/payload-codec/lib/payload.ts +0 -26
  16. package/src/payload-codec/temporal/index.ts +0 -36
  17. package/src/workflow/index.ts +0 -108
  18. package/src/workflow/lib/action-input-validator.ts +0 -160
  19. package/src/workflow/lib/compiler/action-shape.ts +0 -71
  20. package/src/workflow/lib/compiler/canonical-json.ts +0 -66
  21. package/src/workflow/lib/compiler/compile.ts +0 -1742
  22. package/src/workflow/lib/compiler/concurrency.ts +0 -223
  23. package/src/workflow/lib/compiler/dag.ts +0 -108
  24. package/src/workflow/lib/compiler/gate-defaults.ts +0 -153
  25. package/src/workflow/lib/compiler/index.ts +0 -11
  26. package/src/workflow/lib/compiler/inputs.ts +0 -254
  27. package/src/workflow/lib/compiler/installation-resource-validator.ts +0 -114
  28. package/src/workflow/lib/compiler/manifest-source.ts +0 -71
  29. package/src/workflow/lib/compiler/matrix.ts +0 -135
  30. package/src/workflow/lib/compiler/mount-plan.ts +0 -190
  31. package/src/workflow/lib/compiler/payload-reach-in.ts +0 -497
  32. package/src/workflow/lib/compiler/permissions.ts +0 -64
  33. package/src/workflow/lib/compiler/retry-timeout.ts +0 -105
  34. package/src/workflow/lib/compiler/review-step.ts +0 -548
  35. package/src/workflow/lib/compiler/types.ts +0 -172
  36. package/src/workflow/lib/compiler/variable-requirements.ts +0 -208
  37. package/src/workflow/lib/deliverable-spec-introspection-error.ts +0 -63
  38. package/src/workflow/lib/deliverable-spec-keys.ts +0 -147
  39. package/src/workflow/lib/deliverable-spec-source-scan.ts +0 -280
  40. package/src/workflow/lib/dispatch-inputs/index.ts +0 -160
  41. package/src/workflow/lib/dispatch-inputs/to-json-schema.ts +0 -60
  42. package/src/workflow/lib/duration.ts +0 -43
  43. package/src/workflow/lib/errors.ts +0 -37
  44. package/src/workflow/lib/expression/ast.ts +0 -108
  45. package/src/workflow/lib/expression/context.ts +0 -148
  46. package/src/workflow/lib/expression/evaluator.ts +0 -492
  47. package/src/workflow/lib/expression/index.ts +0 -28
  48. package/src/workflow/lib/expression/interpolation.ts +0 -84
  49. package/src/workflow/lib/expression/parser.ts +0 -264
  50. package/src/workflow/lib/expression/template.ts +0 -117
  51. package/src/workflow/lib/expression/tokenizer.ts +0 -200
  52. package/src/workflow/lib/expression/tokens.ts +0 -30
  53. package/src/workflow/lib/expression/walk-artifact-refs.ts +0 -232
  54. package/src/workflow/lib/installation-resource-kind.ts +0 -107
  55. package/src/workflow/lib/schemas-loader.ts +0 -64
  56. package/src/workflow/lib/serializer.ts +0 -30
  57. package/src/workflow/lib/types.ts +0 -417
  58. package/src/workflow/lib/validate.ts +0 -199
  59. package/src/workspace-manifest/index.ts +0 -27
  60. package/src/workspace-manifest/lib/compile.ts +0 -619
  61. package/src/workspace-manifest/lib/interpolate.ts +0 -166
  62. package/src/workspace-manifest/lib/resolve-extends.ts +0 -260
  63. package/src/workspace-manifest/lib/schema.ts +0 -692
  64. package/src/workspace-manifest/lib/types.ts +0 -446
@@ -1,84 +0,0 @@
1
- import { WorkflowErrorCode } from '@xemahq/kernel-contracts/workflow';
2
- import { WorkflowDslError } from '../errors';
3
-
4
- /**
5
- * Authored expressions in YAML are wrapped as `${{ ... }}`. These helpers
6
- * strip the wrapping deterministically and detect partial interpolation.
7
- *
8
- * Design choice: partial interpolation (`hello ${{ inputs.name }}!`) is
9
- * NOT supported in v1. Every authored expression must be the whole value
10
- * of its field. String templating introduces ambiguity around type
11
- * coercion — we keep the surface small and composable instead.
12
- */
13
- const INTERPOLATION_RE = /^\s*\$\{\{\s*(?<body>[\s\S]+?)\s*\}\}\s*$/;
14
- export const ANY_INTERPOLATION_RE = /\$\{\{/;
15
-
16
- /** True if the raw string is a single `${{ ... }}` expression (and only that). */
17
- export function isInterpolationWrapped(raw: string): boolean {
18
- return INTERPOLATION_RE.test(raw);
19
- }
20
-
21
- /**
22
- * Strip `${{ ... }}` wrapping. Throws DSL_EXPRESSION_INVALID if the raw
23
- * string is not fully wrapped (rejects partial interpolation).
24
- */
25
- export function stripInterpolation(raw: string): string {
26
- const match = INTERPOLATION_RE.exec(raw);
27
- if (!match) {
28
- throw new WorkflowDslError(
29
- WorkflowErrorCode.DSL_EXPRESSION_INVALID,
30
- `Expression must be fully wrapped in \${{ ... }}: ${raw}`,
31
- { raw },
32
- );
33
- }
34
- return match.groups?.['body'] ?? '';
35
- }
36
-
37
- /**
38
- * Walks an arbitrary value tree and returns every `${{ ... }}` source it
39
- * finds. Used by the compiler's validation pass: collect all expressions
40
- * in `with`, `outputs`, `if`, `strategy.dynamic.from`, etc., and compile
41
- * each one so authored expressions are caught at compile time.
42
- */
43
- export function extractInterpolations(node: unknown, path: string[] = []): ExtractedExpression[] {
44
- if (typeof node === 'string') {
45
- if (isInterpolationWrapped(node)) {
46
- return [{ path: [...path], source: stripInterpolation(node) }];
47
- }
48
- if (ANY_INTERPOLATION_RE.test(node)) {
49
- // Partial interpolation — reject fast so authors can't accidentally
50
- // rely on broken string templating.
51
- throw new WorkflowDslError(
52
- WorkflowErrorCode.DSL_EXPRESSION_INVALID,
53
- `Partial \${{ ... }} interpolation is not supported at ${pathLabel(path)}. Use a fully wrapped expression or plain text.`,
54
- { path },
55
- );
56
- }
57
- return [];
58
- }
59
- if (Array.isArray(node)) {
60
- const collected: ExtractedExpression[] = [];
61
- for (let i = 0; i < node.length; i++) {
62
- collected.push(...extractInterpolations(node[i], [...path, String(i)]));
63
- }
64
- return collected;
65
- }
66
- if (node !== null && typeof node === 'object') {
67
- const record = node as Record<string, unknown>;
68
- const collected: ExtractedExpression[] = [];
69
- for (const key of Object.keys(record)) {
70
- collected.push(...extractInterpolations(record[key], [...path, key]));
71
- }
72
- return collected;
73
- }
74
- return [];
75
- }
76
-
77
- export interface ExtractedExpression {
78
- readonly path: readonly string[];
79
- readonly source: string;
80
- }
81
-
82
- function pathLabel(path: readonly string[]): string {
83
- return path.length === 0 ? '<root>' : path.join('.');
84
- }
@@ -1,264 +0,0 @@
1
- import { WorkflowErrorCode } from '@xemahq/kernel-contracts/workflow';
2
- import { WorkflowDslError } from '../errors';
3
- import {
4
- type BinaryNode,
5
- type ExpressionNode,
6
- ExpressionFunction,
7
- ExpressionNodeKind,
8
- ExpressionRoot,
9
- type IdentifierNode,
10
- type IndexNode,
11
- type LiteralNode,
12
- type MemberNode,
13
- } from './ast';
14
- import { type Token, TokenKind } from './tokens';
15
-
16
- /**
17
- * Recursive-descent parser for the Xema expression subset. Grammar is a
18
- * closed subset inspired by GitHub Actions' expression language:
19
- *
20
- * expression := logicalOr
21
- * logicalOr := logicalAnd ('||' logicalAnd)*
22
- * logicalAnd := equality ('&&' equality)*
23
- * equality := unary (('==' | '!=') unary)*
24
- * unary := '!' unary | postfix
25
- * postfix := primary ('.' IDENT | '[' expression ']')*
26
- * primary := '(' expression ')'
27
- * | literal
28
- * | identifierOrCall
29
- * identifierOrCall := IDENT ('(' argList? ')')?
30
- * argList := expression (',' expression)*
31
- *
32
- * Root identifiers must be one of {needs, matrix, inputs, vars, trigger, job}.
33
- * Call callees must be one of {fromJSON, toJSON}.
34
- */
35
- export function parseExpression(source: string, tokens: readonly Token[]): ExpressionNode {
36
- const parser = new Parser(source, tokens);
37
- const node = parser.parseExpression();
38
- parser.expect(TokenKind.EOF);
39
- return node;
40
- }
41
-
42
- class Parser {
43
- private index = 0;
44
-
45
- constructor(
46
- private readonly source: string,
47
- private readonly tokens: readonly Token[],
48
- ) {}
49
-
50
- parseExpression(): ExpressionNode {
51
- return this.parseOr();
52
- }
53
-
54
- expect(kind: TokenKind): Token {
55
- const token = this.peek();
56
- if (token.kind !== kind) {
57
- throw this.error(
58
- `Expected ${kind} but found ${token.kind === TokenKind.EOF ? 'end of expression' : token.kind}`,
59
- token.column,
60
- );
61
- }
62
- this.index++;
63
- return token;
64
- }
65
-
66
- private peek(): Token {
67
- const token = this.tokens[this.index];
68
- if (!token) {
69
- throw this.error('Parser consumed past end of token stream', this.source.length + 1);
70
- }
71
- return token;
72
- }
73
-
74
- private consume(): Token {
75
- const token = this.peek();
76
- this.index++;
77
- return token;
78
- }
79
-
80
- private match(kind: TokenKind): boolean {
81
- return this.peek().kind === kind;
82
- }
83
-
84
- private parseOr(): ExpressionNode {
85
- let left = this.parseAnd();
86
- while (this.match(TokenKind.OR)) {
87
- this.consume();
88
- const right = this.parseAnd();
89
- left = binary(ExpressionNodeKind.BINARY_OR, left, right);
90
- }
91
- return left;
92
- }
93
-
94
- private parseAnd(): ExpressionNode {
95
- let left = this.parseEquality();
96
- while (this.match(TokenKind.AND)) {
97
- this.consume();
98
- const right = this.parseEquality();
99
- left = binary(ExpressionNodeKind.BINARY_AND, left, right);
100
- }
101
- return left;
102
- }
103
-
104
- private parseEquality(): ExpressionNode {
105
- let left = this.parseUnary();
106
- for (;;) {
107
- if (this.match(TokenKind.EQ)) {
108
- this.consume();
109
- const right = this.parseUnary();
110
- left = binary(ExpressionNodeKind.BINARY_EQ, left, right);
111
- } else if (this.match(TokenKind.NEQ)) {
112
- this.consume();
113
- const right = this.parseUnary();
114
- left = binary(ExpressionNodeKind.BINARY_NEQ, left, right);
115
- } else {
116
- break;
117
- }
118
- }
119
- return left;
120
- }
121
-
122
- private parseUnary(): ExpressionNode {
123
- if (this.match(TokenKind.BANG)) {
124
- this.consume();
125
- const operand = this.parseUnary();
126
- return { kind: ExpressionNodeKind.UNARY_NOT, operand };
127
- }
128
- return this.parsePostfix();
129
- }
130
-
131
- private parsePostfix(): ExpressionNode {
132
- let node = this.parsePrimary();
133
- for (;;) {
134
- if (this.match(TokenKind.DOT)) {
135
- this.consume();
136
- const propertyToken = this.expect(TokenKind.IDENTIFIER);
137
- const member: MemberNode = {
138
- kind: ExpressionNodeKind.MEMBER,
139
- target: node,
140
- property: propertyToken.value,
141
- };
142
- node = member;
143
- } else if (this.match(TokenKind.LBRACKET)) {
144
- this.consume();
145
- const indexExpr = this.parseExpression();
146
- this.expect(TokenKind.RBRACKET);
147
- const indexNode: IndexNode = {
148
- kind: ExpressionNodeKind.INDEX,
149
- target: node,
150
- index: indexExpr,
151
- };
152
- node = indexNode;
153
- } else {
154
- break;
155
- }
156
- }
157
- return node;
158
- }
159
-
160
- private parsePrimary(): ExpressionNode {
161
- const token = this.peek();
162
- switch (token.kind) {
163
- case TokenKind.LPAREN: {
164
- this.consume();
165
- const expr = this.parseExpression();
166
- this.expect(TokenKind.RPAREN);
167
- return expr;
168
- }
169
- case TokenKind.NUMBER: {
170
- this.consume();
171
- const literal: LiteralNode = { kind: ExpressionNodeKind.LITERAL, value: Number(token.value) };
172
- return literal;
173
- }
174
- case TokenKind.STRING: {
175
- this.consume();
176
- const literal: LiteralNode = { kind: ExpressionNodeKind.LITERAL, value: token.value };
177
- return literal;
178
- }
179
- case TokenKind.BOOLEAN: {
180
- this.consume();
181
- const literal: LiteralNode = { kind: ExpressionNodeKind.LITERAL, value: token.value === 'true' };
182
- return literal;
183
- }
184
- case TokenKind.NULL: {
185
- this.consume();
186
- const literal: LiteralNode = { kind: ExpressionNodeKind.LITERAL, value: null };
187
- return literal;
188
- }
189
- case TokenKind.IDENTIFIER: {
190
- return this.parseIdentifierOrCall();
191
- }
192
- default:
193
- throw this.error(
194
- `Unexpected token '${token.value}' at column ${token.column}`,
195
- token.column,
196
- );
197
- }
198
- }
199
-
200
- private parseIdentifierOrCall(): ExpressionNode {
201
- const identifierToken = this.expect(TokenKind.IDENTIFIER);
202
- const name = identifierToken.value;
203
-
204
- if (this.match(TokenKind.LPAREN)) {
205
- this.consume();
206
- if (!isExpressionFunction(name)) {
207
- throw this.error(
208
- `Unknown function '${name}' at column ${identifierToken.column}. Allowed: ${Object.values(ExpressionFunction).join(', ')}.`,
209
- identifierToken.column,
210
- );
211
- }
212
- const args: ExpressionNode[] = [];
213
- if (!this.match(TokenKind.RPAREN)) {
214
- args.push(this.parseExpression());
215
- while (this.match(TokenKind.COMMA)) {
216
- this.consume();
217
- args.push(this.parseExpression());
218
- }
219
- }
220
- this.expect(TokenKind.RPAREN);
221
- return {
222
- kind: ExpressionNodeKind.CALL,
223
- callee: name,
224
- args,
225
- };
226
- }
227
-
228
- // Bare identifier: must be one of the allowed roots.
229
- if (!isExpressionRoot(name)) {
230
- throw this.error(
231
- `Unknown identifier '${name}' at column ${identifierToken.column}. Allowed roots: ${Object.values(ExpressionRoot).join(', ')}.`,
232
- identifierToken.column,
233
- );
234
- }
235
- const ident: IdentifierNode = { kind: ExpressionNodeKind.IDENTIFIER, name };
236
- return ident;
237
- }
238
-
239
- private error(message: string, column: number): WorkflowDslError {
240
- return new WorkflowDslError(
241
- WorkflowErrorCode.DSL_EXPRESSION_INVALID,
242
- `${message} [expression: ${this.source}]`,
243
- { expression: this.source, column },
244
- );
245
- }
246
- }
247
-
248
- function binary(
249
- kind: BinaryNode['kind'],
250
- left: ExpressionNode,
251
- right: ExpressionNode,
252
- ): BinaryNode {
253
- return { kind, left, right };
254
- }
255
-
256
- function isExpressionRoot(name: string): name is ExpressionRoot {
257
- const roots: readonly string[] = Object.values(ExpressionRoot);
258
- return roots.includes(name);
259
- }
260
-
261
- function isExpressionFunction(name: string): name is ExpressionFunction {
262
- const fns: readonly string[] = Object.values(ExpressionFunction);
263
- return fns.includes(name);
264
- }
@@ -1,117 +0,0 @@
1
- import { WorkflowErrorCode } from '@xemahq/kernel-contracts/workflow';
2
- import { WorkflowDslError } from '../errors';
3
-
4
- /**
5
- * Template parsing for fields that explicitly accept partial interpolation
6
- * (`literal-text-${{ expression }}-more-text`). Most authored expressions in
7
- * the DSL must be fully wrapped — see `interpolation.ts` and the policy on
8
- * `with`, `outputs`, `if`, `strategy.dynamic.from`. Concurrency `group` is
9
- * the documented exception, so it composes from this primitive instead.
10
- *
11
- * The parser is deterministic and total: every input string maps to one
12
- * sequence of segments. Authoring errors (an open `${{` without a matching
13
- * `}}`, an empty body) raise `DSL_EXPRESSION_INVALID` at the position they
14
- * occur — never at <root>, never silently coerced.
15
- */
16
- export enum TemplateSegmentKind {
17
- Literal = 'literal',
18
- Expression = 'expression',
19
- }
20
-
21
- export interface TemplateLiteralSegment {
22
- readonly kind: TemplateSegmentKind.Literal;
23
- readonly value: string;
24
- }
25
-
26
- export interface TemplateExpressionSegment {
27
- readonly kind: TemplateSegmentKind.Expression;
28
- readonly source: string;
29
- /** Byte offset of the opening `${{` within the input source. */
30
- readonly start: number;
31
- /** Byte offset just past the closing `}}` within the input source. */
32
- readonly end: number;
33
- }
34
-
35
- export type TemplateSegment = TemplateLiteralSegment | TemplateExpressionSegment;
36
-
37
- const OPEN_DELIM = '${{';
38
- const CLOSE_DELIM = '}}';
39
-
40
- /**
41
- * Parse a string into a sequence of literal + expression segments. Pure
42
- * literals (no interpolation) return a single literal segment; an empty
43
- * input returns no segments. Unbalanced delimiters fail fast with a typed
44
- * `WorkflowDslError`.
45
- */
46
- export function parseTemplate(
47
- source: string,
48
- authoredPath: readonly string[] = [],
49
- ): readonly TemplateSegment[] {
50
- const segments: TemplateSegment[] = [];
51
- let cursor = 0;
52
-
53
- while (cursor < source.length) {
54
- const openIdx = source.indexOf(OPEN_DELIM, cursor);
55
- if (openIdx === -1) {
56
- segments.push({
57
- kind: TemplateSegmentKind.Literal,
58
- value: source.slice(cursor),
59
- });
60
- return segments;
61
- }
62
-
63
- if (openIdx > cursor) {
64
- segments.push({
65
- kind: TemplateSegmentKind.Literal,
66
- value: source.slice(cursor, openIdx),
67
- });
68
- }
69
-
70
- const bodyStart = openIdx + OPEN_DELIM.length;
71
- const closeIdx = source.indexOf(CLOSE_DELIM, bodyStart);
72
- if (closeIdx === -1) {
73
- throw new WorkflowDslError(
74
- WorkflowErrorCode.DSL_EXPRESSION_INVALID,
75
- `Unterminated template interpolation at ${pathLabel(authoredPath)} (offset ${openIdx}): missing closing '}}'.`,
76
- { authoredPath: [...authoredPath], offset: openIdx },
77
- );
78
- }
79
-
80
- const rawBody = source.slice(bodyStart, closeIdx);
81
- const trimmed = rawBody.trim();
82
- if (trimmed.length === 0) {
83
- throw new WorkflowDslError(
84
- WorkflowErrorCode.DSL_EXPRESSION_INVALID,
85
- `Empty template interpolation at ${pathLabel(authoredPath)} (offset ${openIdx}): '\${{ }}' must contain an expression.`,
86
- { authoredPath: [...authoredPath], offset: openIdx },
87
- );
88
- }
89
-
90
- segments.push({
91
- kind: TemplateSegmentKind.Expression,
92
- source: trimmed,
93
- start: openIdx,
94
- end: closeIdx + CLOSE_DELIM.length,
95
- });
96
-
97
- cursor = closeIdx + CLOSE_DELIM.length;
98
- }
99
-
100
- return segments;
101
- }
102
-
103
- /**
104
- * True iff the parsed template has at least one expression segment. Cheap
105
- * shortcut for callers that need to distinguish "static literal" from
106
- * "needs evaluation" without re-parsing.
107
- */
108
- export function templateHasExpressions(segments: readonly TemplateSegment[]): boolean {
109
- for (const segment of segments) {
110
- if (segment.kind === TemplateSegmentKind.Expression) return true;
111
- }
112
- return false;
113
- }
114
-
115
- function pathLabel(path: readonly string[]): string {
116
- return path.length === 0 ? '<root>' : path.join('.');
117
- }
@@ -1,200 +0,0 @@
1
- import { WorkflowErrorCode } from '@xemahq/kernel-contracts/workflow';
2
- import { WorkflowDslError } from '../errors';
3
- import { type Token, TokenKind } from './tokens';
4
-
5
- /**
6
- * Hand-rolled tokenizer for the expression subset. Deterministic: same
7
- * input always produces the same token stream. Fails fast on any character
8
- * outside the grammar.
9
- *
10
- * Grammar (surface):
11
- * identifiers: [A-Za-z_][A-Za-z0-9_-]*
12
- * numbers: [0-9]+(\.[0-9]+)?
13
- * strings: '...' (single-quoted; no escapes — keep grammar simple
14
- * and deterministic. If escapes are needed later, add once
15
- * with full test coverage; do not retrofit ad-hoc.)
16
- * operators: == != && || ! ( ) [ ] , .
17
- * booleans: true | false
18
- * null: null
19
- * whitespace: spaces and tabs (ignored)
20
- */
21
- export function tokenize(source: string): readonly Token[] {
22
- const tokens: Token[] = [];
23
- const length = source.length;
24
- let index = 0;
25
-
26
- while (index < length) {
27
- const ch = source.charAt(index);
28
-
29
- if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') {
30
- index++;
31
- continue;
32
- }
33
-
34
- const column = index + 1;
35
-
36
- if (ch === '(') {
37
- tokens.push({ kind: TokenKind.LPAREN, value: '(', column });
38
- index++;
39
- continue;
40
- }
41
- if (ch === ')') {
42
- tokens.push({ kind: TokenKind.RPAREN, value: ')', column });
43
- index++;
44
- continue;
45
- }
46
- if (ch === '[') {
47
- tokens.push({ kind: TokenKind.LBRACKET, value: '[', column });
48
- index++;
49
- continue;
50
- }
51
- if (ch === ']') {
52
- tokens.push({ kind: TokenKind.RBRACKET, value: ']', column });
53
- index++;
54
- continue;
55
- }
56
- if (ch === ',') {
57
- tokens.push({ kind: TokenKind.COMMA, value: ',', column });
58
- index++;
59
- continue;
60
- }
61
- if (ch === '.') {
62
- tokens.push({ kind: TokenKind.DOT, value: '.', column });
63
- index++;
64
- continue;
65
- }
66
-
67
- if (ch === '=' && source.charAt(index + 1) === '=') {
68
- tokens.push({ kind: TokenKind.EQ, value: '==', column });
69
- index += 2;
70
- continue;
71
- }
72
-
73
- if (ch === '!' && source.charAt(index + 1) === '=') {
74
- tokens.push({ kind: TokenKind.NEQ, value: '!=', column });
75
- index += 2;
76
- continue;
77
- }
78
-
79
- if (ch === '!') {
80
- tokens.push({ kind: TokenKind.BANG, value: '!', column });
81
- index++;
82
- continue;
83
- }
84
-
85
- if (ch === '&' && source.charAt(index + 1) === '&') {
86
- tokens.push({ kind: TokenKind.AND, value: '&&', column });
87
- index += 2;
88
- continue;
89
- }
90
-
91
- if (ch === '|' && source.charAt(index + 1) === '|') {
92
- tokens.push({ kind: TokenKind.OR, value: '||', column });
93
- index += 2;
94
- continue;
95
- }
96
-
97
- if (ch === "'") {
98
- const { value, next } = readString(source, index);
99
- tokens.push({ kind: TokenKind.STRING, value, column });
100
- index = next;
101
- continue;
102
- }
103
-
104
- if (isDigit(ch)) {
105
- const { value, next } = readNumber(source, index);
106
- tokens.push({ kind: TokenKind.NUMBER, value, column });
107
- index = next;
108
- continue;
109
- }
110
-
111
- if (isIdentStart(ch)) {
112
- const { value, next } = readIdentifier(source, index);
113
- if (value === 'true' || value === 'false') {
114
- tokens.push({ kind: TokenKind.BOOLEAN, value, column });
115
- } else if (value === 'null') {
116
- tokens.push({ kind: TokenKind.NULL, value, column });
117
- } else {
118
- tokens.push({ kind: TokenKind.IDENTIFIER, value, column });
119
- }
120
- index = next;
121
- continue;
122
- }
123
-
124
- throw new WorkflowDslError(
125
- WorkflowErrorCode.DSL_EXPRESSION_INVALID,
126
- `Unexpected character '${ch}' at column ${column}`,
127
- { source, column },
128
- );
129
- }
130
-
131
- tokens.push({ kind: TokenKind.EOF, value: '', column: length + 1 });
132
- return tokens;
133
- }
134
-
135
- function readString(source: string, start: number): { value: string; next: number } {
136
- const length = source.length;
137
- let end = start + 1;
138
- while (end < length && source.charAt(end) !== "'") {
139
- if (source.charAt(end) === '\\') {
140
- throw new WorkflowDslError(
141
- WorkflowErrorCode.DSL_EXPRESSION_INVALID,
142
- `String escapes are not supported in expression literals (column ${end + 1}).`,
143
- { source, column: end + 1 },
144
- );
145
- }
146
- end++;
147
- }
148
- if (end >= length) {
149
- throw new WorkflowDslError(
150
- WorkflowErrorCode.DSL_EXPRESSION_INVALID,
151
- `Unterminated string literal starting at column ${start + 1}`,
152
- { source, column: start + 1 },
153
- );
154
- }
155
- return { value: source.slice(start + 1, end), next: end + 1 };
156
- }
157
-
158
- function readNumber(source: string, start: number): { value: string; next: number } {
159
- const length = source.length;
160
- let end = start;
161
- while (end < length && isDigit(source.charAt(end))) {
162
- end++;
163
- }
164
- if (end < length && source.charAt(end) === '.') {
165
- end++;
166
- const fractionStart = end;
167
- while (end < length && isDigit(source.charAt(end))) {
168
- end++;
169
- }
170
- if (end === fractionStart) {
171
- throw new WorkflowDslError(
172
- WorkflowErrorCode.DSL_EXPRESSION_INVALID,
173
- `Number literal with trailing dot at column ${start + 1}`,
174
- { source, column: start + 1 },
175
- );
176
- }
177
- }
178
- return { value: source.slice(start, end), next: end };
179
- }
180
-
181
- function readIdentifier(source: string, start: number): { value: string; next: number } {
182
- const length = source.length;
183
- let end = start;
184
- while (end < length && isIdentPart(source.charAt(end))) {
185
- end++;
186
- }
187
- return { value: source.slice(start, end), next: end };
188
- }
189
-
190
- function isDigit(ch: string): boolean {
191
- return ch >= '0' && ch <= '9';
192
- }
193
-
194
- function isIdentStart(ch: string): boolean {
195
- return (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || ch === '_';
196
- }
197
-
198
- function isIdentPart(ch: string): boolean {
199
- return isIdentStart(ch) || isDigit(ch) || ch === '-';
200
- }
@@ -1,30 +0,0 @@
1
- /**
2
- * Token kinds for the Xema expression subset. Closed set; every token
3
- * produced by the tokenizer picks one of these. Unknown input is rejected.
4
- */
5
- export enum TokenKind {
6
- IDENTIFIER = 'identifier',
7
- NUMBER = 'number',
8
- STRING = 'string',
9
- BOOLEAN = 'boolean',
10
- NULL = 'null',
11
- LPAREN = '(',
12
- RPAREN = ')',
13
- LBRACKET = '[',
14
- RBRACKET = ']',
15
- COMMA = ',',
16
- DOT = '.',
17
- BANG = '!',
18
- EQ = '==',
19
- NEQ = '!=',
20
- AND = '&&',
21
- OR = '||',
22
- EOF = 'eof',
23
- }
24
-
25
- export interface Token {
26
- readonly kind: TokenKind;
27
- readonly value: string;
28
- /** 1-based column in the source expression for diagnostic output. */
29
- readonly column: number;
30
- }