18#include "../system/system_utils.h"
22#include <unordered_map>
23#include <unordered_set>
34 for (
size_t i = 0;
i <
issues.size(); ++
i)
44 for (
size_t i = 0;
i <
issues.size(); ++
i)
64 const std::string& ruleID,
65 const std::string& message)
69 issue.nodeID = nodeID;
70 issue.ruleID = ruleID;
71 issue.message = message;
81 SYSTEM_LOG <<
"[VSGraphVerifier] Starting verification of graph '"
82 <<
graph.Name <<
"' (" <<
graph.Nodes.size() <<
" nodes)\n";
121 SYSTEM_LOG <<
"[VSGraphVerifier] Verification complete: "
122 << result.
issues.size() <<
" issue(s) found"
123 <<
" (errors=" << (result.
HasErrors() ?
"yes" :
"no")
124 <<
" warnings=" << (result.
HasWarnings() ?
"yes" :
"no") <<
")\n";
136 for (
size_t i = 0;
i <
g.Nodes.size(); ++
i)
146 "Graph has no EntryPoint node. Exactly one EntryPoint is required.");
150 std::ostringstream
oss;
152 <<
" EntryPoint nodes. Exactly one EntryPoint is required.";
154 "E001_MultipleEntryPoints",
165 for (
size_t i = 0;
i <
g.Nodes.size(); ++
i)
183 for (
size_t j = 0;
j <
g.ExecConnections.size(); ++
j)
186 if (
c.TargetNodeID ==
node.NodeID)
188 if (
c.SourceNodeID ==
node.NodeID)
196 std::ostringstream
oss;
197 oss <<
"Node #" <<
node.NodeID <<
" ('" <<
node.NodeName
198 <<
"') has no exec connections (dangling node).";
213 std::map<int, std::vector<int> >
adj;
214 for (
size_t i = 0;
i <
g.ExecConnections.size(); ++
i)
217 adj[
c.SourceNodeID].push_back(
c.TargetNodeID);
221 std::unordered_set<int>
nodeIDs;
222 for (
size_t i = 0;
i <
g.ExecConnections.size(); ++
i)
224 nodeIDs.insert(
g.ExecConnections[
i].SourceNodeID);
225 nodeIDs.insert(
g.ExecConnections[
i].TargetNodeID);
230 std::unordered_set<int>
visited;
233 for (std::unordered_set<int>::const_iterator
startIt =
nodeIDs.begin();
242 std::vector<int> path;
243 std::unordered_set<int>
inStack;
244 std::vector<std::pair<int, size_t> >
dfsStack;
266 std::ostringstream
oss;
267 oss <<
"Exec cycle detected involving node #" <<
next <<
".";
276 path.push_back(
next);
277 dfsStack.push_back(std::make_pair(
next,
static_cast<size_t>(0)));
292 for (
size_t p = 0;
p < path.size(); ++
p)
303 for (
size_t i = 0;
i <
g.Nodes.size(); ++
i)
308 if (
node.SubGraphPath.empty())
312 if (
node.SubGraphPath ==
g.Name)
314 std::ostringstream
oss;
316 <<
"') references itself as a SubGraph (SubGraphPath == graph.Name '"
317 <<
g.Name <<
"'). This would cause infinite recursion.";
319 "E004_CircularSubGraph",
333 for (
size_t i = 0;
i <
g.Nodes.size(); ++
i)
336 for (
size_t i = 0;
i <
g.ExecConnections.size(); ++
i)
342 std::ostringstream
oss;
343 oss <<
"Exec connection references unknown source node #" <<
c.
SourceNodeID <<
".";
345 "E005_UnknownExecSourceNode",
351 std::ostringstream
oss;
352 oss <<
"Exec connection references unknown target node #" <<
c.TargetNodeID <<
".";
354 "E005_UnknownExecTargetNode",
366 for (
size_t i = 0;
i <
g.DataConnections.size(); ++
i)
374 for (
size_t ni = 0;
ni <
g.Nodes.size(); ++
ni)
377 if (
node.NodeID ==
c.SourceNodeID)
379 for (
size_t pi = 0;
pi <
node.DataPins.size(); ++
pi)
381 if (
node.DataPins[
pi].PinName ==
c.SourcePinName)
388 if (
node.NodeID ==
c.TargetNodeID)
390 for (
size_t pi = 0;
pi <
node.DataPins.size(); ++
pi)
392 if (
node.DataPins[
pi].PinName ==
c.TargetPinName)
410 std::ostringstream
oss;
411 oss <<
"Data pin type mismatch: node #" <<
c.SourceNodeID
412 <<
" pin '" <<
c.SourcePinName <<
"' -> node #" <<
c.TargetNodeID
413 <<
" pin '" <<
c.TargetPinName <<
"'. Types are incompatible.";
415 "E006_DataPinTypeMismatch",
427 for (
size_t i = 0;
i <
g.DataConnections.size(); ++
i)
434 for (
size_t ni = 0;
ni <
g.Nodes.size(); ++
ni)
437 if (
node.NodeID ==
c.SourceNodeID)
439 for (
size_t pi = 0;
pi <
node.DataPins.size(); ++
pi)
441 if (
node.DataPins[
pi].PinName ==
c.SourcePinName)
448 if (
node.NodeID ==
c.TargetNodeID)
450 for (
size_t pi = 0;
pi <
node.DataPins.size(); ++
pi)
452 if (
node.DataPins[
pi].PinName ==
c.TargetPinName)
466 std::ostringstream
oss;
467 oss <<
"Data connection from node #" <<
c.SourceNodeID
468 <<
" pin '" <<
c.SourcePinName
469 <<
"': source pin must be Output direction.";
471 "E007_InvertedPinDirection",
477 std::ostringstream
oss;
478 oss <<
"Data connection to node #" <<
c.TargetNodeID
479 <<
" pin '" <<
c.TargetPinName
480 <<
"': destination pin must be Input direction.";
482 "E007_InvertedPinDirection",
495 if (
g.Blackboard.empty())
498 for (
size_t i = 0;
i <
g.Nodes.size(); ++
i)
505 if (
node.BBKey.empty())
509 for (
size_t j = 0;
j <
g.Blackboard.size(); ++
j)
511 if (
g.Blackboard[
j].Key ==
node.BBKey)
520 std::ostringstream
oss;
522 <<
"') references unknown blackboard key '" <<
node.BBKey <<
"'.";
536 if (
g.Blackboard.empty())
539 for (
size_t i = 0;
i <
g.Nodes.size(); ++
i)
546 if (
node.BBKey.empty())
551 for (
size_t j = 0;
j <
g.Blackboard.size(); ++
j)
553 if (
g.Blackboard[
j].Key ==
node.BBKey)
564 for (
size_t pi = 0;
pi <
node.DataPins.size(); ++
pi)
572 std::ostringstream
oss;
573 oss <<
"Node #" <<
node.NodeID <<
" ('" <<
node.NodeName
575 <<
"' type does not match blackboard key '"
576 <<
node.BBKey <<
"' declared type.";
578 "E009_BBTypeMismatch",
596 for (
size_t i = 0;
i <
g.Nodes.size(); ++
i)
603 if (
node.switchVariable.empty())
605 std::ostringstream
oss;
607 <<
"') is a Switch node with no switchVariable assigned."
608 " Assign a Blackboard key to switch on.";
610 "E010_SwitchMissingVariable",
615 std::unordered_map<std::string, size_t>
seenValues;
616 for (
size_t ci = 0;
ci <
node.switchCases.size(); ++
ci)
619 if (
sc.value.empty())
624 std::ostringstream
oss;
625 oss <<
"Node #" <<
node.NodeID <<
" ('" <<
node.NodeName
626 <<
"') has duplicate switch case value '" <<
sc.
value
627 <<
"' at index " <<
ci
628 <<
" (first seen at index " <<
it->second <<
").";
630 "E011_SwitchDuplicateCaseValue",
640 for (
size_t ci = 0;
ci <
node.switchCases.size(); ++
ci)
643 if (
sc.pinName.empty())
645 std::ostringstream
oss;
646 oss <<
"Node #" <<
node.NodeID <<
" ('" <<
node.NodeName
647 <<
"') has a switch case at index " <<
ci
648 <<
" with an empty pin name.";
650 "E012_SwitchEmptyPinName",
659 for (
size_t i = 0;
i <
g.Nodes.size(); ++
i)
666 std::ostringstream
oss;
668 <<
"') is an AtomicTask with no AtomicTaskID assigned.";
670 "W001_EmptyAtomicTaskID",
677 std::ostringstream
oss;
678 oss <<
"Node #" <<
node.NodeID <<
" ('" <<
node.NodeName
679 <<
"') Delay node has DelaySeconds=" <<
node.DelaySeconds
680 <<
" (<= 0). The delay will complete immediately.";
682 "W002_NonPositiveDelay",
696 auto it =
node.Parameters.find(
"subgraph_path");
697 if (
it !=
node.Parameters.end() &&
706 std::ostringstream
oss;
707 oss <<
"Node #" <<
node.NodeID <<
" ('" <<
node.NodeName
708 <<
"') is a SubGraph node with no SubGraphPath assigned.";
710 "W003_EmptySubGraphPath",
718 std::ostringstream
oss;
719 oss <<
"Node #" <<
node.NodeID <<
" ('" <<
node.NodeName
720 <<
"') is a MathOp node with no MathOperator assigned.";
722 "W004_EmptyMathOperator",
736 for (
size_t i = 0;
i <
g.Nodes.size(); ++
i)
750 std::map<int, std::vector<int> >
adj;
751 for (
size_t i = 0;
i <
g.ExecConnections.size(); ++
i)
754 adj[
c.SourceNodeID].push_back(
c.TargetNodeID);
758 std::unordered_set<int>
visited;
759 std::vector<int>
stack;
762 while (!
stack.empty())
774 for (
size_t j = 0;
j <
it->second.size(); ++
j)
780 for (
size_t i = 0;
i <
g.Nodes.size(); ++
i)
792 std::ostringstream
oss;
794 <<
"') is not reachable from the EntryPoint.";
796 "I001_UnreachableNode",
810 for (
size_t i = 0;
i <
g.Nodes.size(); ++
i)
816 if (
node.AtomicTaskID.empty())
819 if (
reg.GetTaskSpec(
node.AtomicTaskID) ==
nullptr)
821 std::ostringstream
oss;
823 <<
"'): AtomicTaskID '" <<
node.AtomicTaskID
824 <<
"' is not registered in AtomicTaskUIRegistry (no display metadata).";
826 "W005_UnknownAtomicTaskID",
oss.str());
839 for (
size_t i = 0;
i <
g.Nodes.size(); ++
i)
845 if (
node.ConditionID.empty())
848 if (
reg.GetConditionSpec(
node.ConditionID) ==
nullptr)
850 std::ostringstream
oss;
852 <<
"'): ConditionID '" <<
node.ConditionID
853 <<
"' is not registered in ConditionRegistry.";
855 "E021_UnknownConditionID",
oss.str());
866 for (
size_t i = 0;
i <
g.Nodes.size(); ++
i)
872 if (
node.MathOperator.empty())
877 std::ostringstream
oss;
879 <<
"'): MathOperator '" <<
node.MathOperator
880 <<
"' is not a valid operator. Expected one of: +, -, *, /, %.";
882 "E023_InvalidMathOperator",
oss.str());
893 for (
size_t i = 0;
i <
g.Nodes.size(); ++
i)
903 SYSTEM_LOG <<
"[VSGraphVerifier::CheckSubGraphPaths] Node #" <<
node.NodeID
909 auto it =
node.Parameters.find(
"subgraph_path");
910 if (
it !=
node.Parameters.end())
912 SYSTEM_LOG <<
"[VSGraphVerifier::CheckSubGraphPaths] Found Parameters[subgraph_path], Type="
913 <<
static_cast<int>(
it->second.Type) <<
"\n";
918 SYSTEM_LOG <<
"[VSGraphVerifier::CheckSubGraphPaths] Resolved from Literal: '"
924 SYSTEM_LOG <<
"[VSGraphVerifier::CheckSubGraphPaths] No Parameters[subgraph_path] found\n";
930 std::ostringstream
oss;
931 oss <<
"Node #" <<
node.NodeID <<
" ('" <<
node.NodeName
932 <<
"'): SubGraphPath is empty. Set a valid .ats file path.";
934 "E024_EmptySubGraphPath",
oss.str());
947 for (
size_t i = 0;
i <
g.Nodes.size(); ++
i)
953 if (
node.ConditionID.empty())
960 for (
size_t p = 0;
p <
spec->parameters.size(); ++
p)
972 std::ostringstream
oss;
973 oss <<
"Node #" <<
node.NodeID <<
" ('" <<
node.NodeName
974 <<
"'): Required parameter '" <<
param.
name
975 <<
"' is missing for condition '" <<
node.ConditionID <<
"'.";
977 "E025_MissingConditionParam",
oss.str());
990 std::unordered_map<std::string, VariableType>
bbTypes;
991 for (
size_t i = 0;
i <
g.Blackboard.size(); ++
i)
993 bbTypes[
g.Blackboard[
i].Key] =
g.Blackboard[
i].Type;
996 for (
size_t i = 0;
i <
g.Nodes.size(); ++
i)
1006 std::ostringstream
oss;
1008 <<
"'): ForEach expects a List variable, but '"
1009 <<
node.BBKey <<
"' is declared as "
1010 <<
static_cast<int>(
it->second) <<
".";
1012 "W010_BBKeyTypeIncompatible",
oss.str());
1026 std::unordered_map<std::string, VariableType>
bbTypes;
1027 for (
size_t i = 0;
i <
g.Blackboard.size(); ++
i)
1028 bbTypes[
g.Blackboard[
i].Key] =
g.Blackboard[
i].Type;
1033 for (
size_t i = 0;
i <
g.DataConnections.size(); ++
i)
1035 std::ostringstream
k;
1036 k <<
g.DataConnections[
i].TargetNodeID <<
":" <<
g.DataConnections[
i].TargetPinName;
1040 for (
size_t ni = 0;
ni <
g.Nodes.size(); ++
ni)
1045 if (
node.conditions.empty())
1048 for (
size_t ci = 0;
ci <
node.conditions.size(); ++
ci)
1051 const std::string
condIdx = std::to_string(
ci);
1058 const std::string& mode =
cond.leftMode;
1059 const std::string&
pin =
cond.leftPin;
1060 const std::string&
var =
cond.leftVariable;
1067 std::ostringstream
oss;
1068 oss <<
"Node #" <<
node.NodeID <<
" ('" <<
node.NodeName
1069 <<
"'): condition[" <<
condIdx
1070 <<
"] left side: Pin mode selected but pin reference is empty.";
1072 "E040_ConditionPinEmpty",
oss.str());
1080 const std::string
prefix =
"Node#";
1086 if (
dotPos != std::string::npos)
1090 for (
size_t dc = 0;
dc <
g.DataConnections.size(); ++
dc)
1092 std::ostringstream
key;
1093 key <<
g.DataConnections[
dc].SourceNodeID
1094 <<
":" <<
g.DataConnections[
dc].SourcePinName;
1096 std::ostringstream
tkey;
1110 std::ostringstream
oss;
1111 oss <<
"Node #" <<
node.NodeID <<
" ('" <<
node.NodeName
1112 <<
"'): condition[" <<
condIdx
1113 <<
"] left side: Pin mode references '" <<
pin
1114 <<
"' but no DataConnection is wired from that pin.";
1116 "W016_ConditionPinNotWired",
oss.str());
1120 else if (mode ==
"Variable")
1124 std::ostringstream
oss;
1125 oss <<
"Node #" <<
node.NodeID <<
" ('" <<
node.NodeName
1126 <<
"'): condition[" <<
condIdx
1127 <<
"] left side: Variable mode but variable name is empty (E041).";
1129 "E041_ConditionVariableNotFound",
oss.str());
1134 std::ostringstream
oss;
1135 oss <<
"Node #" <<
node.NodeID <<
" ('" <<
node.NodeName
1136 <<
"'): condition[" <<
condIdx
1137 <<
"] left side: Variable '" <<
var
1138 <<
"' not declared in blackboard.";
1140 "E041_ConditionVariableNotFound",
oss.str());
1148 const std::string& mode =
cond.rightMode;
1149 const std::string&
pin =
cond.rightPin;
1150 const std::string&
var =
cond.rightVariable;
1156 std::ostringstream
oss;
1157 oss <<
"Node #" <<
node.NodeID <<
" ('" <<
node.NodeName
1158 <<
"'): condition[" <<
condIdx
1159 <<
"] right side: Pin mode selected but pin reference is empty.";
1161 "E040_ConditionPinEmpty",
oss.str());
1165 const std::string
prefix =
"Node#";
1171 if (
dotPos != std::string::npos)
1175 for (
size_t dc = 0;
dc <
g.DataConnections.size(); ++
dc)
1177 std::ostringstream
key;
1178 key <<
g.DataConnections[
dc].SourceNodeID
1179 <<
":" <<
g.DataConnections[
dc].SourcePinName;
1180 std::ostringstream
tkey;
1192 std::ostringstream
oss;
1193 oss <<
"Node #" <<
node.NodeID <<
" ('" <<
node.NodeName
1194 <<
"'): condition[" <<
condIdx
1195 <<
"] right side: Pin mode references '" <<
pin
1196 <<
"' but no DataConnection is wired from that pin.";
1198 "W016_ConditionPinNotWired",
oss.str());
1202 else if (mode ==
"Variable")
1206 std::ostringstream
oss;
1207 oss <<
"Node #" <<
node.NodeID <<
" ('" <<
node.NodeName
1208 <<
"'): condition[" <<
condIdx
1209 <<
"] right side: Variable mode but variable name is empty (E041).";
1211 "E041_ConditionVariableNotFound",
oss.str());
1215 std::ostringstream
oss;
1216 oss <<
"Node #" <<
node.NodeID <<
" ('" <<
node.NodeName
1217 <<
"'): condition[" <<
condIdx
1218 <<
"] right side: Variable '" <<
var
1219 <<
"' not declared in blackboard.";
1221 "E041_ConditionVariableNotFound",
oss.str());
1227 if (
cond.leftMode ==
"Const" &&
cond.rightMode ==
"Const")
1229 std::ostringstream
oss;
1230 oss <<
"Node #" <<
node.NodeID <<
" ('" <<
node.NodeName
1231 <<
"'): condition[" <<
condIdx
1232 <<
"] is Const vs Const — this will always evaluate to the same"
1233 " boolean value. Consider simplifying.";
1235 "W015_ConstVsConstCondition",
oss.str());
1244 if (
cond.leftMode ==
"Variable")
1249 else if (
cond.leftMode ==
"Const")
1255 if (
cond.rightMode ==
"Variable")
1260 else if (
cond.rightMode ==
"Const")
1275 std::ostringstream
oss;
1276 oss <<
"Node #" <<
node.NodeID <<
" ('" <<
node.NodeName
1277 <<
"'): condition[" <<
condIdx
1278 <<
"] type mismatch: left type="
1280 <<
" right type=" <<
static_cast<int>(
rightType)
1281 <<
" (E042). Comparison will fail at runtime.";
1283 "E042_ConditionTypeMismatch",
oss.str());
UI-side registry of available atomic tasks with display metadata.
Registry of available condition types for Branch/While node dropdowns.
ComponentTypeID GetComponentTypeID_Static()
Hardcoded lists of math and comparison operators for dropdown editors.
Global graph verifier for ATS Visual Script graphs (Phase 21-A).
Singleton registry mapping task IDs to TaskSpec metadata.
static AtomicTaskUIRegistry & Get()
Returns the singleton instance.
Singleton registry of available condition types.
static ConditionRegistry & Get()
Returns the singleton instance.
static bool IsValidMathOperator(const std::string &op)
Returns true if the given symbol is a valid math operator.
Immutable, shareable task graph asset.
static void CheckDanglingNodes(const TaskGraphTemplate &g, VSVerificationResult &r)
static void CheckDataPinTypes(const TaskGraphTemplate &g, VSVerificationResult &r)
static void CheckSubGraphCircular(const TaskGraphTemplate &g, VSVerificationResult &r)
static void CheckExecCycles(const TaskGraphTemplate &g, VSVerificationResult &r)
static void CheckBBKeyCompatibility(const TaskGraphTemplate &g, VSVerificationResult &r)
static void CheckConditionIDs(const TaskGraphTemplate &g, VSVerificationResult &r)
static void CheckSubGraphPaths(const TaskGraphTemplate &g, VSVerificationResult &r)
static void CheckEntryPoint(const TaskGraphTemplate &g, VSVerificationResult &r)
static void CheckReachability(const TaskGraphTemplate &g, VSVerificationResult &r)
static void CheckConditionParams(const TaskGraphTemplate &g, VSVerificationResult &r)
static void CheckBlackboardKeys(const TaskGraphTemplate &g, VSVerificationResult &r)
static void CheckExecPinTypes(const TaskGraphTemplate &g, VSVerificationResult &r)
static void CheckConditionStructure(const TaskGraphTemplate &g, VSVerificationResult &r)
static void CheckPinDirections(const TaskGraphTemplate &g, VSVerificationResult &r)
static void CheckSwitchNodes(const TaskGraphTemplate &g, VSVerificationResult &r)
static void AddIssue(VSVerificationResult &r, VSVerificationSeverity sev, int nodeID, const std::string &ruleID, const std::string &message)
static void CheckNodeParameterWarnings(const TaskGraphTemplate &g, VSVerificationResult &r)
static void CheckMathOperators(const TaskGraphTemplate &g, VSVerificationResult &r)
static VSVerificationResult Verify(const TaskGraphTemplate &graph)
Run all verification rules on the given graph.
static void CheckAtomicTaskIDs(const TaskGraphTemplate &g, VSVerificationResult &r)
static void CheckBlackboardTypes(const TaskGraphTemplate &g, VSVerificationResult &r)
< Provides AssetID and INVALID_ASSET_ID
VariableType
Type tags used by TaskValue to identify stored data.
@ Int
32-bit signed integer
@ Float
Single-precision float.
@ List
std::vector<TaskValue> (used by ForEach node)
@ None
Uninitialized / empty value.
@ Literal
Value is embedded directly in the template.
@ AtomicTask
Leaf node that executes a single atomic task.
@ While
Conditional loop (Loop / Completed exec outputs)
@ SubGraph
Sub-graph call (SubTask)
@ Delay
Timer (Completed exec output after N seconds)
@ GetBBValue
Data node – reads a Blackboard key.
@ MathOp
Data node – arithmetic operation (+, -, *, /)
@ SetBBValue
Data node – writes a Blackboard key.
@ ForEach
Iterate over BB list (Loop Body / Completed exec outputs)
@ Switch
Multi-branch on value (N exec outputs)
@ EntryPoint
Unique entry node for VS graphs (replaces Root)
@ Branch
If/Else conditional (Then / Else exec outputs)
@ Output
Value produced by the node.
@ Input
Value consumed by the node.
Single entry in the graph's declared blackboard schema (local or global).
Describes one parameter of a condition (e.g.
std::string name
Parameter name (e.g. "Key", "Operator")
Full metadata for a single condition type.
Describes a single condition expression for Branch/While nodes.
Explicit connection between an output data pin of a source node and an input data pin of a target nod...
Describes a data pin declared on a Visual Script node.
std::string PinName
Pin name ("Value", "Result", etc.)
Explicit connection between a named exec-out pin of a source node and the exec-in pin of a target nod...
Describes a single case branch on a Switch node.
std::string value
The value to match (int as decimal string or raw string)
Full description of a single node in the task graph.
std::string SubGraphPath
For SubGraph: path to the sub-graph JSON.
std::vector< DataPinDefinition > DataPins
Data pins declared on this node.
int32_t NodeID
Unique ID within this template.
VSVerificationSeverity severity
std::vector< VSVerificationIssue > issues
bool IsValid() const
true if no Error issues