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.
- codegen/__init__.py +38 -0
- codegen/c_code_generator.py +1752 -0
- codegen/code_merger.py +371 -0
- codegen/code_templates.py +509 -0
- codegen/config.py +167 -0
- codegen/enum_generator.py +377 -0
- codegen/event_queue_generator.py +303 -0
- codegen/interrupt_generator.py +345 -0
- codegen/naming_convention.py +171 -0
- codegen/osal_generator.py +282 -0
- codegen/role_function_generator.py +1244 -0
- codegen/sample_data.py +269 -0
- codegen/state_actions_generator.py +395 -0
- codegen/struct_generator.py +721 -0
- codegen/timer_generator.py +299 -0
- codegen/transition_generator.py +907 -0
- codegen/type_mapper.py +69 -0
- codegen/validate/__init__.py +38 -0
- codegen/validate/change_actions.py +90 -0
- codegen/validate/change_applier.py +416 -0
- codegen/validate/clipboard_manager.py +47 -0
- codegen/validate/data/__init__.py +30 -0
- codegen/validate/data/action_definitions.py +200 -0
- codegen/validate/data/keywords.py +46 -0
- codegen/validate/data/prompt_templates.py +176 -0
- codegen/validate/data/validation_rules.py +205 -0
- codegen/validate/items/__init__.py +30 -0
- codegen/validate/items/base_validator.py +25 -0
- codegen/validate/items/cell_validator.py +275 -0
- codegen/validate/items/custom_type_validator.py +75 -0
- codegen/validate/items/event_validator.py +78 -0
- codegen/validate/items/flag_validator.py +74 -0
- codegen/validate/items/interrupt_validator.py +75 -0
- codegen/validate/items/queue_validator.py +73 -0
- codegen/validate/items/role_function_validator.py +175 -0
- codegen/validate/items/state_validator.py +88 -0
- codegen/validate/items/timer_validator.py +120 -0
- codegen/validate/items/transition_validator.py +113 -0
- codegen/validate/items/variable_validator.py +132 -0
- codegen/validate/logger.py +52 -0
- codegen/validate/models.py +177 -0
- codegen/validate/prompt_generator.py +724 -0
- codegen/validate/response_parser.py +154 -0
- codegen/validate/response_validator.py +275 -0
- codegen/validate/validation_dialog.py +322 -0
- codegen/validate/validator.py +94 -0
- codegen/variable_generator.py +419 -0
- statable/__init__.py +71 -0
- statable/__main__.py +49 -0
- statable/cli.py +251 -0
- statable/global_defs.py +256 -0
- statable/mermaid_gen.py +144 -0
- statable/model.py +426 -0
- statable/parser.py +64 -0
- statable/sample_data.py +435 -0
- statable/shared/__init__.py +24 -0
- statable/shared/condition_library.py +63 -0
- statable/shared/literal_library.py +66 -0
- statable/shared/role_function_library.py +150 -0
- statable/state_machine.py +121 -0
- statable/xml_io.py +1164 -0
- statable-3.1.0.dist-info/METADATA +151 -0
- statable-3.1.0.dist-info/RECORD +117 -0
- statable-3.1.0.dist-info/WHEEL +5 -0
- statable-3.1.0.dist-info/entry_points.txt +2 -0
- statable-3.1.0.dist-info/top_level.txt +3 -0
- statable_gui/__init__.py +23 -0
- statable_gui/action_edit_dialog.py +214 -0
- statable_gui/code_generation_dialog.py +486 -0
- statable_gui/code_generation_settings_dialog.py +522 -0
- statable_gui/common_widgets.py +537 -0
- statable_gui/condition_builder_dialog.py +500 -0
- statable_gui/condition_edit_dialog.py +123 -0
- statable_gui/condition_template_dialog.py +58 -0
- statable_gui/config.py +21 -0
- statable_gui/dialogs.py +439 -0
- statable_gui/event_definition_dialog.py +605 -0
- statable_gui/event_delivery_settings_dialog.py +191 -0
- statable_gui/event_queue_dialog.py +245 -0
- statable_gui/global_defs.py +167 -0
- statable_gui/global_defs_dialog.py +796 -0
- statable_gui/i18n/__init__.py +118 -0
- statable_gui/interrupt_handler_edit_dialog.py +810 -0
- statable_gui/layer_settings_dialog.py +180 -0
- statable_gui/libcntrl/__init__.py +23 -0
- statable_gui/libcntrl/condition_library.py +13 -0
- statable_gui/libcntrl/literal_library.py +13 -0
- statable_gui/libcntrl/literal_management_dialog.py +185 -0
- statable_gui/libcntrl/role_function_edit_dialog.py +145 -0
- statable_gui/libcntrl/role_function_library.py +13 -0
- statable_gui/literal_definition_dialog.py +62 -0
- statable_gui/logger.py +83 -0
- statable_gui/main_window.py +1380 -0
- statable_gui/matrix_table.py +413 -0
- statable_gui/preference_keys.py +18 -0
- statable_gui/preferences.py +74 -0
- statable_gui/role_function_dialog.py +244 -0
- statable_gui/sample_data.py +11 -0
- statable_gui/state_actions_dialog.py +398 -0
- statable_gui/symbol_picker.py +170 -0
- statable_gui/traceball.py +37 -0
- statable_gui/transition_editor_direct/__init__.py +15 -0
- statable_gui/transition_editor_direct/actions_tab.py +856 -0
- statable_gui/transition_editor_direct/code_widget.py +344 -0
- statable_gui/transition_editor_direct/coverage_analyzer.py +163 -0
- statable_gui/transition_editor_direct/dialog.py +335 -0
- statable_gui/transition_editor_direct/draft.py +270 -0
- statable_gui/transition_editor_direct/edit_dialogs.py +160 -0
- statable_gui/transition_editor_direct/overview_tab.py +173 -0
- statable_gui/transition_editor_direct/palette_widget.py +233 -0
- statable_gui/transition_editor_direct/relations_edit_dialog.py +156 -0
- statable_gui/transition_editor_direct/relations_tab.py +230 -0
- statable_gui/transition_editor_direct/system_global_dialog.py +52 -0
- statable_gui/transition_editor_direct/transition_actions_dialog.py +109 -0
- statable_gui/transition_editor_direct/transitions_tab.py +620 -0
- statable_gui/validation_dialog.py +285 -0
- statable_gui/widgets.py +1315 -0
|
@@ -0,0 +1,724 @@
|
|
|
1
|
+
# codegen/validate/prompt_generator.py
|
|
2
|
+
"""
|
|
3
|
+
AI prompt generation class
|
|
4
|
+
|
|
5
|
+
[v2.8.0 / SPEC_AI_PROMPT_v1.1]
|
|
6
|
+
- Full prompt refresh (XML-style tags).
|
|
7
|
+
- `_format_data` extended to include role_functions, cells, and
|
|
8
|
+
global_definitions (previously missing).
|
|
9
|
+
- `_format_validation` now emits structured <issue> elements
|
|
10
|
+
(severity / category / code / target / suggestion).
|
|
11
|
+
- `_format_actions` renders the 17 ACTION_DEFINITIONS as XML.
|
|
12
|
+
- Reserved role-function fields and used_* symbol trackers are
|
|
13
|
+
deliberately NOT sent (not consumed by codegen / not actionable).
|
|
14
|
+
- Backward compatible: `generate_diagnosis_prompt` signature is
|
|
15
|
+
unchanged; the legacy FEW_SHOT_EXAMPLE / VALIDATION_POINTS
|
|
16
|
+
constants remain importable from prompt_templates.
|
|
17
|
+
|
|
18
|
+
[v2.8.0-fix1]
|
|
19
|
+
- `_format_cells` now returns " <cells/>" for empty / missing
|
|
20
|
+
sections, consistent with every other <context> sub-builder.
|
|
21
|
+
|
|
22
|
+
[v2.8.0-fix2]
|
|
23
|
+
- `_format_global_definitions` now skips a pristine `timer_base`
|
|
24
|
+
(a freshly constructed GlobalDefinitions() has a default
|
|
25
|
+
TimerBaseDef that carries no user information). New helper
|
|
26
|
+
`_has_timer_content` decides whether the timer section is
|
|
27
|
+
worth emitting.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
import sys
|
|
31
|
+
import os
|
|
32
|
+
from typing import Optional
|
|
33
|
+
|
|
34
|
+
sys.path.append(
|
|
35
|
+
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
from .logger import logger
|
|
39
|
+
from .data.prompt_templates import (
|
|
40
|
+
PROMPT_TEMPLATES,
|
|
41
|
+
OUTPUT_SCHEMA,
|
|
42
|
+
FEW_SHOT_EXAMPLE_1,
|
|
43
|
+
FEW_SHOT_EXAMPLE_2,
|
|
44
|
+
)
|
|
45
|
+
from .data.action_definitions import ACTION_DEFINITIONS
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# ======================================================================
|
|
49
|
+
# XML attribute escaping
|
|
50
|
+
# ======================================================================
|
|
51
|
+
def _esc(value) -> str:
|
|
52
|
+
"""Escape a value for safe inclusion in an XML attribute."""
|
|
53
|
+
if value is None:
|
|
54
|
+
return ""
|
|
55
|
+
s = str(value)
|
|
56
|
+
return (s.replace("&", "&")
|
|
57
|
+
.replace("<", "<")
|
|
58
|
+
.replace(">", ">")
|
|
59
|
+
.replace('"', """))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _bool_attr(value: bool) -> str:
|
|
63
|
+
return "true" if value else "false"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# ======================================================================
|
|
67
|
+
# Normalization helpers (mirrors xml_io._normalize_actions)
|
|
68
|
+
# ======================================================================
|
|
69
|
+
def _as_str_list(value):
|
|
70
|
+
"""Return a clean list of strings for legacy List[str] fields."""
|
|
71
|
+
if value is None:
|
|
72
|
+
return []
|
|
73
|
+
if isinstance(value, str):
|
|
74
|
+
s = value.strip()
|
|
75
|
+
return [s] if s else []
|
|
76
|
+
if isinstance(value, list):
|
|
77
|
+
return [str(x) for x in value if str(x).strip()]
|
|
78
|
+
return [str(value)]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class AIPromptGenerator:
|
|
82
|
+
"""AI prompt generation class."""
|
|
83
|
+
|
|
84
|
+
def __init__(self):
|
|
85
|
+
logger.debug("AIPromptGenerator.__init__ started")
|
|
86
|
+
self.templates = PROMPT_TEMPLATES
|
|
87
|
+
self.output_schema = OUTPUT_SCHEMA
|
|
88
|
+
self.example_1 = FEW_SHOT_EXAMPLE_1
|
|
89
|
+
self.example_2 = FEW_SHOT_EXAMPLE_2
|
|
90
|
+
logger.debug("AIPromptGenerator.__init__ completed")
|
|
91
|
+
|
|
92
|
+
# ------------------------------------------------------------------
|
|
93
|
+
# Public entry point
|
|
94
|
+
# ------------------------------------------------------------------
|
|
95
|
+
def generate_diagnosis_prompt(self, sm, gd, validation_result=None) -> str:
|
|
96
|
+
"""Build the full diagnosis prompt as a single string."""
|
|
97
|
+
template = self.templates['diagnosis']['template']
|
|
98
|
+
return template.format(
|
|
99
|
+
output_schema=self.output_schema,
|
|
100
|
+
example_1=self.example_1,
|
|
101
|
+
example_2=self.example_2,
|
|
102
|
+
context=self._format_data(sm, gd),
|
|
103
|
+
validation=self._format_validation(validation_result),
|
|
104
|
+
actions=self._format_actions(),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# ------------------------------------------------------------------
|
|
108
|
+
# <context> builder
|
|
109
|
+
# ------------------------------------------------------------------
|
|
110
|
+
def _format_data(self, sm, gd) -> str:
|
|
111
|
+
"""Build the inner content of <context>...</context> as XML.
|
|
112
|
+
|
|
113
|
+
Includes: <layer>, <states>, <events>, <transitions>,
|
|
114
|
+
<role_functions>, <cells>, <global_definitions>.
|
|
115
|
+
Empty sections are emitted as self-closing tags so the AI
|
|
116
|
+
can tell them apart from a missing section.
|
|
117
|
+
"""
|
|
118
|
+
parts = [
|
|
119
|
+
self._format_layer(sm),
|
|
120
|
+
self._format_states(sm),
|
|
121
|
+
self._format_events(sm),
|
|
122
|
+
self._format_transitions(sm),
|
|
123
|
+
self._format_role_functions(sm),
|
|
124
|
+
self._format_cells(sm),
|
|
125
|
+
self._format_global_definitions(gd),
|
|
126
|
+
]
|
|
127
|
+
return "\n".join(p for p in parts if p)
|
|
128
|
+
|
|
129
|
+
# ---------- <layer> ----------
|
|
130
|
+
def _format_layer(self, sm) -> str:
|
|
131
|
+
name = getattr(sm, 'layer_name', '') or 'Untitled'
|
|
132
|
+
priority = getattr(sm, 'layer_priority', 5)
|
|
133
|
+
attrs = [f'name="{_esc(name)}"', f'priority="{priority}"']
|
|
134
|
+
initial = getattr(sm, 'initial_state', None)
|
|
135
|
+
if initial:
|
|
136
|
+
attrs.append(f'initial="{_esc(initial)}"')
|
|
137
|
+
desc = getattr(sm, 'layer_description', '') or ''
|
|
138
|
+
if desc:
|
|
139
|
+
attrs.append(f'description="{_esc(desc)}"')
|
|
140
|
+
return f" <layer {' '.join(attrs)}/>"
|
|
141
|
+
|
|
142
|
+
# ---------- <states> ----------
|
|
143
|
+
def _format_states(self, sm) -> str:
|
|
144
|
+
states = getattr(sm, 'states', None) or {}
|
|
145
|
+
if not states:
|
|
146
|
+
return " <states/>"
|
|
147
|
+
lines = [" <states>"]
|
|
148
|
+
for state in states.values():
|
|
149
|
+
attrs = [f'name="{_esc(state.name)}"',
|
|
150
|
+
f'type="{state.type.value}"']
|
|
151
|
+
if state.parent:
|
|
152
|
+
attrs.append(f'parent="{_esc(state.parent)}"')
|
|
153
|
+
if state.description:
|
|
154
|
+
attrs.append(f'description="{_esc(state.description)}"')
|
|
155
|
+
|
|
156
|
+
entry = self._format_action_list(getattr(state, 'entry', []) or [])
|
|
157
|
+
exit_ = self._format_action_list(getattr(state, 'exit', []) or [])
|
|
158
|
+
do = self._format_action_list(
|
|
159
|
+
getattr(state, 'do_actions', []) or [])
|
|
160
|
+
|
|
161
|
+
if not (entry or exit_ or do):
|
|
162
|
+
lines.append(f" <state {' '.join(attrs)}/>")
|
|
163
|
+
continue
|
|
164
|
+
|
|
165
|
+
lines.append(f" <state {' '.join(attrs)}>")
|
|
166
|
+
if entry:
|
|
167
|
+
lines.append(" <entry>")
|
|
168
|
+
lines.extend(entry)
|
|
169
|
+
lines.append(" </entry>")
|
|
170
|
+
if exit_:
|
|
171
|
+
lines.append(" <exit>")
|
|
172
|
+
lines.extend(exit_)
|
|
173
|
+
lines.append(" </exit>")
|
|
174
|
+
if do:
|
|
175
|
+
lines.append(" <do>")
|
|
176
|
+
lines.extend(do)
|
|
177
|
+
lines.append(" </do>")
|
|
178
|
+
lines.append(" </state>")
|
|
179
|
+
lines.append(" </states>")
|
|
180
|
+
return "\n".join(lines)
|
|
181
|
+
|
|
182
|
+
def _format_action_list(self, actions) -> list:
|
|
183
|
+
"""Render List[ActionStep] as indented <action/> lines.
|
|
184
|
+
|
|
185
|
+
Simple role-only actions are emitted as
|
|
186
|
+
`<action role_function="X"/>` to match xml_io's canonical
|
|
187
|
+
output. action_type="custom" is skipped: it is user code,
|
|
188
|
+
not an AI-actionable change.
|
|
189
|
+
"""
|
|
190
|
+
result = []
|
|
191
|
+
for a in actions or []:
|
|
192
|
+
role = getattr(a, 'role_function', '') or ''
|
|
193
|
+
cond = getattr(a, 'condition', '') or ''
|
|
194
|
+
atype = getattr(a, 'action_type', 'role') or 'role'
|
|
195
|
+
ename = getattr(a, 'event_name', '') or ''
|
|
196
|
+
|
|
197
|
+
if atype == 'custom':
|
|
198
|
+
continue
|
|
199
|
+
|
|
200
|
+
attrs = []
|
|
201
|
+
if atype == 'fire_event':
|
|
202
|
+
attrs.append('action_type="fire_event"')
|
|
203
|
+
if ename:
|
|
204
|
+
attrs.append(f'event_name="{_esc(ename)}"')
|
|
205
|
+
if not ename:
|
|
206
|
+
continue
|
|
207
|
+
else:
|
|
208
|
+
if not role:
|
|
209
|
+
continue
|
|
210
|
+
attrs.append(f'role_function="{_esc(role)}"')
|
|
211
|
+
|
|
212
|
+
if cond:
|
|
213
|
+
attrs.append(f'condition="{_esc(cond)}"')
|
|
214
|
+
|
|
215
|
+
result.append(f' <action {" ".join(attrs)}/>')
|
|
216
|
+
return result
|
|
217
|
+
|
|
218
|
+
# ---------- <events> ----------
|
|
219
|
+
def _format_events(self, sm) -> str:
|
|
220
|
+
events = getattr(sm, 'events', None) or {}
|
|
221
|
+
if not events:
|
|
222
|
+
return " <events/>"
|
|
223
|
+
lines = [" <events>"]
|
|
224
|
+
for event in events.values():
|
|
225
|
+
attrs = [
|
|
226
|
+
f'name="{_esc(event.name)}"',
|
|
227
|
+
f'kind="{event.kind.value}"',
|
|
228
|
+
f'delivery_type="{event.delivery_type.value}"',
|
|
229
|
+
f'source_layer="{event.source_layer.value}"',
|
|
230
|
+
]
|
|
231
|
+
if event.description:
|
|
232
|
+
attrs.append(f'description="{_esc(event.description)}"')
|
|
233
|
+
|
|
234
|
+
trigger = self._format_trigger(event)
|
|
235
|
+
if trigger:
|
|
236
|
+
lines.append(f" <event {' '.join(attrs)}>")
|
|
237
|
+
lines.append(trigger)
|
|
238
|
+
lines.append(" </event>")
|
|
239
|
+
else:
|
|
240
|
+
lines.append(f" <event {' '.join(attrs)}/>")
|
|
241
|
+
lines.append(" </events>")
|
|
242
|
+
return "\n".join(lines)
|
|
243
|
+
|
|
244
|
+
def _format_trigger(self, event) -> str:
|
|
245
|
+
"""Render an <event>'s trigger, type-specifically."""
|
|
246
|
+
td = getattr(event, 'trigger_detail', None)
|
|
247
|
+
trigger_text = getattr(event, 'trigger', '') or ''
|
|
248
|
+
|
|
249
|
+
if td is None:
|
|
250
|
+
if not trigger_text:
|
|
251
|
+
return ""
|
|
252
|
+
return (
|
|
253
|
+
' <trigger type="manual" '
|
|
254
|
+
f'description="{_esc(trigger_text)}"/>'
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
t = (getattr(td, 'type', 'manual') or 'manual').lower()
|
|
258
|
+
attrs = [f'type="{_esc(t)}"']
|
|
259
|
+
|
|
260
|
+
def _opt(name, val):
|
|
261
|
+
if val:
|
|
262
|
+
attrs.append(f'{name}="{_esc(val)}"')
|
|
263
|
+
|
|
264
|
+
def _int(name, val):
|
|
265
|
+
if val:
|
|
266
|
+
attrs.append(f'{name}="{int(val)}"')
|
|
267
|
+
|
|
268
|
+
if t == "edge":
|
|
269
|
+
_opt("source", getattr(td, 'source', ''))
|
|
270
|
+
_opt("edge", getattr(td, 'edge', ''))
|
|
271
|
+
_int("debounce_ms", getattr(td, 'debounce_ms', 0))
|
|
272
|
+
_opt("description", getattr(td, 'description', ''))
|
|
273
|
+
elif t == "polling":
|
|
274
|
+
_opt("source", getattr(td, 'source', ''))
|
|
275
|
+
_int("period_ms", getattr(td, 'period_ms', 0))
|
|
276
|
+
_opt("description", getattr(td, 'description', ''))
|
|
277
|
+
elif t == "timer":
|
|
278
|
+
_opt("source", getattr(td, 'source', ''))
|
|
279
|
+
_int("period_ms", getattr(td, 'period_ms', 0))
|
|
280
|
+
if not getattr(td, 'auto_reload', True):
|
|
281
|
+
attrs.append('auto_reload="false"')
|
|
282
|
+
_opt("description", getattr(td, 'description', ''))
|
|
283
|
+
elif t == "call":
|
|
284
|
+
_opt("caller", getattr(td, 'caller', ''))
|
|
285
|
+
_opt("description", getattr(td, 'description', ''))
|
|
286
|
+
elif t == "comparison":
|
|
287
|
+
_opt("condition", getattr(td, 'condition', ''))
|
|
288
|
+
_int("poll_period_ms", getattr(td, 'poll_period_ms', 0))
|
|
289
|
+
_opt("description", getattr(td, 'description', ''))
|
|
290
|
+
else: # manual / unknown
|
|
291
|
+
_opt("source", getattr(td, 'source', ''))
|
|
292
|
+
_opt("description", getattr(td, 'description', ''))
|
|
293
|
+
|
|
294
|
+
return f' <trigger {" ".join(attrs)}/>'
|
|
295
|
+
|
|
296
|
+
# ---------- <transitions> ----------
|
|
297
|
+
def _format_transitions(self, sm) -> str:
|
|
298
|
+
transitions = getattr(sm, 'transitions', None) or []
|
|
299
|
+
if not transitions:
|
|
300
|
+
return " <transitions/>"
|
|
301
|
+
lines = [" <transitions>"]
|
|
302
|
+
for t in transitions:
|
|
303
|
+
attrs = [
|
|
304
|
+
f'source="{_esc(t.source)}"',
|
|
305
|
+
f'event="{_esc(t.event)}"',
|
|
306
|
+
f'target="{_esc(t.target)}"',
|
|
307
|
+
]
|
|
308
|
+
if t.condition:
|
|
309
|
+
attrs.append(f'condition="{_esc(t.condition)}"')
|
|
310
|
+
attrs.append(f'early_return="{_bool_attr(t.early_return)}"')
|
|
311
|
+
if t.label:
|
|
312
|
+
attrs.append(f'label="{_esc(t.label)}"')
|
|
313
|
+
attrs.append(f'has_else="{_bool_attr(t.has_else)}"')
|
|
314
|
+
if t.else_target:
|
|
315
|
+
attrs.append(f'else_target="{_esc(t.else_target)}"')
|
|
316
|
+
|
|
317
|
+
pre = _as_str_list(getattr(t, 'pre_actions', []))
|
|
318
|
+
els = _as_str_list(getattr(t, 'else_actions', []))
|
|
319
|
+
|
|
320
|
+
if not (pre or els):
|
|
321
|
+
lines.append(f" <transition {' '.join(attrs)}/>")
|
|
322
|
+
continue
|
|
323
|
+
|
|
324
|
+
lines.append(f" <transition {' '.join(attrs)}>")
|
|
325
|
+
if pre:
|
|
326
|
+
lines.append(" <pre_actions>")
|
|
327
|
+
for p in pre:
|
|
328
|
+
lines.append(f' <action name="{_esc(p)}"/>')
|
|
329
|
+
lines.append(" </pre_actions>")
|
|
330
|
+
if els:
|
|
331
|
+
lines.append(" <else_actions>")
|
|
332
|
+
for e in els:
|
|
333
|
+
lines.append(f' <action name="{_esc(e)}"/>')
|
|
334
|
+
lines.append(" </else_actions>")
|
|
335
|
+
lines.append(" </transition>")
|
|
336
|
+
lines.append(" </transitions>")
|
|
337
|
+
return "\n".join(lines)
|
|
338
|
+
|
|
339
|
+
# ---------- <role_functions> ----------
|
|
340
|
+
def _format_role_functions(self, sm) -> str:
|
|
341
|
+
"""Send only name / namespace / description.
|
|
342
|
+
|
|
343
|
+
Reserved fields (return_type, arg1_*, arg2_*) and GUI-only
|
|
344
|
+
symbol trackers (used_*) are deliberately omitted:
|
|
345
|
+
- Reserved fields do not affect codegen or the prompt's
|
|
346
|
+
semantic content.
|
|
347
|
+
- used_* mirrors are GUI metadata, not actionable by AI.
|
|
348
|
+
"""
|
|
349
|
+
rfs = getattr(sm, 'role_functions', None) or {}
|
|
350
|
+
if not rfs:
|
|
351
|
+
return " <role_functions/>"
|
|
352
|
+
lines = [" <role_functions>"]
|
|
353
|
+
for rf in rfs.values():
|
|
354
|
+
attrs = [f'name="{_esc(rf.name)}"']
|
|
355
|
+
ns = getattr(rf, 'namespace', '') or ''
|
|
356
|
+
if ns:
|
|
357
|
+
attrs.append(f'namespace="{_esc(ns)}"')
|
|
358
|
+
desc = getattr(rf, 'description', '') or ''
|
|
359
|
+
if desc:
|
|
360
|
+
attrs.append(f'description="{_esc(desc)}"')
|
|
361
|
+
lines.append(f" <role_function {' '.join(attrs)}/>")
|
|
362
|
+
lines.append(" </role_functions>")
|
|
363
|
+
return "\n".join(lines)
|
|
364
|
+
|
|
365
|
+
# ---------- <cells> ----------
|
|
366
|
+
def _format_cells(self, sm) -> str:
|
|
367
|
+
"""Render <cells>. Empty/missing section still emits <cells/>.
|
|
368
|
+
|
|
369
|
+
[v2.8.0-fix1]
|
|
370
|
+
Consistency: every other <context> sub-builder emits a
|
|
371
|
+
self-closing tag for empty sections. This method now matches
|
|
372
|
+
that convention so the AI can always tell whether a section
|
|
373
|
+
was intentionally empty vs. missing entirely.
|
|
374
|
+
"""
|
|
375
|
+
try:
|
|
376
|
+
cell_keys = sm.get_cell_keys()
|
|
377
|
+
except AttributeError:
|
|
378
|
+
return " <cells/>"
|
|
379
|
+
if not cell_keys:
|
|
380
|
+
return " <cells/>"
|
|
381
|
+
|
|
382
|
+
lines = [" <cells>"]
|
|
383
|
+
for (source, event) in cell_keys:
|
|
384
|
+
lines.append(
|
|
385
|
+
f' <cell source="{_esc(source)}" event="{_esc(event)}">'
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
actions = []
|
|
389
|
+
try:
|
|
390
|
+
actions = sm.get_actions_for_cell(source, event) or []
|
|
391
|
+
except Exception:
|
|
392
|
+
actions = []
|
|
393
|
+
relations = []
|
|
394
|
+
try:
|
|
395
|
+
relations = sm.get_relations_for_cell(source, event) or []
|
|
396
|
+
except Exception:
|
|
397
|
+
relations = []
|
|
398
|
+
|
|
399
|
+
if actions:
|
|
400
|
+
lines.append(" <actions>")
|
|
401
|
+
for a in actions:
|
|
402
|
+
a_attrs = []
|
|
403
|
+
role = getattr(a, 'role_function', '') or ''
|
|
404
|
+
trig = getattr(a, 'trigger', 'before_transitions') \
|
|
405
|
+
or 'before_transitions'
|
|
406
|
+
if role:
|
|
407
|
+
a_attrs.append(f'role_function="{_esc(role)}"')
|
|
408
|
+
if trig and trig != 'before_transitions':
|
|
409
|
+
a_attrs.append(f'trigger="{_esc(trig)}"')
|
|
410
|
+
if not a_attrs:
|
|
411
|
+
continue
|
|
412
|
+
lines.append(f' <action {" ".join(a_attrs)}/>')
|
|
413
|
+
lines.append(" </actions>")
|
|
414
|
+
|
|
415
|
+
if relations:
|
|
416
|
+
lines.append(" <relations>")
|
|
417
|
+
for r in relations:
|
|
418
|
+
lines.extend(self._format_relation(r, indent=8))
|
|
419
|
+
lines.append(" </relations>")
|
|
420
|
+
|
|
421
|
+
lines.append(" </cell>")
|
|
422
|
+
lines.append(" </cells>")
|
|
423
|
+
return "\n".join(lines)
|
|
424
|
+
|
|
425
|
+
def _format_relation(self, rel, indent: int) -> list:
|
|
426
|
+
pad = ' ' * indent
|
|
427
|
+
attrs = [f'kind="{_esc(getattr(rel, "kind", "sequential"))}"']
|
|
428
|
+
members = getattr(rel, 'members', None) or []
|
|
429
|
+
if members:
|
|
430
|
+
joined = ",".join(_esc(m) for m in members)
|
|
431
|
+
attrs.append(f'members="{joined}"')
|
|
432
|
+
shared = getattr(rel, 'shared_condition', '') or ''
|
|
433
|
+
if shared:
|
|
434
|
+
attrs.append(f'shared_condition="{_esc(shared)}"')
|
|
435
|
+
|
|
436
|
+
children = getattr(rel, 'children', None) or []
|
|
437
|
+
if not children:
|
|
438
|
+
return [f'{pad}<relation {" ".join(attrs)}/>']
|
|
439
|
+
|
|
440
|
+
result = [f'{pad}<relation {" ".join(attrs)}>']
|
|
441
|
+
for c in children:
|
|
442
|
+
result.extend(self._format_relation(c, indent + 2))
|
|
443
|
+
result.append(f'{pad}</relation>')
|
|
444
|
+
return result
|
|
445
|
+
|
|
446
|
+
def _timer_variable_names(self, gd) -> set:
|
|
447
|
+
"""Names of variables auto-generated by add_timer_variables().
|
|
448
|
+
|
|
449
|
+
[v2.8.0-fix3]
|
|
450
|
+
GlobalDefinitions.__init__ calls add_timer_variables(),
|
|
451
|
+
which registers the timer_base and derived timers into
|
|
452
|
+
gd.variables so the code generator can emit them. Those
|
|
453
|
+
entries are already shown in <timers>, so we exclude them
|
|
454
|
+
from <variables> to avoid duplication and to allow an
|
|
455
|
+
empty GlobalDefinitions() to collapse to <global_definitions/>.
|
|
456
|
+
"""
|
|
457
|
+
names = set()
|
|
458
|
+
tb = getattr(gd, 'timer_base', None)
|
|
459
|
+
if tb is not None:
|
|
460
|
+
if getattr(tb, 'variable_name', ''):
|
|
461
|
+
names.add(tb.variable_name)
|
|
462
|
+
for d in getattr(tb, 'derived', None) or []:
|
|
463
|
+
if getattr(d, 'variable_name', ''):
|
|
464
|
+
names.add(d.variable_name)
|
|
465
|
+
for et in getattr(gd, 'extra_timers', None) or []:
|
|
466
|
+
if getattr(et, 'variable_name', ''):
|
|
467
|
+
names.add(et.variable_name)
|
|
468
|
+
for d in getattr(et, 'derived', None) or []:
|
|
469
|
+
if getattr(d, 'variable_name', ''):
|
|
470
|
+
names.add(d.variable_name)
|
|
471
|
+
return names
|
|
472
|
+
|
|
473
|
+
# ---------- <global_definitions> ----------
|
|
474
|
+
def _format_global_definitions(self, gd) -> str:
|
|
475
|
+
if gd is None:
|
|
476
|
+
return " <global_definitions/>"
|
|
477
|
+
|
|
478
|
+
sections = []
|
|
479
|
+
|
|
480
|
+
# Variables
|
|
481
|
+
# [v2.8.0-fix3] Exclude auto-generated timer variables;
|
|
482
|
+
# they are already shown in <timers>.
|
|
483
|
+
timer_names = self._timer_variable_names(gd)
|
|
484
|
+
variables = [
|
|
485
|
+
v for v in (getattr(gd, 'variables', None) or [])
|
|
486
|
+
if v.name not in timer_names
|
|
487
|
+
]
|
|
488
|
+
if variables:
|
|
489
|
+
v_lines = [" <variables>"]
|
|
490
|
+
for v in variables:
|
|
491
|
+
v_attrs = [f'name="{_esc(v.name)}"',
|
|
492
|
+
f'type="{_esc(v.type)}"']
|
|
493
|
+
if getattr(v, 'group', ''):
|
|
494
|
+
v_attrs.append(f'group="{_esc(v.group)}"')
|
|
495
|
+
if getattr(v, 'description', ''):
|
|
496
|
+
v_attrs.append(
|
|
497
|
+
f'description="{_esc(v.description)}"')
|
|
498
|
+
v_lines.append(
|
|
499
|
+
f' <variable {" ".join(v_attrs)}/>')
|
|
500
|
+
v_lines.append(" </variables>")
|
|
501
|
+
sections.append("\n".join(v_lines))
|
|
502
|
+
|
|
503
|
+
# Flags
|
|
504
|
+
flags = getattr(gd, 'flags', None) or []
|
|
505
|
+
if flags:
|
|
506
|
+
f_lines = [" <flags>"]
|
|
507
|
+
for f in flags:
|
|
508
|
+
f_attrs = [
|
|
509
|
+
f'name="{_esc(f.name)}"',
|
|
510
|
+
f'min_value="{getattr(f, "min_value", 0)}"',
|
|
511
|
+
f'max_value="{getattr(f, "max_value", 0)}"',
|
|
512
|
+
]
|
|
513
|
+
if getattr(f, 'group', ''):
|
|
514
|
+
f_attrs.append(f'group="{_esc(f.group)}"')
|
|
515
|
+
if getattr(f, 'description', ''):
|
|
516
|
+
f_attrs.append(
|
|
517
|
+
f'description="{_esc(f.description)}"')
|
|
518
|
+
f_lines.append(f' <flag {" ".join(f_attrs)}/>')
|
|
519
|
+
f_lines.append(" </flags>")
|
|
520
|
+
sections.append("\n".join(f_lines))
|
|
521
|
+
|
|
522
|
+
# Interrupts
|
|
523
|
+
interrupts = getattr(gd, 'interrupts', None) or []
|
|
524
|
+
if interrupts:
|
|
525
|
+
i_lines = [" <interrupts>"]
|
|
526
|
+
for intr in interrupts:
|
|
527
|
+
i_attrs = [f'name="{_esc(intr.name)}"']
|
|
528
|
+
evs = getattr(intr, 'event_names', None) or []
|
|
529
|
+
if evs:
|
|
530
|
+
i_attrs.append(
|
|
531
|
+
f'event_names="{",".join(_esc(e) for e in evs)}"')
|
|
532
|
+
if getattr(intr, 'is_timer', False):
|
|
533
|
+
i_attrs.append('is_timer="true"')
|
|
534
|
+
if getattr(intr, 'description', ''):
|
|
535
|
+
i_attrs.append(
|
|
536
|
+
f'description="{_esc(intr.description)}"')
|
|
537
|
+
|
|
538
|
+
actions = getattr(intr, 'actions', None) or []
|
|
539
|
+
if not actions:
|
|
540
|
+
i_lines.append(
|
|
541
|
+
f' <interrupt {" ".join(i_attrs)}/>')
|
|
542
|
+
else:
|
|
543
|
+
i_lines.append(
|
|
544
|
+
f' <interrupt {" ".join(i_attrs)}>')
|
|
545
|
+
for act in actions:
|
|
546
|
+
a_attrs = []
|
|
547
|
+
if getattr(act, 'condition', ''):
|
|
548
|
+
a_attrs.append(
|
|
549
|
+
f'condition="{_esc(act.condition)}"')
|
|
550
|
+
if getattr(act, 'action', ''):
|
|
551
|
+
a_attrs.append(
|
|
552
|
+
f'action="{_esc(act.action)}"')
|
|
553
|
+
if a_attrs:
|
|
554
|
+
i_lines.append(
|
|
555
|
+
f' <action {" ".join(a_attrs)}/>')
|
|
556
|
+
i_lines.append(" </interrupt>")
|
|
557
|
+
i_lines.append(" </interrupts>")
|
|
558
|
+
sections.append("\n".join(i_lines))
|
|
559
|
+
|
|
560
|
+
# Timers (timer_base + extra_timers)
|
|
561
|
+
# [v2.8.0-fix2] A freshly constructed GlobalDefinitions() has
|
|
562
|
+
# a default timer_base with only pristine values. That carries
|
|
563
|
+
# no information for the AI, so skip it. Only emit the timers
|
|
564
|
+
# section when the timer_base has been customized or when
|
|
565
|
+
# extra timers exist.
|
|
566
|
+
tb = getattr(gd, 'timer_base', None)
|
|
567
|
+
if tb is not None and not self._has_timer_content(tb):
|
|
568
|
+
tb = None
|
|
569
|
+
extra = getattr(gd, 'extra_timers', None) or []
|
|
570
|
+
if tb is not None or extra:
|
|
571
|
+
t_lines = [" <timers>"]
|
|
572
|
+
if tb is not None:
|
|
573
|
+
t_lines.extend(self._format_timer(tb, 'timer_base', 6))
|
|
574
|
+
for et in extra:
|
|
575
|
+
t_lines.extend(self._format_timer(et, 'extra_timer', 6))
|
|
576
|
+
t_lines.append(" </timers>")
|
|
577
|
+
sections.append("\n".join(t_lines))
|
|
578
|
+
|
|
579
|
+
# Event queues
|
|
580
|
+
queues = getattr(gd, 'event_queues', None) or []
|
|
581
|
+
if queues:
|
|
582
|
+
q_lines = [" <event_queues>"]
|
|
583
|
+
for q in queues:
|
|
584
|
+
q_attrs = [
|
|
585
|
+
f'name="{_esc(q.name)}"',
|
|
586
|
+
f'size="{getattr(q, "size", 8)}"',
|
|
587
|
+
f'element_type="{_esc(getattr(q, "element_type", "uint8_t"))}"',
|
|
588
|
+
]
|
|
589
|
+
if getattr(q, 'priority_enabled', False):
|
|
590
|
+
q_attrs.append('priority_enabled="true"')
|
|
591
|
+
if not getattr(q, 'interrupt_safe', True):
|
|
592
|
+
q_attrs.append('interrupt_safe="false"')
|
|
593
|
+
if getattr(q, 'rtos_enabled', False):
|
|
594
|
+
q_attrs.append('rtos_enabled="true"')
|
|
595
|
+
if getattr(q, 'description', ''):
|
|
596
|
+
q_attrs.append(
|
|
597
|
+
f'description="{_esc(q.description)}"')
|
|
598
|
+
q_lines.append(f' <queue {" ".join(q_attrs)}/>')
|
|
599
|
+
q_lines.append(" </event_queues>")
|
|
600
|
+
sections.append("\n".join(q_lines))
|
|
601
|
+
|
|
602
|
+
if not sections:
|
|
603
|
+
return " <global_definitions/>"
|
|
604
|
+
|
|
605
|
+
return (" <global_definitions>\n"
|
|
606
|
+
+ "\n".join(sections)
|
|
607
|
+
+ "\n </global_definitions>")
|
|
608
|
+
|
|
609
|
+
def _has_timer_content(self, tb) -> bool:
|
|
610
|
+
"""Return True if the timer_base has user-meaningful content.
|
|
611
|
+
|
|
612
|
+
[v2.8.0-fix2]
|
|
613
|
+
A freshly constructed GlobalDefinitions() has a default
|
|
614
|
+
TimerBaseDef with only pristine values. Those defaults
|
|
615
|
+
carry no information for the AI, so a pristine timer_base
|
|
616
|
+
can be skipped in <global_definitions>. Any deviation
|
|
617
|
+
from the defaults (or any derived timers) makes this
|
|
618
|
+
method return True.
|
|
619
|
+
|
|
620
|
+
[v2.8.0-fix4]
|
|
621
|
+
`TimerBaseDef.title` is auto-populated to
|
|
622
|
+
"Timer base: <variable_name>" by __post_init__, so it is
|
|
623
|
+
NOT a reliable indicator of user customization. The title
|
|
624
|
+
field is excluded from the pristine comparison; the
|
|
625
|
+
semantic identity of a timer is determined by
|
|
626
|
+
variable_name / unit / data_type / derived.
|
|
627
|
+
"""
|
|
628
|
+
if getattr(tb, 'derived', None):
|
|
629
|
+
return True
|
|
630
|
+
defaults = {
|
|
631
|
+
'variable_name': 'g_system_tick',
|
|
632
|
+
'unit': '1ms',
|
|
633
|
+
'data_type': 'volatile uint32_t',
|
|
634
|
+
'interrupt_name': '',
|
|
635
|
+
}
|
|
636
|
+
for attr, expected in defaults.items():
|
|
637
|
+
if getattr(tb, attr, '') != expected:
|
|
638
|
+
return True
|
|
639
|
+
return False
|
|
640
|
+
|
|
641
|
+
def _format_timer(self, timer, tag: str, indent: int) -> list:
|
|
642
|
+
pad = ' ' * indent
|
|
643
|
+
attrs = [f'variable_name="{_esc(timer.variable_name)}"']
|
|
644
|
+
if getattr(timer, 'unit', ''):
|
|
645
|
+
attrs.append(f'unit="{_esc(timer.unit)}"')
|
|
646
|
+
if getattr(timer, 'data_type', ''):
|
|
647
|
+
attrs.append(f'data_type="{_esc(timer.data_type)}"')
|
|
648
|
+
|
|
649
|
+
derived = getattr(timer, 'derived', None) or []
|
|
650
|
+
if not derived:
|
|
651
|
+
return [f'{pad}<{tag} {" ".join(attrs)}/>']
|
|
652
|
+
|
|
653
|
+
result = [f'{pad}<{tag} {" ".join(attrs)}>']
|
|
654
|
+
for d in derived:
|
|
655
|
+
d_attrs = [
|
|
656
|
+
f'period_name="{_esc(d.period_name)}"',
|
|
657
|
+
f'multiplier="{getattr(d, "multiplier", 1)}"',
|
|
658
|
+
]
|
|
659
|
+
if getattr(d, 'variable_name', ''):
|
|
660
|
+
d_attrs.append(
|
|
661
|
+
f'variable_name="{_esc(d.variable_name)}"')
|
|
662
|
+
if getattr(d, 'data_type', ''):
|
|
663
|
+
d_attrs.append(f'data_type="{_esc(d.data_type)}"')
|
|
664
|
+
result.append(f'{pad} <derived {" ".join(d_attrs)}/>')
|
|
665
|
+
result.append(f'{pad}</{tag}>')
|
|
666
|
+
return result
|
|
667
|
+
|
|
668
|
+
# ------------------------------------------------------------------
|
|
669
|
+
# <validation> builder
|
|
670
|
+
# ------------------------------------------------------------------
|
|
671
|
+
def _format_validation(self, validation_result) -> str:
|
|
672
|
+
"""Emit each issue as a structured <issue> element.
|
|
673
|
+
|
|
674
|
+
Only category/code/target/suggestion are guaranteed to be
|
|
675
|
+
ASCII-safe metadata; `message` is passed through verbatim
|
|
676
|
+
(may be localized) so the AI can use it as context.
|
|
677
|
+
"""
|
|
678
|
+
issues = []
|
|
679
|
+
if validation_result is not None:
|
|
680
|
+
issues = getattr(validation_result, 'issues', None) or []
|
|
681
|
+
|
|
682
|
+
if not issues:
|
|
683
|
+
return " <!-- No issues -->"
|
|
684
|
+
|
|
685
|
+
lines = []
|
|
686
|
+
for issue in issues:
|
|
687
|
+
sev = issue.severity.value.upper()
|
|
688
|
+
attrs = [f'severity="{sev}"']
|
|
689
|
+
if issue.category:
|
|
690
|
+
attrs.append(f'category="{_esc(issue.category)}"')
|
|
691
|
+
if issue.code:
|
|
692
|
+
attrs.append(f'code="{_esc(issue.code)}"')
|
|
693
|
+
if issue.target:
|
|
694
|
+
attrs.append(f'target="{_esc(issue.target)}"')
|
|
695
|
+
if issue.suggestion:
|
|
696
|
+
attrs.append(
|
|
697
|
+
f'suggestion="{_esc(issue.suggestion)}"')
|
|
698
|
+
msg = _esc(issue.message or '')
|
|
699
|
+
lines.append(f' <issue {" ".join(attrs)}>{msg}</issue>')
|
|
700
|
+
return "\n".join(lines)
|
|
701
|
+
|
|
702
|
+
# ------------------------------------------------------------------
|
|
703
|
+
# <actions> builder
|
|
704
|
+
# ------------------------------------------------------------------
|
|
705
|
+
def _format_actions(self) -> str:
|
|
706
|
+
"""Render the 17 ACTION_DEFINITIONS as XML."""
|
|
707
|
+
lines = []
|
|
708
|
+
for name, definition in ACTION_DEFINITIONS.items():
|
|
709
|
+
desc = _esc(definition.get('description', ''))
|
|
710
|
+
lines.append(
|
|
711
|
+
f' <action name="{_esc(name)}" description="{desc}">'
|
|
712
|
+
)
|
|
713
|
+
for pname, pinfo in definition.get('params', {}).items():
|
|
714
|
+
p_attrs = [
|
|
715
|
+
f'name="{_esc(pname)}"',
|
|
716
|
+
f'type="{_esc(pinfo.get("type", "str"))}"',
|
|
717
|
+
f'required="{_bool_attr(bool(pinfo.get("required")))}"',
|
|
718
|
+
]
|
|
719
|
+
if 'values' in pinfo:
|
|
720
|
+
vals = ",".join(_esc(v) for v in pinfo['values'])
|
|
721
|
+
p_attrs.append(f'values="{vals}"')
|
|
722
|
+
lines.append(f' <param {" ".join(p_attrs)}/>')
|
|
723
|
+
lines.append(' </action>')
|
|
724
|
+
return "\n".join(lines)
|