@vaudience/aigentflow-flow-validator 0.23.0 → 0.25.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,45 @@ 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.25.0 — **the `atlas://` executor scheme and seven rules**. `atlas` becomes a known
10
+ > scheme (no `unknown_executor_scheme` warning). The URL form is `atlas://<provider>/<resource>`:
11
+ > provider `agent` has the single resource `run`, provider `capability` takes a capability reference
12
+ > as its resource. Seven new **errors**, all with the step as `field` (a loop sub-step reports its
13
+ > sub-step path and the composite `parent.child` step id):
14
+ > `atlas_operation_unknown` (any other provider, or `agent` with a resource other than `run`);
15
+ > `atlas_agent_missing` (an agent run whose `query.agent` is absent, empty or not a string);
16
+ > `atlas_agent_not_literal` (`query.agent` contains `{{`: the agent is always written out, never
17
+ > computed from input); `atlas_capabilities_not_literal` (a string entry of `query.capabilities`
18
+ > contains `{{`); `atlas_prompt_missing` and `atlas_prompt_both` (an agent run takes exactly one of
19
+ > `query.prompt` and `query.prompt_template`; an empty string counts as absent);
20
+ > `atlas_capability_ref_invalid` (the reference fails `^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$`).
21
+ > Templates in `prompt`, in `inputs` values and in a capability's `args` are fine. A templated
22
+ > executor is skipped, an unparsable URL stays `invalid_executor_url`'s, and the scheme match is
23
+ > exact (`ATLAS://` is a known scheme but gets none of these rules). The shape check runs first, so
24
+ > a reference containing a character outside letters, digits, `_`, `-` and `/` (for example a dot
25
+ > or colon) is reported as `invalid_executor_url` and never reaches the reference pattern.
26
+ > `specVersion` is unchanged (`2.831.0`). Module: `validators/executors.ts`; spec block
27
+ > `atlasExecutor`. Gates: thirteen `invalid-atlas-*` / `valid-atlas-*` fixtures (the valid ones
28
+ > forbid every atlas code and `unknown_executor_scheme`), shared byte for byte with the Go port, and
29
+ > `test/v0250-atlas-executor.test.ts`. On the 227 bundled example flows both ports report no new
30
+ > findings.
31
+
32
+ > package 0.24.0 — **`potential_infinite_loop` fires only on a cycle with no way out**, in step
33
+ > with the reference. It used to fire on every cycle reachable from the start step, so a deliberate
34
+ > ask-again loop (a step that routes back until a condition sends it onward) carried a warning that
35
+ > told the author nothing. The reference now computes the strongly connected sets (Tarjan) over
36
+ > `next.default` and `next.conditions[].goto` edges reachable from start (markers `null` /
37
+ > `orchestrator` and unknown steps are not followed; `end` is an ordinary step) and warns once when
38
+ > a cyclic set (more than one step, or a self-loop) has no exit. A member leaves the set when it has
39
+ > no `next`, its `next` has `parallel` (a fan-out is treated as an exit, not guessed at), its
40
+ > `next.default` is empty (when no condition matches the flow ends), or any default or condition
41
+ > target is `null`, `orchestrator` or a step outside the set. Code and severity are unchanged; the
42
+ > message now ends "a cycle of steps has no way out" and the suggestion asks for a `next.conditions`
43
+ > branch that leaves the cycle. It cannot prove an exit ever fires; that is undecidable from the
44
+ > YAML. Module: `validators/connectivity.ts`. Gates: ten `valid-cycle-*` / `warn-cycle-*` fixtures
45
+ > (the valid ones use `forbidWarningCodes`, the only way to assert a false-positive risk), shared
46
+ > byte for byte with the Go port. On the 227 bundled example flows both ports report 0 findings.
47
+
9
48
  > package 0.23.0 — **`unknown_function_operation`**, in step with the reference (AIgentFlow
10
49
  > v2.831.0; `specVersion` 2.831.0). One new **error**: a step or loop sub-step whose executor
11
50
  > is `function://<a>/<b>` where `<a>/<b>` is not one of the 32 registered built-ins. Before
package/dist/cli.js CHANGED
@@ -463,6 +463,7 @@ var aigentflow_spec_default = {
463
463
  "ai",
464
464
  "aiv",
465
465
  "async",
466
+ "atlas",
466
467
  "browser",
467
468
  "chart",
468
469
  "db",
@@ -1006,6 +1007,49 @@ var aigentflow_spec_default = {
1006
1007
  "utils/parse-date"
1007
1008
  ]
1008
1009
  },
