statable 3.1.0__py3-none-any.whl

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.
Files changed (117) hide show
  1. codegen/__init__.py +38 -0
  2. codegen/c_code_generator.py +1752 -0
  3. codegen/code_merger.py +371 -0
  4. codegen/code_templates.py +509 -0
  5. codegen/config.py +167 -0
  6. codegen/enum_generator.py +377 -0
  7. codegen/event_queue_generator.py +303 -0
  8. codegen/interrupt_generator.py +345 -0
  9. codegen/naming_convention.py +171 -0
  10. codegen/osal_generator.py +282 -0
  11. codegen/role_function_generator.py +1244 -0
  12. codegen/sample_data.py +269 -0
  13. codegen/state_actions_generator.py +395 -0
  14. codegen/struct_generator.py +721 -0
  15. codegen/timer_generator.py +299 -0
  16. codegen/transition_generator.py +907 -0
  17. codegen/type_mapper.py +69 -0
  18. codegen/validate/__init__.py +38 -0
  19. codegen/validate/change_actions.py +90 -0
  20. codegen/validate/change_applier.py +416 -0
  21. codegen/validate/clipboard_manager.py +47 -0
  22. codegen/validate/data/__init__.py +30 -0
  23. codegen/validate/data/action_definitions.py +200 -0
  24. codegen/validate/data/keywords.py +46 -0
  25. codegen/validate/data/prompt_templates.py +176 -0
  26. codegen/validate/data/validation_rules.py +205 -0
  27. codegen/validate/items/__init__.py +30 -0
  28. codegen/validate/items/base_validator.py +25 -0
  29. codegen/validate/items/cell_validator.py +275 -0
  30. codegen/validate/items/custom_type_validator.py +75 -0
  31. codegen/validate/items/event_validator.py +78 -0
  32. codegen/validate/items/flag_validator.py +74 -0
  33. codegen/validate/items/interrupt_validator.py +75 -0
  34. codegen/validate/items/queue_validator.py +73 -0
  35. codegen/validate/items/role_function_validator.py +175 -0
  36. codegen/validate/items/state_validator.py +88 -0
  37. codegen/validate/items/timer_validator.py +120 -0
  38. codegen/validate/items/transition_validator.py +113 -0
  39. codegen/validate/items/variable_validator.py +132 -0
  40. codegen/validate/logger.py +52 -0
  41. codegen/validate/models.py +177 -0
  42. codegen/validate/prompt_generator.py +724 -0
  43. codegen/validate/response_parser.py +154 -0
  44. codegen/validate/response_validator.py +275 -0
  45. codegen/validate/validation_dialog.py +322 -0
  46. codegen/validate/validator.py +94 -0
  47. codegen/variable_generator.py +419 -0
  48. statable/__init__.py +71 -0
  49. statable/__main__.py +49 -0
  50. statable/cli.py +251 -0
  51. statable/global_defs.py +256 -0
  52. statable/mermaid_gen.py +144 -0
  53. statable/model.py +426 -0
  54. statable/parser.py +64 -0
  55. statable/sample_data.py +435 -0
  56. statable/shared/__init__.py +24 -0
  57. statable/shared/condition_library.py +63 -0
  58. statable/shared/literal_library.py +66 -0
  59. statable/shared/role_function_library.py +150 -0
  60. statable/state_machine.py +121 -0
  61. statable/xml_io.py +1164 -0
  62. statable-3.1.0.dist-info/METADATA +151 -0
  63. statable-3.1.0.dist-info/RECORD +117 -0
  64. statable-3.1.0.dist-info/WHEEL +5 -0
  65. statable-3.1.0.dist-info/entry_points.txt +2 -0
  66. statable-3.1.0.dist-info/top_level.txt +3 -0
  67. statable_gui/__init__.py +23 -0
  68. statable_gui/action_edit_dialog.py +214 -0
  69. statable_gui/code_generation_dialog.py +486 -0
  70. statable_gui/code_generation_settings_dialog.py +522 -0
  71. statable_gui/common_widgets.py +537 -0
  72. statable_gui/condition_builder_dialog.py +500 -0
  73. statable_gui/condition_edit_dialog.py +123 -0
  74. statable_gui/condition_template_dialog.py +58 -0
  75. statable_gui/config.py +21 -0
  76. statable_gui/dialogs.py +439 -0
  77. statable_gui/event_definition_dialog.py +605 -0
  78. statable_gui/event_delivery_settings_dialog.py +191 -0
  79. statable_gui/event_queue_dialog.py +245 -0
  80. statable_gui/global_defs.py +167 -0
  81. statable_gui/global_defs_dialog.py +796 -0
  82. statable_gui/i18n/__init__.py +118 -0
  83. statable_gui/interrupt_handler_edit_dialog.py +810 -0
  84. statable_gui/layer_settings_dialog.py +180 -0
  85. statable_gui/libcntrl/__init__.py +23 -0
  86. statable_gui/libcntrl/condition_library.py +13 -0
  87. statable_gui/libcntrl/literal_library.py +13 -0
  88. statable_gui/libcntrl/literal_management_dialog.py +185 -0
  89. statable_gui/libcntrl/role_function_edit_dialog.py +145 -0
  90. statable_gui/libcntrl/role_function_library.py +13 -0
  91. statable_gui/literal_definition_dialog.py +62 -0
  92. statable_gui/logger.py +83 -0
  93. statable_gui/main_window.py +1380 -0
  94. statable_gui/matrix_table.py +413 -0
  95. statable_gui/preference_keys.py +18 -0
  96. statable_gui/preferences.py +74 -0
  97. statable_gui/role_function_dialog.py +244 -0
  98. statable_gui/sample_data.py +11 -0
  99. statable_gui/state_actions_dialog.py +398 -0
  100. statable_gui/symbol_picker.py +170 -0
  101. statable_gui/traceball.py +37 -0
  102. statable_gui/transition_editor_direct/__init__.py +15 -0
  103. statable_gui/transition_editor_direct/actions_tab.py +856 -0
  104. statable_gui/transition_editor_direct/code_widget.py +344 -0
  105. statable_gui/transition_editor_direct/coverage_analyzer.py +163 -0
  106. statable_gui/transition_editor_direct/dialog.py +335 -0
  107. statable_gui/transition_editor_direct/draft.py +270 -0
  108. statable_gui/transition_editor_direct/edit_dialogs.py +160 -0
  109. statable_gui/transition_editor_direct/overview_tab.py +173 -0
  110. statable_gui/transition_editor_direct/palette_widget.py +233 -0
  111. statable_gui/transition_editor_direct/relations_edit_dialog.py +156 -0
  112. statable_gui/transition_editor_direct/relations_tab.py +230 -0
  113. statable_gui/transition_editor_direct/system_global_dialog.py +52 -0
  114. statable_gui/transition_editor_direct/transition_actions_dialog.py +109 -0
  115. statable_gui/transition_editor_direct/transitions_tab.py +620 -0
  116. statable_gui/validation_dialog.py +285 -0
  117. statable_gui/widgets.py +1315 -0
