@vaudience/aigentflow-flow-validator 0.23.0 → 0.24.0

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/PARITY.md CHANGED
@@ -6,6 +6,22 @@ discipline for keeping the two in sync.
6
6
 
7
7
  **Tracks AIgentFlow flow schema: `v2.831.0`** (`SPEC_VERSION` in [`src/spec/aigentflow-spec.json`](./src/spec/aigentflow-spec.json)), which adds the rule `unknown_function_operation` and a clearer `unknown_executor_scheme` message (package 0.23.0, below). Earlier: the rule `flow_data_key_reserved` (package 0.22.0). Earlier: the `here` executor scheme, the `strict_templates` flow key and the rule `template_default_piped` (package 0.21.0). Earlier: the warning `compliance_residency_unenforced` (package 0.20.0). `orchestrator.region` / `orchestrator.enforce_region` (package 0.19.0). Earlier: the AigentChat executor was removed (package 0.18.0). Earlier additions: the `examples:` block (package 0.17.0), `server_owned_query_key` (package 0.15.1) from the AIgentFlow release after v2.790.0, `credential_endpoint_unpaired` (package 0.15.2) from AIgentFlow v2.793.0 and the release after it, and `display_name_too_long` (package 0.15.3) from AIgentFlow v2.802.0.
8
8
 
9
+ > package 0.24.0 — **`potential_infinite_loop` fires only on a cycle with no way out**, in step
10
+ > with the reference. It used to fire on every cycle reachable from the start step, so a deliberate
11
+ > ask-again loop (a step that routes back until a condition sends it onward) carried a warning that
12
+ > told the author nothing. The reference now computes the strongly connected sets (Tarjan) over
13
+ > `next.default` and `next.conditions[].goto` edges reachable from start (markers `null` /
14
+ > `orchestrator` and unknown steps are not followed; `end` is an ordinary step) and warns once when
15
+ > a cyclic set (more than one step, or a self-loop) has no exit. A member leaves the set when it has
16
+ > no `next`, its `next` has `parallel` (a fan-out is treated as an exit, not guessed at), its
17
+ > `next.default` is empty (when no condition matches the flow ends), or any default or condition
18
+ > target is `null`, `orchestrator` or a step outside the set. Code and severity are unchanged; the
19
+ > message now ends "a cycle of steps has no way out" and the suggestion asks for a `next.conditions`
20
+ > branch that leaves the cycle. It cannot prove an exit ever fires; that is undecidable from the
21
+ > YAML. Module: `validators/connectivity.ts`. Gates: ten `valid-cycle-*` / `warn-cycle-*` fixtures
22
+ > (the valid ones use `forbidWarningCodes`, the only way to assert a false-positive risk), shared
23
+ > byte for byte with the Go port. On the 227 bundled example flows both ports report 0 findings.
24
+
9
25
  > package 0.23.0 — **`unknown_function_operation`**, in step with the reference (AIgentFlow
10
26
  > v2.831.0; `specVersion` 2.831.0). One new **error**: a step or loop sub-step whose executor
11
27
  > is `function://<a>/<b>` where `<a>/<b>` is not one of the 32 registered built-ins. Before
package/dist/cli.js CHANGED
@@ -2732,31 +2732,81 @@ function validateConnectivity(flow, issues) {
2732
2732
  });
2733
2733
  }
2734
2734
  }