1010
+ atlasExecutor: {
1011
+ $comment: "atlas://<provider>/<resource>: calls into the Atlas platform. Providers are agent (the one resource is run) and capability (the resource is a capability reference). All findings are ERRORS, field = the step (loop sub-steps use the composite step id and the sub-step path). Templated executors, unparsable URLs and other schemes are skipped. A literal agent and literal capabilities keep a step from being steered by data: query.agent and every string in query.capabilities may not contain templateActionOpen, while prompt, inputs and capability args may. A message format takes the quoted step id first.",
1012
+ scheme: "atlas",
1013
+ providerAgent: "agent",
1014
+ providerCapability: "capability",
1015
+ agentResource: "run",
1016
+ capabilityRefPattern: "^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$",
1017
+ operationUnknown: {
1018
+ code: "atlas_operation_unknown",
1019
+ messageFormat: "step %s uses %s, which is not an Atlas operation. The providers are agent (resource run) and capability (resource: a capability reference).",
1020
+ suggestion: "Use atlas://agent/run or atlas://capability/<capability-ref>."
1021
+ },
1022
+ agentMissing: {
1023
+ code: "atlas_agent_missing",
1024
+ messageFormat: "step %s runs an Atlas agent but query.agent is missing or empty.",
1025
+ suggestion: "Set query.agent to the agent to run, written out as a literal such as @acme/reporter."
1026
+ },
1027
+ agentNotLiteral: {
1028
+ code: "atlas_agent_not_literal",
1029
+ messageFormat: "step %s computes query.agent from a template. The agent must be a fixed literal, never derived from input.",
1030
+ suggestion: "Write the agent out as a literal such as @acme/reporter. Data may go into the prompt or the inputs instead."
1031
+ },
1032
+ capabilitiesNotLiteral: {
1033
+ code: "atlas_capabilities_not_literal",
1034
+ messageFormat: "step %s computes an entry of query.capabilities from a template. Capabilities must be fixed literals, never derived from input.",
1035
+ suggestion: "Write each capability out as a literal. Data may go into the prompt or the inputs instead."
1036
+ },
1037
+ promptMissing: {
1038
+ code: "atlas_prompt_missing",
1039
+ messageFormat: "step %s runs an Atlas agent with neither query.prompt nor query.prompt_template.",
1040
+ suggestion: "Set exactly one of query.prompt or query.prompt_template."
1041
+ },
1042
+ promptBoth: {
1043
+ code: "atlas_prompt_both",
1044
+ messageFormat: "step %s sets both query.prompt and query.prompt_template.",
1045
+ suggestion: "Keep exactly one of query.prompt or query.prompt_template."
1046
+ },
1047
+ capabilityRefInvalid: {
1048
+ code: "atlas_capability_ref_invalid",
1049
+ messageFormat: "step %s uses atlas://capability/%s, which is not a valid capability reference.",
1050
+ suggestion: "A capability reference starts with a letter or digit and continues with letters, digits and _ . : - up to 128 characters in all."
1051
+ }
1052
+ },
1009
1053
  flowDataReservedKeys: {
1010
1054
  $comment: "flow_data_key_reserved (ERROR, no loop demotion): a key of the top-level data: map that names engine state. data: is seeded into the mission's .data namespace, where the engine keeps its own bookkeeping, so a flow declaring such a key would forge it. Matching is exact and case-sensitive. A key is reserved when it is one of exact (each with its reason), or starts with one of prefixes (checked in order, first wins; each with its reason). The finding's field is fieldFormat with the key; the message is messageFormat with the key (JSON-quoted, as Go's %q) then the reason; the suggestion is verbatim. Findings are sorted by key. Reference: FlowDataKeyReservation, validateFlowDataKeysAreNotReserved (aigentflow#247 review).",
1011
1055
  code: "flow_data_key_reserved",
@@ -2129,6 +2173,21 @@ var FUNCTION_OPERATIONS = {
2129
2173
  operations: new Set(aigentflow_spec_default.functionOperations.operations),
2130
2174
  sorted: aigentflow_spec_default.functionOperations.operations
2131
2175
  };
2176
+ var atlas = aigentflow_spec_default.atlasExecutor;
2177
+ var ATLAS_EXECUTOR = {
2178
+ scheme: atlas.scheme,
2179
+ providerAgent: atlas.providerAgent,
2180
+ providerCapability: atlas.providerCapability,
2181
+ agentResource: atlas.agentResource,
2182
+ capabilityRefPattern: new RegExp(atlas.capabilityRefPattern),
2183
+ operationUnknown: atlas.operationUnknown,
2184
+ agentMissing: atlas.agentMissing,
2185
+ agentNotLiteral: atlas.agentNotLiteral,
2186
+ capabilitiesNotLiteral: atlas.capabilitiesNotLiteral,
2187
+ promptMissing: atlas.promptMissing,
2188
+ promptBoth: atlas.promptBoth,
2189
+ capabilityRefInvalid: atlas.capabilityRefInvalid
2190
+ };
2132
2191
 
2133
2192
  // src/validators/executors.ts
2134
2193
  var SCHEME_SEPARATOR = "://";
@@ -2192,6 +2251,51 @@ function checkExecutor(executor, field, stepID, issues) {
2192
2251
  }
2193
2252
  checkFunctionOperation(executor, field, stepID, templated, issues);
2194
2253
  }
