@o-lang/olang 1.4.1 → 1.4.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@o-lang/olang",
3
- "version": "1.4.1",
3
+ "version": "1.4.2",
4
4
  "author": "Olalekan Ogundipe <info@olang.cloud>",
5
5
  "description": "O-Lang: A governance language for user-directed, rule-enforced agent workflows with native African language PII protection.",
6
6
  "main": "./src/runtime/index.js",
@@ -77,6 +77,11 @@ function parseWorkflowLines(lines, filename) {
77
77
  let escalationLevels = [];
78
78
  let currentLevel = null;
79
79
 
80
+ // ✅ ENHANCED: Track labels for blocks
81
+ let currentIfLabel = null;
82
+ let currentParallelLabel = null;
83
+ let currentEscalationLabel = null;
84
+
80
85
  const flushCurrentStep = () => {
81
86
  if (currentStep) {
82
87
  workflow.steps.push(currentStep);
@@ -88,6 +93,15 @@ function parseWorkflowLines(lines, filename) {
88
93
  let line = lines[i++].trim();
89
94
  if (line === '' || line.startsWith('#')) continue;
90
95
 
96
+ // ✅ ENHANCED: Extract "Step N:" label if present
97
+ let stepLabel = null;
98
+ let strippedLine = line;
99
+ const labelMatch = line.match(/^Step\s+(\d+)\s*:\s*(.+)$/i);
100
+ if (labelMatch) {
101
+ stepLabel = `Step ${labelMatch[1]}`;
102
+ strippedLine = labelMatch[2].trim();
103
+ }
104
+
91
105
  // --- 1. Workflow Declaration ---
92
106
  if (line.startsWith('Workflow ')) {
93
107
  const match = line.match(/^Workflow\s+"([^"]+)"(?:\s+with\s+(.+))?$/i);
@@ -134,11 +148,13 @@ function parseWorkflowLines(lines, filename) {
134
148
  }
135
149
 
136
150
  // --- 4. Block: Escalation ---
137
- if (line.match(/^Run in parallel with escalation:$/i)) {
151
+ // ✅ ENHANCED: Use strippedLine to match "Step N: Run in parallel with escalation:"
152
+ if (strippedLine.match(/^Run in parallel with escalation:$/i)) {
138
153
  flushCurrentStep();
139
154
  inEscalationBlock = true;
140
155
  escalationLevels = [];
141
156
  currentLevel = null;
157
+ currentEscalationLabel = stepLabel; // ✅ Save label
142
158
  continue;
143
159
  }
144
160
 
@@ -148,8 +164,10 @@ function parseWorkflowLines(lines, filename) {
148
164
  workflow.steps.push({
149
165
  type: 'escalation',
150
166
  levels: escalationLevels,
151
- stepNumber: workflow.steps.length + 1
167
+ stepNumber: workflow.steps.length + 1,
168
+ label: currentEscalationLabel // ✅ ENHANCED: Add label
152
169
  });
170
+ currentEscalationLabel = null; // ✅ Reset
153
171
  inEscalationBlock = false;
154
172
  continue;
155
173
  } else if (line.match(/^Level \d+:/i)) {
@@ -185,7 +203,8 @@ function parseWorkflowLines(lines, filename) {
185
203
  }
186
204
 
187
205
  // --- 5. Block: Parallel ---
188
- const timedParMatch = line.match(/^Run in parallel for (\d+)\s*([smhd])$/i);
206
+ // ENHANCED: Use strippedLine to match "Step N: Run in parallel for Xs"
207
+ const timedParMatch = strippedLine.match(/^Run in parallel for (\d+)\s*([smhd])$/i);
189
208
  if (timedParMatch) {
190
209
  flushCurrentStep();
191
210
  const value = parseInt(timedParMatch[1]);
@@ -195,14 +214,17 @@ function parseWorkflowLines(lines, filename) {
195
214
 
196
215
  inParallelBlock = true;
197
216
  parallelSteps = [];
217
+ currentParallelLabel = stepLabel; // ✅ Save label
198
218
  continue;
199
219
  }
200
220
 
201
- if (line.match(/^Run in parallel$/i)) {
221
+ // ✅ ENHANCED: Use strippedLine to match "Step N: Run in parallel"
222
+ if (strippedLine.match(/^Run in parallel$/i)) {
202
223
  flushCurrentStep();
203
224
  inParallelBlock = true;
204
225
  parallelSteps = [];
205
226
  parallelTimeout = null;
227
+ currentParallelLabel = stepLabel; // ✅ Save label
206
228
  continue;
207
229
  }
208
230
 
@@ -214,8 +236,10 @@ function parseWorkflowLines(lines, filename) {
214
236
  type: 'parallel',
215
237
  steps: parsedParallel,
216
238
  timeout: parallelTimeout,
217
- stepNumber: workflow.steps.length + 1
239
+ stepNumber: workflow.steps.length + 1,
240
+ label: currentParallelLabel // ✅ ENHANCED: Add label
218
241
  });
242
+ currentParallelLabel = null; // ✅ Reset
219
243
  inParallelBlock = false;
220
244
  parallelTimeout = null;
221
245
  continue;
@@ -227,12 +251,13 @@ function parseWorkflowLines(lines, filename) {
227
251
 
228
252
  // --- 6. Block: If / Else If / Else (FIXED STATE MACHINE) ---
229
253
 
230
- // Start If
231
- if (line.match(/^(?:If|When)\s+(.+)$/i)) {
254
+ // ENHANCED: Use strippedLine to match "Step N: If ..."
255
+ if (strippedLine.match(/^(?:If|When)\s+(.+)$/i)) {
232
256
  flushCurrentStep();
233
- const ifMatch = line.match(/^(?:If|When)\s+(.+)$/i);
257
+ const ifMatch = strippedLine.match(/^(?:If|When)\s+(.+)$/i);
234
258
  ifCondition = ifMatch[1].trim();
235
259
  inIfBlock = true;
260
+ currentIfLabel = stepLabel; // ✅ Save label
236
261
 
237
262
  // Reset accumulators
238
263
  mainIfBody = [];
@@ -291,8 +316,10 @@ function parseWorkflowLines(lines, filename) {
291
316
  body: parseBlock(mainIfBody), // Main body correctly isolated
292
317
  elseIf: elseIfChain, // Else-if chain
293
318
  elseBranch: inElseBlock ? parseBlock(currentBranchBody) : [], // Else body
294
- stepNumber: workflow.steps.length + 1
319
+ stepNumber: workflow.steps.length + 1,
320
+ label: currentIfLabel // ✅ ENHANCED: Add label
295
321
  });
322
+ currentIfLabel = null; // ✅ Reset
296
323
 
297
324
  // Reset State
298
325
  inIfBlock = false;
@@ -316,7 +343,8 @@ function parseWorkflowLines(lines, filename) {
316
343
  // --- 7. Standard Steps & Keywords ---
317
344
 
318
345
  // Calculate (NEW v1.4.0 — math expression evaluation)
319
- const calcMatch = line.match(/^Calculate\s+(.+)$/i);
346
+ // ENHANCED: Use strippedLine
347
+ const calcMatch = strippedLine.match(/^Calculate\s+(.+)$/i);
320
348
  if (calcMatch) {
321
349
  flushCurrentStep();
322
350
  workflow.steps.push({
@@ -324,13 +352,15 @@ function parseWorkflowLines(lines, filename) {
324
352
  expression: calcMatch[1].trim(),
325
353
  stepNumber: workflow.steps.length + 1,
326
354
  saveAs: null,
327
- constraints: {}
355
+ constraints: {},
356
+ label: stepLabel // ✅ ENHANCED: Add label
328
357
  });
329
358
  continue;
330
359
  }
331
360
 
332
361
  // Connect: Connect "name" to url "..." OR Connect "name" to resolver "..."
333
- const connectMatch = line.match(/^Connect\s+"([^"]+)"\s+to\s+(url|resolver)\s+"([^"]+)"$/i);
362
+ // ENHANCED: Use strippedLine
363
+ const connectMatch = strippedLine.match(/^Connect\s+"([^"]+)"\s+to\s+(url|resolver)\s+"([^"]+)"$/i);
334
364
  if (connectMatch) {
335
365
  flushCurrentStep();
336
366
  workflow.steps.push({
@@ -338,20 +368,23 @@ function parseWorkflowLines(lines, filename) {
338
368
  resource: connectMatch[1],
339
369
  endpoint: connectMatch[3],
340
370
  targetType: connectMatch[2].toLowerCase(),
341
- stepNumber: workflow.steps.length + 1
371
+ stepNumber: workflow.steps.length + 1,
372
+ label: stepLabel // ✅ ENHANCED: Add label
342
373
  });
343
374
  continue;
344
375
  }
345
376
 
346
377
  // Use: Use "logicalName" as "resource"
347
- const useMatch = line.match(/^Use\s+"([^"]+)"\s+as\s+"([^"]+)"$/i);
378
+ // ENHANCED: Use strippedLine
379
+ const useMatch = strippedLine.match(/^Use\s+"([^"]+)"\s+as\s+"([^"]+)"$/i);
348
380
  if (useMatch) {
349
381
  flushCurrentStep();
350
382
  workflow.steps.push({
351
383
  type: 'agent_use',
352
384
  logicalName: useMatch[1],
353
385
  resource: useMatch[2],
354
- stepNumber: workflow.steps.length + 1
386
+ stepNumber: workflow.steps.length + 1,
387
+ label: stepLabel // ✅ ENHANCED: Add label
355
388
  });
356
389
  continue;
357
390
  }
@@ -365,7 +398,8 @@ function parseWorkflowLines(lines, filename) {
365
398
  stepNumber: parseInt(stepMatch[1], 10),
366
399
  actionRaw: stepMatch[2].trim(),
367
400
  saveAs: null,
368
- constraints: {}
401
+ constraints: {},
402
+ label: `Step ${stepMatch[1]}` // ✅ ENHANCED: Add label
369
403
  };
370
404
  continue;
371
405
  }
@@ -378,59 +412,68 @@ function parseWorkflowLines(lines, filename) {
378
412
  }
379
413
 
380
414
  // Debrief
381
- const debriefMatch = line.match(/^Debrief\s+([^\s]+)\s+with\s+"([^"]*)"$/i);
415
+ // ENHANCED: Use strippedLine
416
+ const debriefMatch = strippedLine.match(/^Debrief\s+([^\s]+)\s+with\s+"([^"]*)"$/i);
382
417
  if (debriefMatch) {
383
418
  flushCurrentStep();
384
419
  workflow.steps.push({
385
420
  type: 'debrief',
386
421
  agent: debriefMatch[1].trim(),
387
422
  message: debriefMatch[2],
388
- stepNumber: workflow.steps.length + 1
423
+ stepNumber: workflow.steps.length + 1,
424
+ label: stepLabel // ✅ ENHANCED: Add label
389
425
  });
390
426
  continue;
391
427
  }
392
428
 
393
429
  // Prompt
394
- const promptMatch = line.match(/^Prompt user to\s+"([^"]*)"$/i);
430
+ // ENHANCED: Use strippedLine
431
+ const promptMatch = strippedLine.match(/^Prompt user to\s+"([^"]*)"$/i);
395
432
  if (promptMatch) {
396
433
  flushCurrentStep();
397
434
  workflow.steps.push({
398
435
  type: 'prompt',
399
436
  question: promptMatch[1],
400
437
  stepNumber: workflow.steps.length + 1,
401
- saveAs: null
438
+ saveAs: null,
439
+ label: stepLabel // ✅ ENHANCED: Add label
402
440
  });
403
441
  continue;
404
442
  }
405
443
 
406
444
  // Persist
407
- const persistMatch = line.match(/^Persist\s+([^\s]+)\s+to\s+"([^"]*)"$/i);
445
+ // ENHANCED: Use strippedLine
446
+ const persistMatch = strippedLine.match(/^Persist\s+([^\s]+)\s+to\s+"([^"]*)"$/i);
408
447
  if (persistMatch) {
409
448
  flushCurrentStep();
410
449
  workflow.steps.push({
411
450
  type: 'persist',
412
451
  variable: persistMatch[1].trim(),
413
452
  target: persistMatch[2],
414
- stepNumber: workflow.steps.length + 1
453
+ stepNumber: workflow.steps.length + 1,
454
+ label: stepLabel // ✅ ENHANCED: Add label
415
455
  });
416
456
  continue;
417
457
  }
418
458
 
419
459
  // Emit
420
- const emitMatch = line.match(/^Emit\s+"([^"]+)"\s+with\s+(.+)$/i);
460
+ // ENHANCED: Use strippedLine
461
+ const emitMatch = strippedLine.match(/^Emit\s+"([^"]+)"\s+with\s+(.+)$/i);
421
462
  if (emitMatch) {
422
463
  flushCurrentStep();
423
464
  workflow.steps.push({
424
465
  type: 'emit',
425
466
  event: emitMatch[1],
426
467
  payload: emitMatch[2].trim(),
427
- stepNumber: workflow.steps.length + 1
468
+ stepNumber: workflow.steps.length + 1,
469
+ label: stepLabel // ✅ ENHANCED: Add label
428
470
  });
429
471
  continue;
430
472
  }
431
473
 
432
474
  // Ask (Multiline support)
433
- const askMatch = line.match(/^Ask\s+(.+)$/i);
475
+ // ENHANCED: Use strippedLine
476
+ const askMatch = strippedLine.match(/^Ask\s+(.+)$/i);
434
477
  if (askMatch) {
435
478
  flushCurrentStep();
436
479
  let actionContent = askMatch[1].trim();
@@ -448,7 +491,8 @@ function parseWorkflowLines(lines, filename) {
448
491
  actionRaw: `Action ${actionContent}`,
449
492
  stepNumber: workflow.steps.length + 1,
450
493
  saveAs: null,
451
- constraints: {}
494
+ constraints: {},
495
+ label: stepLabel // ✅ ENHANCED: Add label
452
496
  });
453
497
  continue;
454
498
  }
@@ -465,17 +509,19 @@ function parseWorkflowLines(lines, filename) {
465
509
  }
466
510
 
467
511
  // Fallback: Treat as action
468
- if (line.trim() !== '') {
512
+ // ENHANCED: Use strippedLine for actionRaw
513
+ if (strippedLine.trim() !== '') {
469
514
  if (!currentStep) {
470
515
  currentStep = {
471
516
  type: 'action',
472
517
  stepNumber: workflow.steps.length + 1,
473
- actionRaw: line,
518
+ actionRaw: strippedLine,
474
519
  saveAs: null,
475
- constraints: {}
520
+ constraints: {},
521
+ label: stepLabel // ✅ ENHANCED: Add label
476
522
  };
477
523
  } else {
478
- currentStep.actionRaw += ' ' + line;
524
+ currentStep.actionRaw += ' ' + strippedLine;
479
525
  }
480
526
  }
481
527
  }
@@ -573,7 +619,8 @@ function parseBlock(lines) {
573
619
  stepNumber: parseInt(stepMatch[1], 10),
574
620
  actionRaw: stepMatch[2].trim(),
575
621
  saveAs: null,
576
- constraints: {}
622
+ constraints: {},
623
+ label: `Step ${stepMatch[1]}` // ✅ ENHANCED: Add label
577
624
  };
578
625
  continue;
579
626
  }
@@ -1624,8 +1624,15 @@ class RuntimeAPI {
1624
1624
  }
1625
1625
  }