2735
- const visited = /* @__PURE__ */ new Set();
2736
- const recStack = /* @__PURE__ */ new Set();
2737
- const hasCycle = (stepID) => {
2738
- if (recStack.has(stepID)) return true;
2739
- if (visited.has(stepID)) return false;
2740
- visited.add(stepID);
2741
- recStack.add(stepID);
2742
- const step = steps[stepID];
2743
- if (isRecord(step) && isRecord(step.next)) {
2744
- const next = step.next;
2745
- for (const target of cycleTargets(next, `steps.${stepID}.next`, issues)) {
2746
- if (hasCycle(target)) return true;
2747
- }
2748
- }
2749
- recStack.delete(stepID);
2750
- return false;
2751
- };
2752
- if (hasCycle(start)) {
2735
+ if (cycleWithoutExit(steps, start, issues)) {
2753
2736
  issues.warn({
2754
2737
  field: "steps",
2755
- message: "Potential infinite loop detected in step flow",
2738
+ message: "Potential infinite loop detected in step flow: a cycle of steps has no way out",
2756
2739
  code: "potential_infinite_loop",
2757
- suggestion: "Review your step transitions to ensure they eventually terminate"
2740
+ suggestion: "Give one step in the cycle a `next.conditions` branch that leaves it (or end the flow there)"
2741
+ });
2742
+ }
2743
+ }
2744
+ function cycleEdges(step, stepID, issues) {
2745
+ if (!isRecord(step) || !isRecord(step.next)) return [];
2746
+ const next = step.next;
2747
+ const nextPath = `steps.${stepID}.next`;
2748
+ const out = [];
2749
+ const def = issues.stringOf(next, "default", nextPath);
2750
+ if (def !== null && def !== "") out.push(def);
2751
+ if (Array.isArray(next.conditions)) {
2752
+ next.conditions.forEach((cond, i) => {
2753
+ if (!isRecord(cond)) return;
2754
+ const goto = issues.stringOf(cond, "goto", `${nextPath}.conditions[${i}]`);
2755
+ if (goto !== null && goto !== "") out.push(goto);
2758
2756
  });
2759
2757
  }
2758
+ return out;
2759
+ }
2760
+ function stepLeavesCycle(steps, stepID, inCycle, issues) {
2761
+ const step = steps[stepID];
2762
+ if (!isRecord(step) || !isRecord(step.next)) return true;
2763
+ const next = step.next;
2764
+ if (next.parallel !== void 0 && next.parallel !== null) return true;
2765
+ const def = issues.stringOf(next, "default", `steps.${stepID}.next`);
2766
+ if (def === null || def === "") return true;
2767
+ for (const target of cycleEdges(step, stepID, issues)) {
2768
+ if (isMarker(target) || !inCycle.has(target)) return true;
2769
+ }
2770
+ return false;
2771
+ }
2772
+ function cycleWithoutExit(steps, start, issues) {
2773
+ let index = 0;
2774
+ const indices = /* @__PURE__ */ new Map();
2775
+ const low = /* @__PURE__ */ new Map();
2776
+ const onStack = /* @__PURE__ */ new Set();
2777
+ const stack = [];
2778
+ let found = false;
2779
+ const strong = (id) => {
2780
+ indices.set(id, index);
2781
+ low.set(id, index);
2782
+ index++;
2783
+ stack.push(id);
2784
+ onStack.add(id);
2785
+ for (const target of cycleEdges(steps[id], id, issues)) {
2786
+ if (isMarker(target) || !Object.prototype.hasOwnProperty.call(steps, target)) continue;
2787
+ if (!indices.has(target)) {
2788
+ strong(target);
2789
+ low.set(id, Math.min(low.get(id), low.get(target)));
2790
+ } else if (onStack.has(target)) {
2791
+ low.set(id, Math.min(low.get(id), indices.get(target)));
2792
+ }
2793
+ }
2794
+ if (low.get(id) !== indices.get(id)) return;
2795
+ const component = [];
2796
+ for (; ; ) {
2797
+ const top = stack.pop();
2798
+ onStack.delete(top);
2799
+ component.push(top);
2800
+ if (top === id) break;
2801
+ }
2802
+ const inCycle = new Set(component);
2803
+ const cyclic = component.length > 1 || cycleEdges(steps[id], id, issues).includes(id);
2804
+ if (!cyclic) return;
2805
+ if (component.some((member) => stepLeavesCycle(steps, member, inCycle, issues))) return;
2806
+ found = true;
2807
+ };
2808
+ if (Object.prototype.hasOwnProperty.call(steps, start)) strong(start);
2809
+ return found;
2760
2810
  }
2761
2811
 
2762
2812
  // src/validators/nextLogic.ts
package/dist/index.cjs CHANGED
@@ -2633,31 +2633,81 @@ function validateConnectivity(flow, issues) {
2633
2633
  });
2634
2634
  }
2635
2635
  }