2254
+ function checkAtlasStep(executor, query, stepField, stepID, issues) {
2255
+ if (executor.includes(TEMPLATE_ACTION_OPEN)) return;
2256
+ const m = EXECUTOR_URL_PATTERN.exec(executor);
2257
+ if (m === null || m[1] !== ATLAS_EXECUTOR.scheme) return;
2258
+ const provider = m[2];
2259
+ const resource = m[3];
2260
+ const quoted = JSON.stringify(stepID);
2261
+ const report = (rule, ...args) => {
2262
+ let message = rule.messageFormat.replace("%s", () => quoted);
2263
+ for (const a of args) message = message.replace("%s", () => a);
2264
+ issues.error({
2265
+ field: stepField,
2266
+ message,
2267
+ code: rule.code,
2268
+ stepId: stepID,
2269
+ suggestion: rule.suggestion
2270
+ });
2271
+ };
2272
+ const q2 = isRecord(query) ? query : {};
2273
+ if (provider === ATLAS_EXECUTOR.providerCapability) {
2274
+ if (!ATLAS_EXECUTOR.capabilityRefPattern.test(resource)) {
2275
+ report(ATLAS_EXECUTOR.capabilityRefInvalid, resource);
2276
+ }
2277
+ } else if (provider === ATLAS_EXECUTOR.providerAgent && resource === ATLAS_EXECUTOR.agentResource) {
2278
+ const agent = q2.agent;
2279
+ if (typeof agent !== "string" || agent === "") {
2280
+ report(ATLAS_EXECUTOR.agentMissing);
2281
+ } else if (agent.includes(TEMPLATE_ACTION_OPEN)) {
2282
+ report(ATLAS_EXECUTOR.agentNotLiteral);
2283
+ }
2284
+ const prompt = q2.prompt;
2285
+ const promptTemplate = q2.prompt_template;
2286
+ const hasPrompt = prompt !== void 0 && prompt !== null && prompt !== "";
2287
+ const hasTemplate = promptTemplate !== void 0 && promptTemplate !== null && promptTemplate !== "";
2288
+ if (!hasPrompt && !hasTemplate) report(ATLAS_EXECUTOR.promptMissing);
2289
+ else if (hasPrompt && hasTemplate) report(ATLAS_EXECUTOR.promptBoth);
2290
+ } else {
2291
+ report(ATLAS_EXECUTOR.operationUnknown, executor);
2292
+ return;
2293
+ }
2294
+ const caps = q2.capabilities;
2295
+ if (Array.isArray(caps) && caps.some((c) => typeof c === "string" && c.includes(TEMPLATE_ACTION_OPEN))) {
2296
+ report(ATLAS_EXECUTOR.capabilitiesNotLiteral);
2297
+ }
2298
+ }
2195
2299
  function checkFunctionOperation(executor, field, stepID, templated, issues) {
2196
2300
  if (templated) return;
2197
2301
  const m = EXECUTOR_URL_PATTERN.exec(executor);
@@ -2216,6 +2320,7 @@ function validateExecutors(flow, issues) {
2216
2320
  const executor = issues.stringOf(step, "executor", `steps.${stepID}`);
2217
2321
  if (executor !== null && executor !== "") {
2218
2322
  checkExecutor(executor, `steps.${stepID}.executor`, stepID, issues);
2323
+ checkAtlasStep(executor, step.query, `steps.${stepID}`, stepID, issues);
2219
2324
  }
2220
2325
  const loop = step.loop;
2221
2326
  if (isRecord(loop) && Array.isArray(loop.steps)) {
@@ -2232,6 +2337,7 @@ function validateExecutors(flow, issues) {
2232
2337
  `${stepID}.${subID}`,
2233
2338
  issues
2234
2339
  );
2340
+ checkAtlasStep(subExecutor, subStep.query, subPath, `${stepID}.${subID}`, issues);
2235
2341
  }
2236
2342
  });
