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,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
|