@o-lang/olang 1.2.38 → 1.4.0-alpha.1

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.
@@ -8,8 +8,6 @@ function normalizeSymbol(raw) {
8
8
  return raw.split(/\s+/)[0].replace(/[^\w$]/g, '');
9
9
  }
10
10
 
11
- // ❌ REMOVED: normalizeAction() function (was stripping "Action" prefix → broke resolver matching)
12
-
13
11
  function parse(content, filename = '<unknown>') {
14
12
  if (typeof content === 'string') {
15
13
  // ✅ Strip UTF-8 BOM if present (0xFEFF = Unicode BOM)
@@ -39,6 +37,10 @@ function parseLines(lines, filename) {
39
37
  return parseWorkflowLines(lines, filename);
40
38
  }
41
39
 
40
+ // ─────────────────────────────────────────────────────────────────────────────
41
+ // CORE WORKFLOW PARSER
42
+ // ─────────────────────────────────────────────────────────────────────────────
43
+
42
44
  function parseWorkflowLines(lines, filename) {
43
45
  const workflow = {
44
46
  type: 'workflow',
@@ -55,17 +57,26 @@ function parseWorkflowLines(lines, filename) {
55
57
  let i = 0;
56
58
  let currentStep = null;
57
59
  let inAllowResolvers = false;
60
+
61
+ // If/Else State Machine Variables (FIXED: Separate accumulators)
58
62
  let inIfBlock = false;
59
63
  let ifCondition = null;
60
- let ifBody = [];
64
+ let mainIfBody = []; // Accumulates main if branch lines
65
+ let currentBranchBody = []; // Accumulates else-if or else branches
66
+ let elseIfChain = [];
67
+ let currentElseIf = null;
68
+ let inElseBlock = false;
69
+
70
+ // Parallel State
61
71
  let inParallelBlock = false;
62
72
  let parallelSteps = [];
63
73
  let parallelTimeout = null;
74
+
75
+ // Escalation State
64
76
  let inEscalationBlock = false;
65
77
  let escalationLevels = [];
66
78
  let currentLevel = null;
67
79
 
68
- // ✅ Helper to flush currentStep
69
80
  const flushCurrentStep = () => {
70
81
  if (currentStep) {
71
82
  workflow.steps.push(currentStep);
@@ -75,12 +86,9 @@ function parseWorkflowLines(lines, filename) {
75
86
 
76
87
  while (i < lines.length) {
77
88
  let line = lines[i++].trim();
89
+ if (line === '' || line.startsWith('#')) continue;
78
90
 
79
- if (line === '' || line.startsWith('#')) {
80
- continue;
81
- }
82
-
83
- // Workflow declaration
91
+ // --- 1. Workflow Declaration ---
84
92
  if (line.startsWith('Workflow ')) {
85
93
  const match = line.match(/^Workflow\s+"([^"]+)"(?:\s+with\s+(.+))?$/i);
86
94
  if (match) {
@@ -94,18 +102,16 @@ function parseWorkflowLines(lines, filename) {
94
102
  continue;
95
103
  }
96
104
 
97
- // Global Constraint: max_generations
105
+ // --- 2. Global Constraints ---
98
106
  if (line.startsWith('Constraint: max_generations = ')) {
99
107
  const match = line.match(/^Constraint:\s+max_generations\s*=\s*(\d+)$/i);
100
108
  if (match) {
101
109
  workflow.maxGenerations = parseInt(match[1], 10);
102
- } else {
103
- workflow.__warnings.push(`Invalid Constraint syntax: ${line}`);
104
110
  }
105
111
  continue;
106
112
  }
107
113
 
108
- // Allow resolvers section
114
+ // --- 3. Allow Resolvers Section ---
109
115
  if (line === 'Allow resolvers:') {
110
116
  inAllowResolvers = true;
111
117
  continue;
@@ -121,13 +127,13 @@ function parseWorkflowLines(lines, filename) {
121
127
  continue;
122
128
  } else {
123
129
  inAllowResolvers = false;
124
- i--;
130
+ i--; // Re-process this line as normal step
125
131
  continue;
126
132
  }
127
133
  continue;
128
134
  }
129
135
 
130
- // Parse Escalation block
136
+ // --- 4. Block: Escalation ---
131
137
  if (line.match(/^Run in parallel with escalation:$/i)) {
132
138
  flushCurrentStep();
133
139
  inEscalationBlock = true;
@@ -138,9 +144,7 @@ function parseWorkflowLines(lines, filename) {
138
144
 
139
145
  if (inEscalationBlock) {
140
146
  if (line.match(/^End$/i)) {
141
- if (currentLevel) {
142
- escalationLevels.push(currentLevel);
143
- }
147
+ if (currentLevel) escalationLevels.push(currentLevel);
144
148
  workflow.steps.push({
145
149
  type: 'escalation',
146
150
  levels: escalationLevels,
@@ -149,14 +153,10 @@ function parseWorkflowLines(lines, filename) {
149
153
  inEscalationBlock = false;
150
154
  continue;
151
155
  } else if (line.match(/^Level \d+:/i)) {
152
- // Parse level declaration
153
156
  const levelMatch = line.match(/^Level (\d+):\s+(.+)$/i);
154
157
  if (levelMatch) {
155
- if (currentLevel) {
156
- escalationLevels.push(currentLevel);
157
- }
158
+ if (currentLevel) escalationLevels.push(currentLevel);
158
159
 
159
- // Parse timeout from level description
160
160
  let timeoutMs = null;
161
161
  const desc = levelMatch[2].trim().toLowerCase();
162
162
  if (desc.includes('immediately')) {
@@ -174,34 +174,30 @@ function parseWorkflowLines(lines, filename) {
174
174
  currentLevel = {
175
175
  levelNumber: parseInt(levelMatch[1]),
176
176
  timeout: timeoutMs,
177
- steps: []
177
+ steps: []
178
178
  };
179
179
  continue;
180
180
  }
181
181
  } else if (currentLevel) {
182
- // Parse steps within level
183
182
  currentLevel.steps.push(line);
184
183
  continue;
185
184
  }
186
185
  }
187
186
 
188
- // Parse Timed Parallel block (EXACT FORMAT - NO DUPLICATION)
187
+ // --- 5. Block: Parallel ---
189
188
  const timedParMatch = line.match(/^Run in parallel for (\d+)\s*([smhd])$/i);
190
189
  if (timedParMatch) {
191
190
  flushCurrentStep();
192
-
193
191
  const value = parseInt(timedParMatch[1]);
194
192
  const unit = timedParMatch[2].toLowerCase();
195
193
  const multipliers = { s: 1000, m: 60000, h: 3600000, d: 86400000 };
196
- const timeoutMs = value * (multipliers[unit] || 1000);
194
+ parallelTimeout = value * (multipliers[unit] || 1000);
197
195
 
198
196
  inParallelBlock = true;
199
197
  parallelSteps = [];
200
- parallelTimeout = timeoutMs;
201
198
  continue;
202
199
  }
203
200
 
204
- // ✅ Parse Normal Parallel block (backward compatible)
205
201
  if (line.match(/^Run in parallel$/i)) {
206
202
  flushCurrentStep();
207
203
  inParallelBlock = true;
@@ -212,7 +208,7 @@ function parseWorkflowLines(lines, filename) {
212
208
 
213
209
  if (inParallelBlock) {
214
210
  if (line.match(/^End$/i)) {
215
- flushCurrentStep(); // ✅ Flush last parallel step
211
+ flushCurrentStep();
216
212
  const parsedParallel = parseBlock(parallelSteps);
217
213
  workflow.steps.push({
218
214
  type: 'parallel',
@@ -229,96 +225,144 @@ function parseWorkflowLines(lines, filename) {
229
225
  }
230
226
  }
231
227
 
232
- // FLUSH before If/When block
228
+ // --- 6. Block: If / Else If / Else (FIXED STATE MACHINE) ---
229
+
230
+ // Start If
233
231
  if (line.match(/^(?:If|When)\s+(.+)$/i)) {
234
232
  flushCurrentStep();
235
233
  const ifMatch = line.match(/^(?:If|When)\s+(.+)$/i);
236
234
  ifCondition = ifMatch[1].trim();
237
235
  inIfBlock = true;
238
- ifBody = [];
236
+
237
+ // Reset accumulators
238
+ mainIfBody = [];
239
+ currentBranchBody = [];
240
+ elseIfChain = [];
241
+ currentElseIf = null;
242
+ inElseBlock = false;
239
243
  continue;
240
244
  }
241
245
 
242
246
  if (inIfBlock) {
247
+ // Handle Else If
248
+ if (line.match(/^Else If\s+(.+)$/i) || line.match(/^Elseif\s+(.+)$/i)) {
249
+ const eiMatch = line.match(/^(?:Else If|Elseif)\s+(.+)$/i);
250
+
251
+ // Save previous branch (either main if or previous else-if)
252
+ if (currentElseIf) {
253
+ currentElseIf.body = parseBlock(currentBranchBody);
254
+ elseIfChain.push(currentElseIf);
255
+ }
256
+
257
+ // Reset for new else-if branch
258
+ currentBranchBody = [];
259
+ currentElseIf = { condition: eiMatch[1].trim(), body: [] };
260
+ continue;
261
+ }
262
+
263
+ // Handle Else
264
+ if (line.match(/^Else$/i)) {
265
+ // Save previous else-if if it exists
266
+ if (currentElseIf) {
267
+ currentElseIf.body = parseBlock(currentBranchBody);
268
+ elseIfChain.push(currentElseIf);
269
+ currentElseIf = null;
270
+ }
271
+
272
+ inElseBlock = true;
273
+ currentBranchBody = [];
274
+ continue;
275
+ }
276
+
277
+ // Handle End
243
278
  if (line.match(/^End(?:If)?$/i)) {
244
- flushCurrentStep(); // ✅ Flush last if step
245
- const parsedIfBody = parseBlock(ifBody);
279
+ flushCurrentStep();
280
+
281
+ // Close any open else-if
282
+ if (currentElseIf) {
283
+ currentElseIf.body = parseBlock(currentBranchBody);
284
+ elseIfChain.push(currentElseIf);
285
+ currentElseIf = null;
286
+ }
287
+
246
288
  workflow.steps.push({
247
289
  type: 'if',
248
290
  condition: ifCondition,
249
- body: parsedIfBody,
291
+ body: parseBlock(mainIfBody), // Main body correctly isolated
292
+ elseIf: elseIfChain, // Else-if chain
293
+ elseBranch: inElseBlock ? parseBlock(currentBranchBody) : [], // Else body
250
294
  stepNumber: workflow.steps.length + 1
251
295
  });
296
+
297
+ // Reset State
252
298
  inIfBlock = false;
253
299
  ifCondition = null;
254
- ifBody = [];
300
+ mainIfBody = [];
301
+ currentBranchBody = [];
302
+ elseIfChain = [];
303
+ inElseBlock = false;
255
304
  continue;
305
+ }
306
+
307
+ // Accumulate lines into correct bucket
308
+ if (inElseBlock || currentElseIf) {
309
+ currentBranchBody.push(line);
256
310
  } else {
257
- ifBody.push(line);
258
- continue;
311
+ mainIfBody.push(line); // Main if body accumulates here
259
312
  }
313
+ continue;
314
+ }
315
+
316
+ // --- 7. Standard Steps & Keywords ---
317
+
318
+ // Connect: Connect "name" to url "..." OR Connect "name" to resolver "..."
319
+ const connectMatch = line.match(/^Connect\s+"([^"]+)"\s+to\s+(url|resolver)\s+"([^"]+)"$/i);
320
+ if (connectMatch) {
321
+ flushCurrentStep();
322
+ workflow.steps.push({
323
+ type: 'connect',
324
+ resource: connectMatch[1],
325
+ endpoint: connectMatch[3],
326
+ targetType: connectMatch[2].toLowerCase(),
327
+ stepNumber: workflow.steps.length + 1
328
+ });
329
+ continue;
330
+ }
331
+
332
+ // Use: Use "logicalName" as "resource"
333
+ const useMatch = line.match(/^Use\s+"([^"]+)"\s+as\s+"([^"]+)"$/i);
334
+ if (useMatch) {
335
+ flushCurrentStep();
336
+ workflow.steps.push({
337
+ type: 'agent_use',
338
+ logicalName: useMatch[1],
339
+ resource: useMatch[2],
340
+ stepNumber: workflow.steps.length + 1
341
+ });
342
+ continue;
260
343
  }
261
344
 
262
- // Step declaration - ✅ PRESERVE ACTION EXACTLY (NO NORMALIZATION)
345
+ // Step Declaration
263
346
  const stepMatch = line.match(/^Step\s+(\d+)\s*:\s*(.+)$/i);
264
347
  if (stepMatch) {
265
- flushCurrentStep(); // ✅ Flush previous step
266
- const stepNumber = parseInt(stepMatch[1], 10);
267
- const stepContent = stepMatch[2].trim(); // ← PRESERVED EXACTLY (no normalizeAction)
268
-
348
+ flushCurrentStep();
269
349
  currentStep = {
270
350
  type: 'action',
271
- stepNumber: stepNumber,
272
- actionRaw: stepContent, // ← CRITICAL: No normalization here
351
+ stepNumber: parseInt(stepMatch[1], 10),
352
+ actionRaw: stepMatch[2].trim(),
273
353
  saveAs: null,
274
354
  constraints: {}
275
355
  };
276
356
  continue;
277
357
  }
278
358
 
279
- // Save as - ✅ Apply normalization (safe for symbol names)
359
+ // Save As
280
360
  const saveMatch = line.match(/^Save as\s+(.+)$/i);
281
361
  if (saveMatch && currentStep) {
282
362
  currentStep.saveAs = normalizeSymbol(saveMatch[1].trim());
283
363
  continue;
284
364
  }
285
365
 
286
- // Constraint (per-step)
287
- const constraintMatch = line.match(/^Constraint:\s*(.+)$/i);
288
- if (constraintMatch && currentStep) {
289
- const constraintLine = constraintMatch[1].trim();
290
- const eq = constraintLine.match(/^([^=]+)=\s*(.+)$/);
291
- if (eq) {
292
- let key = eq[1].trim();
293
- let value = eq[2].trim();
294
-
295
- if (value.startsWith('[') && value.endsWith(']')) {
296
- value = value.slice(1, -1).split(',').map(v => v.trim().replace(/^"/, '').replace(/"$/, ''));
297
- } else if (!isNaN(value)) {
298
- value = Number(value);
299
- } else if (value.startsWith('"') && value.endsWith('"')) {
300
- value = value.slice(1, -1);
301
- }
302
-
303
- currentStep.constraints[key] = value;
304
- }
305
- continue;
306
- }
307
-
308
- // ✅ ADD: Set keyword (e.g., Set analysis_result = "")
309
- const setMatch = line.match(/^Set\s+(\w+)\s*=\s*(.+)$/i);
310
- if (setMatch) {
311
- flushCurrentStep();
312
- workflow.steps.push({
313
- type: 'calculate',
314
- stepNumber: workflow.steps.length + 1,
315
- actionRaw: setMatch[2].trim(), // e.g., '""' or '"N/A"'
316
- saveAs: setMatch[1].trim(), // e.g., 'analysis_result'
317
- constraints: {}
318
- });
319
- continue;
320
- }
321
-
322
366
  // Debrief
323
367
  const debriefMatch = line.match(/^Debrief\s+([^\s]+)\s+with\s+"([^"]*)"$/i);
324
368
  if (debriefMatch) {
@@ -332,19 +376,6 @@ function parseWorkflowLines(lines, filename) {
332
376
  continue;
333
377
  }
334
378
 
335
- // Evolve
336
- const evolveMatch = line.match(/^Evolve\s+([^\s]+)\s+using\s+feedback:\s*"([^"]*)"$/i);
337
- if (evolveMatch) {
338
- flushCurrentStep();
339
- workflow.steps.push({
340
- type: 'evolve',
341
- targetResolver: evolveMatch[1].trim(),
342
- feedback: evolveMatch[2],
343
- stepNumber: workflow.steps.length + 1
344
- });
345
- continue;
346
- }
347
-
348
379
  // Prompt
349
380
  const promptMatch = line.match(/^Prompt user to\s+"([^"]*)"$/i);
350
381
  if (promptMatch) {
@@ -384,29 +415,12 @@ function parseWorkflowLines(lines, filename) {
384
415
  continue;
385
416
  }
386
417
 
387
- // Use (for Notify-like actions) - ✅ PRESERVE TOOL EXACTLY (NO NORMALIZATION)
388
- const useMatch = line.match(/^Use\s+(.+)$/i);
389
- if (useMatch) {
390
- flushCurrentStep();
391
- workflow.steps.push({
392
- type: 'use',
393
- tool: useMatch[1].trim(), // ← PRESERVED EXACTLY (no normalizeAction)
394
- stepNumber: workflow.steps.length + 1,
395
- saveAs: null,
396
- constraints: {}
397
- });
398
- continue;
399
- }
400
-
401
- // Ask (for Notify/resolver calls) - ✅ PRESERVE TARGET EXACTLY (NO NORMALIZATION)
418
+ // Ask (Multiline support)
402
419
  const askMatch = line.match(/^Ask\s+(.+)$/i);
403
420
  if (askMatch) {
404
421
  flushCurrentStep();
405
422
  let actionContent = askMatch[1].trim();
406
-
407
- // ✅ Multiline heredoc support: Ask llm-groq """
408
423
  if (actionContent.endsWith('"""')) {
409
- // Consume lines until closing """
410
424
  let multiline = actionContent.slice(0, -3).trim() + ' ';
411
425
  while (i < lines.length) {
412
426
  const nextLine = lines[i++].trim();
@@ -415,7 +429,6 @@ function parseWorkflowLines(lines, filename) {
415
429
  }
416
430
  actionContent = multiline.trim();
417
431
  }
418
-
419
432
  workflow.steps.push({
420
433
  type: 'action',
421
434
  actionRaw: `Action ${actionContent}`,
@@ -426,59 +439,36 @@ function parseWorkflowLines(lines, filename) {
426
439
  continue;
427
440
  }
428
441
 
429
- // ✅ ADD: Return keyword support (ensures wf.returnValues is populated)
430
- // ADD: Return keyword support (with debug logging)
431
- const returnMatch = line.match(/^Return\s+(.+)$/i);
432
- if (returnMatch) {
433
- flushCurrentStep();
434
- const rawReturns = returnMatch[1];
435
- console.log(`[PARSER DEBUG] Return line: "${line}"`);
436
- console.log(`[PARSER DEBUG] Return match[1]: "${rawReturns}"`);
437
- console.log(`[PARSER DEBUG] Return match[1] char codes: ${Array.from(rawReturns).map(c => c.charCodeAt(0))}`);
438
-
439
- workflow.returnValues = rawReturns
440
- .split(',')
441
- .map(r => r.trim())
442
- .filter(r => r !== '');
443
-
444
- console.log(`[PARSER DEBUG] Parsed returnValues: ${JSON.stringify(workflow.returnValues)}`);
445
- continue;
446
- }
447
- // Fallback: treat as action
442
+ // Return
443
+ const returnMatch = line.match(/^Return\s+(.+)$/i);
444
+ if (returnMatch) {
445
+ flushCurrentStep();
446
+ workflow.returnValues = returnMatch[1]
447
+ .split(',')
448
+ .map(r => r.trim())
449
+ .filter(r => r !== '');
450
+ continue;
451
+ }
452
+
453
+ // Fallback: Treat as action
448
454
  if (line.trim() !== '') {
449
455
  if (!currentStep) {
450
456
  currentStep = {
451
457
  type: 'action',
452
458
  stepNumber: workflow.steps.length + 1,
453
- actionRaw: line, // ← PRESERVED EXACTLY (no normalizeAction)
459
+ actionRaw: line,
454
460
  saveAs: null,
455
461
  constraints: {}
456
462
  };
457
463
  } else {
458
- currentStep.actionRaw += ' ' + line; // ← PRESERVED EXACTLY (no normalizeAction)
464
+ currentStep.actionRaw += ' ' + line;
459
465
  }
460
466
  }
461
467
  }
462
468
 
463
- flushCurrentStep(); // ✅ Final flush
464
-
465
- // ✅ FALLBACK: Scan raw lines for Return if regex missed it (Windows line endings, hidden chars, etc.)
466
- if (workflow.returnValues.length === 0) {
467
- for (let j = 0; j < lines.length; j++) {
468
- const clean = lines[j].replace(/\r/g, '').trim();
469
- const match = clean.match(/^Return\s+(.+)$/i);
470
- if (match) {
471
- console.log(`[PARSER] Recovered Return at line ${j+1}: "${clean}"`);
472
- workflow.returnValues = match[1]
473
- .split(',')
474
- .map(r => r.trim())
475
- .filter(r => r !== '');
476
- break;
477
- }
478
- }
479
- }
469
+ flushCurrentStep();
480
470
 
481
- // Post-process Save as in actionRaw - ✅ Apply normalization
471
+ // Post-process Save as in actionRaw
482
472
  workflow.steps.forEach(step => {
483
473
  if (step.actionRaw && step.saveAs === null) {
484
474
  const saveInAction = step.actionRaw.match(/(.+?)\s+Save as\s+(.+)$/i);
@@ -487,27 +477,18 @@ if (returnMatch) {
487
477
  step.saveAs = normalizeSymbol(saveInAction[2].trim());
488
478
  }
489
479
  }
490
- // ✅ Also normalize any existing saveAs values
491
480
  if (step.saveAs) {
492
481
  step.saveAs = normalizeSymbol(step.saveAs);
493
482
  }
494
483
  });
495
484
 
496
- // Validation warnings
497
- if (!workflow.name) {
498
- workflow.__warnings.push('Workflow name not found');
499
- }
500
- if (workflow.steps.length === 0) {
501
- workflow.__warnings.push('No steps found in workflow');
502
- }
503
- if (workflow.returnValues.length === 0 && workflow.steps.length > 0) {
504
- workflow.__warnings.push('No Return statement found');
505
- }
506
-
507
485
  return workflow;
508
486
  }
509
487
 
510
- // Parses blocks (for parallel, if, escalation levels) - ✅ PRESERVE ALL FUNCTIONALITY
488
+ // ─────────────────────────────────────────────────────────────────────────────
489
+ // BLOCK PARSER (Exported for RuntimeAPI Escalation)
490
+ // ─────────────────────────────────────────────────────────────────────────────
491
+
511
492
  function parseBlock(lines) {
512
493
  const steps = [];
513
494
  let current = null;
@@ -523,31 +504,53 @@ function parseBlock(lines) {
523
504
  line = line.trim();
524
505
  if (!line || line.startsWith('#')) continue;
525
506
 
526
- // Step declaration in block - ✅ PRESERVE ACTION EXACTLY (NO NORMALIZATION)
507
+ // Connect in Block
508
+ const connectMatch = line.match(/^Connect\s+"([^"]+)"\s+to\s+(url|resolver)\s+"([^"]+)"$/i);
509
+ if (connectMatch) {
510
+ flush();
511
+ steps.push({
512
+ type: 'connect',
513
+ resource: connectMatch[1],
514
+ endpoint: connectMatch[3],
515
+ targetType: connectMatch[2].toLowerCase()
516
+ });
517
+ continue;
518
+ }
519
+
520
+ // Use in Block
521
+ const useMatch = line.match(/^Use\s+"([^"]+)"\s+as\s+"([^"]+)"$/i);
522
+ if (useMatch) {
523
+ flush();
524
+ steps.push({
525
+ type: 'agent_use',
526
+ logicalName: useMatch[1],
527
+ resource: useMatch[2]
528
+ });
529
+ continue;
530
+ }
531
+
532
+ // Step in Block
527
533
  const stepMatch = line.match(/^Step\s+(\d+)\s*:\s*(.+)$/i);
528
534
  if (stepMatch) {
529
535
  flush();
530
- const stepNumber = parseInt(stepMatch[1], 10);
531
- const stepContent = stepMatch[2].trim(); // ← PRESERVED EXACTLY
532
-
533
536
  current = {
534
537
  type: 'action',
535
- stepNumber: stepNumber,
536
- actionRaw: stepContent, // ← CRITICAL: No normalization
538
+ stepNumber: parseInt(stepMatch[1], 10),
539
+ actionRaw: stepMatch[2].trim(),
537
540
  saveAs: null,
538
541
  constraints: {}
539
542
  };
540
543
  continue;
541
544
  }
542
545
 
543
- // Save as - ✅ Apply normalization
546
+ // Save As in Block
544
547
  const saveMatch = line.match(/^Save as\s+(.+)$/i);
545
548
  if (saveMatch && current) {
546
549
  current.saveAs = normalizeSymbol(saveMatch[1].trim());
547
550
  continue;
548
551
  }
549
552
 
550
- // Handle all special steps inside blocks
553
+ // Debrief in Block
551
554
  const debriefMatch = line.match(/^Debrief\s+([^\s]+)\s+with\s+"([^"]*)"$/i);
552
555
  if (debriefMatch) {
553
556
  flush();
@@ -555,20 +558,7 @@ function parseBlock(lines) {
555
558
  continue;
556
559
  }
557
560
 
558
- const evolveMatch = line.match(/^Evolve\s+([^\s]+)\s+using\s+feedback:\s*"([^"]*)"$/i);
559
- if (evolveMatch) {
560
- flush();
561
- steps.push({ type: 'evolve', targetResolver: evolveMatch[1].trim(), feedback: evolveMatch[2] });
562
- continue;
563
- }
564
-
565
- const promptMatch = line.match(/^Prompt user to\s+"([^"]*)"$/i);
566
- if (promptMatch) {
567
- flush();
568
- steps.push({ type: 'prompt', question: promptMatch[1], saveAs: null });
569
- continue;
570
- }
571
-
561
+ // Persist in Block
572
562
  const persistMatch = line.match(/^Persist\s+([^\s]+)\s+to\s+"([^"]*)"$/i);
573
563
  if (persistMatch) {
574
564
  flush();
@@ -576,6 +566,7 @@ function parseBlock(lines) {
576
566
  continue;
577
567
  }
578
568
 
569
+ // Emit in Block
579
570
  const emitMatch = line.match(/^Emit\s+"([^"]+)"\s+with\s+(.+)$/i);
580
571
  if (emitMatch) {
581
572
  flush();
@@ -583,20 +574,7 @@ function parseBlock(lines) {
583
574
  continue;
584
575
  }
585
576
 
586
- // FIXED: Use in block — consistent with top-level Use handler (Bug 1 + Bug 2)
587
- const useMatch = line.match(/^Use\s+(.+)$/i);
588
- if (useMatch) {
589
- flush();
590
- steps.push({
591
- type: 'use',
592
- tool: useMatch[1].trim(),
593
- saveAs: null,
594
- constraints: {}
595
- });
596
- continue;
597
- }
598
-
599
- // Ask in block — CANONICALIZE AT PARSE TIME
577
+ // Ask in Block
600
578
  const askMatch = line.match(/^Ask\s+(.+)$/i);
601
579
  if (askMatch) {
602
580
  flush();
@@ -609,56 +587,15 @@ function parseBlock(lines) {
609
587
  continue;
610
588
  }
611
589
 
612
- // Constraint inside block
613
- const constraintMatch = line.match(/^Constraint:\s*(.+)$/i);
614
- if (constraintMatch && current) {
615
- const constraintLine = constraintMatch[1].trim();
616
- const eq = constraintLine.match(/^([^=]+)=\s*(.+)$/);
617
- if (eq) {
618
- let key = eq[1].trim();
619
- let value = eq[2].trim();
620
- if (value.startsWith('[') && value.endsWith(']')) {
621
- value = value.slice(1, -1).split(',').map(v => v.trim().replace(/^"/, '').replace(/"$/, ''));
622
- } else if (!isNaN(value)) {
623
- value = Number(value);
624
- } else if (value.startsWith('"') && value.endsWith('"')) {
625
- value = value.slice(1, -1);
626
- }
627
- current.constraints[key] = value;
628
- }
629
- continue;
630
- }
631
-
632
- // ✅ ADD: Set keyword inside blocks (e.g., Set analysis_result = "")
633
- const setMatch = line.match(/^Set\s+(\w+)\s*=\s*(.+)$/i);
634
- if (setMatch) {
635
- flush(); // Flush any pending step
636
- steps.push({
637
- type: 'calculate',
638
- actionRaw: setMatch[2].trim(), // e.g., '""' or '"N/A"'
639
- saveAs: setMatch[1].trim(), // e.g., 'analysis_result'
640
- constraints: {}
641
- });
642
- continue;
643
- }
644
-
645
- // ✅ ADD: Return keyword inside blocks (rare but supported)
646
- const returnMatch = line.match(/^Return\s+(.+)$/i);
647
- if (returnMatch) {
648
- flush();
649
- // Note: Return inside blocks is unusual; this just parses it
650
- continue;
651
- }
652
-
653
- // Fallback
590
+ // Fallback for block actions
654
591
  if (current) {
655
- current.actionRaw += ' ' + line; // ← PRESERVED EXACTLY (no normalizeAction)
592
+ current.actionRaw += ' ' + line;
656
593
  }
657
594
  }
658
595
 
659
- flush(); // ✅ Final flush in block
596
+ flush();
660
597
 
661
- // Post-process Save as for steps inside blocks - Apply normalization
598
+ // Post-process Save as in block steps
662
599
  steps.forEach(step => {
663
600
  if (step.actionRaw && step.saveAs === null) {
664
601
  const saveInAction = step.actionRaw.match(/(.+?)\s+Save as\s+(.+)$/i);
@@ -667,7 +604,6 @@ function parseBlock(lines) {
667
604
  step.saveAs = normalizeSymbol(saveInAction[2].trim());
668
605
  }
669
606
  }
670
- // ✅ Also normalize any existing saveAs values
671
607
  if (step.saveAs) {
672
608
  step.saveAs = normalizeSymbol(step.saveAs);
673
609
  }
@@ -676,12 +612,5 @@ function parseBlock(lines) {
676
612
  return steps;
677
613
  }
678
614
 
679
- function validate(workflow) {
680
- const errors = [];
681
- if (workflow.maxGenerations !== null && workflow.maxGenerations <= 0) {
682
- errors.push('max_generations must be positive');
683
- }
684
- return errors;
685
- }
686
-
687
- module.exports = { parse, parseFromFile, parseLines, validate };
615
+ // EXPORTS: Added parseBlock so RuntimeAPI can use it for Escalation
616
+ module.exports = { parse, parseFromFile, parseLines, parseBlock };