2237
2343
  }
@@ -2732,31 +2838,81 @@ function validateConnectivity(flow, issues) {
2732
2838
  });
2733
2839
  }
2734
2840
  }
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)) {
2841
+ if (cycleWithoutExit(steps, start, issues)) {
2753
2842
  issues.warn({
2754
2843
  field: "steps",
2755
- message: "Potential infinite loop detected in step flow",
2844
+ message: "Potential infinite loop detected in step flow: a cycle of steps has no way out",
2756
2845
  code: "potential_infinite_loop",
2757
- suggestion: "Review your step transitions to ensure they eventually terminate"
2846
+ suggestion: "Give one step in the cycle a `next.conditions` branch that leaves it (or end the flow there)"
2847
+ });
2848
+ }
2849
+ }
2850
+ function cycleEdges(step, stepID, issues) {
2851
+ if (!isRecord(step) || !isRecord(step.next)) return [];
2852
+ const next = step.next;
2853
+ const nextPath = `steps.${stepID}.next`;
2854
+ const out = [];
2855
+ const def = issues.stringOf(next, "default", nextPath);
2856
+ if (def !== null && def !== "") out.push(def);
2857
+ if (Array.isArray(next.conditions)) {
2858
+ next.conditions.forEach((cond, i) => {
2859
+ if (!isRecord(cond)) return;
2860
+ const goto = issues.stringOf(cond, "goto", `${nextPath}.conditions[${i}]`);
2861
+ if (goto !== null && goto !== "") out.push(goto);
2758
2862
  });
2759
2863
  }
2864
+ return out;
2865
+ }
2866
+ function stepLeavesCycle(steps, stepID, inCycle, issues) {
2867
+ const step = steps[stepID];
2868
+ if (!isRecord(step) || !isRecord(step.next)) return true;
2869
+ const next = step.next;
2870
+ if (next.parallel !== void 0 && next.parallel !== null) return true;
2871
+ const def = issues.stringOf(next, "default", `steps.${stepID}.next`);
2872
+ if (def === null || def === "") return true;
2873
+ for (const target of cycleEdges(step, stepID, issues)) {
2874
+ if (isMarker(target) || !inCycle.has(target)) return true;
2875
+ }
2876
+ return false;
2877
+ }
2878
+ function cycleWithoutExit(steps, start, issues) {
2879
+ let index = 0;
2880
+ const indices = /* @__PURE__ */ new Map();
2881
+ const low = /* @__PURE__ */ new Map();
2882
+ const onStack = /* @__PURE__ */ new Set();
2883
+ const stack = [];
2884
+ let found = false;
2885
+ const strong = (id) => {
2886
+ indices.set(id, index);
2887
+ low.set(id, index);
2888
+ index++;
2889
+ stack.push(id);
2890
+ onStack.add(id);
2891
+ for (const target of cycleEdges(steps[id], id, issues)) {
2892
+ if (isMarker(target) || !Object.prototype.hasOwnProperty.call(steps, target)) continue;
2893
+ if (!indices.has(target)) {
2894
+ strong(target);
2895
+ low.set(id, Math.min(low.get(id), low.get(target)));
2896
+ } else if (onStack.has(target)) {
2897
+ low.set(id, Math.min(low.get(id), indices.get(target)));
2898
+ }
2899
+ }
2900
+ if (low.get(id) !== indices.get(id)) return;
2901
+ const component = [];
2902
+ for (; ; ) {
2903
+ const top = stack.pop();
2904
+ onStack.delete(top);
2905
+ component.push(top);
2906
+ if (top === id) break;
2907
+ }
2908
+ const inCycle = new Set(component);
2909
+ const cyclic = component.length > 1 || cycleEdges(steps[id], id, issues).includes(id);
2910
+ if (!cyclic) return;
2911
+ if (component.some((member) => stepLeavesCycle(steps, member, inCycle, issues))) return;
2912
+ found = true;
2913
+ };
2914
+ if (Object.prototype.hasOwnProperty.call(steps, start)) strong(start);
2915
+ return found;
2760
2916
  }
