@probelabs/visor 0.1.89 → 0.1.90

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- process.env.VISOR_VERSION = '0.1.89';
2
+ process.env.VISOR_VERSION = '0.1.90';
3
3
  process.env.PROBE_VERSION = '0.6.0-rc125';
4
4
  /******/ (() => { // webpackBootstrap
5
5
  /******/ var __webpack_modules__ = ({
@@ -101913,6 +101913,9 @@ class CheckExecutionEngine {
101913
101913
  if (Array.isArray(output)) {
101914
101914
  normalizedOutput = output;
101915
101915
  }
101916
+ else if (output && typeof output === 'object' && Array.isArray(output.items)) {
101917
+ normalizedOutput = output.items;
101918
+ }
101916
101919
  else if (typeof output === 'string') {
101917
101920
  try {
101918
101921
  const parsed = JSON.parse(output);
@@ -102139,8 +102142,10 @@ class CheckExecutionEngine {
102139
102142
  templateContent = await fs.readFile(templatePath, 'utf-8');
102140
102143
  }
102141
102144
  // Prepare template data
102145
+ // Filter out system-level issues (fail_if conditions, internal errors) which should not appear in output
102146
+ const filteredIssues = (reviewSummary.issues || []).filter(issue => !(issue.file === 'system' && issue.line === 0));
102142
102147
  const templateData = {
102143
- issues: reviewSummary.issues || [],
102148
+ issues: filteredIssues,
102144
102149
  checkName: checkName,
102145
102150
  };
102146
102151
  const rendered = await liquid.parseAndRender(templateContent, templateData);
@@ -102254,6 +102259,15 @@ class CheckExecutionEngine {
102254
102259
  ],
102255
102260
  };
102256
102261
  }
102262
+ // Build children-by-parent mapping for inline branch-first execution
102263
+ const childrenByParent = new Map();
102264
+ for (const [child, depsArr] of Object.entries(dependencies)) {
102265
+ for (const p of depsArr || []) {
102266
+ if (!childrenByParent.has(p))
102267
+ childrenByParent.set(p, []);
102268
+ childrenByParent.get(p).push(child);
102269
+ }
102270
+ }
102257
102271
  // Log execution plan
102258
102272
  const stats = dependency_resolver_1.DependencyResolver.getExecutionStats(dependencyGraph);
102259
102273
  if (debug) {
@@ -102287,8 +102301,15 @@ class CheckExecutionEngine {
102287
102301
  if (debug) {
102288
102302
  log(`🔧 Debug: Executing level ${executionGroup.level} with ${executionGroup.parallel.length} checks (parallelism: ${actualParallelism})`);
102289
102303
  }
102290
- // Create task functions for checks in this level
102291
- const levelTaskFunctions = executionGroup.parallel.map(checkName => async () => {
102304
+ // Create task functions for checks in this level, skip those already completed inline
102305
+ const levelChecks = executionGroup.parallel.filter(name => !results.has(name));
102306
+ const levelTaskFunctions = levelChecks.map(checkName => async () => {
102307
+ // Skip if this check was already completed by item-level branch scheduler
102308
+ if (results.has(checkName)) {
102309
+ if (debug)
102310
+ log(`🔧 Debug: Skipping ${checkName} (already satisfied earlier)`);
102311
+ return { checkName, error: null, result: results.get(checkName) };
102312
+ }
102292
102313
  const checkConfig = config.checks[checkName];
102293
102314
  if (!checkConfig) {
102294
102315
  return {
@@ -102297,6 +102318,7 @@ class CheckExecutionEngine {
102297
102318
  result: null,
102298
102319
  };
102299
102320
  }
102321
+ // (no early gating; rely on per-item scheduler after parents run)
102300
102322
  const checkStartTime = Date.now();
102301
102323
  completedChecksCount++;
102302
102324
  logger_1.logger.step(`Running check: ${checkName} [${completedChecksCount}/${totalChecksCount}]`);
@@ -102365,30 +102387,41 @@ class CheckExecutionEngine {
102365
102387
  const id = issue.ruleId || '';
102366
102388
  return id.endsWith('/__skipped');
102367
102389
  });
102368
- // Treat these as fatal in direct dependencies:
102390
+ // If dependency is a forEach parent, do NOT apply global fatal gating here.
102391
+ // We'll gate per-item inside the forEach loop to avoid stopping other branches.
102392
+ const depExtended = depRes;
102393
+ const isDepForEachParent = !!depExtended.isForEach;
102394
+ // Treat these as fatal in direct dependencies (non-forEach only):
102369
102395
  // - command provider execution/transform failures
102370
102396
  // - forEach validation/iteration errors
102371
102397
  // - fail_if conditions (global or check-specific)
102372
- let hasFatalFailure = (depRes.issues || []).some(issue => {
102373
- const id = issue.ruleId || '';
102374
- return (id === 'command/execution_error' ||
102375
- id.endsWith('/command/execution_error') ||
102376
- id === 'command/timeout' ||
102377
- id.endsWith('/command/timeout') ||
102378
- id === 'command/transform_js_error' ||
102379
- id.endsWith('/command/transform_js_error') ||
102380
- id === 'command/transform_error' ||
102381
- id.endsWith('/command/transform_error') ||
102382
- id.endsWith('/forEach/iteration_error') ||
102383
- id === 'forEach/undefined_output' ||
102384
- id.endsWith('/forEach/undefined_output') ||
102385
- id.endsWith('_fail_if') ||
102386
- id.endsWith('/global_fail_if'));
102387
- });
102388
- // As a fallback, evaluate fail_if on the dependency result now (in case the provider path didn't add issues yet)
102389
- if (!hasFatalFailure && config && (config.fail_if || config.checks[depId]?.fail_if)) {
102390
- const failIfResults = await this.evaluateFailureConditions(depId, depRes, config);
102391
- hasFatalFailure = failIfResults.some(r => r.failed);
102398
+ // For non-forEach parents, only provider-fatal or fail_if/global_fail_if should gate.
102399
+ let hasFatalFailure = false;
102400
+ if (!isDepForEachParent) {
102401
+ const issues = depRes.issues || [];
102402
+ hasFatalFailure = issues.some(issue => {
102403
+ const id = issue.ruleId || '';
102404
+ return (id === 'command/execution_error' ||
102405
+ id.endsWith('/command/execution_error') ||
102406
+ id === 'command/timeout' ||
102407
+ id.endsWith('/command/timeout') ||
102408
+ id === 'command/transform_js_error' ||
102409
+ id.endsWith('/command/transform_js_error') ||
102410
+ id === 'command/transform_error' ||
102411
+ id.endsWith('/command/transform_error') ||
102412
+ id === 'forEach/undefined_output' ||
102413
+ id.endsWith('/forEach/undefined_output') ||
102414
+ id.endsWith('/forEach/iteration_error') ||
102415
+ id.endsWith('_fail_if') ||
102416
+ id.endsWith('/global_fail_if'));
102417
+ });
102418
+ // As a fallback, evaluate fail_if on the dependency result now
102419
+ if (!hasFatalFailure && config && (config.fail_if || config.checks[depId]?.fail_if)) {
102420
+ try {
102421
+ hasFatalFailure = await this.failIfTriggered(depId, depRes, config);
102422
+ }
102423
+ catch { }
102424
+ }
102392
102425
  }
102393
102426
  if (debug) {
102394
102427
  log(`🔧 Debug: gating check '${checkName}' against dep '${depId}': wasSkipped=${wasSkipped} hasFatalFailure=${hasFatalFailure}`);
@@ -102413,11 +102446,17 @@ class CheckExecutionEngine {
102413
102446
  const depResult = results.get(depId);
102414
102447
  // Check if this dependency has forEach enabled
102415
102448
  const depForEachResult = depResult;
102416
- if (depForEachResult.isForEach && Array.isArray(depForEachResult.forEachItems)) {
102449
+ if (depForEachResult.isForEach ||
102450
+ Array.isArray(depForEachResult.forEachItemResults) ||
102451
+ Array.isArray(depForEachResult.forEachItems)) {
102417
102452
  if (!isForEachDependent) {
102418
102453
  // First forEach dependency found - use it as the primary
102419
102454
  isForEachDependent = true;
102420
- forEachItems = depForEachResult.forEachItems;
102455
+ forEachItems = Array.isArray(depForEachResult.forEachItems)
102456
+ ? depForEachResult.forEachItems
102457
+ : new Array(Array.isArray(depForEachResult.forEachItemResults)
102458
+ ? depForEachResult.forEachItemResults.length
102459
+ : 0).fill(undefined);
102421
102460
  forEachParentName = depId;
102422
102461
  }
102423
102462
  // Track all forEach parents for unwrapping
@@ -102460,6 +102499,18 @@ class CheckExecutionEngine {
102460
102499
  // Handle forEach dependent execution
102461
102500
  let finalResult;
102462
102501
  if (isForEachDependent && forEachParentName) {
102502
+ if (!Array.isArray(forEachItems)) {
102503
+ forEachItems = [];
102504
+ }
102505
+ if (!Array.isArray(forEachItems)) {
102506
+ this.recordSkip(checkName, 'dependency_failed');
102507
+ return {
102508
+ checkName,
102509
+ error: null,
102510
+ result: { issues: [] },
102511
+ skipped: true,
102512
+ };
102513
+ }
102463
102514
  // Record forEach preview items
102464
102515
  this.recordForEachPreview(checkName, forEachItems);
102465
102516
  // If the forEach parent returned an empty array, skip this check entirely
@@ -102468,6 +102519,7 @@ class CheckExecutionEngine {
102468
102519
  log(`🔄 Debug: Skipping check "${checkName}" - forEach check "${forEachParentName}" returned 0 items`);
102469
102520
  }
102470
102521
  logger_1.logger.info(` forEach: no items from "${forEachParentName}", skipping check...`);
102522
+ this.recordSkip(checkName, 'dependency_failed');
102471
102523
  // Return a special marker result so that dependent checks can detect the skip
102472
102524
  finalResult = {
102473
102525
  issues: [],
@@ -102479,14 +102531,176 @@ class CheckExecutionEngine {
102479
102531
  // Skip to the end - don't execute this check
102480
102532
  }
102481
102533
  else {
102534
+ // Emit explicit debug to stdout so CLI e2e can assert it
102482
102535
  if (debug) {
102483
- log(`🔄 Debug: Check "${checkName}" depends on forEach check "${forEachParentName}", executing ${forEachItems.length} times`);
102536
+ console.log(`🔄 Debug: Check "${checkName}" depends on forEach check "${forEachParentName}", executing ${forEachItems.length} times`);
102484
102537
  }
102485
102538
  // Log forEach processing start (non-debug)
102486
- logger_1.logger.info(` forEach: processing ${forEachItems.length} items from "${forEachParentName}"...`);
102539
+ const __itemCount = Array.isArray(forEachItems) ? forEachItems.length : 0;
102540
+ logger_1.logger.info(` forEach: processing ${__itemCount} items from "${forEachParentName}"...`);
102487
102541
  const allIssues = [];
102488
- const allOutputs = [];
102542
+ const allOutputs = new Array(forEachItems.length);
102489
102543
  const aggregatedContents = [];
102544
+ const perItemResults = new Array(forEachItems.length);
102545
+ // Aggregators for inline descendant execution (branch-first mode for simple chains)
102546
+ const inlineAgg = new Map();
102547
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
102548
+ const execInlineDescendants = async (parentName, itemIndex, baseDeps) => {
102549
+ const children = (childrenByParent.get(parentName) || []).filter(child => {
102550
+ const deps = dependencies[child] || [];
102551
+ // Only handle simple chains inline: exactly one dependency which is the parent
102552
+ return deps.length === 1 && deps[0] === parentName;
102553
+ });
102554
+ for (const childName of children) {
102555
+ const childCfg = config.checks[childName];
102556
+ const childProviderType = childCfg.type || 'ai';
102557
+ const childProv = this.providerRegistry.getProviderOrThrow(childProviderType);
102558
+ this.setProviderWebhookContext(childProv);
102559
+ const childProviderConfig = {
102560
+ type: childProviderType,
102561
+ prompt: childCfg.prompt,
102562
+ exec: childCfg.exec,
102563
+ focus: childCfg.focus || this.mapCheckNameToFocus(childName),
102564
+ schema: childCfg.schema,
102565
+ group: childCfg.group,
102566
+ checkName: childName,
102567
+ eventContext: prInfo.eventContext,
102568
+ transform: childCfg.transform,
102569
+ transform_js: childCfg.transform_js,
102570
+ env: childCfg.env,
102571
+ forEach: childCfg.forEach,
102572
+ ai: {
102573
+ timeout: timeout || 600000,
102574
+ debug: debug,
102575
+ ...(childCfg.ai || {}),
102576
+ },
102577
+ };
102578
+ // Build per-item dependency results for child, including transitive ancestors
102579
+ const childDepResults = new Map();
102580
+ const childAllDeps = dependency_resolver_1.DependencyResolver.getAllDependencies(childName, dependencyGraph.nodes);
102581
+ for (const dep of childAllDeps) {
102582
+ const baseRes = baseDeps.get(dep);
102583
+ if (baseRes) {
102584
+ childDepResults.set(dep, baseRes);
102585
+ continue;
102586
+ }
102587
+ const globalRes = results.get(dep);
102588
+ if (!globalRes)
102589
+ continue;
102590
+ if (globalRes &&
102591
+ (globalRes.isForEach ||
102592
+ Array.isArray(globalRes.forEachItemResults) ||
102593
+ Array.isArray(globalRes.output))) {
102594
+ // Prefer precise per-item result when available
102595
+ if (Array.isArray(globalRes.forEachItemResults) &&
102596
+ globalRes.forEachItemResults[itemIndex]) {
102597
+ childDepResults.set(dep, globalRes.forEachItemResults[itemIndex]);
102598
+ }
102599
+ else if (Array.isArray(globalRes.output) &&
102600
+ globalRes.output[itemIndex] !== undefined) {
102601
+ childDepResults.set(dep, {
102602
+ issues: [],
102603
+ output: globalRes.output[itemIndex],
102604
+ });
102605
+ }
102606
+ else {
102607
+ childDepResults.set(dep, globalRes);
102608
+ }
102609
+ }
102610
+ else {
102611
+ childDepResults.set(dep, globalRes);
102612
+ }
102613
+ }
102614
+ // If the parent item had a fatal failure, skip this child for this branch
102615
+ const parentItemRes = childDepResults.get(parentName);
102616
+ if (parentItemRes) {
102617
+ // Skip when parent explicitly signaled error in its output for this item
102618
+ try {
102619
+ const pout = parentItemRes.output;
102620
+ if (pout && typeof pout === 'object' && pout.error === true) {
102621
+ continue;
102622
+ }
102623
+ }
102624
+ catch { }
102625
+ const fatal = (parentItemRes.issues || []).some(issue => {
102626
+ const id = issue.ruleId || '';
102627
+ const sev = issue.severity || 'error';
102628
+ return (sev === 'error' ||
102629
+ sev === 'critical' ||
102630
+ id === 'command/execution_error' ||
102631
+ id.endsWith('/command/execution_error') ||
102632
+ id === 'command/timeout' ||
102633
+ id.endsWith('/command/timeout') ||
102634
+ id === 'command/transform_js_error' ||
102635
+ id.endsWith('/command/transform_js_error') ||
102636
+ id === 'command/transform_error' ||
102637
+ id.endsWith('/command/transform_error') ||
102638
+ id.endsWith('/forEach/iteration_error') ||
102639
+ id === 'forEach/undefined_output' ||
102640
+ id.endsWith('/forEach/undefined_output') ||
102641
+ id.endsWith('_fail_if') ||
102642
+ id.endsWith('/global_fail_if'));
102643
+ });
102644
+ if (fatal) {
102645
+ continue;
102646
+ }
102647
+ }
102648
+ // Evaluate per-item if condition
102649
+ if (childCfg.if) {
102650
+ const condResults = new Map(results);
102651
+ for (const [k, v] of childDepResults)
102652
+ condResults.set(k, v);
102653
+ const shouldRunChild = await this.evaluateCheckCondition(childName, childCfg.if, prInfo, condResults, debug);
102654
+ if (!shouldRunChild) {
102655
+ continue;
102656
+ }
102657
+ }
102658
+ // Execute child for this item (record stats)
102659
+ const childIterStart = this.recordIterationStart(childName);
102660
+ const childItemRes = await this.executeWithRouting(childName, childCfg, childProv, childProviderConfig, prInfo, childDepResults, sessionInfo, config, dependencyGraph, debug, results, { index: itemIndex, total: forEachItems.length, parent: parentName });
102661
+ // Per-item fail_if
102662
+ if (config && (config.fail_if || childCfg.fail_if)) {
102663
+ const fRes = await this.evaluateFailureConditions(childName, childItemRes, config);
102664
+ if (fRes.length > 0) {
102665
+ const fIssues = fRes
102666
+ .filter(f => f.failed)
102667
+ .map(f => ({
102668
+ file: 'system',
102669
+ line: 0,
102670
+ ruleId: f.conditionName,
102671
+ message: f.message || `Failure condition met: ${f.expression}`,
102672
+ severity: (f.severity || 'error'),
102673
+ category: 'logic',
102674
+ }));
102675
+ childItemRes.issues = [...(childItemRes.issues || []), ...fIssues];
102676
+ }
102677
+ }
102678
+ if (!inlineAgg.has(childName)) {
102679
+ inlineAgg.set(childName, {
102680
+ issues: [],
102681
+ outputs: new Array(forEachItems.length),
102682
+ contents: [],
102683
+ perItemResults: new Array(forEachItems.length),
102684
+ });
102685
+ }
102686
+ const agg = inlineAgg.get(childName);
102687
+ if (childItemRes.issues)
102688
+ agg.issues.push(...childItemRes.issues);
102689
+ const out = childItemRes.output;
102690
+ agg.outputs[itemIndex] = out;
102691
+ agg.perItemResults[itemIndex] = childItemRes;
102692
+ const c = childItemRes.content;
102693
+ if (typeof c === 'string' && c.trim())
102694
+ agg.contents.push(c.trim());
102695
+ // Record iteration completion for stats
102696
+ const childHadFatal = this.hasFatal(childItemRes.issues || []);
102697
+ this.recordIterationComplete(childName, childIterStart, !childHadFatal, childItemRes.issues || [], childItemRes.output);
102698
+ // Recurse further for simple chains
102699
+ const nextBase = new Map(baseDeps);
102700
+ nextBase.set(childName, childItemRes);
102701
+ await execInlineDescendants(childName, itemIndex, nextBase);
102702
+ }
102703
+ };
102490
102704
  // Create task functions (not executed yet) - these will be executed with controlled concurrency
102491
102705
  // via executeWithLimitedParallelism to respect maxParallelism setting
102492
102706
  const itemTasks = forEachItems.map((item, itemIndex) => async () => {
@@ -102497,15 +102711,25 @@ class CheckExecutionEngine {
102497
102711
  if (forEachParents.includes(depName)) {
102498
102712
  // This is a forEach parent - unwrap its output for this iteration
102499
102713
  const depForEachResult = depResult;
102500
- if (Array.isArray(depForEachResult.output) &&
102714
+ if (Array.isArray(depForEachResult.forEachItemResults) &&
102715
+ depForEachResult.forEachItemResults[itemIndex]) {
102716
+ // Use precise per-item result (includes issues + output)
102717
+ forEachDependencyResults.set(depName, depForEachResult.forEachItemResults[itemIndex]);
102718
+ // Also provide -raw access to the full array
102719
+ const rawResult = {
102720
+ issues: [],
102721
+ output: depForEachResult.output,
102722
+ };
102723
+ forEachDependencyResults.set(`${depName}-raw`, rawResult);
102724
+ }
102725
+ else if (Array.isArray(depForEachResult.output) &&
102501
102726
  depForEachResult.output[itemIndex] !== undefined) {
102502
- // Unwrap to the item at the current index
102727
+ // Fallback to output-only unwrapping
102503
102728
  const modifiedResult = {
102504
102729
  issues: [],
102505
102730
  output: depForEachResult.output[itemIndex],
102506
102731
  };
102507
102732
  forEachDependencyResults.set(depName, modifiedResult);
102508
- // Also provide -raw access to the full array
102509
102733
  const rawResult = {
102510
102734
  issues: [],
102511
102735
  output: depForEachResult.output,
@@ -102521,6 +102745,55 @@ class CheckExecutionEngine {
102521
102745
  forEachDependencyResults.set(depName, depResult);
102522
102746
  }
102523
102747
  }
102748
+ // Per-item dependency gating for forEach parents: if a dependency failed for this item, skip this iteration
102749
+ if ((checkConfig.depends_on || []).length > 0) {
102750
+ const directDeps = checkConfig.depends_on || [];
102751
+ for (const depId of directDeps) {
102752
+ if (!forEachParents.includes(depId))
102753
+ continue;
102754
+ const depItemRes = forEachDependencyResults.get(depId);
102755
+ if (!depItemRes)
102756
+ continue;
102757
+ const wasSkippedDep = (depItemRes.issues || []).some(i => (i.ruleId || '').endsWith('/__skipped'));
102758
+ let hasFatalDepFailure = (depItemRes.issues || []).some(issue => {
102759
+ const id = issue.ruleId || '';
102760
+ return (id === 'command/execution_error' ||
102761
+ id.endsWith('/command/execution_error') ||
102762
+ id === 'command/timeout' ||
102763
+ id.endsWith('/command/timeout') ||
102764
+ id === 'command/transform_js_error' ||
102765
+ id.endsWith('/command/transform_js_error') ||
102766
+ id === 'command/transform_error' ||
102767
+ id.endsWith('/command/transform_error') ||
102768
+ id.endsWith('/forEach/iteration_error') ||
102769
+ id === 'forEach/undefined_output' ||
102770
+ id.endsWith('/forEach/undefined_output') ||
102771
+ id.endsWith('_fail_if') ||
102772
+ id.endsWith('/global_fail_if'));
102773
+ });
102774
+ if (!hasFatalDepFailure &&
102775
+ config &&
102776
+ (config.fail_if || config.checks[depId]?.fail_if)) {
102777
+ try {
102778
+ const depFailures = await this.evaluateFailureConditions(depId, depItemRes, config);
102779
+ hasFatalDepFailure = depFailures.some(f => f.failed);
102780
+ }
102781
+ catch { }
102782
+ }
102783
+ const depAgg = dependencyResults.get(depId);
102784
+ const maskFatal = !!depAgg?.forEachFatalMask && depAgg.forEachFatalMask[itemIndex] === true;
102785
+ if (wasSkippedDep || hasFatalDepFailure || maskFatal) {
102786
+ if (debug) {
102787
+ log(`🔄 Debug: Skipping item ${itemIndex + 1}/${forEachItems.length} for check "${checkName}" due to failed dependency '${depId}'`);
102788
+ }
102789
+ return {
102790
+ index: itemIndex,
102791
+ itemResult: { issues: [] },
102792
+ skipped: true,
102793
+ };
102794
+ }
102795
+ }
102796
+ }
102524
102797
  // Evaluate if condition for this forEach item
102525
102798
  if (checkConfig.if) {
102526
102799
  // Merge current results with forEach-specific dependency results for condition evaluation
@@ -102553,6 +102826,24 @@ class CheckExecutionEngine {
102553
102826
  total: forEachItems.length,
102554
102827
  parent: forEachParentName,
102555
102828
  });
102829
+ // no-op
102830
+ // Evaluate fail_if per item so a single failing branch does not stop others
102831
+ if (config && (config.fail_if || checkConfig.fail_if)) {
102832
+ const itemFailures = await this.evaluateFailureConditions(checkName, itemResult, config);
102833
+ if (itemFailures.length > 0) {
102834
+ const failureIssues = itemFailures
102835
+ .filter(f => f.failed)
102836
+ .map(f => ({
102837
+ file: 'system',
102838
+ line: 0,
102839
+ ruleId: f.conditionName,
102840
+ message: f.message || `Failure condition met: ${f.expression}`,
102841
+ severity: (f.severity || 'error'),
102842
+ category: 'logic',
102843
+ }));
102844
+ itemResult.issues = [...(itemResult.issues || []), ...failureIssues];
102845
+ }
102846
+ }
102556
102847
  // Record iteration completion
102557
102848
  // Check if this iteration had fatal errors
102558
102849
  const hadFatalError = (itemResult.issues || []).some(issue => {
@@ -102569,15 +102860,299 @@ class CheckExecutionEngine {
102569
102860
  const iterationDuration = (Date.now() - iterationStart) / 1000;
102570
102861
  this.recordIterationComplete(checkName, iterationStart, !hadFatalError, // Success if no fatal errors
102571
102862
  itemResult.issues || [], itemResult.output);
102863
+ // General branch-first scheduling for this item: execute all descendants (from current node only) when ready
102864
+ const descendantSet = (() => {
102865
+ const visited = new Set();
102866
+ const stack = [checkName];
102867
+ while (stack.length) {
102868
+ const p = stack.pop();
102869
+ const kids = childrenByParent.get(p) || [];
102870
+ for (const k of kids) {
102871
+ if (!visited.has(k)) {
102872
+ visited.add(k);
102873
+ stack.push(k);
102874
+ }
102875
+ }
102876
+ }
102877
+ return visited;
102878
+ })();
102879
+ const perItemDone = new Set([...forEachParents, checkName]);
102880
+ const perItemDepMap = new Map();
102881
+ for (const [k, v] of forEachDependencyResults)
102882
+ perItemDepMap.set(k, v);
102883
+ perItemDepMap.set(checkName, itemResult);
102884
+ const isFatal = (r) => {
102885
+ if (!r)
102886
+ return true;
102887
+ return this.hasFatal(r.issues || []);
102888
+ };
102889
+ while (true) {
102890
+ let progressed = false;
102891
+ for (const node of descendantSet) {
102892
+ if (perItemDone.has(node))
102893
+ continue;
102894
+ const nodeCfg = config.checks[node];
102895
+ if (!nodeCfg)
102896
+ continue;
102897
+ const deps = dependencies[node] || [];
102898
+ // Are all deps satisfied for this item?
102899
+ let ready = true;
102900
+ const childDepsMap = new Map();
102901
+ for (const d of deps) {
102902
+ // Prefer per-item result if already computed in this branch
102903
+ const perItemRes = perItemDepMap.get(d);
102904
+ if (perItemRes) {
102905
+ if (isFatal(perItemRes)) {
102906
+ ready = false;
102907
+ break;
102908
+ }
102909
+ childDepsMap.set(d, perItemRes);
102910
+ continue;
102911
+ }
102912
+ const agg = results.get(d);
102913
+ // If dependency is a forEach-capable result, require per-item unwrap
102914
+ if (agg && (agg.isForEach || Array.isArray(agg.forEachItemResults))) {
102915
+ const r = (agg.forEachItemResults && agg.forEachItemResults[itemIndex]) ||
102916
+ undefined;
102917
+ const maskFatal = !!agg.forEachFatalMask && agg.forEachFatalMask[itemIndex] === true;
102918
+ if (!r || maskFatal || isFatal(r)) {
102919
+ ready = false;
102920
+ break;
102921
+ }
102922
+ childDepsMap.set(d, r);
102923
+ continue;
102924
+ }
102925
+ // Fallback: use global dependency result (non-forEach)
102926
+ if (!agg || isFatal(agg)) {
102927
+ ready = false;
102928
+ break;
102929
+ }
102930
+ childDepsMap.set(d, agg);
102931
+ }
102932
+ if (!ready)
102933
+ continue;
102934
+ // if condition per item
102935
+ if (nodeCfg.if) {
102936
+ const condResults = new Map(results);
102937
+ for (const [k, v] of childDepsMap)
102938
+ condResults.set(k, v);
102939
+ const shouldRun = await this.evaluateCheckCondition(node, nodeCfg.if, prInfo, condResults, debug);
102940
+ if (!shouldRun) {
102941
+ perItemDone.add(node);
102942
+ progressed = true;
102943
+ continue;
102944
+ }
102945
+ }
102946
+ // Execute node for this item
102947
+ const nodeProvType = nodeCfg.type || 'ai';
102948
+ const nodeProv = this.providerRegistry.getProviderOrThrow(nodeProvType);
102949
+ this.setProviderWebhookContext(nodeProv);
102950
+ const nodeProviderConfig = {
102951
+ type: nodeProvType,
102952
+ prompt: nodeCfg.prompt,
102953
+ exec: nodeCfg.exec,
102954
+ focus: nodeCfg.focus || this.mapCheckNameToFocus(node),
102955
+ schema: nodeCfg.schema,
102956
+ group: nodeCfg.group,
102957
+ checkName: node,
102958
+ eventContext: prInfo.eventContext,
102959
+ transform: nodeCfg.transform,
102960
+ transform_js: nodeCfg.transform_js,
102961
+ env: nodeCfg.env,
102962
+ forEach: nodeCfg.forEach,
102963
+ ai: { timeout: timeout || 600000, debug: debug, ...(nodeCfg.ai || {}) },
102964
+ };
102965
+ const iterStart = this.recordIterationStart(node);
102966
+ // Expand dependency map for execution context to include transitive ancestors
102967
+ const execDepMap = new Map(childDepsMap);
102968
+ const nodeAllDeps = dependency_resolver_1.DependencyResolver.getAllDependencies(node, dependencyGraph.nodes);
102969
+ for (const dep of nodeAllDeps) {
102970
+ if (execDepMap.has(dep))
102971
+ continue;
102972
+ // Prefer per-item map first
102973
+ const perItemRes = perItemDepMap.get(dep);
102974
+ if (perItemRes) {
102975
+ execDepMap.set(dep, perItemRes);
102976
+ continue;
102977
+ }
102978
+ const agg = results.get(dep);
102979
+ if (!agg)
102980
+ continue;
102981
+ if (agg &&
102982
+ (agg.isForEach ||
102983
+ Array.isArray(agg.forEachItemResults) ||
102984
+ Array.isArray(agg.output))) {
102985
+ if (Array.isArray(agg.forEachItemResults) &&
102986
+ agg.forEachItemResults[itemIndex]) {
102987
+ execDepMap.set(dep, agg.forEachItemResults[itemIndex]);
102988
+ }
102989
+ else if (Array.isArray(agg.output) &&
102990
+ agg.output[itemIndex] !== undefined) {
102991
+ execDepMap.set(dep, {
102992
+ issues: [],
102993
+ output: agg.output[itemIndex],
102994
+ });
102995
+ }
102996
+ else {
102997
+ execDepMap.set(dep, agg);
102998
+ }
102999
+ }
103000
+ else {
103001
+ execDepMap.set(dep, agg);
103002
+ }
103003
+ }
103004
+ const nodeItemRes = await this.executeWithRouting(node, nodeCfg, nodeProv, nodeProviderConfig, prInfo, execDepMap, sessionInfo, config, dependencyGraph, debug, results, { index: itemIndex, total: forEachItems.length, parent: forEachParentName });
103005
+ if (config && (config.fail_if || nodeCfg.fail_if)) {
103006
+ const fRes = await this.evaluateFailureConditions(node, nodeItemRes, config);
103007
+ if (fRes.length > 0) {
103008
+ const fIssues = fRes
103009
+ .filter(f => f.failed)
103010
+ .map(f => ({
103011
+ file: 'system',
103012
+ line: 0,
103013
+ ruleId: f.conditionName,
103014
+ message: f.message || `Failure condition met: ${f.expression}`,
103015
+ severity: (f.severity || 'error'),
103016
+ category: 'logic',
103017
+ }));
103018
+ nodeItemRes.issues = [...(nodeItemRes.issues || []), ...fIssues];
103019
+ }
103020
+ }
103021
+ const hadFatal = isFatal(nodeItemRes);
103022
+ this.recordIterationComplete(node, iterStart, !hadFatal, nodeItemRes.issues || [], nodeItemRes.output);
103023
+ // Aggregate results for this node across items
103024
+ if (!inlineAgg.has(node))
103025
+ inlineAgg.set(node, {
103026
+ issues: [],
103027
+ outputs: [],
103028
+ contents: [],
103029
+ perItemResults: [],
103030
+ });
103031
+ const agg = inlineAgg.get(node);
103032
+ if (nodeItemRes.issues)
103033
+ agg.issues.push(...nodeItemRes.issues);
103034
+ const nout = nodeItemRes.output;
103035
+ if (nout !== undefined)
103036
+ agg.outputs.push(nout);
103037
+ agg.perItemResults.push(nodeItemRes);
103038
+ const ncontent = nodeItemRes.content;
103039
+ if (typeof ncontent === 'string' && ncontent.trim())
103040
+ agg.contents.push(ncontent.trim());
103041
+ perItemDepMap.set(node, nodeItemRes);
103042
+ perItemDone.add(node);
103043
+ progressed = true;
103044
+ }
103045
+ if (!progressed)
103046
+ break;
103047
+ }
102572
103048
  // Log iteration progress
102573
103049
  logger_1.logger.info(` ✔ ${itemIndex + 1}/${forEachItems.length} (${iterationDuration.toFixed(1)}s)`);
103050
+ perItemResults[itemIndex] = itemResult;
102574
103051
  return { index: itemIndex, itemResult };
102575
103052
  });
102576
- const forEachConcurrency = Math.max(1, Math.min(forEachItems.length, effectiveMaxParallelism));
103053
+ // Determine runnable indices by intersecting masks across all direct forEach parents
103054
+ const directForEachParents = (checkConfig.depends_on || []).filter(dep => {
103055
+ const r = results.get(dep);
103056
+ return (!!r &&
103057
+ (r.isForEach ||
103058
+ Array.isArray(r.forEachItemResults) ||
103059
+ Array.isArray(r.forEachItems)));
103060
+ });
103061
+ if (directForEachParents.length > 0) {
103062
+ logger_1.logger.debug(` forEach: direct parents for "${checkName}": ${directForEachParents.join(', ')}`);
103063
+ }
103064
+ const isIndexFatalForParent = async (parent, idx) => {
103065
+ const agg = results.get(parent);
103066
+ if (!agg)
103067
+ return false; // if missing, do not gate
103068
+ if (agg.forEachFatalMask && agg.forEachFatalMask[idx] === true)
103069
+ return true;
103070
+ const r = (agg.forEachItemResults && agg.forEachItemResults[idx]) || undefined;
103071
+ if (!r)
103072
+ return false;
103073
+ // 1) Issues-based fatality (provider/transform/timeout/fail_if markers)
103074
+ const hadFatalByIssues = this.hasFatal(r.issues || []);
103075
+ if (hadFatalByIssues)
103076
+ return true;
103077
+ // 2) Fail_if based fatality evaluated directly on the parent per-item result
103078
+ try {
103079
+ if (config && (config.fail_if || config.checks[parent]?.fail_if)) {
103080
+ // If output is a string, try parsing JSON (full or tail) to honor fail_if semantics
103081
+ let rForEval = r;
103082
+ const rawOut = r?.output;
103083
+ if (typeof rawOut === 'string') {
103084
+ const parseTail = (text) => {
103085
+ try {
103086
+ const lines = text.split('\n');
103087
+ for (let i = lines.length - 1; i >= 0; i--) {
103088
+ const t = lines[i].trim();
103089
+ if (t.startsWith('{') || t.startsWith('[')) {
103090
+ const candidate = lines.slice(i).join('\n').trim();
103091
+ if ((candidate.startsWith('{') && candidate.endsWith('}')) ||
103092
+ (candidate.startsWith('[') && candidate.endsWith(']'))) {
103093
+ return JSON.parse(candidate);
103094
+ }
103095
+ }
103096
+ }
103097
+ }
103098
+ catch { }
103099
+ try {
103100
+ return JSON.parse(text);
103101
+ }
103102
+ catch {
103103
+ return null;
103104
+ }
103105
+ };
103106
+ const parsed = parseTail(rawOut);
103107
+ if (parsed && typeof parsed === 'object') {
103108
+ rForEval = { ...r, output: parsed };
103109
+ }
103110
+ }
103111
+ const failures = await this.evaluateFailureConditions(parent, rForEval, config);
103112
+ if (failures.some(f => f.failed)) {
103113
+ // Temporary: surface why index is gated
103114
+ }
103115
+ if (failures.some(f => f.failed))
103116
+ return true;
103117
+ }
103118
+ }
103119
+ catch { }
103120
+ return false;
103121
+ };
103122
+ const runnableIndices = [];
103123
+ for (let idx = 0; idx < forEachItems.length; idx++) {
103124
+ let ok = true;
103125
+ for (const p of directForEachParents) {
103126
+ if (await isIndexFatalForParent(p, idx)) {
103127
+ ok = false;
103128
+ break;
103129
+ }
103130
+ }
103131
+ // Only schedule indices that have a corresponding task function
103132
+ if (ok && typeof itemTasks[idx] === 'function')
103133
+ runnableIndices.push(idx);
103134
+ }
103135
+ // no-op
103136
+ // Early skip if no runnable items after intersecting masks across all direct forEach parents
103137
+ if (runnableIndices.length === 0) {
103138
+ this.recordSkip(checkName, 'dependency_failed');
103139
+ logger_1.logger.info(`⏭ Skipped (dependency failed: no runnable items)`);
103140
+ return {
103141
+ checkName,
103142
+ error: null,
103143
+ result: { issues: [] },
103144
+ skipped: true,
103145
+ };
103146
+ }
103147
+ const forEachConcurrency = Math.max(1, Math.min(runnableIndices.length, effectiveMaxParallelism));
102577
103148
  if (debug && forEachConcurrency > 1) {
102578
103149
  log(`🔄 Debug: Limiting forEach concurrency for check "${checkName}" to ${forEachConcurrency}`);
102579
103150
  }
102580
- const forEachResults = await this.executeWithLimitedParallelism(itemTasks, forEachConcurrency, false);
103151
+ const scheduledTasks = runnableIndices
103152
+ .map(i => itemTasks[i])
103153
+ .filter(fn => typeof fn === 'function');
103154
+ const forEachResults = await this.executeWithLimitedParallelism(scheduledTasks, forEachConcurrency, false);
103155
+ let processedCount = 0;
102581
103156
  for (const result of forEachResults) {
102582
103157
  if (result.status === 'rejected') {
102583
103158
  // Instead of throwing, record the failure and continue with other iterations
@@ -102597,56 +103172,137 @@ class CheckExecutionEngine {
102597
103172
  }
102598
103173
  continue;
102599
103174
  }
102600
- // Skip results from skipped items (those that failed if condition)
103175
+ // Skip results from skipped items (those gated by dependencies/if)
102601
103176
  if (result.value.skipped) {
102602
103177
  continue;
102603
103178
  }
102604
- const { itemResult } = result.value;
103179
+ const { index: finishedIndex, itemResult } = result.value;
103180
+ processedCount++;
102605
103181
  if (itemResult.issues) {
102606
103182
  allIssues.push(...itemResult.issues);
102607
103183
  }
102608
103184
  const resultWithOutput = itemResult;
102609
- if (resultWithOutput.output !== undefined) {
102610
- allOutputs.push(resultWithOutput.output);
102611
- }
103185
+ allOutputs[finishedIndex] = resultWithOutput.output;
102612
103186
  const itemContent = resultWithOutput.content;
102613
103187
  if (typeof itemContent === 'string' && itemContent.trim()) {
102614
103188
  aggregatedContents.push(itemContent.trim());
102615
103189
  }
103190
+ else {
103191
+ const outStr = typeof resultWithOutput.output === 'string'
103192
+ ? resultWithOutput.output.trim()
103193
+ : '';
103194
+ if (outStr)
103195
+ aggregatedContents.push(outStr);
103196
+ }
103197
+ }
103198
+ // If no items were processed (all gated), mark this check as skipped for dependency_failed
103199
+ if (processedCount === 0) {
103200
+ this.recordSkip(checkName, 'dependency_failed');
103201
+ logger_1.logger.info(`⏭ Skipped (dependency failed for all items)`);
103202
+ return {
103203
+ checkName,
103204
+ error: null,
103205
+ result: { issues: [] },
103206
+ skipped: true,
103207
+ };
102616
103208
  }
102617
103209
  const finalOutput = allOutputs.length > 0 ? allOutputs : undefined;
102618
103210
  finalResult = {
102619
103211
  issues: allIssues,
102620
103212
  ...(finalOutput !== undefined ? { output: finalOutput } : {}),
102621
103213
  };
102622
- // Evaluate fail_if for aggregated forEach results (applies to the whole check)
102623
- if (config && (config.fail_if || checkConfig.fail_if)) {
102624
- const failureResults = await this.evaluateFailureConditions(checkName, finalResult, config);
102625
- if (failureResults.length > 0) {
102626
- const failureIssues = failureResults
102627
- .filter(f => f.failed)
102628
- .map(f => ({
102629
- file: 'system',
102630
- line: 0,
102631
- ruleId: f.conditionName,
102632
- message: f.message || `Failure condition met: ${f.expression}`,
102633
- severity: (f.severity || 'error'),
102634
- category: 'logic',
102635
- }));
102636
- finalResult.issues = [...(finalResult.issues || []), ...failureIssues];
102637
- }
102638
- }
102639
- // IMPORTANT: Mark this result as forEach-capable so that checks depending on it
102640
- // will also iterate over the items (propagate forEach behavior down the chain)
102641
- if (allOutputs.length > 0) {
102642
- finalResult.isForEach = true;
102643
- finalResult.forEachItems = allOutputs;
103214
+ // Mark this result as forEach-capable and attach per-item results for precise downstream gating
103215
+ finalResult.isForEach = true;
103216
+ finalResult.forEachItems = allOutputs;
103217
+ finalResult.forEachItemResults =
103218
+ perItemResults;
103219
+ // Compute fatal mask
103220
+ try {
103221
+ const mask = finalResult.forEachItemResults
103222
+ ? await Promise.all(Array.from({ length: forEachItems.length }, async (_, idx) => {
103223
+ const r = finalResult.forEachItemResults[idx];
103224
+ if (!r)
103225
+ return false; // no result (skipped) not fatal for descendants
103226
+ let hadFatal = this.hasFatal(r.issues || []);
103227
+ try {
103228
+ const ids = (r.issues || []).map(i => i.ruleId).join(',');
103229
+ logger_1.logger.debug(` forEach: item ${idx + 1}/${forEachItems.length} issues=${(r.issues || []).length} ids=[${ids}]`);
103230
+ }
103231
+ catch { }
103232
+ if (!hadFatal && config && (config.fail_if || checkConfig.fail_if)) {
103233
+ try {
103234
+ const failures = await this.evaluateFailureConditions(checkName, r, config);
103235
+ hadFatal = failures.some(f => f.failed);
103236
+ }
103237
+ catch { }
103238
+ }
103239
+ return hadFatal;
103240
+ }))
103241
+ : [];
103242
+ finalResult.forEachFatalMask = mask;
103243
+ logger_1.logger.debug(` forEach: mask for "${checkName}" → fatals=${mask.filter(Boolean).length}/${mask.length}`);
102644
103244
  }
103245
+ catch { }
102645
103246
  if (aggregatedContents.length > 0) {
102646
103247
  finalResult.content =
102647
103248
  aggregatedContents.join('\n');
102648
103249
  }
102649
- log(`🔄 Debug: Completed forEach execution for check "${checkName}", total issues: ${allIssues.length}`);
103250
+ // Finalize inline descendant aggregations to full results, so later levels skip them
103251
+ for (const [childName, agg] of inlineAgg.entries()) {
103252
+ const childCfg = config.checks[childName];
103253
+ const childEnrichedIssues = (agg.issues || []).map(issue => ({
103254
+ ...issue,
103255
+ checkName: childName,
103256
+ ruleId: `${childName}/${issue.ruleId}`,
103257
+ group: childCfg.group,
103258
+ schema: typeof childCfg.schema === 'object' ? 'custom' : childCfg.schema,
103259
+ template: childCfg.template,
103260
+ timestamp: Date.now(),
103261
+ }));
103262
+ const childFinal = {
103263
+ issues: childEnrichedIssues,
103264
+ ...(agg.outputs.length > 0 ? { output: agg.outputs } : {}),
103265
+ isForEach: true,
103266
+ forEachItems: agg.outputs,
103267
+ forEachItemResults: agg.perItemResults,
103268
+ ...(agg.contents.length > 0 ? { content: agg.contents.join('\n') } : {}),
103269
+ };
103270
+ // Compute fatal mask for child aggregate
103271
+ try {
103272
+ const mask = Array.from({ length: agg.perItemResults.length }, (_, idx) => {
103273
+ const r = agg.perItemResults[idx];
103274
+ if (!r)
103275
+ return false; // skipped item is not fatal for descendants
103276
+ const hadFatal = (r.issues || []).some(issue => {
103277
+ const id = issue.ruleId || '';
103278
+ return (issue.severity === 'error' ||
103279
+ issue.severity === 'critical' ||
103280
+ id === 'command/execution_error' ||
103281
+ id.endsWith('/command/execution_error') ||
103282
+ id === 'command/timeout' ||
103283
+ id.endsWith('/command/timeout') ||
103284
+ id === 'command/transform_js_error' ||
103285
+ id.endsWith('/command/transform_js_error') ||
103286
+ id === 'command/transform_error' ||
103287
+ id.endsWith('/command/transform_error') ||
103288
+ id.endsWith('/forEach/iteration_error') ||
103289
+ id === 'forEach/undefined_output' ||
103290
+ id.endsWith('/forEach/undefined_output') ||
103291
+ id.endsWith('_fail_if') ||
103292
+ id.endsWith('/global_fail_if'));
103293
+ });
103294
+ return hadFatal;
103295
+ });
103296
+ childFinal.forEachFatalMask = mask;
103297
+ }
103298
+ catch { }
103299
+ results.set(childName, childFinal);
103300
+ }
103301
+ if (debug &&
103302
+ process.env.VISOR_OUTPUT_FORMAT !== 'json' &&
103303
+ process.env.VISOR_OUTPUT_FORMAT !== 'sarif') {
103304
+ console.log(`🔄 Debug: Completed forEach execution for check "${checkName}", total issues: ${allIssues.length}`);
103305
+ }
102650
103306
  } // End of else block for forEachItems.length > 0
102651
103307
  }
102652
103308
  else {
@@ -102767,7 +103423,7 @@ class CheckExecutionEngine {
102767
103423
  };
102768
103424
  }
102769
103425
  catch (error) {
102770
- const errorMessage = error instanceof Error ? error.message : String(error);
103426
+ const errorMessage = error instanceof Error ? `${error.message}\n${error.stack || ''}` : String(error);
102771
103427
  const checkDuration = ((Date.now() - checkStartTime) / 1000).toFixed(1);
102772
103428
  // Record error in stats
102773
103429
  this.recordError(checkName, error instanceof Error ? error : new Error(String(error)));
@@ -102786,8 +103442,9 @@ class CheckExecutionEngine {
102786
103442
  // Execute checks in this level with controlled parallelism
102787
103443
  const levelResults = await this.executeWithLimitedParallelism(levelTaskFunctions, actualParallelism, effectiveFailFast);
102788
103444
  // Process results and store them for next level
103445
+ const levelChecksList = executionGroup.parallel.filter(name => !results.has(name));
102789
103446
  for (let i = 0; i < levelResults.length; i++) {
102790
- const checkName = executionGroup.parallel[i];
103447
+ const checkName = levelChecksList[i];
102791
103448
  const result = levelResults[i];
102792
103449
  const checkConfig = config.checks[checkName];
102793
103450
  if (result.status === 'fulfilled' && result.value.result && !result.value.error) {
@@ -103920,11 +104577,28 @@ class CheckExecutionEngine {
103920
104577
  */
103921
104578
  recordForEachPreview(checkName, items) {
103922
104579
  const stats = this.executionStats.get(checkName);
103923
- if (!stats || !items.length)
104580
+ if (!stats)
104581
+ return;
104582
+ if (!Array.isArray(items) || items.length === 0)
103924
104583
  return;
103925
104584
  // Store preview of first 3 items
103926
104585
  const preview = items.slice(0, 3).map(item => {
103927
- const str = typeof item === 'string' ? item : JSON.stringify(item);
104586
+ let str;
104587
+ if (typeof item === 'string') {
104588
+ str = item;
104589
+ }
104590
+ else if (item === undefined || item === null) {
104591
+ str = '(empty)';
104592
+ }
104593
+ else {
104594
+ try {
104595
+ const j = JSON.stringify(item);
104596
+ str = typeof j === 'string' ? j : String(item);
104597
+ }
104598
+ catch {
104599
+ str = String(item);
104600
+ }
104601
+ }
103928
104602
  return str.length > 50 ? str.substring(0, 47) + '...' : str;
103929
104603
  });
103930
104604
  if (items.length > 3) {
@@ -103961,6 +104635,36 @@ class CheckExecutionEngine {
103961
104635
  checks,
103962
104636
  };
103963
104637
  }
104638
+ // Generic fatality helpers to avoid duplication
104639
+ isFatalRule(id, severity) {
104640
+ const sev = (severity || '').toLowerCase();
104641
+ return (sev === 'error' ||
104642
+ sev === 'critical' ||
104643
+ id === 'command/execution_error' ||
104644
+ id.endsWith('/command/execution_error') ||
104645
+ id === 'command/timeout' ||
104646
+ id.endsWith('/command/timeout') ||
104647
+ id === 'command/transform_js_error' ||
104648
+ id.endsWith('/command/transform_js_error') ||
104649
+ id === 'command/transform_error' ||
104650
+ id.endsWith('/command/transform_error') ||
104651
+ id.endsWith('/forEach/iteration_error') ||
104652
+ id === 'forEach/undefined_output' ||
104653
+ id.endsWith('/forEach/undefined_output') ||
104654
+ id.endsWith('_fail_if') ||
104655
+ id.endsWith('/global_fail_if'));
104656
+ }
104657
+ hasFatal(issues) {
104658
+ if (!issues || issues.length === 0)
104659
+ return false;
104660
+ return issues.some(i => this.isFatalRule(i.ruleId || '', i.severity));
104661
+ }
104662
+ async failIfTriggered(checkName, result, config) {
104663
+ if (!config)
104664
+ return false;
104665
+ const failures = await this.evaluateFailureConditions(checkName, result, config);
104666
+ return failures.some(f => f.failed);
104667
+ }
103964
104668
  /**
103965
104669
  * Truncate a string to max length with ellipsis
103966
104670
  */
@@ -106196,7 +106900,17 @@ class FailureConditionEvaluator {
106196
106900
  async evaluateSimpleCondition(checkName, checkSchema, checkGroup, reviewSummary, expression, previousOutputs) {
106197
106901
  const context = this.buildEvaluationContext(checkName, checkSchema, checkGroup, reviewSummary, previousOutputs);
106198
106902
  try {
106199
- return this.evaluateExpression(expression, context);
106903
+ try {
106904
+ const isObj = context.output && typeof context.output === 'object';
106905
+ const keys = isObj ? Object.keys(context.output).join(',') : typeof context.output;
106906
+ let errorVal = undefined;
106907
+ if (isObj && context.output.error !== undefined)
106908
+ errorVal = context.output.error;
106909
+ (__nccwpck_require__(86999).logger).debug(` fail_if: evaluating '${expression}' with output keys=${keys} error=${String(errorVal)}`);
106910
+ }
106911
+ catch { }
106912
+ const res = this.evaluateExpression(expression, context);
106913
+ return res;
106200
106914
  }
106201
106915
  catch (error) {
106202
106916
  console.warn(`Failed to evaluate fail_if expression: ${error}`);
@@ -106303,6 +107017,12 @@ class FailureConditionEvaluator {
106303
107017
  results.length = 0;
106304
107018
  results.push(...filteredResults, ...checkResults);
106305
107019
  }
107020
+ try {
107021
+ if (checkName === 'B') {
107022
+ console.error(`🔧 Debug: fail_if results for ${checkName}: ${JSON.stringify(results)} context.output=${JSON.stringify(context.output)}`);
107023
+ }
107024
+ }
107025
+ catch { }
106306
107026
  return results;
106307
107027
  }
106308
107028
  /**
@@ -106521,6 +107241,10 @@ class FailureConditionEvaluator {
106521
107241
  exec = this.sandbox.compile(`return (${normalizedExpr});`);
106522
107242
  }
106523
107243
  const result = exec(scope).run();
107244
+ try {
107245
+ (__nccwpck_require__(86999).logger).debug(` fail_if: result=${Boolean(result)}`);
107246
+ }
107247
+ catch { }
106524
107248
  // Ensure we return a boolean
106525
107249
  return Boolean(result);
106526
107250
  }
@@ -106592,6 +107316,73 @@ class FailureConditionEvaluator {
106592
107316
  else if (extractedOutput && typeof extractedOutput === 'object') {
106593
107317
  Object.assign(aggregatedOutput, extractedOutput);
106594
107318
  }
107319
+ // If provider attached a raw transform snapshot, merge its fields generically.
107320
+ try {
107321
+ const raw = reviewSummaryWithOutput.__raw;
107322
+ if (raw && typeof raw === 'object') {
107323
+ Object.assign(aggregatedOutput, raw);
107324
+ }
107325
+ }
107326
+ catch { }
107327
+ // If output is a string, try to parse JSON (full or from end) to enrich context,
107328
+ // and also derive common boolean flags generically (e.g., key:true/false) for fail_if usage.
107329
+ try {
107330
+ if (typeof extractedOutput === 'string') {
107331
+ const parsed = this.tryExtractJsonFromEnd(extractedOutput) ??
107332
+ (() => {
107333
+ try {
107334
+ return JSON.parse(extractedOutput);
107335
+ }
107336
+ catch {
107337
+ return null;
107338
+ }
107339
+ })();
107340
+ if (parsed !== null) {
107341
+ if (Array.isArray(parsed)) {
107342
+ aggregatedOutput.items = parsed;
107343
+ }
107344
+ else if (typeof parsed === 'object') {
107345
+ Object.assign(aggregatedOutput, parsed);
107346
+ }
107347
+ }
107348
+ // Generic boolean key extraction for simple text outputs (no special provider cases)
107349
+ const lower = extractedOutput.toLowerCase();
107350
+ const boolFrom = (key) => {
107351
+ const reTrue = new RegExp(`(?:^|[^a-z0-9_])${key}[^a-z0-9_]*[:=][^a-z0-9_]*true(?:[^a-z0-9_]|$)`);
107352
+ const reFalse = new RegExp(`(?:^|[^a-z0-9_])${key}[^a-z0-9_]*[:=][^a-z0-9_]*false(?:[^a-z0-9_]|$)`);
107353
+ if (reTrue.test(lower))
107354
+ return true;
107355
+ if (reFalse.test(lower))
107356
+ return false;
107357
+ return null;
107358
+ };
107359
+ const keys = ['error'];
107360
+ for (const k of keys) {
107361
+ const v = boolFrom(k);
107362
+ if (v !== null && aggregatedOutput[k] === undefined) {
107363
+ aggregatedOutput[k] = v;
107364
+ }
107365
+ }
107366
+ }
107367
+ }
107368
+ catch { }
107369
+ // Try to parse JSON from content as a last resort when no structured output is present
107370
+ try {
107371
+ const rsAny = reviewSummaryWithOutput;
107372
+ const hasStructuredOutput = extractedOutput !== undefined && extractedOutput !== null;
107373
+ if (!hasStructuredOutput && typeof rsAny?.content === 'string') {
107374
+ const parsedFromContent = this.tryExtractJsonFromEnd(rsAny.content);
107375
+ if (parsedFromContent !== null && parsedFromContent !== undefined) {
107376
+ if (Array.isArray(parsedFromContent)) {
107377
+ aggregatedOutput.items = parsedFromContent;
107378
+ }
107379
+ else if (typeof parsedFromContent === 'object') {
107380
+ Object.assign(aggregatedOutput, parsedFromContent);
107381
+ }
107382
+ }
107383
+ }
107384
+ }
107385
+ catch { }
106595
107386
  const context = {
106596
107387
  output: aggregatedOutput,
106597
107388
  outputs: (() => {
@@ -106622,6 +107413,24 @@ class FailureConditionEvaluator {
106622
107413
  }
106623
107414
  return context;
106624
107415
  }
107416
+ // Minimal JSON-from-end extractor for fail_if context fallback
107417
+ tryExtractJsonFromEnd(text) {
107418
+ try {
107419
+ const lines = text.split('\n');
107420
+ for (let i = lines.length - 1; i >= 0; i--) {
107421
+ const t = lines[i].trim();
107422
+ if (t.startsWith('{') || t.startsWith('[')) {
107423
+ const candidate = lines.slice(i).join('\n').trim();
107424
+ if ((candidate.startsWith('{') && candidate.endsWith('}')) ||
107425
+ (candidate.startsWith('[') && candidate.endsWith(']'))) {
107426
+ return JSON.parse(candidate);
107427
+ }
107428
+ }
107429
+ }
107430
+ }
107431
+ catch { }
107432
+ return null;
107433
+ }
106625
107434
  /**
106626
107435
  * Check if any failure condition requires halting execution
106627
107436
  */
@@ -110435,6 +111244,18 @@ function configureLoggerFromCli(options) {
110435
111244
  verbose: options.verbose,
110436
111245
  quiet: options.quiet,
110437
111246
  });
111247
+ // Expose output format and debug to process env for modules that need to gate
111248
+ // stdout emissions without plumbing the value through every call site.
111249
+ try {
111250
+ if (options.output)
111251
+ process.env.VISOR_OUTPUT_FORMAT = String(options.output);
111252
+ if (typeof options.debug === 'boolean') {
111253
+ process.env.VISOR_DEBUG = options.debug ? 'true' : 'false';
111254
+ }
111255
+ }
111256
+ catch {
111257
+ // ignore
111258
+ }
110438
111259
  }
110439
111260
 
110440
111261
 
@@ -112689,6 +113510,10 @@ class CommandCheckProvider extends check_provider_interface_1.CheckProvider {
112689
113510
  return true;
112690
113511
  }
112691
113512
  async execute(prInfo, config, dependencyResults) {
113513
+ try {
113514
+ logger_1.logger.info(` command provider: executing check=${String(config.checkName || config.type)} hasTransformJs=${Boolean(config.transform_js)}`);
113515
+ }
113516
+ catch { }
112692
113517
  const command = config.exec;
112693
113518
  const transform = config.transform;
112694
113519
  const transformJs = config.transform_js;
@@ -112746,6 +113571,7 @@ class CommandCheckProvider extends check_provider_interface_1.CheckProvider {
112746
113571
  // Keep raw output for transforms
112747
113572
  const rawOutput = stdout.trim();
112748
113573
  // Try to parse output as JSON for default behavior
113574
+ // no debug
112749
113575
  let output = rawOutput;
112750
113576
  try {
112751
113577
  // Attempt to parse as JSON
@@ -112755,38 +113581,40 @@ class CommandCheckProvider extends check_provider_interface_1.CheckProvider {
112755
113581
  }
112756
113582
  catch {
112757
113583
  // Try to extract JSON from the end of output (for commands with debug logs)
112758
- const extracted = this.extractJsonFromEnd(rawOutput);
112759
- if (extracted) {
113584
+ const extractedTail = this.extractJsonFromEnd(rawOutput);
113585
+ if (extractedTail) {
112760
113586
  try {
112761
- output = JSON.parse(extracted);
112762
- logger_1.logger.debug(`🔧 Debug: Extracted and parsed JSON from end of output (${extracted.length} chars from ${rawOutput.length} total)`);
112763
- logger_1.logger.debug(`🔧 Debug: Extracted JSON content: ${extracted.slice(0, 200)}`);
113587
+ output = JSON.parse(extractedTail);
112764
113588
  }
112765
- catch (parseError) {
112766
- // Extraction found something but it's not valid JSON
112767
- logger_1.logger.debug(`🔧 Debug: Extracted text is not valid JSON: ${parseError instanceof Error ? parseError.message : 'Unknown error'}`);
113589
+ catch {
112768
113590
  output = rawOutput;
112769
113591
  }
112770
113592
  }
112771
113593
  else {
112772
- // Not JSON, keep as string
112773
- logger_1.logger.debug(`🔧 Debug: No JSON found in output, keeping as string`);
112774
- output = rawOutput;
112775
- }
112776
- }
112777
- // Log the parsed structure for debugging
112778
- if (output !== rawOutput) {
112779
- try {
112780
- const outputType = Array.isArray(output) ? `array[${output.length}]` : typeof output;
112781
- logger_1.logger.debug(`🔧 Debug: Parsed output type: ${outputType}`);
112782
- if (typeof output === 'object' && output !== null) {
112783
- logger_1.logger.debug(`🔧 Debug: Parsed output keys: ${Object.keys(output).join(', ')}`);
113594
+ // Try to extract any balanced JSON substring anywhere
113595
+ const extractedAny = this.extractJsonAnywhere(rawOutput);
113596
+ if (extractedAny) {
113597
+ try {
113598
+ output = JSON.parse(extractedAny);
113599
+ }
113600
+ catch {
113601
+ output = rawOutput;
113602
+ }
113603
+ }
113604
+ else {
113605
+ // Last resort: detect common boolean flags like error:true or error=false for fail_if gating
113606
+ const m = /\berror\b\s*[:=]\s*(true|false)/i.exec(rawOutput);
113607
+ if (m) {
113608
+ output = { error: m[1].toLowerCase() === 'true' };
113609
+ }
113610
+ else {
113611
+ output = rawOutput;
113612
+ }
112784
113613
  }
112785
- }
112786
- catch {
112787
- // Ignore logging errors
112788
113614
  }
112789
113615
  }
113616
+ // Log the parsed structure for debugging
113617
+ // no debug
112790
113618
  // Apply transform if specified (Liquid or JavaScript)
112791
113619
  let finalOutput = output;
112792
113620
  // First apply Liquid transform if present
@@ -112839,64 +113667,147 @@ class CommandCheckProvider extends check_provider_interface_1.CheckProvider {
112839
113667
  // Compile and execute the JavaScript expression
112840
113668
  // Use direct property access instead of destructuring to avoid syntax issues
112841
113669
  const trimmedTransform = transformJs.trim();
112842
- let transformExpression;
112843
- if (/return\s+/.test(trimmedTransform)) {
112844
- transformExpression = `(() => {\n${trimmedTransform}\n})()`;
112845
- }
112846
- else {
112847
- const lines = trimmedTransform.split('\n');
112848
- if (lines.length > 1) {
112849
- const lastLine = lines[lines.length - 1].trim();
112850
- const remaining = lines.slice(0, -1).join('\n');
112851
- if (lastLine && !lastLine.includes('}') && !lastLine.includes('{')) {
112852
- const returnTarget = lastLine.replace(/;$/, '');
112853
- transformExpression = `(() => {\n${remaining}\nreturn ${returnTarget};\n})()`;
112854
- }
112855
- else {
112856
- transformExpression = `(${trimmedTransform})`;
112857
- }
113670
+ // Build a safe function body that supports statements + implicit last-expression return.
113671
+ const buildBodyWithReturn = (raw) => {
113672
+ const t = raw.trim();
113673
+ // Find last non-empty line
113674
+ const lines = t.split(/\n/);
113675
+ let i = lines.length - 1;
113676
+ while (i >= 0 && lines[i].trim().length === 0)
113677
+ i--;
113678
+ if (i < 0)
113679
+ return 'return undefined;';
113680
+ const lastLine = lines[i].trim();
113681
+ if (/^return\b/i.test(lastLine)) {
113682
+ return t;
112858
113683
  }
112859
- else {
112860
- transformExpression = `(${trimmedTransform})`;
112861
- }
112862
- }
113684
+ const idx = t.lastIndexOf(lastLine);
113685
+ const head = idx >= 0 ? t.slice(0, idx) : '';
113686
+ const lastExpr = lastLine.replace(/;\s*$/, '');
113687
+ return `${head}\nreturn (${lastExpr});`;
113688
+ };
113689
+ const bodyWithReturn = buildBodyWithReturn(trimmedTransform);
112863
113690
  const code = `
112864
113691
  const output = scope.output;
112865
113692
  const pr = scope.pr;
112866
113693
  const files = scope.files;
112867
113694
  const outputs = scope.outputs;
112868
113695
  const env = scope.env;
112869
- const log = (...args) => {
112870
- console.log('🔍 Debug:', ...args);
112871
- };
112872
- return ${transformExpression};
113696
+ const log = (...args) => { console.log('🔍 Debug:', ...args); };
113697
+ const __result = (function(){
113698
+ ${bodyWithReturn}
113699
+ })();
113700
+ return __result;
112873
113701
  `;
113702
+ // Execute user code exclusively inside the sandbox
113703
+ if (!this.sandbox) {
113704
+ this.sandbox = this.createSecureSandbox();
113705
+ }
113706
+ // Try to serialize result to JSON string inside sandbox to preserve primitives like booleans
113707
+ let parsedFromSandboxJson = undefined;
112874
113708
  try {
112875
- logger_1.logger.debug(`🔧 Debug: JavaScript transform code: ${code}`);
112876
- logger_1.logger.debug(`🔧 Debug: JavaScript context: ${JSON.stringify(jsContext).slice(0, 200)}`);
113709
+ const stringifyCode = `
113710
+ const output = scope.output;
113711
+ const pr = scope.pr;
113712
+ const files = scope.files;
113713
+ const outputs = scope.outputs;
113714
+ const env = scope.env;
113715
+ const log = (...args) => { console.log('🔍 Debug:', ...args); };
113716
+ const __ret = (function(){
113717
+ ${bodyWithReturn}
113718
+ })();
113719
+ return typeof __ret === 'object' && __ret !== null ? JSON.stringify(__ret) : null;
113720
+ `;
113721
+ const stringifyExec = this.sandbox.compile(stringifyCode);
113722
+ const jsonStr = stringifyExec({ scope: jsContext }).run();
113723
+ if (typeof jsonStr === 'string' && jsonStr.trim().startsWith('{')) {
113724
+ parsedFromSandboxJson = JSON.parse(jsonStr);
113725
+ }
112877
113726
  }
112878
- catch {
112879
- // Ignore logging errors
113727
+ catch { }
113728
+ if (parsedFromSandboxJson !== undefined) {
113729
+ finalOutput = parsedFromSandboxJson;
112880
113730
  }
112881
- if (!this.sandbox) {
112882
- this.sandbox = this.createSecureSandbox();
113731
+ else {
113732
+ const exec = this.sandbox.compile(code);
113733
+ finalOutput = exec({ scope: jsContext }).run();
112883
113734
  }
112884
- const exec = this.sandbox.compile(code);
112885
- finalOutput = exec({ scope: jsContext }).run();
112886
- logger_1.logger.verbose(`✓ Applied JavaScript transform successfully`);
113735
+ // Fallback: if sandbox could not preserve primitives (e.g., booleans lost),
113736
+ // attempt to re-evaluate the transform in a locked Node VM context to get plain JS values.
112887
113737
  try {
112888
- const preview = JSON.stringify(finalOutput);
112889
- logger_1.logger.debug(`🔧 Debug: transform_js result: ${typeof preview === 'string' ? preview.slice(0, 200) : String(preview).slice(0, 200)}`);
113738
+ if (finalOutput &&
113739
+ typeof finalOutput === 'object' &&
113740
+ !Array.isArray(finalOutput) &&
113741
+ (finalOutput.error === undefined ||
113742
+ finalOutput.issues === undefined)) {
113743
+ const vm = await Promise.resolve().then(() => __importStar(__nccwpck_require__(30714)));
113744
+ const vmContext = vm.createContext({ scope: jsContext });
113745
+ const vmCode = `
113746
+ (function(){
113747
+ const output = scope.output; const pr = scope.pr; const files = scope.files; const outputs = scope.outputs; const env = scope.env; const log = ()=>{};
113748
+ ${bodyWithReturn}
113749
+ })()
113750
+ `;
113751
+ const vmResult = vm.runInContext(vmCode, vmContext, { timeout: 1000 });
113752
+ if (vmResult && typeof vmResult === 'object') {
113753
+ finalOutput = vmResult;
113754
+ }
113755
+ }
112890
113756
  }
112891
- catch {
112892
- try {
112893
- const preview = String(finalOutput);
112894
- logger_1.logger.debug(`🔧 Debug: transform_js result: ${preview.slice(0, 200)}`);
113757
+ catch { }
113758
+ // Create a plain JSON snapshot of the transform result to avoid proxy/getter surprises
113759
+ // Prefer JSON stringify inside the sandbox realm (so it knows how to serialize its own objects),
113760
+ // then fall back to host-side JSON clone and finally to a shallow copy of own enumerable properties.
113761
+ let finalSnapshot = null;
113762
+ try {
113763
+ if (finalOutput && typeof finalOutput === 'object' && !Array.isArray(finalOutput)) {
113764
+ // Try realm-local stringify first
113765
+ try {
113766
+ const stringifyExec = this.sandbox.compile('return JSON.stringify(scope.obj);');
113767
+ const jsonStr = stringifyExec({ obj: finalOutput }).run();
113768
+ if (typeof jsonStr === 'string' && jsonStr.trim().startsWith('{')) {
113769
+ finalSnapshot = JSON.parse(jsonStr);
113770
+ }
113771
+ }
113772
+ catch { }
113773
+ if (!finalSnapshot) {
113774
+ try {
113775
+ finalSnapshot = JSON.parse(JSON.stringify(finalOutput));
113776
+ }
113777
+ catch { }
113778
+ }
113779
+ if (!finalSnapshot) {
113780
+ const tmp = {};
113781
+ for (const k of Object.keys(finalOutput)) {
113782
+ tmp[k] = finalOutput[k];
113783
+ }
113784
+ finalSnapshot = tmp;
113785
+ }
112895
113786
  }
112896
- catch {
112897
- // Ignore logging errors
113787
+ }
113788
+ catch { }
113789
+ // @ts-ignore store for later extraction path
113790
+ this.__lastTransformSnapshot = finalSnapshot;
113791
+ try {
113792
+ const isObj = finalOutput && typeof finalOutput === 'object' && !Array.isArray(finalOutput);
113793
+ const keys = isObj
113794
+ ? Object.keys(finalOutput).join(',')
113795
+ : typeof finalOutput;
113796
+ logger_1.logger.debug(` transform_js: output typeof=${Array.isArray(finalOutput) ? 'array' : typeof finalOutput} keys=${keys}`);
113797
+ if (isObj && finalOutput.issues) {
113798
+ const mi = finalOutput.issues;
113799
+ logger_1.logger.debug(` transform_js: issues typeof=${Array.isArray(mi) ? 'array' : typeof mi} len=${(mi && mi.length) || 0}`);
112898
113800
  }
113801
+ try {
113802
+ if (isObj)
113803
+ logger_1.logger.debug(` transform_js: error value=${String(finalOutput.error)}`);
113804
+ }
113805
+ catch { }
112899
113806
  }
113807
+ catch { }
113808
+ logger_1.logger.verbose(`✓ Applied JavaScript transform successfully`);
113809
+ // Already normalized in sandbox result
113810
+ // no debug
112900
113811
  }
112901
113812
  catch (error) {
112902
113813
  logger_1.logger.error(`✗ Failed to apply JavaScript transform: ${error instanceof Error ? error.message : 'Unknown error'}`);
@@ -112915,15 +113826,205 @@ class CommandCheckProvider extends check_provider_interface_1.CheckProvider {
112915
113826
  }
112916
113827
  }
112917
113828
  // Extract structured issues when the command returns them (skip for forEach parents)
113829
+ // no debug
112918
113830
  let issues = [];
112919
113831
  let outputForDependents = finalOutput;
113832
+ // Capture a shallow snapshot created earlier if available (within transform_js path)
113833
+ // @ts-ignore - finalSnapshot is defined in the transform_js scope above when applicable
113834
+ // @ts-ignore retrieve snapshot captured after transform_js (if any)
113835
+ const snapshotForExtraction = this.__lastTransformSnapshot || null;
113836
+ try {
113837
+ if (snapshotForExtraction) {
113838
+ logger_1.logger.debug(` provider: snapshot keys=${Object.keys(snapshotForExtraction).join(',')}`);
113839
+ }
113840
+ else {
113841
+ logger_1.logger.debug(` provider: snapshot is null`);
113842
+ }
113843
+ }
113844
+ catch { }
113845
+ // Some shells may wrap JSON output inside a one-element array due to quoting.
113846
+ // If we see a single-element array containing a JSON string or object, unwrap it.
113847
+ try {
113848
+ if (Array.isArray(outputForDependents) && outputForDependents.length === 1) {
113849
+ const first = outputForDependents[0];
113850
+ if (typeof first === 'string') {
113851
+ try {
113852
+ outputForDependents = JSON.parse(first);
113853
+ }
113854
+ catch { }
113855
+ }
113856
+ else if (first && typeof first === 'object') {
113857
+ outputForDependents = first;
113858
+ }
113859
+ }
113860
+ }
113861
+ catch { }
112920
113862
  let content;
112921
113863
  let extracted = null;
112922
113864
  const trimmedRawOutput = typeof rawOutput === 'string' ? rawOutput.trim() : undefined;
112923
113865
  const commandConfig = config;
112924
113866
  const isForEachParent = commandConfig.forEach === true;
112925
113867
  if (!isForEachParent) {
112926
- extracted = this.extractIssuesFromOutput(finalOutput);
113868
+ // Generic: if transform output is an object and contains an 'issues' field,
113869
+ // expose all other fields to dependents regardless of whether we successfully
113870
+ // normalized the issues array. This preserves flags like 'error' for fail_if.
113871
+ try {
113872
+ const baseObj = (snapshotForExtraction || finalOutput);
113873
+ if (baseObj &&
113874
+ typeof baseObj === 'object' &&
113875
+ Object.prototype.hasOwnProperty.call(baseObj, 'issues')) {
113876
+ const remaining = { ...baseObj };
113877
+ delete remaining.issues;
113878
+ outputForDependents = Object.keys(remaining).length > 0 ? remaining : undefined;
113879
+ try {
113880
+ const k = outputForDependents && typeof outputForDependents === 'object'
113881
+ ? Object.keys(outputForDependents).join(',')
113882
+ : String(outputForDependents);
113883
+ logger_1.logger.debug(` provider: generic-remaining keys=${k}`);
113884
+ }
113885
+ catch { }
113886
+ }
113887
+ }
113888
+ catch { }
113889
+ // Fast path for transform_js objects that include an issues array (realm-agnostic)
113890
+ const objForExtraction = (snapshotForExtraction || finalOutput);
113891
+ if (objForExtraction && typeof objForExtraction === 'object') {
113892
+ try {
113893
+ const rec = objForExtraction;
113894
+ const maybeIssues = rec.issues;
113895
+ const toPlainArray = (v) => {
113896
+ if (Array.isArray(v))
113897
+ return v;
113898
+ try {
113899
+ if (v && typeof v === 'object' && typeof v[Symbol.iterator] === 'function') {
113900
+ return Array.from(v);
113901
+ }
113902
+ }
113903
+ catch { }
113904
+ const len = Number((v || {}).length);
113905
+ if (Number.isFinite(len) && len >= 0) {
113906
+ const arr = [];
113907
+ for (let i = 0; i < len; i++)
113908
+ arr.push(v[i]);
113909
+ return arr;
113910
+ }
113911
+ try {
113912
+ const cloned = JSON.parse(JSON.stringify(v));
113913
+ return Array.isArray(cloned) ? cloned : null;
113914
+ }
113915
+ catch {
113916
+ return null;
113917
+ }
113918
+ };
113919
+ try {
113920
+ const ctor = maybeIssues && maybeIssues.constructor
113921
+ ? maybeIssues.constructor.name
113922
+ : 'unknown';
113923
+ logger_1.logger.debug(` provider: issues inspect typeof=${typeof maybeIssues} Array.isArray=${Array.isArray(maybeIssues)} ctor=${ctor} keys=${Object.keys((maybeIssues || {})).join(',')}`);
113924
+ }
113925
+ catch { }
113926
+ const arr = toPlainArray(maybeIssues);
113927
+ if (arr) {
113928
+ const norm = this.normalizeIssueArray(arr);
113929
+ if (norm) {
113930
+ issues = norm;
113931
+ const remaining = { ...rec };
113932
+ delete remaining.issues;
113933
+ outputForDependents = Object.keys(remaining).length > 0 ? remaining : undefined;
113934
+ try {
113935
+ const keys = outputForDependents && typeof outputForDependents === 'object'
113936
+ ? Object.keys(outputForDependents).join(',')
113937
+ : String(outputForDependents);
113938
+ logger_1.logger.info(` provider: fast-path issues=${issues.length} remaining keys=${keys}`);
113939
+ }
113940
+ catch { }
113941
+ }
113942
+ else {
113943
+ try {
113944
+ logger_1.logger.info(' provider: fast-path norm failed');
113945
+ }
113946
+ catch { }
113947
+ }
113948
+ }
113949
+ else {
113950
+ try {
113951
+ logger_1.logger.info(' provider: fast-path arr unavailable');
113952
+ }
113953
+ catch { }
113954
+ }
113955
+ }
113956
+ catch { }
113957
+ }
113958
+ // Normalize extraction target: unwrap one-element arrays like ["{...}"] or [{...}]
113959
+ let extractionTarget = snapshotForExtraction || finalOutput;
113960
+ try {
113961
+ if (Array.isArray(extractionTarget) && extractionTarget.length === 1) {
113962
+ const first = extractionTarget[0];
113963
+ if (typeof first === 'string') {
113964
+ try {
113965
+ extractionTarget = JSON.parse(first);
113966
+ }
113967
+ catch {
113968
+ extractionTarget = first;
113969
+ }
113970
+ }
113971
+ else if (first && typeof first === 'object') {
113972
+ extractionTarget = first;
113973
+ }
113974
+ }
113975
+ }
113976
+ catch { }
113977
+ extracted = this.extractIssuesFromOutput(extractionTarget);
113978
+ try {
113979
+ if (extractionTarget !== (snapshotForExtraction || finalOutput)) {
113980
+ finalOutput = extractionTarget;
113981
+ }
113982
+ }
113983
+ catch { }
113984
+ // no debug
113985
+ // Handle cross-realm Arrays from sandbox: issues may look like an array but fail Array.isArray
113986
+ if (!extracted && finalOutput && typeof finalOutput === 'object') {
113987
+ try {
113988
+ const rec = finalOutput;
113989
+ const maybeIssues = rec.issues;
113990
+ if (maybeIssues && typeof maybeIssues === 'object') {
113991
+ let arr = null;
113992
+ // Prefer iterator if present
113993
+ try {
113994
+ if (typeof maybeIssues[Symbol.iterator] === 'function') {
113995
+ arr = Array.from(maybeIssues);
113996
+ }
113997
+ }
113998
+ catch { }
113999
+ // Fallback to length-based copy
114000
+ if (!arr) {
114001
+ const len = Number(maybeIssues.length);
114002
+ if (Number.isFinite(len) && len >= 0) {
114003
+ arr = [];
114004
+ for (let i = 0; i < len; i++)
114005
+ arr.push(maybeIssues[i]);
114006
+ }
114007
+ }
114008
+ // Last resort: JSON clone
114009
+ if (!arr) {
114010
+ try {
114011
+ arr = JSON.parse(JSON.stringify(maybeIssues));
114012
+ }
114013
+ catch { }
114014
+ }
114015
+ if (arr && Array.isArray(arr)) {
114016
+ const norm = this.normalizeIssueArray(arr);
114017
+ if (norm) {
114018
+ issues = norm;
114019
+ const remaining = { ...rec };
114020
+ delete remaining.issues;
114021
+ outputForDependents = Object.keys(remaining).length > 0 ? remaining : undefined;
114022
+ }
114023
+ }
114024
+ }
114025
+ }
114026
+ catch { }
114027
+ }
112927
114028
  if (!extracted && typeof finalOutput === 'string') {
112928
114029
  // Attempt to parse string output as JSON and extract issues again
112929
114030
  try {
@@ -112932,15 +114033,52 @@ class CommandCheckProvider extends check_provider_interface_1.CheckProvider {
112932
114033
  if (extracted) {
112933
114034
  issues = extracted.issues;
112934
114035
  outputForDependents = extracted.remainingOutput;
114036
+ // If remainingOutput carries a content field, pick it up
114037
+ if (typeof extracted.remainingOutput === 'object' &&
114038
+ extracted.remainingOutput !== null &&
114039
+ typeof extracted.remainingOutput.content === 'string') {
114040
+ const c = String(extracted.remainingOutput.content).trim();
114041
+ if (c)
114042
+ content = c;
114043
+ }
112935
114044
  }
112936
114045
  }
112937
114046
  catch {
112938
- // Ignore JSON parse errors leave output as-is
114047
+ // Try to salvage JSON from anywhere within the string (stripped logs/ansi)
114048
+ try {
114049
+ const any = this.extractJsonAnywhere(finalOutput);
114050
+ if (any) {
114051
+ const parsed = JSON.parse(any);
114052
+ extracted = this.extractIssuesFromOutput(parsed);
114053
+ if (extracted) {
114054
+ issues = extracted.issues;
114055
+ outputForDependents = extracted.remainingOutput;
114056
+ if (typeof extracted.remainingOutput === 'object' &&
114057
+ extracted.remainingOutput !== null &&
114058
+ typeof extracted.remainingOutput.content === 'string') {
114059
+ const c = String(extracted.remainingOutput.content).trim();
114060
+ if (c)
114061
+ content = c;
114062
+ }
114063
+ }
114064
+ }
114065
+ }
114066
+ catch {
114067
+ // leave as-is
114068
+ }
112939
114069
  }
112940
114070
  }
112941
114071
  else if (extracted) {
112942
114072
  issues = extracted.issues;
112943
114073
  outputForDependents = extracted.remainingOutput;
114074
+ // Also propagate embedded content when remainingOutput is an object { content, ... }
114075
+ if (typeof extracted.remainingOutput === 'object' &&
114076
+ extracted.remainingOutput !== null &&
114077
+ typeof extracted.remainingOutput.content === 'string') {
114078
+ const c = String(extracted.remainingOutput.content).trim();
114079
+ if (c)
114080
+ content = c;
114081
+ }
112944
114082
  }
112945
114083
  if (!issues.length && this.shouldTreatAsTextOutput(trimmedRawOutput)) {
112946
114084
  content = trimmedRawOutput;
@@ -112951,26 +114089,204 @@ class CommandCheckProvider extends check_provider_interface_1.CheckProvider {
112951
114089
  content = trimmed;
112952
114090
  }
112953
114091
  }
114092
+ // Generic fallback: if issues are still empty, try to parse raw stdout as JSON and extract issues.
114093
+ if (!issues.length && typeof trimmedRawOutput === 'string') {
114094
+ try {
114095
+ const tryParsed = JSON.parse(trimmedRawOutput);
114096
+ const reextract = this.extractIssuesFromOutput(tryParsed);
114097
+ if (reextract && reextract.issues && reextract.issues.length) {
114098
+ issues = reextract.issues;
114099
+ if (!outputForDependents && reextract.remainingOutput) {
114100
+ outputForDependents = reextract.remainingOutput;
114101
+ }
114102
+ }
114103
+ else if (Array.isArray(tryParsed)) {
114104
+ // Treat parsed array as potential issues array or array of { issues: [...] }
114105
+ const first = tryParsed[0];
114106
+ if (first && typeof first === 'object' && Array.isArray(first.issues)) {
114107
+ const merged = [];
114108
+ for (const el of tryParsed) {
114109
+ if (el && typeof el === 'object' && Array.isArray(el.issues)) {
114110
+ merged.push(...el.issues);
114111
+ }
114112
+ }
114113
+ const flat = this.normalizeIssueArray(merged);
114114
+ if (flat)
114115
+ issues = flat;
114116
+ }
114117
+ else {
114118
+ // Try to parse string elements into JSON objects and extract
114119
+ const converted = [];
114120
+ for (const el of tryParsed) {
114121
+ if (typeof el === 'string') {
114122
+ try {
114123
+ const obj = JSON.parse(el);
114124
+ converted.push(obj);
114125
+ }
114126
+ catch {
114127
+ // keep as-is
114128
+ }
114129
+ }
114130
+ else {
114131
+ converted.push(el);
114132
+ }
114133
+ }
114134
+ const flat = this.normalizeIssueArray(converted);
114135
+ if (flat)
114136
+ issues = flat;
114137
+ }
114138
+ }
114139
+ }
114140
+ catch { }
114141
+ if (!issues.length) {
114142
+ try {
114143
+ const any = this.extractJsonAnywhere(trimmedRawOutput);
114144
+ if (any) {
114145
+ const tryParsed = JSON.parse(any);
114146
+ const reextract = this.extractIssuesFromOutput(tryParsed);
114147
+ if (reextract && reextract.issues && reextract.issues.length) {
114148
+ issues = reextract.issues;
114149
+ if (!outputForDependents && reextract.remainingOutput) {
114150
+ outputForDependents = reextract.remainingOutput;
114151
+ }
114152
+ }
114153
+ }
114154
+ }
114155
+ catch { }
114156
+ }
114157
+ }
114158
+ // Preserve all primitive flags (boolean/number/string) from original transform output
114159
+ try {
114160
+ const srcObj = (snapshotForExtraction || finalOutput);
114161
+ if (outputForDependents &&
114162
+ typeof outputForDependents === 'object' &&
114163
+ srcObj &&
114164
+ typeof srcObj === 'object') {
114165
+ for (const k of Object.keys(srcObj)) {
114166
+ const v = srcObj[k];
114167
+ if (typeof v === 'boolean' || typeof v === 'number' || typeof v === 'string') {
114168
+ outputForDependents[k] = v;
114169
+ }
114170
+ }
114171
+ }
114172
+ }
114173
+ catch { }
114174
+ // Normalize output object to a plain shallow object (avoid JSON stringify drop of false booleans)
114175
+ try {
114176
+ if (outputForDependents &&
114177
+ typeof outputForDependents === 'object' &&
114178
+ !Array.isArray(outputForDependents)) {
114179
+ const plain = {};
114180
+ for (const k of Object.keys(outputForDependents)) {
114181
+ plain[k] = outputForDependents[k];
114182
+ }
114183
+ outputForDependents = plain;
114184
+ }
114185
+ }
114186
+ catch { }
112954
114187
  }
112955
114188
  if (!content && this.shouldTreatAsTextOutput(trimmedRawOutput) && !isForEachParent) {
112956
114189
  content = trimmedRawOutput;
112957
114190
  }
114191
+ // Normalize output object to plain JSON to avoid cross-realm proxy quirks
114192
+ try {
114193
+ if (outputForDependents && typeof outputForDependents === 'object') {
114194
+ outputForDependents = JSON.parse(JSON.stringify(outputForDependents));
114195
+ }
114196
+ }
114197
+ catch { }
114198
+ // Promote primitive flags from original transform output to top-level result fields (schema-agnostic)
114199
+ const promoted = {};
114200
+ try {
114201
+ const srcObj = (snapshotForExtraction || finalOutput);
114202
+ if (srcObj && typeof srcObj === 'object') {
114203
+ for (const k of Object.keys(srcObj)) {
114204
+ const v = srcObj[k];
114205
+ if (typeof v === 'boolean') {
114206
+ if (v === true && promoted[k] === undefined)
114207
+ promoted[k] = true;
114208
+ }
114209
+ else if ((typeof v === 'number' || typeof v === 'string') &&
114210
+ promoted[k] === undefined) {
114211
+ promoted[k] = v;
114212
+ }
114213
+ }
114214
+ }
114215
+ }
114216
+ catch { }
112958
114217
  // Return the output and issues as part of the review summary so dependent checks can use them
112959
114218
  const result = {
112960
114219
  issues,
112961
114220
  output: outputForDependents,
112962
114221
  ...(content ? { content } : {}),
114222
+ ...promoted,
112963
114223
  };
112964
- if (transformJs) {
112965
- try {
112966
- const outputValue = result.output;
112967
- const stringified = JSON.stringify(outputValue);
112968
- logger_1.logger.debug(`🔧 Debug: Command provider returning output: ${stringified ? stringified.slice(0, 200) : '(empty)'}`);
114224
+ // Attach raw transform object only when transform_js was used (avoid polluting plain command outputs)
114225
+ try {
114226
+ if (transformJs) {
114227
+ const rawObj = (snapshotForExtraction || finalOutput);
114228
+ if (rawObj && typeof rawObj === 'object') {
114229
+ result.__raw = rawObj;
114230
+ }
112969
114231
  }
112970
- catch {
112971
- // Ignore logging errors
114232
+ }
114233
+ catch { }
114234
+ // Final safeguard: ensure primitive flags from original transform output are present in result.output.
114235
+ // Do this without dropping explicit false values (important for fail_if like `output.error`).
114236
+ try {
114237
+ const srcObj = (snapshotForExtraction || finalOutput);
114238
+ const srcErr = (() => {
114239
+ try {
114240
+ if (snapshotForExtraction &&
114241
+ typeof snapshotForExtraction === 'object' &&
114242
+ snapshotForExtraction.error !== undefined) {
114243
+ return Boolean(snapshotForExtraction.error);
114244
+ }
114245
+ if (finalOutput &&
114246
+ typeof finalOutput === 'object' &&
114247
+ finalOutput.error !== undefined) {
114248
+ return Boolean(finalOutput.error);
114249
+ }
114250
+ }
114251
+ catch { }
114252
+ return undefined;
114253
+ })();
114254
+ const dst = result.output;
114255
+ if (srcObj && typeof srcObj === 'object' && dst && typeof dst === 'object') {
114256
+ try {
114257
+ logger_1.logger.debug(` provider: safeguard src.error typeof=${typeof srcObj.error} val=${String(srcObj.error)} dst.hasErrorBefore=${String(dst.error !== undefined)}`);
114258
+ }
114259
+ catch { }
114260
+ for (const k of Object.keys(srcObj)) {
114261
+ const v = srcObj[k];
114262
+ if (typeof v === 'boolean' || typeof v === 'number' || typeof v === 'string') {
114263
+ dst[k] = v;
114264
+ }
114265
+ }
114266
+ // Explicitly normalize a common flag used in tests/pipelines
114267
+ if (srcErr !== undefined && dst.error === undefined) {
114268
+ dst.error = srcErr;
114269
+ try {
114270
+ const k = Object.keys(dst).join(',');
114271
+ logger_1.logger.debug(` provider: safeguard merged error -> output keys=${k} val=${String(dst.error)}`);
114272
+ }
114273
+ catch { }
114274
+ }
114275
+ }
114276
+ }
114277
+ catch { }
114278
+ try {
114279
+ const out = result.output;
114280
+ if (out && typeof out === 'object') {
114281
+ const k = Object.keys(out).join(',');
114282
+ logger_1.logger.debug(` provider: return output keys=${k}`);
114283
+ }
114284
+ else {
114285
+ logger_1.logger.debug(` provider: return output type=${typeof out}`);
112972
114286
  }
112973
114287
  }
114288
+ catch { }
114289
+ // no debug
112974
114290
  return result;
112975
114291
  }
112976
114292
  catch (error) {
@@ -113134,25 +114450,106 @@ class CommandCheckProvider extends check_provider_interface_1.CheckProvider {
113134
114450
  * Looks for the last occurrence of { or [ and tries to parse from there
113135
114451
  */
113136
114452
  extractJsonFromEnd(text) {
113137
- // Strategy: Find the last line that starts with { or [
113138
- // Then try to parse from that point to the end
113139
- const lines = text.split('\n');
113140
- // Search backwards for a line starting with { or [
113141
- for (let i = lines.length - 1; i >= 0; i--) {
113142
- const trimmed = lines[i].trim();
113143
- if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
113144
- // Found potential JSON start - take everything from here to the end
113145
- const candidate = lines.slice(i).join('\n');
113146
- // Quick validation: does it look like valid JSON structure?
113147
- const trimmedCandidate = candidate.trim();
113148
- if ((trimmedCandidate.startsWith('{') && trimmedCandidate.endsWith('}')) ||
113149
- (trimmedCandidate.startsWith('[') && trimmedCandidate.endsWith(']'))) {
113150
- return trimmedCandidate;
114453
+ // Robust strategy: find the last closing brace/bracket, then walk backwards to the matching opener
114454
+ const lastBrace = Math.max(text.lastIndexOf('}'), text.lastIndexOf(']'));
114455
+ if (lastBrace === -1)
114456
+ return null;
114457
+ // Scan backwards to find matching opener with a simple counter
114458
+ let open = 0;
114459
+ for (let i = lastBrace; i >= 0; i--) {
114460
+ const ch = text[i];
114461
+ if (ch === '}' || ch === ']')
114462
+ open++;
114463
+ else if (ch === '{' || ch === '[')
114464
+ open--;
114465
+ if (open === 0 && (ch === '{' || ch === '[')) {
114466
+ const candidate = text.slice(i, lastBrace + 1).trim();
114467
+ try {
114468
+ JSON.parse(candidate);
114469
+ return candidate;
114470
+ }
114471
+ catch {
114472
+ return null;
113151
114473
  }
113152
114474
  }
113153
114475
  }
113154
114476
  return null;
113155
114477
  }
114478
+ // Extract any balanced JSON object/array substring from anywhere in the text
114479
+ extractJsonAnywhere(text) {
114480
+ const n = text.length;
114481
+ let best = null;
114482
+ for (let i = 0; i < n; i++) {
114483
+ const start = text[i];
114484
+ if (start !== '{' && start !== '[')
114485
+ continue;
114486
+ let open = 0;
114487
+ let inString = false;
114488
+ let escape = false;
114489
+ for (let j = i; j < n; j++) {
114490
+ const ch = text[j];
114491
+ if (escape) {
114492
+ escape = false;
114493
+ continue;
114494
+ }
114495
+ if (ch === '\\') {
114496
+ escape = true;
114497
+ continue;
114498
+ }
114499
+ if (ch === '"') {
114500
+ inString = !inString;
114501
+ continue;
114502
+ }
114503
+ if (inString)
114504
+ continue;
114505
+ if (ch === '{' || ch === '[')
114506
+ open++;
114507
+ else if (ch === '}' || ch === ']')
114508
+ open--;
114509
+ if (open === 0 && (ch === '}' || ch === ']')) {
114510
+ const candidate = text.slice(i, j + 1).trim();
114511
+ try {
114512
+ JSON.parse(candidate);
114513
+ best = candidate; // keep the last valid one we find
114514
+ }
114515
+ catch {
114516
+ // Try a loose-to-strict conversion (quote keys and barewords)
114517
+ const strict = this.looseJsonToStrict(candidate);
114518
+ if (strict) {
114519
+ try {
114520
+ JSON.parse(strict);
114521
+ best = strict;
114522
+ }
114523
+ catch { }
114524
+ }
114525
+ }
114526
+ break;
114527
+ }
114528
+ }
114529
+ }
114530
+ return best;
114531
+ }
114532
+ // Best-effort conversion of object-literal-like strings to strict JSON
114533
+ looseJsonToStrict(candidate) {
114534
+ try {
114535
+ let s = candidate.trim();
114536
+ // Convert single quotes to double quotes conservatively
114537
+ s = s.replace(/'/g, '"');
114538
+ // Quote unquoted keys: {key: ...} or ,key: ...
114539
+ s = s.replace(/([\{,]\s*)([A-Za-z_][A-Za-z0-9_-]*)\s*:/g, '$1"$2":');
114540
+ // Quote bareword values except true/false/null and numbers
114541
+ s = s.replace(/:\s*([A-Za-z_][A-Za-z0-9_-]*)\s*(?=[,}])/g, (m, word) => {
114542
+ const lw = String(word).toLowerCase();
114543
+ if (lw === 'true' || lw === 'false' || lw === 'null')
114544
+ return `:${lw}`;
114545
+ return `:"${word}"`;
114546
+ });
114547
+ return s;
114548
+ }
114549
+ catch {
114550
+ return null;
114551
+ }
114552
+ }
113156
114553
  /**
113157
114554
  * Recursively apply JSON-smart wrapper to outputs object values
113158
114555
  */
@@ -113202,6 +114599,14 @@ class CommandCheckProvider extends check_provider_interface_1.CheckProvider {
113202
114599
  ];
113203
114600
  }
113204
114601
  extractIssuesFromOutput(output) {
114602
+ try {
114603
+ logger_1.logger.info(` extractIssuesFromOutput: typeof=${Array.isArray(output) ? 'array' : typeof output}`);
114604
+ if (typeof output === 'object' && output) {
114605
+ const rec = output;
114606
+ logger_1.logger.info(` extractIssuesFromOutput: keys=${Object.keys(rec).join(',')} issuesIsArray=${Array.isArray(rec.issues)}`);
114607
+ }
114608
+ }
114609
+ catch { }
113205
114610
  if (output === null || output === undefined) {
113206
114611
  return null;
113207
114612
  }
@@ -113210,9 +114615,30 @@ class CommandCheckProvider extends check_provider_interface_1.CheckProvider {
113210
114615
  return null;
113211
114616
  }
113212
114617
  if (Array.isArray(output)) {
113213
- const issues = this.normalizeIssueArray(output);
113214
- if (issues) {
113215
- return { issues, remainingOutput: undefined };
114618
+ // Two supported shapes:
114619
+ // 1) Array<ReviewIssue-like>
114620
+ // 2) Array<{ issues: Array<ReviewIssue-like> }>
114621
+ const first = output[0];
114622
+ if (first &&
114623
+ typeof first === 'object' &&
114624
+ !Array.isArray(first.message) &&
114625
+ Array.isArray(first.issues)) {
114626
+ // flatten nested issues arrays
114627
+ const merged = [];
114628
+ for (const el of output) {
114629
+ if (el && typeof el === 'object' && Array.isArray(el.issues)) {
114630
+ merged.push(...el.issues);
114631
+ }
114632
+ }
114633
+ const flat = this.normalizeIssueArray(merged);
114634
+ if (flat)
114635
+ return { issues: flat, remainingOutput: undefined };
114636
+ }
114637
+ else {
114638
+ const issues = this.normalizeIssueArray(output);
114639
+ if (issues) {
114640
+ return { issues, remainingOutput: undefined };
114641
+ }
113216
114642
  }
113217
114643
  return null;
113218
114644
  }
@@ -113343,11 +114769,36 @@ class CommandCheckProvider extends check_provider_interface_1.CheckProvider {
113343
114769
  }
113344
114770
  async renderCommandTemplate(template, context) {
113345
114771
  try {
113346
- return await this.liquid.parseAndRender(template, context);
114772
+ // Best-effort compatibility: allow double-quoted bracket keys inside Liquid tags.
114773
+ // e.g., {{ outputs["fetch-tickets"].key }} → {{ outputs['fetch-tickets'].key }}
114774
+ let tpl = template;
114775
+ if (tpl.includes('{{')) {
114776
+ tpl = tpl.replace(/\{\{([\s\S]*?)\}\}/g, (_m, inner) => {
114777
+ const fixed = String(inner).replace(/\[\"/g, "['").replace(/\"\]/g, "']");
114778
+ return `{{ ${fixed} }}`;
114779
+ });
114780
+ }
114781
+ let rendered = await this.liquid.parseAndRender(tpl, context);
114782
+ // If Liquid left unresolved tags (common when users write JS expressions inside {{ }}),
114783
+ // fall back to a safe JS-expression renderer for the remaining tags.
114784
+ if (/\{\{[\s\S]*?\}\}/.test(rendered)) {
114785
+ try {
114786
+ rendered = this.renderWithJsExpressions(rendered, context);
114787
+ }
114788
+ catch {
114789
+ // keep Liquid-rendered result as-is
114790
+ }
114791
+ }
114792
+ return rendered;
113347
114793
  }
113348
114794
  catch (error) {
113349
- logger_1.logger.debug(`🔧 Debug: Liquid rendering failed, falling back to JS evaluation: ${error}`);
113350
- return this.renderWithJsExpressions(template, context);
114795
+ logger_1.logger.debug(`🔧 Debug: Liquid templating failed, trying JS-expression fallback: ${error}`);
114796
+ try {
114797
+ return this.renderWithJsExpressions(template, context);
114798
+ }
114799
+ catch {
114800
+ return template;
114801
+ }
113351
114802
  }
113352
114803
  }
113353
114804
  renderWithJsExpressions(template, context) {
@@ -113360,9 +114811,8 @@ class CommandCheckProvider extends check_provider_interface_1.CheckProvider {
113360
114811
  const expressionRegex = /\{\{\s*([^{}]+?)\s*\}\}/g;
113361
114812
  return template.replace(expressionRegex, (_match, expr) => {
113362
114813
  const expression = String(expr).trim();
113363
- if (!expression) {
114814
+ if (!expression)
113364
114815
  return '';
113365
- }
113366
114816
  try {
113367
114817
  const evalCode = `
113368
114818
  const pr = scope.pr;
@@ -113371,15 +114821,13 @@ class CommandCheckProvider extends check_provider_interface_1.CheckProvider {
113371
114821
  const env = scope.env;
113372
114822
  return (${expression});
113373
114823
  `;
113374
- if (!this.sandbox) {
114824
+ if (!this.sandbox)
113375
114825
  this.sandbox = this.createSecureSandbox();
113376
- }
113377
114826
  const evaluator = this.sandbox.compile(evalCode);
113378
114827
  const result = evaluator({ scope }).run();
113379
114828
  return result === undefined || result === null ? '' : String(result);
113380
114829
  }
113381
- catch (evaluationError) {
113382
- logger_1.logger.debug(`🔧 Debug: Failed to evaluate expression: ${expression} - ${evaluationError}`);
114830
+ catch {
113383
114831
  return '';
113384
114832
  }
113385
114833
  });
@@ -115856,6 +117304,14 @@ module.exports = require("node:util");
115856
117304
 
115857
117305
  /***/ }),
115858
117306
 
117307
+ /***/ 30714:
117308
+ /***/ ((module) => {
117309
+
117310
+ "use strict";
117311
+ module.exports = require("node:vm");
117312
+
117313
+ /***/ }),
117314
+
115859
117315
  /***/ 70857:
115860
117316
  /***/ ((module) => {
115861
117317
 
@@ -231814,7 +233270,7 @@ module.exports = /*#__PURE__*/JSON.parse('{"application/1d-interleaved-parityfec
231814
233270
  /***/ ((module) => {
231815
233271
 
231816
233272
  "use strict";
231817
- module.exports = {"rE":"0.1.89"};
233273
+ module.exports = {"rE":"0.1.90"};
231818
233274
 
231819
233275
  /***/ })
231820
233276