1626
1626
 
1627
- let errorMessage = `[O-Lang SAFETY] No resolver handled action: "${action}\n`;
1627
+ // ENHANCED: Extract step location context for debugging
1628
+ const stepLabel = step.label || `unnamed_${step.type}_step`;
1629
+ const branchContext = step._parentLabel ? ` [Inside ${step._parentLabel} → ${step._branch} branch]` : '';
1630
+
1631
+ let errorMessage = `[O-Lang SAFETY] No resolver handled action: "${action}"\n`;
1632
+ errorMessage += ` → Location: ${stepLabel}${branchContext}\n`;
1633
+ errorMessage += ` → Workflow: ${this.context.workflow_name || 'unknown'}\n`;
1628
1634
  errorMessage += `Attempted resolvers:\n`;
1635
+
1629
1636
  resolverAttempts.forEach((attempt, i) => {
1630
1637
  const namePad = attempt.name.padEnd(30);
1631
1638
  if (attempt.status === 'skipped') {
@@ -1682,6 +1689,16 @@ class RuntimeAPI {
1682
1689
  errorMessage += ` → Visit https://www.npmjs.com/search?q=%40o-lang for resolver packages\n`;
1683
1690
  }
1684
1691
  errorMessage += `\n🛑 Workflow halted to prevent unsafe data propagation to LLMs.`;
1692
+
1693
+ // ✅ ENHANCED: Log the failure with full context before throwing
1694
+ this._createAuditEntry('resolver_execution_failed', {
1695
+ step_label: stepLabel,
1696
+ branch_context: step._parentLabel ? `${step._parentLabel} (${step._branch})` : null,
1697
+ action: action,
1698
+ attempted_resolvers: resolverAttempts.map(a => a.name),
1699
+ severity: 'high'
1700
+ });
1701
+
1685
1702
  throw new Error(errorMessage);
1686
1703
  };
1687
1704
 
@@ -1859,7 +1876,9 @@ class RuntimeAPI {
1859
1876
  // 3. Auditability: Logs which condition was evaluated and which branch fired.
1860
1877
  // ─────────────────────────────────────────────────────────────────────────────
1861
1878
 
1862
- case 'if': {
1879
+ case 'if': {
1880
+ const stepLabel = step.label || 'unnamed_if_block';
1881
+
1863
1882
  // 1. Validate all symbols referenced in the main condition
1864
1883
  const condSymbols = step.condition ? step.condition.match(/\{([^\}]+)\}/g) || [] : [];
1865
1884
  let symbolsValid = true;
@@ -1873,6 +1892,7 @@ class RuntimeAPI {
1873
1892
 
1874
1893
  if (!symbolsValid) {
1875
1894
  this._createAuditEntry('condition_skipped', {
1895
+ step_label: stepLabel,
1876
1896
  condition: step.condition,
1877
1897
  reason: 'One or more symbols missing in context',
1878
1898
  severity: 'warn'
@@ -1884,6 +1904,7 @@ class RuntimeAPI {
1884
1904
  const mainPassed = this.evaluateCondition(step.condition, this.context);
1885
1905
 
1886
1906
  this._createAuditEntry('condition_evaluated', {
1907
+ step_label: stepLabel,
1887
1908
  condition: step.condition,
1888
1909
  passed: mainPassed,
1889
1910
  branch: 'if',
@@ -1892,7 +1913,12 @@ class RuntimeAPI {
1892
1913
 
1893
1914
  if (mainPassed) {
1894
1915
  if (step.body && Array.isArray(step.body)) {
1895
- for (const s of step.body) await this.executeStep(s, agentResolver);
1916
+ // ENHANCED: Tag child steps with parent context for debugging
1917
+ for (const s of step.body) {
1918
+ s._parentLabel = stepLabel;
1919
+ s._branch = 'if';
1920
+ await this.executeStep(s, agentResolver);
1921
+ }
1896
1922
  }
1897
1923
  break; // Exit after successful if
1898
1924
  }
@@ -1913,6 +1939,7 @@ class RuntimeAPI {
1913
1939
 
1914
1940
  if (!branchSymbolsValid) {
1915
1941
  this._createAuditEntry('condition_skipped', {
1942
+ step_label: stepLabel,
1916
1943
  condition: branch.condition,
1917
1944
  reason: 'One or more symbols missing in context',
1918
1945
  severity: 'warn'
@@ -1923,6 +1950,7 @@ class RuntimeAPI {
1923
1950
  const branchPassed = this.evaluateCondition(branch.condition, this.context);
1924
1951
 
1925
1952
  this._createAuditEntry('condition_evaluated', {
1953
+ step_label: stepLabel,
1926
1954
  condition: branch.condition,
1927
1955
  passed: branchPassed,
1928
1956
  branch: 'else-if',
@@ -1931,7 +1959,12 @@ class RuntimeAPI {
1931
1959
 
1932
1960
  if (branchPassed) {
1933
1961
  if (branch.body && Array.isArray(branch.body)) {
1934
- for (const s of branch.body) await this.executeStep(s, agentResolver);
1962
+ // ENHANCED: Tag child steps with parent context
1963
+ for (const s of branch.body) {
1964
+ s._parentLabel = stepLabel;
1965
+ s._branch = 'else-if';
1966
+ await this.executeStep(s, agentResolver);
1967
+ }
1935
1968
  }
1936
1969
  elseIfFired = true;
1937
1970
  break;
@@ -1943,17 +1976,23 @@ class RuntimeAPI {
1943
1976
  // 4. else fallback
1944
1977
  if (step.elseBranch && Array.isArray(step.elseBranch)) {
1945
1978
  this._createAuditEntry('condition_evaluated', {
1979
+ step_label: stepLabel,
1946
1980
  condition: 'else',
1947
1981
  passed: true,
1948
1982
  branch: 'else',
1949
1983
  severity: 'info'
1950
1984
  });
1951
- for (const s of step.elseBranch) await this.executeStep(s, agentResolver);
1985
+ // ENHANCED: Tag child steps with parent context
1986
+ for (const s of step.elseBranch) {
1987
+ s._parentLabel = stepLabel;
1988
+ s._branch = 'else';
1989
+ await this.executeStep(s, agentResolver);
1990
+ }
1952
1991
  }
1953
1992
 
1954
1993
  break;
1955
1994
  }
1956
-
1995
+
1957
1996
  // ─────────────────────────────────────────────────────────────────────────────
1958
1997
  // PARALLEL
1959
1998
  //