2761
2917
 
2762
2918
  // src/validators/nextLogic.ts
package/dist/index.cjs CHANGED
@@ -459,6 +459,7 @@ var aigentflow_spec_default = {
459
459
  "ai",
460
460
  "aiv",
461
461
  "async",
462
+ "atlas",
462
463
  "browser",
463
464
  "chart",
464
465
  "db",
@@ -988,6 +989,48 @@ var aigentflow_spec_default = {
988
989
  "utils/parse-date"
989
990
  ]
990
991
  },
992
+ atlasExecutor: {
993
+ scheme: "atlas",
994
+ providerAgent: "agent",
995
+ providerCapability: "capability",
996
+ agentResource: "run",
997
+ capabilityRefPattern: "^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$",
998
+ operationUnknown: {
999
+ code: "atlas_operation_unknown",
1000
+ messageFormat: "step %s uses %s, which is not an Atlas operation. The providers are agent (resource run) and capability (resource: a capability reference).",
1001
+ suggestion: "Use atlas://agent/run or atlas://capability/<capability-ref>."
1002
+ },
1003
+ agentMissing: {
1004
+ code: "atlas_agent_missing",
1005
+ messageFormat: "step %s runs an Atlas agent but query.agent is missing or empty.",
1006
+ suggestion: "Set query.agent to the agent to run, written out as a literal such as @acme/reporter."
1007
+ },
1008
+ agentNotLiteral: {
1009
+ code: "atlas_agent_not_literal",
1010
+ messageFormat: "step %s computes query.agent from a template. The agent must be a fixed literal, never derived from input.",
1011
+ suggestion: "Write the agent out as a literal such as @acme/reporter. Data may go into the prompt or the inputs instead."
1012
+ },
1013
+ capabilitiesNotLiteral: {
1014
+ code: "atlas_capabilities_not_literal",
1015
+ messageFormat: "step %s computes an entry of query.capabilities from a template. Capabilities must be fixed literals, never derived from input.",
1016
+ suggestion: "Write each capability out as a literal. Data may go into the prompt or the inputs instead."
1017
+ },
1018
+ promptMissing: {
1019
+ code: "atlas_prompt_missing",
1020
+ messageFormat: "step %s runs an Atlas agent with neither query.prompt nor query.prompt_template.",
1021
+ suggestion: "Set exactly one of query.prompt or query.prompt_template."
1022
+ },
1023
+ promptBoth: {
1024
+ code: "atlas_prompt_both",
1025
+ messageFormat: "step %s sets both query.prompt and query.prompt_template.",
1026
+ suggestion: "Keep exactly one of query.prompt or query.prompt_template."
1027
+ },
1028
+ capabilityRefInvalid: {
1029
+ code: "atlas_capability_ref_invalid",
1030
+ messageFormat: "step %s uses atlas://capability/%s, which is not a valid capability reference.",
1031
+ suggestion: "A capability reference starts with a letter or digit and continues with letters, digits and _ . : - up to 128 characters in all."
1032
+ }
1033
+ },
991
1034
  flowDataReservedKeys: {
992
1035
  code: "flow_data_key_reserved",
993
1036
  fieldFormat: "data.%s",
@@ -2030,6 +2073,21 @@ var FUNCTION_OPERATIONS = {
2030
2073
  operations: new Set(aigentflow_spec_default.functionOperations.operations),
2031
2074
  sorted: aigentflow_spec_default.functionOperations.operations
2032
2075
  };
2076
+ var atlas = aigentflow_spec_default.atlasExecutor;
2077
+ var ATLAS_EXECUTOR = {
2078
+ scheme: atlas.scheme,
2079
+ providerAgent: atlas.providerAgent,
2080
+ providerCapability: atlas.providerCapability,
2081
+ agentResource: atlas.agentResource,
2082
+ capabilityRefPattern: new RegExp(atlas.capabilityRefPattern),
2083
+ operationUnknown: atlas.operationUnknown,
2084
+ agentMissing: atlas.agentMissing,
2085
+ agentNotLiteral: atlas.agentNotLiteral,
2086
+ capabilitiesNotLiteral: atlas.capabilitiesNotLiteral,
2087
+ promptMissing: atlas.promptMissing,
2088
+ promptBoth: atlas.promptBoth,
2089
+ capabilityRefInvalid: atlas.capabilityRefInvalid
2090
+ };
2033
2091
 
2034
2092
  // src/validators/executors.ts
2035
2093
  var SCHEME_SEPARATOR = "://";
@@ -2093,6 +2151,51 @@ function checkExecutor(executor, field, stepID, issues) {
2093
2151
  }
2094
2152
  checkFunctionOperation(executor, field, stepID, templated, issues);
2095
2153
  }
