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
codegen/type_mapper.py ADDED
@@ -0,0 +1,69 @@
1
+ # codegen/type_mapper.py
2
+ """
3
+ C language type mapping module (dictionary-driven)
4
+ """
5
+
6
+ import sys
7
+ import os
8
+ from typing import Set, List
9
+
10
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
11
+
12
+ try:
13
+ from .code_templates import CodeTemplates
14
+ except ImportError:
15
+ from code_templates import CodeTemplates
16
+
17
+
18
+ class CTypeMapper:
19
+ """Class mapping StaTable types to C language types"""
20
+
21
+ TYPE_MAPPING = {
22
+ 'int': 'int', 'int8': 'int8_t', 'int16': 'int16_t',
23
+ 'int32': 'int32_t', 'int64': 'int64_t',
24
+ 'uint': 'unsigned int', 'uint8': 'uint8_t', 'uint16': 'uint16_t',
25
+ 'uint32': 'uint32_t', 'uint64': 'uint64_t',
26
+ 'float': 'float', 'double': 'double', 'bool': 'bool',
27
+ 'char': 'char', 'string': 'char*', 'void': 'void',
28
+ }
29
+
30
+ TYPE_CATEGORIES = {
31
+ 'int': 'integer', 'int8': 'integer', 'int16': 'integer',
32
+ 'int32': 'integer', 'int64': 'integer',
33
+ 'uint': 'integer', 'uint8': 'integer', 'uint16': 'integer',
34
+ 'uint32': 'integer', 'uint64': 'integer',
35
+ 'float': 'float', 'double': 'float', 'bool': 'boolean',
36
+ 'char': 'character', 'string': 'string', 'void': 'void',
37
+ }
38
+
39
+ HEADER_REQUIREMENTS = {
40
+ 'integer': '#include <stdint.h>',
41
+ 'boolean': '#include <stdbool.h>',
42
+ 'string': '#include <string.h>',
43
+ 'float': '#include <math.h>',
44
+ }
45
+
46
+ def __init__(self):
47
+ self.templates = CodeTemplates()
48
+ self.strings = self.templates.STRINGS
49
+ self.formats = self.templates.FORMATS
50
+
51
+ @classmethod
52
+ def map_type(cls, sta_type):
53
+ return cls.TYPE_MAPPING.get(sta_type, sta_type)
54
+
55
+ @classmethod
56
+ def get_type_category(cls, sta_type):
57
+ return cls.TYPE_CATEGORIES.get(sta_type, 'custom')
58
+
59
+ @classmethod
60
+ def get_required_headers(cls, types):
61
+ headers = set()
62
+ categories = set()
63
+ for type_name in types:
64
+ categories.add(cls.get_type_category(type_name))
65
+ for category in categories:
66
+ header = cls.HEADER_REQUIREMENTS.get(category)
67
+ if header:
68
+ headers.add(header)
69
+ return sorted(headers)
@@ -0,0 +1,38 @@
1
+ # codegen/validate/__init__.py
2
+ """\nValidation / AI integration package\n"""
3
+
4
+ from .logger import logger, setup_logger
5
+ from .models import (
6
+ ValidationSeverity,
7
+ ValidationIssue,
8
+ ValidationResult,
9
+ ValidationContext,
10
+ )
11
+ from .validator import CodeGenerationValidator
12
+ from .prompt_generator import AIPromptGenerator
13
+ from .response_parser import AIResponseParser
14
+ from .change_actions import ChangeRequest, ChangeActionType
15
+ from .change_applier import ChangeApplier
16
+ from .clipboard_manager import ClipboardManager
17
+ from .response_validator import (
18
+ ResponseValidator,
19
+ ResponseValidationResult,
20
+ )
21
+
22
+ __all__ = [
23
+ 'logger',
24
+ 'setup_logger',
25
+ 'ValidationSeverity',
26
+ 'ValidationIssue',
27
+ 'ValidationResult',
28
+ 'ValidationContext',
29
+ 'CodeGenerationValidator',
30
+ 'AIPromptGenerator',
31
+ 'AIResponseParser',
32
+ 'ChangeRequest',
33
+ 'ChangeActionType',
34
+ 'ChangeApplier',
35
+ 'ClipboardManager',
36
+ 'ResponseValidator',
37
+ 'ResponseValidationResult',
38
+ ]
@@ -0,0 +1,90 @@
1
+ # codegen/validate/change_actions.py
2
+ """
3
+ Change action definitions.
4
+
5
+ [v2.2 §12-6 change]
6
+ - Added 7 cell-level AI action types:
7
+ ADD_CELL / REMOVE_CELL
8
+ ADD_ACTION_STEP / REMOVE_ACTION_STEP
9
+ ADD_TRANSITION_RELATION / REMOVE_TRANSITION_RELATION
10
+ SET_EARLY_RETURN
11
+ """
12
+
13
+ from enum import Enum
14
+ from dataclasses import dataclass, field
15
+ from typing import Dict, Any, List
16
+
17
+
18
+ class ChangeActionType(Enum):
19
+ """Change action kind"""
20
+ # --------------------------------------------------------------
21
+ # Legacy actions
22
+ # --------------------------------------------------------------
23
+ SET_INITIAL = "set_initial"
24
+ ADD_TRANSITION = "add_transition"
25
+ ADD_STATE = "add_state"
26
+ ADD_EVENT = "add_event"
27
+ REMOVE_TRANSITION = "remove_transition"
28
+ UPDATE_TRANSITION = "update_transition"
29
+ ADD_ROLE_FUNCTION = "add_role_function"
30
+ REMOVE_ROLE_FUNCTION = "remove_role_function"
31
+ ADD_VARIABLE = "add_variable"
32
+ ADD_FLAG = "add_flag"
33
+
34
+ # --------------------------------------------------------------
35
+ # v2.2 §12-6: Cell-level AI action extensions
36
+ # --------------------------------------------------------------
37
+ ADD_CELL = "add_cell"
38
+ REMOVE_CELL = "remove_cell"
39
+ ADD_ACTION_STEP = "add_action_step"
40
+ REMOVE_ACTION_STEP = "remove_action_step"
41
+ ADD_TRANSITION_RELATION = "add_transition_relation"
42
+ REMOVE_TRANSITION_RELATION = "remove_transition_relation"
43
+ SET_EARLY_RETURN = "set_early_return"
44
+
45
+
46
+ @dataclass
47
+ class ChangeRequest:
48
+ """Change request"""
49
+ action: ChangeActionType
50
+ params: Dict[str, Any] = field(default_factory=dict)
51
+ reason: str = ""
52
+ source: str = "ai"
53
+ # [v2.8.0 Phase B] extended fields
54
+ id: str = ""
55
+ evidence: List[str] = field(default_factory=list)
56
+ priority: str = "medium"
57
+ confidence: float = 1.0
58
+
59
+ def to_dict(self) -> Dict:
60
+ return {
61
+ 'id': self.id,
62
+ 'action': self.action.value,
63
+ 'params': self.params,
64
+ 'reason': self.reason,
65
+ 'source': self.source,
66
+ 'evidence': list(self.evidence),
67
+ 'priority': self.priority,
68
+ 'confidence': self.confidence,
69
+ }
70
+
71
+ @classmethod
72
+ def from_dict(cls, data: Dict) -> 'ChangeRequest':
73
+ try:
74
+ confidence = float(data.get('confidence', 1.0))
75
+ except (TypeError, ValueError):
76
+ confidence = 1.0
77
+ return cls(
78
+ action=ChangeActionType(data['action']),
79
+ params=data.get('params', {}),
80
+ reason=data.get('reason', ''),
81
+ source=data.get('source', 'ai'),
82
+ id=str(data.get('id', '') or ''),
83
+ evidence=list(data.get('evidence', []) or []),
84
+ priority=str(data.get('priority', 'medium') or 'medium'),
85
+ confidence=confidence,
86
+ )
87
+
88
+ def __str__(self) -> str:
89
+ return (f"ChangeRequest(action={self.action.value}, "
90
+ f"params={self.params}, reason={self.reason})")
@@ -0,0 +1,416 @@
1
+ # codegen/validate/change_applier.py
2
+ """
3
+ Change apply engine.
4
+
5
+ [v1.6 change]
6
+ - Added 'remove_role_function' to _handlers
7
+
8
+ [v2.2 §12-6 change]
9
+ - Added 7 cell-level AI action handlers:
10
+ _add_cell / _remove_cell
11
+ _add_action_step / _remove_action_step
12
+ _add_transition_relation / _remove_transition_relation
13
+ _set_early_return
14
+ """
15
+
16
+ import sys
17
+ import os
18
+ from typing import List, Dict, Tuple
19
+
20
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
21
+
22
+ from .logger import logger
23
+ from .change_actions import ChangeRequest, ChangeActionType
24
+
25
+
26
+ class ChangeApplier:
27
+ """Apply change requests to a StateMachine / GlobalDefinitions."""
28
+
29
+ def __init__(self, sm, gd):
30
+ logger.debug("ChangeApplier.__init__ started")
31
+ self.sm = sm
32
+ self.gd = gd
33
+ self.applied_changes = []
34
+ self.failed_changes = []
35
+ # Define handler dict by string key
36
+ self._handlers = {
37
+ 'set_initial': self._set_initial,
38
+ 'add_transition': self._add_transition,
39
+ 'add_state': self._add_state,
40
+ 'add_event': self._add_event,
41
+ 'remove_transition': self._remove_transition,
42
+ 'update_transition': self._update_transition,
43
+ 'add_role_function': self._add_role_function,
44
+ 'remove_role_function': self._remove_role_function,
45
+ 'add_variable': self._add_variable,
46
+ 'add_flag': self._add_flag,
47
+ # v2.2 §12-6 additions
48
+ 'add_cell': self._add_cell,
49
+ 'remove_cell': self._remove_cell,
50
+ 'add_action_step': self._add_action_step,
51
+ 'remove_action_step': self._remove_action_step,
52
+ 'add_transition_relation': self._add_transition_relation,
53
+ 'remove_transition_relation': self._remove_transition_relation,
54
+ 'set_early_return': self._set_early_return,
55
+ }
56
+ logger.debug("ChangeApplier.__init__ completed")
57
+
58
+ def apply(self, change: ChangeRequest) -> Tuple[bool, str]:
59
+ """Apply change"""
60
+ logger.debug(f"apply: action={change.action}")
61
+ action_value = (change.action.value
62
+ if hasattr(change.action, 'value')
63
+ else str(change.action))
64
+ logger.debug(f"action_value: {action_value}")
65
+
66
+ handler = self._handlers.get(action_value)
67
+ if handler is None:
68
+ logger.warning(f"Unsupported action: {action_value}")
69
+ return False, f"Unsupported action: {action_value}"
70
+
71
+ try:
72
+ result = handler(change.params)
73
+ if result[0]:
74
+ self.applied_changes.append(change)
75
+ logger.debug(f"Change applied: {action_value}")
76
+ else:
77
+ self.failed_changes.append((change, result[1]))
78
+ logger.warning(f"Change failed: {result[1]}")
79
+ return result
80
+ except Exception as e:
81
+ logger.error(f"Change threw exception: {e}", exc_info=True)
82
+ self.failed_changes.append((change, str(e)))
83
+ return False, str(e)
84
+
85
+ def apply_all(self, changes: List[ChangeRequest]) -> Dict:
86
+ """Apply all changes"""
87
+ logger.debug(f"apply_all: {len(changes)} changes")
88
+ results = []
89
+ for change in changes:
90
+ success, message = self.apply(change)
91
+ results.append({'change': change, 'success': success,
92
+ 'message': message})
93
+ summary = {
94
+ 'total': len(changes),
95
+ 'applied': len(self.applied_changes),
96
+ 'failed': len(self.failed_changes),
97
+ 'results': results,
98
+ }
99
+ logger.debug(
100
+ f"apply_all completed: applied={summary['applied']}, "
101
+ f"failed={summary['failed']}")
102
+ return summary
103
+
104
+ # ==================================================================
105
+ # Existing handlers (unchanged)
106
+ # ==================================================================
107
+ def _set_initial(self, params: Dict) -> Tuple[bool, str]:
108
+ state = params.get('state', '')
109
+ if state not in self.sm.states:
110
+ return False, f"State '{state}' does not exist"
111
+ self.sm.set_initial(state)
112
+ return True, f"Initial state set to '{state}'"
113
+
114
+ def _add_transition(self, params: Dict) -> Tuple[bool, str]:
115
+ from statable.model import Transition
116
+ source = params.get('source', '')
117
+ event = params.get('event', '')
118
+ target = params.get('target', '')
119
+ if not all([source, event, target]):
120
+ return False, "Transition information is incomplete"
121
+ transition = Transition(
122
+ source=source, event=event, target=target,
123
+ condition=params.get('condition', ''),
124
+ action=params.get('action_name', ''),
125
+ )
126
+ try:
127
+ self.sm.add_transition(transition)
128
+ return True, f"Transition '{source} --[{event}]--> {target}' added"
129
+ except ValueError as e:
130
+ return False, str(e)
131
+
132
+ def _add_state(self, params: Dict) -> Tuple[bool, str]:
133
+ from statable.model import State, StateType
134
+ name = params.get('name', '')
135
+ if not name:
136
+ return False, "State name is not specified"
137
+ if name in self.sm.states:
138
+ return False, f"State '{name}' already exists"
139
+ type_name = params.get('type', 'NORMAL')
140
+ state_type = getattr(StateType, type_name, StateType.NORMAL)
141
+ if not hasattr(StateType, type_name):
142
+ logger.warning(
143
+ f"_add_state: unknown type '{type_name}' -> NORMAL fallback")
144
+ parent = params.get('parent', None) or None
145
+ self.sm.add_state(State(
146
+ name=name, type=state_type, parent=parent,
147
+ description=params.get('description', ''),
148
+ ))
149
+ return True, f"State '{name}' added (type={state_type.name})"
150
+
151
+ def _add_event(self, params: Dict) -> Tuple[bool, str]:
152
+ from statable.model import Event, EventKind
153
+ name = params.get('name', '')
154
+ if not name:
155
+ return False, "Event name is not specified"
156
+ if name in self.sm.events:
157
+ return False, f"Event '{name}' already exists"
158
+ kind = getattr(EventKind, params.get('kind', 'SIGNAL'),
159
+ EventKind.SIGNAL)
160
+ self.sm.add_event(Event(name=name, kind=kind,
161
+ description=params.get('description', '')))
162
+ return True, f"Event '{name}' added"
163
+
164
+ def _remove_transition(self, params: Dict) -> Tuple[bool, str]:
165
+ source = params.get('source', '')
166
+ event = params.get('event', '')
167
+ target = params.get('target', '')
168
+ for t in self.sm.transitions:
169
+ if t.source == source and t.event == event and t.target == target:
170
+ self.sm.remove_transition(t)
171
+ return True, f"Transition '{source} --[{event}]--> {target}' deleted"
172
+ return False, "No matching transition found"
173
+
174
+ def _update_transition(self, params: Dict) -> Tuple[bool, str]:
175
+ source = params.get('source', '')
176
+ event = params.get('event', '')
177
+ for t in self.sm.transitions:
178
+ if t.source == source and t.event == event:
179
+ if 'new_target' in params:
180
+ t.target = params['new_target']
181
+ if 'new_condition' in params:
182
+ t.condition = params['new_condition']
183
+ if 'new_action' in params:
184
+ t.action = params['new_action']
185
+ return True, f"Transition '{source} --[{event}]-->' updated"
186
+ return False, "No matching transition found"
187
+
188
+ def _add_role_function(self, params: Dict) -> Tuple[bool, str]:
189
+ from statable.model import RoleFunction
190
+ name = params.get('name', '')
191
+ if not name:
192
+ return False, "Function name is not specified"
193
+ if name in self.sm.role_functions:
194
+ return False, f"Function '{name}' already exists"
195
+ rf = RoleFunction(
196
+ name=name,
197
+ namespace=params.get('namespace', ''),
198
+ return_type=params.get('return_type', 'void'),
199
+ description=params.get('description', ''),
200
+ )
201
+ self.sm.add_role_function(rf)
202
+ return True, f"Role function '{name}' added"
203
+
204
+ def _remove_role_function(self, params: Dict) -> Tuple[bool, str]:
205
+ """
206
+ Remove a role function.
207
+
208
+ Added in v1.6. Corresponds to ChangeActionType.REMOVE_ROLE_FUNCTION.
209
+ params:
210
+ name: 'HandleErr' or 'Middleware.HandleErr' (either is acceptable)
211
+ """
212
+ name = params.get('name', '')
213
+ if not name:
214
+ return False, "Function name is not specified"
215
+ # Exact match
216
+ if name in self.sm.role_functions:
217
+ self.sm.remove_role_function(name)
218
+ return True, f"Role function '{name}' deleted"
219
+
220
+ # Match by qualified_name
221
+ for key, rf in list(self.sm.role_functions.items()):
222
+ qn = getattr(rf, 'qualified_name', None) or key
223
+ if qn == name:
224
+ self.sm.remove_role_function(key)
225
+ return True, f"Role function '{name}' deleted"
226
+
227
+ # Match by bare name
228
+ for key, rf in list(self.sm.role_functions.items()):
229
+ if getattr(rf, 'name', '') == name:
230
+ self.sm.remove_role_function(key)
231
+ return True, f"Role function '{name}' deleted"
232
+ return False, f"Function '{name}' not found"
233
+
234
+ def _add_variable(self, params: Dict) -> Tuple[bool, str]:
235
+ from statable.global_defs import SystemVariable
236
+ name = params.get('name', '')
237
+ if not name:
238
+ return False, "Variable 'name' is required"
239
+ # [C-28 fix] Duplicate check
240
+ if any(getattr(v, 'name', '') == name for v in self.gd.variables):
241
+ return False, f"Variable '{name}' already exists"
242
+ var = SystemVariable(
243
+ name=name, type=params.get('type', 'uint8'),
244
+ group=params.get('group', ''),
245
+ description=params.get('description', ''))
246
+ self.gd.variables.append(var)
247
+ return True, f"Variable '{name}' added"
248
+
249
+ def _add_flag(self, params: Dict) -> Tuple[bool, str]:
250
+ from statable.global_defs import EventFlag
251
+ name = params.get('name', '')
252
+ if not name:
253
+ return False, "Flag 'name' is required"
254
+ # [C-28 fix] Duplicate check
255
+ if any(getattr(f, 'name', '') == name for f in self.gd.flags):
256
+ return False, f"Flag '{name}' already exists"
257
+ flag = EventFlag(
258
+ name=name, min_value=params.get('min_value', 0),
259
+ max_value=params.get('max_value', 1),
260
+ group=params.get('group', ''))
261
+ self.gd.flags.append(flag)
262
+ return True, f"Flag '{name}' added"
263
+
264
+ # ==================================================================
265
+ # v2.2 §12-6: Cell-level handlers
266
+ # ==================================================================
267
+ def _add_cell(self, params: Dict) -> Tuple[bool, str]:
268
+ """Ensure a cell exists for (source, event). Idempotent."""
269
+ source = params.get('source', '')
270
+ event = params.get('event', '')
271
+ if source not in self.sm.states:
272
+ return False, f"Source state '{source}' does not exist"
273
+ # event may be '' for completion transitions
274
+ if event and event not in self.sm.events:
275
+ return False, f"Event '{event}' does not exist"
276
+ # Ensure cell_actions / cell_relations entries exist (may be empty lists)
277
+ self.sm.cell_actions.setdefault((source, event), [])
278
+ self.sm.cell_relations.setdefault((source, event), [])
279
+ return True, f"Cell ({source}, {event or 'Completion'}) ensured"
280
+
281
+ def _remove_cell(self, params: Dict) -> Tuple[bool, str]:
282
+ """Remove all cell-level metadata (actions + relations)."""
283
+ source = params.get('source', '')
284
+ event = params.get('event', '')
285
+ if source not in self.sm.states:
286
+ return False, f"Source state '{source}' does not exist"
287
+ self.sm.remove_cell_metadata(source, event)
288
+ return True, f"Cell ({source}, {event or 'Completion'}) removed"
289
+
290
+ def _add_action_step(self, params: Dict) -> Tuple[bool, str]:
291
+ from statable.model import ActionStep
292
+ source = params.get('source', '')
293
+ event = params.get('event', '')
294
+ role_function = params.get('role_function', '')
295
+ trigger = params.get('trigger', 'before_transitions')
296
+
297
+ if not role_function:
298
+ return False, "role_function is required"
299
+ if source not in self.sm.states:
300
+ return False, f"Source state '{source}' does not exist"
301
+ if event and event not in self.sm.events:
302
+ return False, f"Event '{event}' does not exist"
303
+ if trigger not in ('before_transitions', 'after_transitions'):
304
+ return False, f"Invalid trigger: {trigger}"
305
+
306
+ existing = list(self.sm.get_actions_for_cell(source, event))
307
+ existing.append(ActionStep(
308
+ role_function=role_function, trigger=trigger,
309
+ title=role_function,
310
+ ))
311
+ self.sm.set_actions_for_cell(source, event, existing)
312
+ return True, (f"Action step '{role_function}' ({trigger}) "
313
+ f"added to ({source}, {event or 'Completion'})")
314
+
315
+ def _remove_action_step(self, params: Dict) -> Tuple[bool, str]:
316
+ source = params.get('source', '')
317
+ event = params.get('event', '')
318
+ role_function = params.get('role_function', '')
319
+
320
+ if not role_function:
321
+ return False, "role_function is required"
322
+ if source not in self.sm.states:
323
+ return False, f"Source state '{source}' does not exist"
324
+
325
+ existing = list(self.sm.get_actions_for_cell(source, event))
326
+ filtered = [a for a in existing if a.role_function != role_function]
327
+ if len(filtered) == len(existing):
328
+ return False, f"Action step '{role_function}' not found"
329
+ self.sm.set_actions_for_cell(source, event, filtered)
330
+ return True, (f"Action step '{role_function}' removed from "
331
+ f"({source}, {event or 'Completion'})")
332
+
333
+ def _add_transition_relation(self, params: Dict) -> Tuple[bool, str]:
334
+ from statable.model import TransitionRelation
335
+ source = params.get('source', '')
336
+ event = params.get('event', '')
337
+ kind = params.get('kind', '')
338
+ members = params.get('members', [])
339
+ shared_condition = params.get('shared_condition', '')
340
+
341
+ if source not in self.sm.states:
342
+ return False, f"Source state '{source}' does not exist"
343
+ if event and event not in self.sm.events:
344
+ return False, f"Event '{event}' does not exist"
345
+ if kind not in ('sequential', 'exclusive', 'group'):
346
+ return False, f"Invalid kind: {kind}"
347
+ if not members:
348
+ return False, "members is required"
349
+ if kind == 'group' and not shared_condition:
350
+ return False, "shared_condition is required for kind == 'group'"
351
+
352
+ # Validate that labels exist in this cell
353
+ existing_labels = {getattr(t, 'label', '') or ''
354
+ for t in self.sm.get_transitions_for_cell(source, event)}
355
+ for m in members:
356
+ if m not in existing_labels:
357
+ return False, f"Label '{m}' does not exist in this cell"
358
+
359
+ existing = list(self.sm.get_relations_for_cell(source, event))
360
+ existing.append(TransitionRelation(
361
+ kind=kind, members=list(members),
362
+ shared_condition=shared_condition,
363
+ ))
364
+ self.sm.set_relations_for_cell(source, event, existing)
365
+ return True, (f"Relation ({kind}) added to "
366
+ f"({source}, {event or 'Completion'})")
367
+
368
+ def _remove_transition_relation(self, params: Dict) -> Tuple[bool, str]:
369
+ source = params.get('source', '')
370
+ event = params.get('event', '')
371
+ kind = params.get('kind', '')
372
+ shared_condition = params.get('shared_condition', None)
373
+
374
+ if source not in self.sm.states:
375
+ return False, f"Source state '{source}' does not exist"
376
+
377
+ existing = list(self.sm.get_relations_for_cell(source, event))
378
+ filtered = []
379
+ removed = False
380
+ for r in existing:
381
+ if removed:
382
+ filtered.append(r)
383
+ continue
384
+ kind_match = (r.kind == kind)
385
+ cond_match = (shared_condition is None
386
+ or r.shared_condition == shared_condition)
387
+ if kind_match and cond_match:
388
+ removed = True
389
+ continue
390
+ filtered.append(r)
391
+
392
+ if not removed:
393
+ return False, f"No matching relation found (kind={kind})"
394
+ self.sm.set_relations_for_cell(source, event, filtered)
395
+ return True, (f"Relation ({kind}) removed from "
396
+ f"({source}, {event or 'Completion'})")
397
+
398
+ def _set_early_return(self, params: Dict) -> Tuple[bool, str]:
399
+ source = params.get('source', '')
400
+ event = params.get('event', '')
401
+ label = params.get('label', '')
402
+ early_return = params.get('early_return', None)
403
+
404
+ if source not in self.sm.states:
405
+ return False, f"Source state '{source}' does not exist"
406
+ if not label:
407
+ return False, "label is required"
408
+ if early_return is None:
409
+ return False, "early_return is required"
410
+
411
+ for t in self.sm.get_transitions_for_cell(source, event):
412
+ if getattr(t, 'label', '') == label:
413
+ t.early_return = bool(early_return)
414
+ return True, (f"Transition '{label}' early_return set to "
415
+ f"{t.early_return}")
416
+ return False, f"Transition label '{label}' not found in ({source}, {event or 'Completion'})"
@@ -0,0 +1,47 @@
1
+ # codegen/validate/clipboard_manager.py
2
+ """Clipboard management (PySide6 only)."""
3
+
4
+ import os
5
+ import sys
6
+
7
+ sys.path.append(
8
+ os.path.dirname(
9
+ os.path.dirname(
10
+ os.path.dirname(os.path.abspath(__file__)))))
11
+
12
+ from .logger import logger
13
+
14
+
15
+ class ClipboardManager:
16
+ """Clipboard management class."""
17
+
18
+ @staticmethod
19
+ def copy_to_clipboard(text: str) -> bool:
20
+ logger.debug(f"copy_to_clipboard: {len(text)} chars")
21
+ try:
22
+ from PySide6.QtWidgets import QApplication
23
+ except ImportError:
24
+ logger.error("PySide6 not available")
25
+ return False
26
+
27
+ try:
28
+ QApplication.clipboard().setText(text)
29
+ return True
30
+ except Exception as e:
31
+ logger.error(f"Clipboard set failed: {e}")
32
+ return False
33
+
34
+ @staticmethod
35
+ def get_from_clipboard() -> str:
36
+ logger.debug("get_from_clipboard started")
37
+ try:
38
+ from PySide6.QtWidgets import QApplication
39
+ except ImportError:
40
+ logger.error("PySide6 not available")
41
+ return ""
42
+
43
+ try:
44
+ return QApplication.clipboard().text()
45
+ except Exception as e:
46
+ logger.error(f"Clipboard get failed: {e}")
47
+ return ""