@@ -0,0 +1,73 @@
1
+ # codegen/validate/items/queue_validator.py
2
+ """\nQueue validation\n"""
3
+
4
+ import sys
5
+ import os
6
+ from typing import List
7
+
8
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
9
+
10
+ from ..logger import logger
11
+ from ..models import ValidationIssue, ValidationSeverity, ValidationContext
12
+ from ..data.validation_rules import VALIDATION_RULES
13
+ from .base_validator import BaseValidator
14
+
15
+
16
+ class QueueValidator(BaseValidator):
17
+ """Queue validation class"""
18
+
19
+ category = "queue"
20
+
21
+ def __init__(self):
22
+ logger.debug("QueueValidator.__init__ started")
23
+ self.rules_data = VALIDATION_RULES.get(self.category, {})
24
+ self.rules = {
25
+ 'QUEUE_INVALID_SIZE': self._check_invalid_size,
26
+ 'QUEUE_UNDEFINED_EVENT': self._check_undefined_event,
27
+ }
28
+ logger.debug(f"QueueValidator.__init__ completed: {len(self.rules)} rules")
29
+
30
+ def validate(self, context: ValidationContext) -> List[ValidationIssue]:
31
+ logger.debug(f"QueueValidator.validate started")
32
+ issues = []
33
+ for code, rule_func in self.rules.items():
34
+ try:
35
+ result = rule_func(context)
36
+ if result:
37
+ issues.extend(result)
38
+ except Exception as e:
39
+ logger.error(f"Rule '{code}' failed: {e}", exc_info=True)
40
+ return issues
41
+
42
+ def _create_issue(self, code: str, **kwargs) -> ValidationIssue:
43
+ rule = self.rules_data.get(code, {})
44
+ message = rule.get('message', '').format(**kwargs)
45
+ severity = ValidationSeverity.from_string(rule.get('severity', 'info'))
46
+ return ValidationIssue(
47
+ category=self.category,
48
+ code=code,
49
+ message=message,
50
+ severity=severity,
51
+ suggestion=rule.get('suggestion', ''),
52
+ target=kwargs.get('name', ''),
53
+ )
54
+
55
+ def _check_invalid_size(self, context):
56
+ issues = []
57
+ for queue in context.event_queues:
58
+ size = getattr(queue, 'size', 0)
59
+ name = getattr(queue, 'name', '')
60
+ if size <= 0:
61
+ issues.append(self._create_issue('QUEUE_INVALID_SIZE', name=name))
62
+ return issues
63
+
64
+ def _check_undefined_event(self, context):
65
+ issues = []
66
+ event_names = set(context.events.keys())
67
+ for queue in context.event_queues:
68
+ event_ids = getattr(queue, 'event_ids', [])
69
+ name = getattr(queue, 'name', '')
70
+ for event_id in event_ids:
71
+ if event_id not in event_names:
72
+ issues.append(self._create_issue('QUEUE_UNDEFINED_EVENT', name=name))
73
+ return issues
@@ -0,0 +1,175 @@
1
+ # codegen/validate/items/role_function_validator.py
2
+ """
3
+ Role function validation
4
+ """
5
+
6
+ import sys
7
+ import os
8
+ from typing import List
9
+
10
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
11
+
12
+ from ..logger import logger
13
+ from ..models import ValidationIssue, ValidationSeverity, ValidationContext
14
+ from ..data.validation_rules import VALIDATION_RULES
15
+ from .base_validator import BaseValidator
16
+
17
+
18
+ class RoleFunctionValidator(BaseValidator):
19
+ """Role function validator class"""
20
+
21
+ category = "role_function"
22
+
23
+ def __init__(self):
24
+ logger.debug("RoleFunctionValidator.__init__ started")
25
+ self.rules_data = VALIDATION_RULES.get(self.category, {})
26
+ self.rules = {
27
+ 'ROLE_FUNC_NO_RETURN_TYPE': self._check_no_return_type,
28
+ 'ROLE_FUNC_ARG_MISMATCH': self._check_arg_mismatch,
29
+ 'ROLE_FUNC_UNUSED': self._check_unused_functions,
30
+ }
31
+ logger.debug(f"RoleFunctionValidator.__init__ completed: {len(self.rules)} rules")
32
+
33
+ def validate(self, context: ValidationContext) -> List[ValidationIssue]:
34
+ logger.debug(f"RoleFunctionValidator.validate started")
35
+ issues = []
36
+ for code, rule_func in self.rules.items():
37
+ try:
38
+ result = rule_func(context)
39
+ if result:
40
+ issues.extend(result)
41
+ except Exception as e:
42
+ logger.error(f"Rule '{code}' failed: {e}", exc_info=True)
43
+ return issues
44
+
45
+ def _create_issue(self, code: str, **kwargs) -> ValidationIssue:
46
+ rule = self.rules_data.get(code, {})
47
+ message = rule.get('message', '').format(**kwargs)
48
+ severity = ValidationSeverity.from_string(rule.get('severity', 'info'))
49
+ return ValidationIssue(
50
+ category=self.category,
51
+ code=code,
52
+ message=message,
53
+ severity=severity,
54
+ suggestion=rule.get('suggestion', ''),
55
+ target=kwargs.get('name', ''),
56
+ )
57
+
58
+ def _check_no_return_type(self, context):
59
+ issues = []
60
+ for name, func in context.role_functions.items():
61
+ return_type = getattr(func, 'return_type', '')
62
+ if not return_type:
63
+ issues.append(self._create_issue('ROLE_FUNC_NO_RETURN_TYPE', name=name))
64
+ return issues
65
+
66
+ def _check_arg_mismatch(self, context):
67
+ issues = []
68
+ for name, func in context.role_functions.items():
69
+ arg1_type = getattr(func, 'arg1_type', '')
70
+ arg1_name = getattr(func, 'arg1_name', '')
71
+ arg2_type = getattr(func, 'arg2_type', '')
72
+ arg2_name = getattr(func, 'arg2_name', '')
73
+ if (arg1_type and not arg1_name) or (arg1_name and not arg1_type):
74
+ issues.append(self._create_issue('ROLE_FUNC_ARG_MISMATCH', name=name))
75
+ if (arg2_type and not arg2_name) or (arg2_name and not arg2_type):
76
+ issues.append(self._create_issue('ROLE_FUNC_ARG_MISMATCH', name=name))
77
+ return issues
78
+
79
+ def _collect_referenced_names(self, context):
80
+ """[v2.7.2 fix] Collect every name that references a role function.
81
+
82
+ Sources:
83
+ - Transition.action / .pre_actions / .else_actions
84
+ - Transition.condition (parsed for RoleFunc_X / NS.Name)
85
+ - State.entry / exit / do_actions
86
+ - Cell actions (via state_machine.get_actions_for_cell)
87
+ - Event.trigger_detail.caller
88
+ """
89
+ import re
90
+ used = set()
91
+
92
+ def add(ref):
93
+ if not ref:
94
+ return
95
+ ref = str(ref).strip()
96
+ if not ref:
97
+ return
98
+ used.add(ref)
99
+ if '.' in ref:
100
+ used.add(ref.split('.', 1)[1])
101
+
102
+ def add_text(text):
103
+ if not text:
104
+ return
105
+ text = str(text)
106
+ # RoleFunc_<NS>_<Name> pattern
107
+ for m in re.finditer(r'RoleFunc_(\w+)', text):
108
+ used.add(m.group(0))
109
+ used.add(m.group(1))
110
+ # NS.Name pattern
111
+ for m in re.finditer(r'([A-Z]\w*)\.([A-Za-z_]\w*)', text):
112
+ used.add(f"{m.group(1)}.{m.group(2)}")
113
+ used.add(m.group(2))
114
+
115
+ # 1. Transitions
116
+ for t in getattr(context, 'transitions', []) or []:
117
+ add(getattr(t, 'action', ''))
118
+ add_text(getattr(t, 'condition', ''))
119
+ for a in (getattr(t, 'pre_actions', []) or []):
120
+ add(a)
121
+ for a in (getattr(t, 'else_actions', []) or []):
122
+ add(a)
123
+
124
+ # 2. Via state_machine (defensive)
125
+ sm = (getattr(context, 'state_machine', None)
126
+ or getattr(context, 'sm', None))
127
+ if sm is not None:
128
+ for state in (getattr(sm, 'states', {}) or {}).values():
129
+ for attr in ('entry', 'exit', 'do_actions'):
130
+ for a in (getattr(state, attr, []) or []):
131
+ rf = getattr(a, 'role_function', None)
132
+ if rf:
133
+ add(rf)
134
+ elif isinstance(a, str):
135
+ add(a)
136
+ for ev in (getattr(sm, 'events', {}) or {}).values():
137
+ td = getattr(ev, 'trigger_detail', None)
138
+ if td is not None:
139
+ add(getattr(td, 'caller', ''))
140
+ try:
141
+ for key in sm.get_cell_keys():
142
+ for a in sm.get_actions_for_cell(*key):
143
+ rf = getattr(a, 'role_function', None)
144
+ if rf:
145
+ add(rf)
146
+ except Exception:
147
+ pass
148
+
149
+ # 3. Fallback: context.states
150
+ for state in (getattr(context, 'states', {}) or {}).values():
151
+ for attr in ('entry', 'exit', 'do_actions'):
152
+ for a in (getattr(state, attr, []) or []):
153
+ rf = getattr(a, 'role_function', None)
154
+ if rf:
155
+ add(rf)
156
+ elif isinstance(a, str):
157
+ add(a)
158
+
159
+ return used
160
+
161
+ def _check_unused_functions(self, context):
162
+ """[v2.7.2 fix] Match refs via bare / qualified / RoleFunc_ aliases."""
163
+ issues = []
164
+ used = self._collect_referenced_names(context)
165
+ for name, func in context.role_functions.items():
166
+ aliases = {name}
167
+ ns = getattr(func, 'namespace', '') or ''
168
+ if ns:
169
+ aliases.add(f"{ns}.{name}")
170
+ aliases.add(f"RoleFunc_{ns}_{name}")
171
+ aliases.add(f"RoleFunc_{name}")
172
+ if aliases & used:
173
+ continue
174
+ issues.append(self._create_issue('ROLE_FUNC_UNUSED', name=name))
175
+ return issues
@@ -0,0 +1,88 @@
1
+ # codegen/validate/items/state_validator.py
2
+ """\nState validation\n"""
3
+
4
+ import sys
5
+ import os
6
+ from typing import List
7
+
8
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
9
+
10
+ from ..logger import logger
11
+ from ..models import ValidationIssue, ValidationSeverity, ValidationContext
12
+ from ..data.validation_rules import VALIDATION_RULES
13
+ from .base_validator import BaseValidator
14
+
15
+
16
+ class StateValidator(BaseValidator):
17
+ """State validation class"""
18
+
19
+ category = "state"
20
+
21
+ def __init__(self):
22
+ logger.debug("StateValidator.__init__ started")
23
+ self.rules_data = VALIDATION_RULES.get(self.category, {})
24
+ self.rules = {
25
+ 'STATE_NO_INITIAL': self._check_initial_state,
26
+ 'STATE_UNREACHABLE': self._check_unreachable_states,
27
+ 'STATE_NO_TRANSITION': self._check_no_transition_states,
28
+ 'STATE_DUPLICATE': self._check_duplicate_states,
29
+ }
30
+ logger.debug(f"StateValidator.__init__ completed: {len(self.rules)} rules")
31
+
32
+ def validate(self, context: ValidationContext) -> List[ValidationIssue]:
33
+ logger.debug(f"StateValidator.validate started")
34
+ issues = []
35
+ for code, rule_func in self.rules.items():
36
+ try:
37
+ result = rule_func(context)
38
+ if result:
39
+ issues.extend(result)
40
+ except Exception as e:
41
+ logger.error(f"Rule '{code}' failed: {e}", exc_info=True)
42
+ return issues
43
+
44
+ def _create_issue(self, code: str, **kwargs) -> ValidationIssue:
45
+ rule = self.rules_data.get(code, {})
46
+ message = rule.get('message', '').format(**kwargs)
47
+ severity = ValidationSeverity.from_string(rule.get('severity', 'info'))
48
+
49
+ return ValidationIssue(
50
+ category=self.category,
51
+ code=code,
52
+ message=message,
53
+ severity=severity,
54
+ suggestion=rule.get('suggestion', ''),
55
+ target=kwargs.get('name', ''),
56
+ )
57
+
58
+ def _check_initial_state(self, context):
59
+ if context.initial_state is None:
60
+ return [self._create_issue('STATE_NO_INITIAL')]
61
+ return []
62
+
63
+ def _check_unreachable_states(self, context):
64
+ issues = []
65
+ reachable = set(t.target for t in context.transitions if t.target)
66
+ for name in context.states.keys():
67
+ if name not in reachable and name != context.initial_state:
68
+ issues.append(self._create_issue('STATE_UNREACHABLE', name=name))
69
+ return issues
70
+
71
+ def _check_no_transition_states(self, context):
72
+ issues = []
73
+ sources = set(t.source for t in context.transitions)
74
+ for name in context.states.keys():
75
+ if name not in sources:
76
+ issues.append(self._create_issue('STATE_NO_TRANSITION', name=name))
77
+ return issues
78
+
79
+ def _check_duplicate_states(self, context):
80
+ issues = []
81
+ lower_names = {}
82
+ for name in context.states.keys():
83
+ lower = name.lower()
84
+ if lower in lower_names:
85
+ issues.append(self._create_issue('STATE_DUPLICATE', name=name, other=lower_names[lower]))
86
+ else:
87
+ lower_names[lower] = name
88
+ return issues
@@ -0,0 +1,120 @@
1
+ # codegen/validate/items/timer_validator.py
2
+ """
3
+ Timer validation
4
+ """
5
+
6
+ import sys
7
+ import os
8
+ from typing import List
9
+
10
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
11
+
12
+ from ..logger import logger
13
+ from ..models import ValidationIssue, ValidationSeverity, ValidationContext
14
+ from ..data.validation_rules import VALIDATION_RULES
15
+ from .base_validator import BaseValidator
16
+
17
+
18
+ class TimerValidator(BaseValidator):
19
+ """Timer validator class"""
20
+
21
+ category = "timer"
22
+
23
+ def __init__(self):
24
+ logger.debug("TimerValidator.__init__ started")
25
+ self.rules_data = VALIDATION_RULES.get(self.category, {})
26
+ self.rules = {
27
+ 'TIMER_DUPLICATE_VARIABLE': self._check_duplicate_variables,
28
+ 'TIMER_INVALID_MULTIPLIER': self._check_invalid_multiplier,
29
+ }
30
+ logger.debug(f"TimerValidator.__init__ completed: {len(self.rules)} rules")
31
+
32
+ def validate(self, context: ValidationContext) -> List[ValidationIssue]:
33
+ logger.debug(f"TimerValidator.validate started")
34
+ issues = []
35
+ for code, rule_func in self.rules.items():
36
+ try:
37
+ result = rule_func(context)
38
+ if result:
39
+ issues.extend(result)
40
+ except Exception as e:
41
+ logger.error(f"Rule '{code}' failed: {e}", exc_info=True)
42
+ return issues
43
+
44
+ def _create_issue(self, code: str, **kwargs) -> ValidationIssue:
45
+ rule = self.rules_data.get(code, {})
46
+ message = rule.get('message', '').format(**kwargs)
47
+ severity = ValidationSeverity.from_string(rule.get('severity', 'info'))
48
+ return ValidationIssue(
49
+ category=self.category,
50
+ code=code,
51
+ message=message,
52
+ severity=severity,
53
+ suggestion=rule.get('suggestion', ''),
54
+ target=kwargs.get('name', ''),
55
+ )
56
+
57
+ def _get_all_timer_variables(self, context):
58
+ timer_vars = []
59
+ timer_base = context.timer_base
60
+ if timer_base:
61
+ timer_vars.append(getattr(timer_base, 'variable_name', ''))
62
+ for derived in getattr(timer_base, 'derived', []):
63
+ timer_vars.append(getattr(derived, 'variable_name', ''))
64
+ for timer in context.extra_timers:
65
+ timer_vars.append(getattr(timer, 'variable_name', ''))
66
+ for derived in getattr(timer, 'derived', []):
67
+ timer_vars.append(getattr(derived, 'variable_name', ''))
68
+ return timer_vars
69
+
70
+ def _check_duplicate_variables(self, context):
71
+ """[v2.7.2 fix] Skip when the conflicting SystemVariable is group='Timer'.
72
+
73
+ The TUTORIAL declares 'g_system_tick' in both:
74
+ - <SystemVariables> with group="Timer"
75
+ - <TimerBase variable_name="g_system_tick">
76
+ This is the intended pattern (timer base names its counter
77
+ variable, which is also a shared SystemVariable). Only flag:
78
+ - two timer bases using the same variable_name
79
+ - timer name conflicting with a non-Timer SystemVariable
80
+ """
81
+ issues = []
82
+ var_groups = {}
83
+ for v in context.variables:
84
+ name = getattr(v, 'name', '')
85
+ group = getattr(v, 'group', '')
86
+ var_groups[name] = group
87
+ timer_vars = self._get_all_timer_variables(context)
88
+ seen = set()
89
+ for var_name in timer_vars:
90
+ if not var_name:
91
+ continue
92
+ if var_name in seen:
93
+ issues.append(self._create_issue(
94
+ 'TIMER_DUPLICATE_VARIABLE', name=var_name))
95
+ continue
96
+ if var_name in var_groups:
97
+ if var_groups[var_name] == 'Timer':
98
+ pass # expected: shared SystemVariable + TimerBase
99
+ else:
100
+ issues.append(self._create_issue(
101
+ 'TIMER_DUPLICATE_VARIABLE', name=var_name))
102
+ seen.add(var_name)
103
+ return issues
104
+
105
+ def _check_invalid_multiplier(self, context):
106
+ issues = []
107
+ timer_base = context.timer_base
108
+ if timer_base:
109
+ for derived in getattr(timer_base, 'derived', []):
110
+ multiplier = getattr(derived, 'multiplier', 0)
111
+ name = getattr(derived, 'variable_name', '')
112
+ if multiplier <= 0:
113
+ issues.append(self._create_issue('TIMER_INVALID_MULTIPLIER', name=name))
114
+ for timer in context.extra_timers:
115
+ for derived in getattr(timer, 'derived', []):
116
+ multiplier = getattr(derived, 'multiplier', 0)
117
+ name = getattr(derived, 'variable_name', '')
118
+ if multiplier <= 0:
119
+ issues.append(self._create_issue('TIMER_INVALID_MULTIPLIER', name=name))
120
+ return issues
@@ -0,0 +1,113 @@
1
+ # codegen/validate/items/transition_validator.py
2
+ """
3
+ Transition validation
4
+
5
+ [v1.8 section 11.2 #6]
6
+ - Unified logger output with StateValidator
7
+ - Added start / completion logs to __init__ / validate
8
+ - Wrapped rule execution in try/except; on failure, log via logger.error
9
+ """
10
+
11
+ import sys
12
+ import os
13
+ from typing import List
14
+
15
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
16
+
17
+ from ..logger import logger
18
+ from ..models import ValidationIssue, ValidationSeverity, ValidationContext
19
+ from ..data.validation_rules import VALIDATION_RULES
20
+ from .base_validator import BaseValidator
21
+
22
+
23
+ class TransitionValidator(BaseValidator):
24
+ """Transition validation class"""
25
+
26
+ category = "transition"
27
+
28
+ def __init__(self):
29
+ logger.debug("TransitionValidator.__init__ started")
30
+ self.rules_data = VALIDATION_RULES.get(self.category, {})
31
+ self.rules = {
32
+ 'TRANSITION_TARGET_UNDEFINED': self._check_target_undefined,
33
+ 'TRANSITION_EVENT_UNDEFINED': self._check_event_undefined,
34
+ 'TRANSITION_SOURCE_UNDEFINED': self._check_source_undefined,
35
+ 'TRANSITION_DUPLICATE': self._check_duplicate_transitions,
36
+ 'TRANSITION_SELF_LOOP': self._check_self_loops,
37
+ }
38
+ logger.debug(f"TransitionValidator.__init__ completed: {len(self.rules)} rules")
39
+
40
+ def validate(self, context: ValidationContext) -> List[ValidationIssue]:
41
+ logger.debug("TransitionValidator.validate started")
42
+ issues = []
43
+ for code, rule_func in self.rules.items():
44
+ try:
45
+ result = rule_func(context)
46
+ if result:
47
+ issues.extend(result)
48
+ except Exception as e:
49
+ logger.error(f"Rule '{code}' failed: {e}", exc_info=True)
50
+ logger.debug(f"TransitionValidator.validate completed: {len(issues)} issues")
51
+ return issues
52
+
53
+ def _create_issue(self, code: str, **kwargs) -> ValidationIssue:
54
+ rule = self.rules_data.get(code, {})
55
+ message = rule.get('message', '').format(**kwargs)
56
+ severity = ValidationSeverity.from_string(rule.get('severity', 'info'))
57
+
58
+ target = kwargs.get('target', '') or kwargs.get('source', '') or kwargs.get('event', '')
59
+
60
+ return ValidationIssue(
61
+ category=self.category,
62
+ code=code,
63
+ message=message,
64
+ severity=severity,
65
+ suggestion=rule.get('suggestion', ''),
66
+ target=target,
67
+ )
68
+
69
+ def _check_target_undefined(self, context):
70
+ issues = []
71
+ state_names = set(context.states.keys())
72
+ for t in context.transitions:
73
+ if t.target and t.target not in state_names:
74
+ issues.append(self._create_issue('TRANSITION_TARGET_UNDEFINED', target=t.target))
75
+ return issues
76
+
77
+ def _check_event_undefined(self, context):
78
+ issues = []
79
+ event_names = set(context.events.keys())
80
+ for t in context.transitions:
81
+ if t.event and t.event not in event_names:
82
+ issues.append(self._create_issue('TRANSITION_EVENT_UNDEFINED', event=t.event))
83
+ return issues
84
+
85
+ def _check_source_undefined(self, context):
86
+ issues = []
87
+ state_names = set(context.states.keys())
88
+ for t in context.transitions:
89
+ if t.source not in state_names:
90
+ issues.append(self._create_issue('TRANSITION_SOURCE_UNDEFINED', source=t.source))
91
+ return issues
92
+
93
+ def _check_duplicate_transitions(self, context):
94
+ issues = []
95
+ seen = set()
96
+ for t in context.transitions:
97
+ key = (t.source, t.event, t.target)
98
+ if key in seen:
99
+ issues.append(self._create_issue(
100
+ 'TRANSITION_DUPLICATE', source=t.source, event=t.event, target=t.target
101
+ ))
102
+ else:
103
+ seen.add(key)
104
+ return issues
105
+
106
+ def _check_self_loops(self, context):
107
+ issues = []
108
+ for t in context.transitions:
109
+ if t.source == t.target:
110
+ issues.append(self._create_issue(
111
+ 'TRANSITION_SELF_LOOP', source=t.source, event=t.event
112
+ ))
113
+ return issues