2154
+ function checkAtlasStep(executor, query, stepField, stepID, issues) {
2155
+ if (executor.includes(TEMPLATE_ACTION_OPEN)) return;
2156
+ const m = EXECUTOR_URL_PATTERN.exec(executor);
2157
+ if (m === null || m[1] !== ATLAS_EXECUTOR.scheme) return;
2158
+ const provider = m[2];
2159
+ const resource = m[3];
2160
+ const quoted = JSON.stringify(stepID);
2161
+ const report = (rule, ...args) => {
2162
+ let message = rule.messageFormat.replace("%s", () => quoted);
2163
+ for (const a of args) message = message.replace("%s", () => a);
2164
+ issues.error({
2165
+ field: stepField,
2166
+ message,
2167
+ code: rule.code,
2168
+ stepId: stepID,
2169
+ suggestion: rule.suggestion
2170
+ });
2171
+ };
2172
+ const q2 = isRecord(query) ? query : {};
2173
+ if (provider === ATLAS_EXECUTOR.providerCapability) {
2174
+ if (!ATLAS_EXECUTOR.capabilityRefPattern.test(resource)) {
2175
+ report(ATLAS_EXECUTOR.capabilityRefInvalid, resource);
2176
+ }
2177
+ } else if (provider === ATLAS_EXECUTOR.providerAgent && resource === ATLAS_EXECUTOR.agentResource) {
2178
+ const agent = q2.agent;
2179
+ if (typeof agent !== "string" || agent === "") {
2180
+ report(ATLAS_EXECUTOR.agentMissing);
2181
+ } else if (agent.includes(TEMPLATE_ACTION_OPEN)) {
2182
+ report(ATLAS_EXECUTOR.agentNotLiteral);
2183
+ }
2184
+ const prompt = q2.prompt;
2185
+ const promptTemplate = q2.prompt_template;
2186
+ const hasPrompt = prompt !== void 0 && prompt !== null && prompt !== "";
2187
+ const hasTemplate = promptTemplate !== void 0 && promptTemplate !== null && promptTemplate !== "";
2188
+ if (!hasPrompt && !hasTemplate) report(ATLAS_EXECUTOR.promptMissing);
2189
+ else if (hasPrompt && hasTemplate) report(ATLAS_EXECUTOR.promptBoth);
2190
+ } else {
2191
+ report(ATLAS_EXECUTOR.operationUnknown, executor);
2192
+ return;
2193
+ }
2194
+ const caps = q2.capabilities;
2195
+ if (Array.isArray(caps) && caps.some((c) => typeof c === "string" && c.includes(TEMPLATE_ACTION_OPEN))) {
2196
+ report(ATLAS_EXECUTOR.capabilitiesNotLiteral);
2197
+ }
2198
+ }
2096
2199
  function checkFunctionOperation(executor, field, stepID, templated, issues) {
2097
2200
  if (templated) return;
2098
2201
  const m = EXECUTOR_URL_PATTERN.exec(executor);
@@ -2117,6 +2220,7 @@ function validateExecutors(flow, issues) {
2117
2220
  const executor = issues.stringOf(step, "executor", `steps.${stepID}`);
2118
2221
  if (executor !== null && executor !== "") {
2119
2222
  checkExecutor(executor, `steps.${stepID}.executor`, stepID, issues);
2223
+ checkAtlasStep(executor, step.query, `steps.${stepID}`, stepID, issues);
2120
2224
  }
2121
2225
  const loop = step.loop;
2122
2226
  if (isRecord(loop) && Array.isArray(loop.steps)) {
@@ -2133,6 +2237,7 @@ function validateExecutors(flow, issues) {
2133
2237
  `${stepID}.${subID}`,
2134
2238
  issues
2135
2239
  );
2240
+ checkAtlasStep(subExecutor, subStep.query, subPath, `${stepID}.${subID}`, issues);
2136
2241
  }
2137
2242
  });
