Olympe Engine 2.0
2D Game Engine with ECS Architecture
Loading...
Searching...
No Matches
VSGraphVerifier.cpp
Go to the documentation of this file.
1/**
2 * @file VSGraphVerifier.cpp
3 * @brief Global graph verifier for ATS Visual Script graphs (Phase 21-A).
4 * @author Olympe Engine
5 * @date 2026-03-14
6 *
7 * @details
8 * Implements 17 verification rules (E001-E012, W001-W004, I001) on a
9 * TaskGraphTemplate. Fully stateless — no ImGui/ImNodes dependency.
10 *
11 * C++14 compliant — no std::optional, structured bindings, std::filesystem.
12 */
13
14#include "VSGraphVerifier.h"
16#include "ConditionRegistry.h"
17#include "OperatorRegistry.h"
18#include "../system/system_utils.h"
19
20#include <map>
21#include <vector>
22#include <unordered_map>
23#include <unordered_set>
24#include <sstream>
25
26namespace Olympe {
27
28// ============================================================================
29// VSVerificationResult — helpers
30// ============================================================================
31
33{
34 for (size_t i = 0; i < issues.size(); ++i)
35 {
36 if (issues[i].severity == VSVerificationSeverity::Error)
37 return true;
38 }
39 return false;
40}
41
43{
44 for (size_t i = 0; i < issues.size(); ++i)
45 {
47 return true;
48 }
49 return false;
50}
51
53{
54 return !HasErrors();
55}
56
57// ============================================================================
58// VSGraphVerifier — private helpers
59// ============================================================================
60
63 int nodeID,
64 const std::string& ruleID,
65 const std::string& message)
66{
69 issue.nodeID = nodeID;
70 issue.ruleID = ruleID;
71 issue.message = message;
72 r.issues.push_back(issue);
73}
74
75// ============================================================================
76// VSGraphVerifier::Verify
77// ============================================================================
78
80{
81 SYSTEM_LOG << "[VSGraphVerifier] Starting verification of graph '"
82 << graph.Name << "' (" << graph.Nodes.size() << " nodes)\n";
83
85
86 // Structural rules
87 CheckEntryPoint(graph, result);
89 CheckExecCycles(graph, result);
91
92 // Type-safety rules
93 CheckExecPinTypes(graph, result);
94 CheckDataPinTypes(graph, result);
96
97 // Blackboard rules
100
101 // Switch rules (Phase 22-A)
102 CheckSwitchNodes(graph, result);
103
104 // Registry rules (Phase 22-C)
105 CheckAtomicTaskIDs(graph, result);
106 CheckConditionIDs(graph, result);
107 CheckMathOperators(graph, result);
108 CheckSubGraphPaths(graph, result);
111
112 // Condition structure rules (Phase 23-B.4)
114
115 // Warning rules
117
118 // Info rules
119 CheckReachability(graph, result);
120
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";
125
126 return result;
127}
128
129// ============================================================================
130// E001 — Exactly one EntryPoint node required
131// ============================================================================
132
134{
135 int count = 0;
136 for (size_t i = 0; i < g.Nodes.size(); ++i)
137 {
138 if (g.Nodes[i].Type == TaskNodeType::EntryPoint)
139 ++count;
140 }
141
142 if (count == 0)
143 {
145 "E001_NoEntryPoint",
146 "Graph has no EntryPoint node. Exactly one EntryPoint is required.");
147 }
148 else if (count > 1)
149 {
150 std::ostringstream oss;
151 oss << "Graph has " << count
152 << " EntryPoint nodes. Exactly one EntryPoint is required.";
154 "E001_MultipleEntryPoints",
155 oss.str());
156 }
157}
158
159// ============================================================================
160// E002 — Dangling node (no exec in or out, except EntryPoint)
161// ============================================================================
162
164{
165 for (size_t i = 0; i < g.Nodes.size(); ++i)
166 {
167 const TaskNodeDefinition& node = g.Nodes[i];
168
169 // EntryPoint has no exec-in by design — skip
170 if (node.Type == TaskNodeType::EntryPoint)
171 continue;
172
173 // Phase 24.3 - CRITICAL: Skip data-pure nodes (GetBBValue, MathOp)
174 // These nodes have no exec pins and are connected via data pins instead
175 // They are NOT dangling — they are pure data computation nodes
176 if (node.Type == TaskNodeType::GetBBValue ||
178 continue;
179
180 bool hasExecIn = false;
181 bool hasExecOut = false;
182
183 for (size_t j = 0; j < g.ExecConnections.size(); ++j)
184 {
185 const ExecPinConnection& c = g.ExecConnections[j];
186 if (c.TargetNodeID == node.NodeID)
187 hasExecIn = true;
188 if (c.SourceNodeID == node.NodeID)
189 hasExecOut = true;
190 if (hasExecIn && hasExecOut)
191 break;
192 }
193
194 if (!hasExecIn && !hasExecOut)
195 {
196 std::ostringstream oss;
197 oss << "Node #" << node.NodeID << " ('" << node.NodeName
198 << "') has no exec connections (dangling node).";
200 "E002_DanglingNode",
201 oss.str());
202 }
203 }
204}
205
206// ============================================================================
207// E003 — Exec cycle detected (iterative DFS on adjacency list)
208// ============================================================================
209
211{
212 // Build adjacency list: nodeID -> list of successor nodeIDs
213 std::map<int, std::vector<int> > adj;
214 for (size_t i = 0; i < g.ExecConnections.size(); ++i)
215 {
216 const ExecPinConnection& c = g.ExecConnections[i];
217 adj[c.SourceNodeID].push_back(c.TargetNodeID);
218 }
219
220 // Collect all node IDs that appear in ExecConnections
221 std::unordered_set<int> nodeIDs;
222 for (size_t i = 0; i < g.ExecConnections.size(); ++i)
223 {
224 nodeIDs.insert(g.ExecConnections[i].SourceNodeID);
225 nodeIDs.insert(g.ExecConnections[i].TargetNodeID);
226 }
227
228 // Run iterative DFS cycle detection from each unvisited node
229 // visited: permanently finished; inStack: currently in DFS path
230 std::unordered_set<int> visited;
231 bool cycleReported = false;
232
233 for (std::unordered_set<int>::const_iterator startIt = nodeIDs.begin();
234 startIt != nodeIDs.end() && !cycleReported; ++startIt)
235 {
236 int startNode = *startIt;
237 if (visited.count(startNode) > 0)
238 continue;
239
240 // Iterative DFS using explicit stack of (node, iterator-into-adj)
241 // We simulate the recursive call stack.
242 std::vector<int> path;
243 std::unordered_set<int> inStack;
244 std::vector<std::pair<int, size_t> > dfsStack;
245
246 dfsStack.push_back(std::make_pair(startNode, static_cast<size_t>(0)));
247 inStack.insert(startNode);
248 path.push_back(startNode);
249
250 while (!dfsStack.empty() && !cycleReported)
251 {
252 std::pair<int, size_t>& top = dfsStack.back();
253 int cur = top.first;
254 size_t& idx = top.second;
255
256 std::map<int, std::vector<int> >::const_iterator adjIt = adj.find(cur);
257
258 if (adjIt != adj.end() && idx < adjIt->second.size())
259 {
260 int next = adjIt->second[idx];
261 ++idx;
262
263 if (inStack.count(next) > 0)
264 {
265 // Cycle found
266 std::ostringstream oss;
267 oss << "Exec cycle detected involving node #" << next << ".";
269 "E003_ExecCycle",
270 oss.str());
271 cycleReported = true;
272 }
273 else if (visited.count(next) == 0)
274 {
275 inStack.insert(next);
276 path.push_back(next);
277 dfsStack.push_back(std::make_pair(next, static_cast<size_t>(0)));
278 }
279 }
280 else
281 {
282 // Finished with cur
283 visited.insert(cur);
284 inStack.erase(cur);
285 dfsStack.pop_back();
286 if (!path.empty())
287 path.pop_back();
288 }
289 }
290
291 // Mark all nodes in this DFS tree as visited
292 for (size_t p = 0; p < path.size(); ++p)
293 visited.insert(path[p]);
294 }
295}
296
297// ============================================================================
298// E004 — Circular SubGraph reference (self-reference check)
299// ============================================================================
300
302{
303 for (size_t i = 0; i < g.Nodes.size(); ++i)
304 {
305 const TaskNodeDefinition& node = g.Nodes[i];
306 if (node.Type != TaskNodeType::SubGraph)
307 continue;
308 if (node.SubGraphPath.empty())
309 continue;
310
311 // Simple self-reference check: SubGraphPath matches graph name
312 if (node.SubGraphPath == g.Name)
313 {
314 std::ostringstream oss;
315 oss << "Node #" << node.NodeID << " ('" << node.NodeName
316 << "') references itself as a SubGraph (SubGraphPath == graph.Name '"
317 << g.Name << "'). This would cause infinite recursion.";
319 "E004_CircularSubGraph",
320 oss.str());
321 }
322 }
323}
324
325// ============================================================================
326// E005 — Exec connection references unknown node
327// ============================================================================
328
330{
331 // Build fast lookup set of valid node IDs
332 std::unordered_set<int> validIDs;
333 for (size_t i = 0; i < g.Nodes.size(); ++i)
334 validIDs.insert(g.Nodes[i].NodeID);
335
336 for (size_t i = 0; i < g.ExecConnections.size(); ++i)
337 {
338 const ExecPinConnection& c = g.ExecConnections[i];
339
340 if (validIDs.count(c.SourceNodeID) == 0)
341 {
342 std::ostringstream oss;
343 oss << "Exec connection references unknown source node #" << c.SourceNodeID << ".";
345 "E005_UnknownExecSourceNode",
346 oss.str());
347 }
348
349 if (validIDs.count(c.TargetNodeID) == 0)
350 {
351 std::ostringstream oss;
352 oss << "Exec connection references unknown target node #" << c.TargetNodeID << ".";
354 "E005_UnknownExecTargetNode",
355 oss.str());
356 }
357 }
358}
359
360// ============================================================================
361// E006 — Incompatible data pin types
362// ============================================================================
363
365{
366 for (size_t i = 0; i < g.DataConnections.size(); ++i)
367 {
368 const DataPinConnection& c = g.DataConnections[i];
369
370 // Find source node and pin
373
374 for (size_t ni = 0; ni < g.Nodes.size(); ++ni)
375 {
376 const TaskNodeDefinition& node = g.Nodes[ni];
377 if (node.NodeID == c.SourceNodeID)
378 {
379 for (size_t pi = 0; pi < node.DataPins.size(); ++pi)
380 {
381 if (node.DataPins[pi].PinName == c.SourcePinName)
382 {
384 break;
385 }
386 }
387 }
388 if (node.NodeID == c.TargetNodeID)
389 {
390 for (size_t pi = 0; pi < node.DataPins.size(); ++pi)
391 {
392 if (node.DataPins[pi].PinName == c.TargetPinName)
393 {
394 dstPin = &node.DataPins[pi];
395 break;
396 }
397 }
398 }
399 }
400
401 if (srcPin == NULL || dstPin == NULL)
402 continue; // Can't validate without pin definitions
403
404 // VariableType::None is treated as "any" — skip type check
405 if (srcPin->PinType == VariableType::None || dstPin->PinType == VariableType::None)
406 continue;
407
408 if (srcPin->PinType != dstPin->PinType)
409 {
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",
416 oss.str());
417 }
418 }
419}
420
421// ============================================================================
422// E007 — Inverted pin direction
423// ============================================================================
424
426{
427 for (size_t i = 0; i < g.DataConnections.size(); ++i)
428 {
429 const DataPinConnection& c = g.DataConnections[i];
430
433
434 for (size_t ni = 0; ni < g.Nodes.size(); ++ni)
435 {
436 const TaskNodeDefinition& node = g.Nodes[ni];
437 if (node.NodeID == c.SourceNodeID)
438 {
439 for (size_t pi = 0; pi < node.DataPins.size(); ++pi)
440 {
441 if (node.DataPins[pi].PinName == c.SourcePinName)
442 {
444 break;
445 }
446 }
447 }
448 if (node.NodeID == c.TargetNodeID)
449 {
450 for (size_t pi = 0; pi < node.DataPins.size(); ++pi)
451 {
452 if (node.DataPins[pi].PinName == c.TargetPinName)
453 {
454 dstPin = &node.DataPins[pi];
455 break;
456 }
457 }
458 }
459 }
460
461 if (srcPin == NULL || dstPin == NULL)
462 continue;
463
464 if (srcPin->Dir != DataPinDir::Output)
465 {
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",
472 oss.str());
473 }
474
475 if (dstPin->Dir != DataPinDir::Input)
476 {
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",
483 oss.str());
484 }
485 }
486}
487
488// ============================================================================
489// E008 — Unknown Blackboard key in GetBBValue/SetBBValue
490// ============================================================================
491
493{
494 // Skip if no blackboard schema declared
495 if (g.Blackboard.empty())
496 return;
497
498 for (size_t i = 0; i < g.Nodes.size(); ++i)
499 {
500 const TaskNodeDefinition& node = g.Nodes[i];
501
503 continue;
504
505 if (node.BBKey.empty())
506 continue; // Will be caught by W001-style rules if needed
507
508 bool found = false;
509 for (size_t j = 0; j < g.Blackboard.size(); ++j)
510 {
511 if (g.Blackboard[j].Key == node.BBKey)
512 {
513 found = true;
514 break;
515 }
516 }
517
518 if (!found)
519 {
520 std::ostringstream oss;
521 oss << "Node #" << node.NodeID << " ('" << node.NodeName
522 << "') references unknown blackboard key '" << node.BBKey << "'.";
524 "E008_UnknownBBKey",
525 oss.str());
526 }
527 }
528}
529
530// ============================================================================
531// E009 — Blackboard type mismatch
532// ============================================================================
533
535{
536 if (g.Blackboard.empty())
537 return;
538
539 for (size_t i = 0; i < g.Nodes.size(); ++i)
540 {
541 const TaskNodeDefinition& node = g.Nodes[i];
542
544 continue;
545
546 if (node.BBKey.empty())
547 continue;
548
549 // Find the BB entry
550 const BlackboardEntry* entry = NULL;
551 for (size_t j = 0; j < g.Blackboard.size(); ++j)
552 {
553 if (g.Blackboard[j].Key == node.BBKey)
554 {
555 entry = &g.Blackboard[j];
556 break;
557 }
558 }
559
560 if (entry == NULL)
561 continue; // Unknown key already reported by E008
562
563 // Find the data pin on the node (first data pin with a declared type)
564 for (size_t pi = 0; pi < node.DataPins.size(); ++pi)
565 {
566 const DataPinDefinition& pin = node.DataPins[pi];
567 if (pin.PinType == VariableType::None)
568 continue;
569
570 if (pin.PinType != entry->Type)
571 {
572 std::ostringstream oss;
573 oss << "Node #" << node.NodeID << " ('" << node.NodeName
574 << "') data pin '" << pin.PinName
575 << "' type does not match blackboard key '"
576 << node.BBKey << "' declared type.";
578 "E009_BBTypeMismatch",
579 oss.str());
580 break; // Report once per node
581 }
582 }
583 }
584}
585
586// ============================================================================
587// Warning rules — W001–W004
588// ============================================================================
589
590// ============================================================================
591// E010–E012 — Switch node validation (Phase 22-A)
592// ============================================================================
593
595{
596 for (size_t i = 0; i < g.Nodes.size(); ++i)
597 {
598 const TaskNodeDefinition& node = g.Nodes[i];
599 if (node.Type != TaskNodeType::Switch)
600 continue;
601
602 // E010 — Switch node missing switchVariable
603 if (node.switchVariable.empty())
604 {
605 std::ostringstream oss;
606 oss << "Node #" << node.NodeID << " ('" << node.NodeName
607 << "') is a Switch node with no switchVariable assigned."
608 " Assign a Blackboard key to switch on.";
610 "E010_SwitchMissingVariable",
611 oss.str());
612 }
613
614 // E011 — duplicate case values
615 std::unordered_map<std::string, size_t> seenValues;
616 for (size_t ci = 0; ci < node.switchCases.size(); ++ci)
617 {
618 const SwitchCaseDefinition& sc = node.switchCases[ci];
619 if (sc.value.empty())
620 continue;
621 auto it = seenValues.find(sc.value);
622 if (it != seenValues.end())
623 {
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",
631 oss.str());
632 }
633 else
634 {
635 seenValues[sc.value] = ci;
636 }
637 }
638
639 // E012 — case with empty pin name
640 for (size_t ci = 0; ci < node.switchCases.size(); ++ci)
641 {
642 const SwitchCaseDefinition& sc = node.switchCases[ci];
643 if (sc.pinName.empty())
644 {
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",
651 oss.str());
652 }
653 }
654 }
655}
656
658{
659 for (size_t i = 0; i < g.Nodes.size(); ++i)
660 {
661 const TaskNodeDefinition& node = g.Nodes[i];
662
663 // W001 — AtomicTask with empty AtomicTaskID
664 if (node.Type == TaskNodeType::AtomicTask && node.AtomicTaskID.empty())
665 {
666 std::ostringstream oss;
667 oss << "Node #" << node.NodeID << " ('" << node.NodeName
668 << "') is an AtomicTask with no AtomicTaskID assigned.";
670 "W001_EmptyAtomicTaskID",
671 oss.str());
672 }
673
674 // W002 — Delay with DelaySeconds <= 0
675 if (node.Type == TaskNodeType::Delay && node.DelaySeconds <= 0.0f)
676 {
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",
683 oss.str());
684 }
685
686 // W003 — SubGraph with empty SubGraphPath
687 if (node.Type == TaskNodeType::SubGraph)
688 {
689 // Check both SubGraphPath field AND Parameters["subgraph_path"]
690 // (UI edits Parameters, but we sync to SubGraphPath only before save)
691 std::string resolvedPath = node.SubGraphPath;
692
693 // Fallback: check Parameters["subgraph_path"] if SubGraphPath is empty
694 if (resolvedPath.empty())
695 {
696 auto it = node.Parameters.find("subgraph_path");
697 if (it != node.Parameters.end() &&
698 it->second.Type == ParameterBindingType::Literal)
699 {
700 resolvedPath = it->second.LiteralValue.to_string();
701 }
702 }
703
704 if (resolvedPath.empty())
705 {
706 std::ostringstream oss;
707 oss << "Node #" << node.NodeID << " ('" << node.NodeName
708 << "') is a SubGraph node with no SubGraphPath assigned.";
710 "W003_EmptySubGraphPath",
711 oss.str());
712 }
713 }
714
715 // W004 — MathOp with empty MathOperator
716 if (node.Type == TaskNodeType::MathOp && node.MathOperator.empty())
717 {
718 std::ostringstream oss;
719 oss << "Node #" << node.NodeID << " ('" << node.NodeName
720 << "') is a MathOp node with no MathOperator assigned.";
722 "W004_EmptyMathOperator",
723 oss.str());
724 }
725 }
726}
727
728// ============================================================================
729// I001 — Node not reachable from EntryPoint
730// ============================================================================
731
733{
734 // Find the EntryPoint node ID
735 int entryID = -1;
736 for (size_t i = 0; i < g.Nodes.size(); ++i)
737 {
738 if (g.Nodes[i].Type == TaskNodeType::EntryPoint)
739 {
740 entryID = g.Nodes[i].NodeID;
741 break;
742 }
743 }
744
745 // No EntryPoint — cannot do reachability (E001 already reported this)
746 if (entryID == -1)
747 return;
748
749 // Build adjacency list from exec connections
750 std::map<int, std::vector<int> > adj;
751 for (size_t i = 0; i < g.ExecConnections.size(); ++i)
752 {
753 const ExecPinConnection& c = g.ExecConnections[i];
754 adj[c.SourceNodeID].push_back(c.TargetNodeID);
755 }
756
757 // BFS/DFS from entryID
758 std::unordered_set<int> visited;
759 std::vector<int> stack;
760 stack.push_back(entryID);
761
762 while (!stack.empty())
763 {
764 int cur = stack.back();
765 stack.pop_back();
766
767 if (visited.count(cur) > 0)
768 continue;
769 visited.insert(cur);
770
771 std::map<int, std::vector<int> >::const_iterator it = adj.find(cur);
772 if (it != adj.end())
773 {
774 for (size_t j = 0; j < it->second.size(); ++j)
775 stack.push_back(it->second[j]);
776 }
777 }
778
779 // Any node not visited is unreachable
780 for (size_t i = 0; i < g.Nodes.size(); ++i)
781 {
782 const TaskNodeDefinition& node = g.Nodes[i];
783
784 // Phase 24.3 - CRITICAL: Skip data-pure nodes (GetBBValue, MathOp)
785 // These nodes are connected via data pins, not exec connections
786 // They don't need to be reachable from EntryPoint via exec flow
788 continue;
789
790 if (visited.count(node.NodeID) == 0)
791 {
792 std::ostringstream oss;
793 oss << "Node #" << node.NodeID << " ('" << node.NodeName
794 << "') is not reachable from the EntryPoint.";
796 "I001_UnreachableNode",
797 oss.str());
798 }
799 }
800}
801
802// ============================================================================
803// E020 — AtomicTask: taskType not registered in AtomicTaskUIRegistry
804// ============================================================================
805
807{
809
810 for (size_t i = 0; i < g.Nodes.size(); ++i)
811 {
812 const TaskNodeDefinition& node = g.Nodes[i];
813 if (node.Type != TaskNodeType::AtomicTask)
814 continue;
815
816 if (node.AtomicTaskID.empty())
817 continue; // W001 handles empty IDs separately
818
819 if (reg.GetTaskSpec(node.AtomicTaskID) == nullptr)
820 {
821 std::ostringstream oss;
822 oss << "Node #" << node.NodeID << " ('" << node.NodeName
823 << "'): AtomicTaskID '" << node.AtomicTaskID
824 << "' is not registered in AtomicTaskUIRegistry (no display metadata).";
826 "W005_UnknownAtomicTaskID", oss.str());
827 }
828 }
829}
830
831// ============================================================================
832// E021 — Branch/While: conditionType not registered in ConditionRegistry
833// ============================================================================
834
836{
838
839 for (size_t i = 0; i < g.Nodes.size(); ++i)
840 {
841 const TaskNodeDefinition& node = g.Nodes[i];
842 if (node.Type != TaskNodeType::Branch && node.Type != TaskNodeType::While)
843 continue;
844
845 if (node.ConditionID.empty())
846 continue; // W003 handles empty condition IDs
847
848 if (reg.GetConditionSpec(node.ConditionID) == nullptr)
849 {
850 std::ostringstream oss;
851 oss << "Node #" << node.NodeID << " ('" << node.NodeName
852 << "'): ConditionID '" << node.ConditionID
853 << "' is not registered in ConditionRegistry.";
855 "E021_UnknownConditionID", oss.str());
856 }
857 }
858}
859
860// ============================================================================
861// E023 — MathOp: operation is not a valid math operator
862// ============================================================================
863
865{
866 for (size_t i = 0; i < g.Nodes.size(); ++i)
867 {
868 const TaskNodeDefinition& node = g.Nodes[i];
869 if (node.Type != TaskNodeType::MathOp)
870 continue;
871
872 if (node.MathOperator.empty())
873 continue; // W004 handles empty MathOperator
874
876 {
877 std::ostringstream oss;
878 oss << "Node #" << node.NodeID << " ('" << node.NodeName
879 << "'): MathOperator '" << node.MathOperator
880 << "' is not a valid operator. Expected one of: +, -, *, /, %.";
882 "E023_InvalidMathOperator", oss.str());
883 }
884 }
885}
886
887// ============================================================================
888// E024 — SubGraph: subGraphPath is empty
889// ============================================================================
890
892{
893 for (size_t i = 0; i < g.Nodes.size(); ++i)
894 {
895 const TaskNodeDefinition& node = g.Nodes[i];
896 if (node.Type != TaskNodeType::SubGraph)
897 continue;
898
899 // Check both SubGraphPath field AND Parameters["subgraph_path"] for Path
900 // (UI edits Parameters, but we sync to SubGraphPath only before save)
901 std::string resolvedPath = node.SubGraphPath;
902
903 SYSTEM_LOG << "[VSGraphVerifier::CheckSubGraphPaths] Node #" << node.NodeID
904 << ": SubGraphPath='" << resolvedPath << "'\n";
905
906 // Fallback: check Parameters["subgraph_path"] if SubGraphPath is empty
907 if (resolvedPath.empty())
908 {
909 auto it = node.Parameters.find("subgraph_path");
910 if (it != node.Parameters.end())
911 {
912 SYSTEM_LOG << "[VSGraphVerifier::CheckSubGraphPaths] Found Parameters[subgraph_path], Type="
913 << static_cast<int>(it->second.Type) << "\n";
914
915 if (it->second.Type == ParameterBindingType::Literal)
916 {
917 resolvedPath = it->second.LiteralValue.to_string();
918 SYSTEM_LOG << "[VSGraphVerifier::CheckSubGraphPaths] Resolved from Literal: '"
919 << resolvedPath << "'\n";
920 }
921 }
922 else
923 {
924 SYSTEM_LOG << "[VSGraphVerifier::CheckSubGraphPaths] No Parameters[subgraph_path] found\n";
925 }
926 }
927
928 if (resolvedPath.empty())
929 {
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());
935 }
936 }
937}
938
939// ============================================================================
940// E025 — Condition: required parameter missing on Branch/While node
941// ============================================================================
942
944{
946
947 for (size_t i = 0; i < g.Nodes.size(); ++i)
948 {
949 const TaskNodeDefinition& node = g.Nodes[i];
950 if (node.Type != TaskNodeType::Branch && node.Type != TaskNodeType::While)
951 continue;
952
953 if (node.ConditionID.empty())
954 continue;
955
956 const ConditionSpec* spec = reg.GetConditionSpec(node.ConditionID);
957 if (!spec)
958 continue; // E021 already reported this
959
960 for (size_t p = 0; p < spec->parameters.size(); ++p)
961 {
962 const ConditionParamSpec& param = spec->parameters[p];
963 if (!param.required)
964 continue;
965
966 // Check if the parameter is present in the node's Parameters map
967 auto it = node.Parameters.find(param.name);
968 bool present = (it != node.Parameters.end());
969
970 if (!present)
971 {
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());
978 }
979 }
980 }
981}
982
983// ============================================================================
984// W010 — BBKey type incompatible with node expectations
985// ============================================================================
986
988{
989 // Build a map of BB key -> declared type for fast lookup
990 std::unordered_map<std::string, VariableType> bbTypes;
991 for (size_t i = 0; i < g.Blackboard.size(); ++i)
992 {
993 bbTypes[g.Blackboard[i].Key] = g.Blackboard[i].Type;
994 }
995
996 for (size_t i = 0; i < g.Nodes.size(); ++i)
997 {
998 const TaskNodeDefinition& node = g.Nodes[i];
999
1000 // ForEach: bbKey should be a List type
1001 if (node.Type == TaskNodeType::ForEach && !node.BBKey.empty())
1002 {
1003 auto it = bbTypes.find(node.BBKey);
1004 if (it != bbTypes.end() && it->second != VariableType::List)
1005 {
1006 std::ostringstream oss;
1007 oss << "Node #" << node.NodeID << " ('" << node.NodeName
1008 << "'): ForEach expects a List variable, but '"
1009 << node.BBKey << "' is declared as "
1010 << static_cast<int>(it->second) << ".";
1012 "W010_BBKeyTypeIncompatible", oss.str());
1013 }
1014 }
1015 }
1016}
1017
1018// ============================================================================
1019// CheckConditionStructure (Phase 23-B.4)
1020// E040, E041, E042, W015, W016
1021// ============================================================================
1022
1024{
1025 // Build BB key set for fast E041 lookup
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;
1029
1030 // Build set of data connections (targetNode:pinName) for W016
1031 // We use a simple set represented as a sorted vector of strings.
1032 std::vector<std::string> dataConnectionKeys;
1033 for (size_t i = 0; i < g.DataConnections.size(); ++i)
1034 {
1035 std::ostringstream k;
1036 k << g.DataConnections[i].TargetNodeID << ":" << g.DataConnections[i].TargetPinName;
1037 dataConnectionKeys.push_back(k.str());
1038 }
1039
1040 for (size_t ni = 0; ni < g.Nodes.size(); ++ni)
1041 {
1042 const TaskNodeDefinition& node = g.Nodes[ni];
1043 if (node.Type != TaskNodeType::Branch && node.Type != TaskNodeType::While)
1044 continue;
1045 if (node.conditions.empty())
1046 continue;
1047
1048 for (size_t ci = 0; ci < node.conditions.size(); ++ci)
1049 {
1050 const Condition& cond = node.conditions[ci];
1051 const std::string condIdx = std::to_string(ci);
1052
1053 // Helper lambda-equivalent: check one side (left or right)
1054 // We repeat the logic for left and right using a small inline helper.
1055
1056 // Check LEFT side
1057 {
1058 const std::string& mode = cond.leftMode;
1059 const std::string& pin = cond.leftPin;
1060 const std::string& var = cond.leftVariable;
1061
1062 if (mode == "Pin")
1063 {
1064 if (pin.empty())
1065 {
1066 // E040: Pin mode but empty reference
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());
1073 }
1074 else
1075 {
1076 // W016: Pin mode but no DataConnection found for that pin
1077 // Parse "Node#<id>.<pinName>" -> check DataConnections
1078 // We check source side: is there a DataConnection whose
1079 // source == "<id>:<pinName>"?
1080 const std::string prefix = "Node#";
1081 bool hasConnection = false;
1082 if (pin.substr(0, prefix.size()) == prefix)
1083 {
1084 const std::string rest = pin.substr(prefix.size());
1085 const size_t dotPos = rest.find('.');
1086 if (dotPos != std::string::npos)
1087 {
1088 const std::string idStr = rest.substr(0, dotPos);
1089 const std::string pName = rest.substr(dotPos + 1);
1090 for (size_t dc = 0; dc < g.DataConnections.size(); ++dc)
1091 {
1092 std::ostringstream key;
1093 key << g.DataConnections[dc].SourceNodeID
1094 << ":" << g.DataConnections[dc].SourcePinName;
1095 // Also check target side
1096 std::ostringstream tkey;
1097 tkey << idStr << ":" << pName;
1098 if (key.str() == tkey.str())
1099 {
1100 hasConnection = true;
1101 break;
1102 }
1103 }
1104 }
1105 }
1106 (void)hasConnection; // W016 is informational; only warn if no connection
1107 // Note: we emit W016 only when no data connection is wired to that pin
1108 if (!hasConnection)
1109 {
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());
1117 }
1118 }
1119 }
1120 else if (mode == "Variable")
1121 {
1122 if (var.empty())
1123 {
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());
1130 }
1131 else if (bbTypes.find(var) == bbTypes.end())
1132 {
1133 // E041: variable not in blackboard
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());
1141 }
1142 }
1143 // Const mode: always valid — no check needed
1144 }
1145
1146 // Check RIGHT side
1147 {
1148 const std::string& mode = cond.rightMode;
1149 const std::string& pin = cond.rightPin;
1150 const std::string& var = cond.rightVariable;
1151
1152 if (mode == "Pin")
1153 {
1154 if (pin.empty())
1155 {
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());
1162 }
1163 else
1164 {
1165 const std::string prefix = "Node#";
1166 bool hasConnection = false;
1167 if (pin.substr(0, prefix.size()) == prefix)
1168 {
1169 const std::string rest = pin.substr(prefix.size());
1170 const size_t dotPos = rest.find('.');
1171 if (dotPos != std::string::npos)
1172 {
1173 const std::string idStr = rest.substr(0, dotPos);
1174 const std::string pName = rest.substr(dotPos + 1);
1175 for (size_t dc = 0; dc < g.DataConnections.size(); ++dc)
1176 {
1177 std::ostringstream key;
1178 key << g.DataConnections[dc].SourceNodeID
1179 << ":" << g.DataConnections[dc].SourcePinName;
1180 std::ostringstream tkey;
1181 tkey << idStr << ":" << pName;
1182 if (key.str() == tkey.str())
1183 {
1184 hasConnection = true;
1185 break;
1186 }
1187 }
1188 }
1189 }
1190 if (!hasConnection)
1191 {
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());
1199 }
1200 }
1201 }
1202 else if (mode == "Variable")
1203 {
1204 if (var.empty())
1205 {
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());
1212 }
1213 else if (bbTypes.find(var) == bbTypes.end())
1214 {
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());
1222 }
1223 }
1224 }
1225
1226 // W015: Const vs Const — always true/false (optimisation hint)
1227 if (cond.leftMode == "Const" && cond.rightMode == "Const")
1228 {
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());
1236 }
1237
1238 // E042: Type mismatch between operands (when both types are known)
1239 {
1242
1243 // Resolve left type
1244 if (cond.leftMode == "Variable")
1245 {
1246 auto it = bbTypes.find(cond.leftVariable);
1247 if (it != bbTypes.end()) leftType = it->second;
1248 }
1249 else if (cond.leftMode == "Const")
1250 {
1251 leftType = cond.leftConstValue.GetType();
1252 }
1253
1254 // Resolve right type
1255 if (cond.rightMode == "Variable")
1256 {
1257 auto it = bbTypes.find(cond.rightVariable);
1258 if (it != bbTypes.end()) rightType = it->second;
1259 }
1260 else if (cond.rightMode == "Const")
1261 {
1262 rightType = cond.rightConstValue.GetType();
1263 }
1264
1265 // Check for type mismatch (excluding None which means "unknown/pin")
1269 {
1270 // Int↔Float promotion is allowed
1273 if (!(leftNumeric && rightNumeric))
1274 {
1275 std::ostringstream oss;
1276 oss << "Node #" << node.NodeID << " ('" << node.NodeName
1277 << "'): condition[" << condIdx
1278 << "] type mismatch: left type="
1279 << static_cast<int>(leftType)
1280 << " right type=" << static_cast<int>(rightType)
1281 << " (E042). Comparison will fail at runtime.";
1283 "E042_ConditionTypeMismatch", oss.str());
1284 }
1285 }
1286 }
1287 }
1288 }
1289}
1290
1291} // namespace Olympe
UI-side registry of available atomic tasks with display metadata.
Registry of available condition types for Branch/While node dropdowns.
ComponentTypeID GetComponentTypeID_Static()
Definition ECS_Entity.h:56
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
#define SYSTEM_LOG