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,154 @@
1
+ # codegen/validate/response_parser.py
2
+ """\nAI answer parser\n"""
3
+
4
+ import sys
5
+ import os
6
+ import json
7
+ import re
8
+ from typing import List, Dict, Optional
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 .change_actions import ChangeRequest, ChangeActionType
14
+ from .data.keywords import MARKERS
15
+
16
+
17
+ class AIResponseParser:
18
+ """AI answer parser"""
19
+
20
+ # [v2.8.0 Phase B]
21
+ # Auto-generated from ChangeActionType so every future action
22
+ # is picked up without touching this file. Covers all 17 types
23
+ # (10 legacy + 7 cell-level).
24
+ ACTION_MAPPING = {t.value: t for t in ChangeActionType}
25
+
26
+ def __init__(self):
27
+ self.markers = MARKERS
28
+
29
+ def parse(self, text: str) -> List[ChangeRequest]:
30
+ changes = self.parse_json_response(text)
31
+ if changes:
32
+ return changes
33
+ return self.parse_text_response(text)
34
+
35
+ def parse_json_response(self, text: str) -> List[ChangeRequest]:
36
+ json_text = self._extract_json(text)
37
+ if not json_text:
38
+ return []
39
+ try:
40
+ data = json.loads(json_text)
41
+ except json.JSONDecodeError:
42
+ return []
43
+
44
+ changes = []
45
+ if 'changes' in data:
46
+ for change_data in data['changes']:
47
+ change = self._parse_change(change_data)
48
+ if change:
49
+ changes.append(change)
50
+ return changes
51
+
52
+ def parse_text_response(self, text: str) -> List[ChangeRequest]:
53
+ changes = []
54
+ for line in text.split('\n'):
55
+ change = self._parse_line(line.strip())
56
+ if change:
57
+ changes.append(change)
58
+ return changes
59
+
60
+ def _extract_json(self, text: str) -> Optional[str]:
61
+ """Extract the JSON payload from an AI reply.
62
+
63
+ Priority (v2.8.0 Phase B):
64
+ 1. <response>...</response> - v2.8.0 marker
65
+ 2. <json>...</json> - legacy marker
66
+ 3. MARKERS-configured markers (keywords.py)
67
+ 4. Brace matching { ... } - last-resort fallback
68
+ """
69
+ # 1. <response>...</response>
70
+ json_text = self._extract_with_markers(
71
+ text, '<response>', '</response>')
72
+ if json_text:
73
+ return json_text
74
+
75
+ # 2. <json>...</json> (legacy)
76
+ json_text = self._extract_with_markers(text, '<json>', '</json>')
77
+ if json_text:
78
+ return json_text
79
+
80
+ # 3. MARKERS-configured markers (keywords.py)
81
+ primary = self.markers.get('primary', {})
82
+ json_text = self._extract_with_markers(
83
+ text, primary.get('start', ''), primary.get('end', ''))
84
+ if json_text:
85
+ return json_text
86
+
87
+ for alt in self.markers.get('alternatives', []):
88
+ json_text = self._extract_with_markers(
89
+ text, alt.get('start', ''), alt.get('end', ''))
90
+ if json_text:
91
+ return json_text
92
+
93
+ # 4. Brace-matching fallback
94
+ start_idx = text.find('{')
95
+ end_idx = text.rfind('}')
96
+ if start_idx != -1 and end_idx != -1 and start_idx < end_idx:
97
+ candidate = text[start_idx:end_idx + 1]
98
+ try:
99
+ json.loads(candidate)
100
+ return candidate
101
+ except json.JSONDecodeError:
102
+ pass
103
+ return None
104
+
105
+ def _extract_with_markers(self, text: str, start: str, end: str) -> Optional[str]:
106
+ if not start or not end:
107
+ return None
108
+ start_idx = text.find(start)
109
+ if start_idx == -1:
110
+ return None
111
+ start_idx += len(start)
112
+ end_idx = text.find(end, start_idx)
113
+ if end_idx == -1:
114
+ return None
115
+ return text[start_idx:end_idx].strip()
116
+
117
+ def _parse_change(self, data: Dict) -> Optional[ChangeRequest]:
118
+ action_str = data.get('action', '')
119
+ if action_str not in self.ACTION_MAPPING:
120
+ return None
121
+ # [v2.8.0 Phase B] Preserve extended fields when present.
122
+ try:
123
+ confidence = float(data.get('confidence', 1.0))
124
+ except (TypeError, ValueError):
125
+ confidence = 1.0
126
+ return ChangeRequest(
127
+ action=self.ACTION_MAPPING[action_str],
128
+ params=data.get('params', {}),
129
+ reason=data.get('reason', ''),
130
+ source='ai',
131
+ id=str(data.get('id', '') or ''),
132
+ evidence=list(data.get('evidence', []) or []),
133
+ priority=str(data.get('priority', 'medium') or 'medium'),
134
+ confidence=confidence,
135
+ )
136
+
137
+ def _parse_line(self, line: str) -> Optional[ChangeRequest]:
138
+ transition_match = re.match(r'(\w+)\s*--\[(\w+)\]-?->\s*(\w+)', line)
139
+ if transition_match:
140
+ return ChangeRequest(
141
+ action=ChangeActionType.ADD_TRANSITION,
142
+ params={
143
+ 'source': transition_match.group(1),
144
+ 'event': transition_match.group(2),
145
+ 'target': transition_match.group(3),
146
+ }
147
+ )
148
+ initial_match = re.match(r'Initial state[::]\s*(\w+)', line)
149
+ if initial_match:
150
+ return ChangeRequest(
151
+ action=ChangeActionType.SET_INITIAL,
152
+ params={'state': initial_match.group(1)}
153
+ )
154
+ return None
@@ -0,0 +1,275 @@
1
+ # codegen/validate/response_validator.py
2
+ """
3
+ Response validator for AI proposals.
4
+
5
+ Validates the ChangeRequests produced by AIResponseParser before they
6
+ are handed to ChangeApplier. Matches SPEC_AI_PROMPT_v1.1 §6.
7
+
8
+ Four validation stages:
9
+
10
+ Stage 1. Schema - action is a known ChangeActionType,
11
+ params is dict, reason is non-empty.
12
+ Stage 2. Action - (parser already filters this; kept for
13
+ callers that bypass the parser)
14
+ Stage 3. Parameters - ACTION_DEFINITIONS required params present
15
+ and of the correct type.
16
+ Stage 4. References - source / target / event / role_function
17
+ refer to existing objects (per-action rules).
18
+
19
+ Invalid requests are moved to `invalid_requests` with a human-readable
20
+ reason. Non-fatal issues go into `warnings`.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ from dataclasses import dataclass, field
26
+ from typing import List, Tuple, Optional, Set
27
+
28
+ from .logger import logger
29
+ from .change_actions import ChangeRequest, ChangeActionType
30
+ from .data.action_definitions import ACTION_DEFINITIONS
31
+
32
+
33
+ # ======================================================================
34
+ # Result container
35
+ # ======================================================================
36
+ @dataclass
37
+ class ResponseValidationResult:
38
+ """Outcome of ResponseValidator.validate()."""
39
+ valid_requests: List[ChangeRequest] = field(default_factory=list)
40
+ invalid_requests: List[Tuple[ChangeRequest, str]] = field(
41
+ default_factory=list)
42
+ warnings: List[str] = field(default_factory=list)
43
+
44
+ @property
45
+ def total(self) -> int:
46
+ return len(self.valid_requests) + len(self.invalid_requests)
47
+
48
+ @property
49
+ def ok(self) -> bool:
50
+ return not self.invalid_requests
51
+
52
+
53
+ # ======================================================================
54
+ # Validator
55
+ # ======================================================================
56
+ class ResponseValidator:
57
+ """Validate ChangeRequests against the current StateMachine."""
58
+
59
+ def __init__(self):
60
+ logger.debug("ResponseValidator.__init__ started")
61
+ self.action_definitions = ACTION_DEFINITIONS
62
+ logger.debug("ResponseValidator.__init__ completed")
63
+
64
+ # ------------------------------------------------------------------
65
+ # Public API
66
+ # ------------------------------------------------------------------
67
+ def validate(
68
+ self,
69
+ requests: List[ChangeRequest],
70
+ sm,
71
+ gd=None,
72
+ ) -> ResponseValidationResult:
73
+ result = ResponseValidationResult()
74
+ for req in requests or []:
75
+ err = self._validate_one(req, sm, gd, result.warnings)
76
+ if err is None:
77
+ result.valid_requests.append(req)
78
+ else:
79
+ result.invalid_requests.append((req, err))
80
+ logger.debug(
81
+ "ResponseValidator: rejected %s: %s",
82
+ getattr(req, 'action', '?'), err)
83
+ return result
84
+
85
+ # ------------------------------------------------------------------
86
+ # Orchestration
87
+ # ------------------------------------------------------------------
88
+ def _validate_one(self, req, sm, gd, warnings) -> Optional[str]:
89
+ for check in (
90
+ lambda: self._check_schema(req),
91
+ lambda: self._check_params(req),
92
+ lambda: self._check_references(req, sm),
93
+ ):
94
+ err = check()
95
+ if err:
96
+ return err
97
+ self._collect_warnings(req, sm, warnings)
98
+ return None
99
+
100
+ # ------------------------------------------------------------------
101
+ # Stage 1: Schema
102
+ # ------------------------------------------------------------------
103
+ def _check_schema(self, req) -> Optional[str]:
104
+ if not isinstance(req, ChangeRequest):
105
+ return "not a ChangeRequest"
106
+ if not isinstance(req.action, ChangeActionType):
107
+ return "action is not a ChangeActionType"
108
+ if not isinstance(req.params, dict):
109
+ return "params is not a dict"
110
+ if not isinstance(req.reason, str) or not req.reason.strip():
111
+ return "reason is empty"
112
+ return None
113
+
114
+ # ------------------------------------------------------------------
115
+ # Stage 3: Parameters
116
+ # ------------------------------------------------------------------
117
+ def _check_params(self, req) -> Optional[str]:
118
+ definition = self.action_definitions.get(req.action.value)
119
+ if definition is None:
120
+ return f"action '{req.action.value}' is not defined"
121
+ params = definition.get('params', {})
122
+ for pname, pinfo in params.items():
123
+ required = bool(pinfo.get('required'))
124
+ if pname not in req.params:
125
+ if required:
126
+ return f"missing required param '{pname}'"
127
+ continue
128
+ value = req.params[pname]
129
+ if required:
130
+ if value is None:
131
+ return f"param '{pname}' is empty"
132
+ if isinstance(value, str) and not value.strip():
133
+ return f"param '{pname}' is empty"
134
+ err = self._check_param_type(pname, value, pinfo)
135
+ if err:
136
+ return err
137
+ return None
138
+
139
+ def _check_param_type(self, name, value, info) -> Optional[str]:
140
+ t = info.get('type', 'str')
141
+ if t == 'str':
142
+ if not isinstance(value, str):
143
+ return f"param '{name}' should be str"
144
+ elif t == 'int':
145
+ if isinstance(value, bool):
146
+ return f"param '{name}' should be int"
147
+ try:
148
+ int(value)
149
+ except (TypeError, ValueError):
150
+ return f"param '{name}' should be int"
151
+ elif t == 'bool':
152
+ if not isinstance(value, bool):
153
+ return f"param '{name}' should be bool"
154
+ elif t == 'list':
155
+ if not isinstance(value, list):
156
+ return f"param '{name}' should be list"
157
+ elif t == 'enum':
158
+ values = info.get('values', [])
159
+ if value not in values:
160
+ joined = ", ".join(str(v) for v in values)
161
+ return f"param '{name}' must be one of {joined}"
162
+ return None
163
+
164
+ # ------------------------------------------------------------------
165
+ # Stage 4: References
166
+ # ------------------------------------------------------------------
167
+ def _check_references(self, req, sm) -> Optional[str]:
168
+ if sm is None:
169
+ return None
170
+ a = req.action
171
+ p = req.params
172
+ if a == ChangeActionType.ADD_TRANSITION:
173
+ return self._ref_add_transition(p, sm)
174
+ if a == ChangeActionType.REMOVE_TRANSITION:
175
+ return self._ref_remove_transition(p, sm)
176
+ if a == ChangeActionType.SET_INITIAL:
177
+ return self._ref_set_initial(p, sm)
178
+ if a == ChangeActionType.ADD_ACTION_STEP:
179
+ return self._ref_add_action_step(p, sm)
180
+ if a == ChangeActionType.ADD_TRANSITION_RELATION:
181
+ return self._ref_add_transition_relation(p, sm)
182
+ # [extension] cell-level checks beyond SPEC §6.3, harmless
183
+ if a == ChangeActionType.SET_EARLY_RETURN:
184
+ return self._ref_set_early_return(p, sm)
185
+ return None
186
+
187
+ def _ref_add_transition(self, p, sm) -> Optional[str]:
188
+ source = p.get('source', '')
189
+ event = p.get('event', '')
190
+ if source not in sm.states:
191
+ return f"source state '{source}' does not exist"
192
+ if event not in sm.events:
193
+ return f"event '{event}' does not exist"
194
+ return None
195
+
196
+ def _ref_remove_transition(self, p, sm) -> Optional[str]:
197
+ source = p.get('source', '')
198
+ event = p.get('event', '')
199
+ target = p.get('target', '')
200
+ for t in sm.transitions:
201
+ if t.source == source and t.event == event \
202
+ and t.target == target:
203
+ return None
204
+ return (
205
+ f"transition {source} --[{event}]--> {target} "
206
+ "does not exist"
207
+ )
208
+
209
+ def _ref_set_initial(self, p, sm) -> Optional[str]:
210
+ state = p.get('state', '')
211
+ if state not in sm.states:
212
+ return f"state '{state}' does not exist"
213
+ return None
214
+
215
+ def _ref_add_action_step(self, p, sm) -> Optional[str]:
216
+ rf = p.get('role_function', '')
217
+ if not self._role_function_exists(rf, sm):
218
+ return f"role function '{rf}' does not exist"
219
+ return None
220
+
221
+ def _ref_add_transition_relation(self, p, sm) -> Optional[str]:
222
+ source = p.get('source', '')
223
+ event = p.get('event', '')
224
+ members = p.get('members', []) or []
225
+ labels = self._cell_labels(sm, source, event)
226
+ for m in members:
227
+ if m not in labels:
228
+ return (
229
+ f"label '{m}' not found in cell ({source}, {event})"
230
+ )
231
+ return None
232
+
233
+ def _ref_set_early_return(self, p, sm) -> Optional[str]:
234
+ source = p.get('source', '')
235
+ event = p.get('event', '')
236
+ label = p.get('label', '')
237
+ labels = self._cell_labels(sm, source, event)
238
+ if label not in labels:
239
+ return f"label '{label}' not found in ({source}, {event})"
240
+ return None
241
+
242
+ # ------------------------------------------------------------------
243
+ # Warnings (non-blocking)
244
+ # ------------------------------------------------------------------
245
+ def _collect_warnings(self, req, sm, warnings):
246
+ if req.confidence < 0.5:
247
+ ident = req.id or req.action.value
248
+ warnings.append(
249
+ f"{ident}: low confidence ({req.confidence:.2f})"
250
+ )
251
+
252
+ # ------------------------------------------------------------------
253
+ # Helpers
254
+ # ------------------------------------------------------------------
255
+ def _role_function_exists(self, name: str, sm) -> bool:
256
+ if not name:
257
+ return False
258
+ if name in sm.role_functions:
259
+ return True
260
+ for rf in sm.role_functions.values():
261
+ if getattr(rf, 'qualified_name', '') == name:
262
+ return True
263
+ return False
264
+
265
+ def _cell_labels(self, sm, source: str, event: str) -> Set[str]:
266
+ labels: Set[str] = set()
267
+ try:
268
+ transitions = sm.get_transitions_for_cell(source, event)
269
+ except AttributeError:
270
+ return labels
271
+ for t in transitions or []:
272
+ label = getattr(t, 'label', '')
273
+ if label:
274
+ labels.add(label)
275
+ return labels