2138
2243
  }
@@ -2633,31 +2738,81 @@ function validateConnectivity(flow, issues) {
2633
2738
  });
2634
2739
  }
2635
2740
  }
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)) {
2741
+ if (cycleWithoutExit(steps, start, issues)) {
2654
2742
  issues.warn({
2655
2743
  field: "steps",
2656
- message: "Potential infinite loop detected in step flow",
2744
+ message: "Potential infinite loop detected in step flow: a cycle of steps has no way out",
2657
2745
  code: "potential_infinite_loop",
2658
- suggestion: "Review your step transitions to ensure they eventually terminate"
2746
+ suggestion: "Give one step in the cycle a `next.conditions` branch that leaves it (or end the flow there)"
2747
+ });
2748
+ }
2749
+ }
2750
+ function cycleEdges(step, stepID, issues) {
2751
+ if (!isRecord(step) || !isRecord(step.next)) return [];
2752
+ const next = step.next;
2753
+ const nextPath = `steps.${stepID}.next`;
2754
+ const out = [];
2755
+ const def = issues.stringOf(next, "default", nextPath);
2756
+ if (def !== null && def !== "") out.push(def);
2757
+ if (Array.isArray(next.conditions)) {
2758
+ next.conditions.forEach((cond, i) => {
2759
+ if (!isRecord(cond)) return;
2760
+ const goto = issues.stringOf(cond, "goto", `${nextPath}.conditions[${i}]`);
2761
+ if (goto !== null && goto !== "") out.push(goto);
2659
2762
  });
2660
2763
  }
2764
+ return out;
2765
+ }
2766
+ function stepLeavesCycle(steps, stepID, inCycle, issues) {
2767
+ const step = steps[stepID];
2768
+ if (!isRecord(step) || !isRecord(step.next)) return true;
2769
+ const next = step.next;
2770
+ if (next.parallel !== void 0 && next.parallel !== null) return true;
2771
+ const def = issues.stringOf(next, "default", `steps.${stepID}.next`);
2772
+ if (def === null || def === "") return true;
2773
+ for (const target of cycleEdges(step, stepID, issues)) {
2774
+ if (isMarker(target) || !inCycle.has(target)) return true;
2775
+ }
2776
+ return false;
2777
+ }
2778
+ function cycleWithoutExit(steps, start, issues) {
2779
+ let index = 0;
2780
+ const indices = /* @__PURE__ */ new Map();
2781
+ const low = /* @__PURE__ */ new Map();
2782
+ const onStack = /* @__PURE__ */ new Set();
2783
+ const stack = [];
2784
+ let found = false;
2785
+ const strong = (id) => {
2786
+ indices.set(id, index);
2787
+ low.set(id, index);
2788
+ index++;
2789
+ stack.push(id);
2790
+ onStack.add(id);
2791
+ for (const target of cycleEdges(steps[id], id, issues)) {
2792
+ if (isMarker(target) || !Object.prototype.hasOwnProperty.call(steps, target)) continue;
2793
+ if (!indices.has(target)) {
2794
+ strong(target);
2795
+ low.set(id, Math.min(low.get(id), low.get(target)));
2796
+ } else if (onStack.has(target)) {
2797
+ low.set(id, Math.min(low.get(id), indices.get(target)));
2798
+ }
2799
+ }
2800
+ if (low.get(id) !== indices.get(id)) return;
2801
+ const component = [];
2802
+ for (; ; ) {
2803
+ const top = stack.pop();
2804
+ onStack.delete(top);
2805
+ component.push(top);
2806
+ if (top === id) break;
2807
+ }
2808
+ const inCycle = new Set(component);
2809
+ const cyclic = component.length > 1 || cycleEdges(steps[id], id, issues).includes(id);
2810
+ if (!cyclic) return;
2811
+ if (component.some((member) => stepLeavesCycle(steps, member, inCycle, issues))) return;
2812
+ found = true;
2813
+ };
2814
+ if (Object.prototype.hasOwnProperty.call(steps, start)) strong(start);
2815
+ return found;
2661
2816
  }
2662
2817
 
2663
2818
  // src/validators/nextLogic.ts