2636
- const visited = /* @__PURE__ */ new Set();
2637
- const recStack = /* @__PURE__ */ new Set();
2638
- const hasCycle = (stepID) => {
2639
- if (recStack.has(stepID)) return true;
2640
- if (visited.has(stepID)) return false;
2641
- visited.add(stepID);
2642
- recStack.add(stepID);
2643
- const step = steps[stepID];
2644
- if (isRecord(step) && isRecord(step.next)) {
2645
- const next = step.next;
2646
- for (const target of cycleTargets(next, `steps.${stepID}.next`, issues)) {
2647
- if (hasCycle(target)) return true;
2648
- }
2649
- }
2650
- recStack.delete(stepID);
2651
- return false;
2652
- };
2653
- if (hasCycle(start)) {
2636
+ if (cycleWithoutExit(steps, start, issues)) {
2654
2637
  issues.warn({
2655
2638
  field: "steps",
2656
- message: "Potential infinite loop detected in step flow",
2639
+ message: "Potential infinite loop detected in step flow: a cycle of steps has no way out",
2657
2640
  code: "potential_infinite_loop",
2658
- suggestion: "Review your step transitions to ensure they eventually terminate"
2641
+ suggestion: "Give one step in the cycle a `next.conditions` branch that leaves it (or end the flow there)"
2642
+ });
2643
+ }
2644
+ }
2645
+ function cycleEdges(step, stepID, issues) {
2646
+ if (!isRecord(step) || !isRecord(step.next)) return [];
2647
+ const next = step.next;
2648
+ const nextPath = `steps.${stepID}.next`;
2649
+ const out = [];
2650
+ const def = issues.stringOf(next, "default", nextPath);
2651
+ if (def !== null && def !== "") out.push(def);
2652
+ if (Array.isArray(next.conditions)) {
2653
+ next.conditions.forEach((cond, i) => {
2654
+ if (!isRecord(cond)) return;
2655
+ const goto = issues.stringOf(cond, "goto", `${nextPath}.conditions[${i}]`);
2656
+ if (goto !== null && goto !== "") out.push(goto);
2659
2657
  });
2660
2658
  }
2659
+ return out;
2660
+ }
2661
+ function stepLeavesCycle(steps, stepID, inCycle, issues) {
2662
+ const step = steps[stepID];
2663
+ if (!isRecord(step) || !isRecord(step.next)) return true;
2664
+ const next = step.next;
2665
+ if (next.parallel !== void 0 && next.parallel !== null) return true;
2666
+ const def = issues.stringOf(next, "default", `steps.${stepID}.next`);
2667
+ if (def === null || def === "") return true;
2668
+ for (const target of cycleEdges(step, stepID, issues)) {
2669
+ if (isMarker(target) || !inCycle.has(target)) return true;
2670
+ }
2671
+ return false;
2672
+ }
2673
+ function cycleWithoutExit(steps, start, issues) {
2674
+ let index = 0;
2675
+ const indices = /* @__PURE__ */ new Map();
2676
+ const low = /* @__PURE__ */ new Map();
2677
+ const onStack = /* @__PURE__ */ new Set();
2678
+ const stack = [];
2679
+ let found = false;
2680
+ const strong = (id) => {
2681
+ indices.set(id, index);
2682
+ low.set(id, index);
2683
+ index++;
2684
+ stack.push(id);
2685
+ onStack.add(id);
2686
+ for (const target of cycleEdges(steps[id], id, issues)) {
2687
+ if (isMarker(target) || !Object.prototype.hasOwnProperty.call(steps, target)) continue;
2688
+ if (!indices.has(target)) {
2689
+ strong(target);
2690
+ low.set(id, Math.min(low.get(id), low.get(target)));
2691
+ } else if (onStack.has(target)) {
2692
+ low.set(id, Math.min(low.get(id), indices.get(target)));
2693
+ }
2694
+ }
2695
+ if (low.get(id) !== indices.get(id)) return;
2696
+ const component = [];
2697
+ for (; ; ) {
2698
+ const top = stack.pop();
2699
+ onStack.delete(top);
2700
+ component.push(top);
2701
+ if (top === id) break;
2702
+ }
2703
+ const inCycle = new Set(component);
2704
+ const cyclic = component.length > 1 || cycleEdges(steps[id], id, issues).includes(id);
2705
+ if (!cyclic) return;
2706
+ if (component.some((member) => stepLeavesCycle(steps, member, inCycle, issues))) return;
2707
+ found = true;
2708
+ };
2709
+ if (Object.prototype.hasOwnProperty.call(steps, start)) strong(start);
2710
+ return found;
2661
2711
  }
2662
2712
 
2663
2713
  // src/validators/nextLogic.ts