code-constraints 0.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.
- code_constraints/__init__.py +1 -0
- code_constraints/cli/__init__.py +0 -0
- code_constraints/cli/__main__.py +1555 -0
- code_constraints/cli/_assets/agents/cdec-architect.md +468 -0
- code_constraints/cli/_assets/agents/oop-refactor-architect.md +317 -0
- code_constraints/cli/_assets/shims/csharp/CodeConstraintsRules.cs +94 -0
- code_constraints/cli/_assets/shims/julia/CdecRules.jl +129 -0
- code_constraints/cli/_assets/shims/lua/cdec_rules.lua +92 -0
- code_constraints/cli/_assets/shims/odin/cdec_rules.odin +67 -0
- code_constraints/cli/_assets/shims/python/cdec_rules.py +94 -0
- code_constraints/cli/_assets/skills/cdec-architecture-loop/SKILL.md +152 -0
- code_constraints/cli/depstamp.py +118 -0
- code_constraints/cli/detect.py +77 -0
- code_constraints/cli/interactive.py +304 -0
- code_constraints/cli/scaffold.py +602 -0
- code_constraints/cli/update.py +157 -0
- code_constraints/core/__init__.py +41 -0
- code_constraints/core/annotations.py +217 -0
- code_constraints/core/associations.py +134 -0
- code_constraints/core/diff.py +302 -0
- code_constraints/core/editor_io.py +280 -0
- code_constraints/core/graph_model.py +681 -0
- code_constraints/core/keys.py +105 -0
- code_constraints/core/model.py +294 -0
- code_constraints/core/model_io.py +65 -0
- code_constraints/core/receivers.py +34 -0
- code_constraints/core/rules.py +177 -0
- code_constraints/core/rulesdoc.py +208 -0
- code_constraints/core/tags.py +114 -0
- code_constraints/core/ts_fingerprint.py +88 -0
- code_constraints/core/xmi_reader.py +358 -0
- code_constraints/core/xmi_writer.py +373 -0
- code_constraints/csharp/__init__.py +3 -0
- code_constraints/csharp/activity.py +250 -0
- code_constraints/csharp/conformance.py +331 -0
- code_constraints/csharp/fingerprint.py +274 -0
- code_constraints/csharp/parser.py +436 -0
- code_constraints/csharp/rules_extract.py +78 -0
- code_constraints/csharp/sequence.py +295 -0
- code_constraints/enforce/__init__.py +15 -0
- code_constraints/enforce/engine.py +122 -0
- code_constraints/enforce/model.py +74 -0
- code_constraints/julia/__init__.py +5 -0
- code_constraints/julia/conformance.py +282 -0
- code_constraints/julia/fingerprint.py +226 -0
- code_constraints/julia/parser.py +523 -0
- code_constraints/julia/rules_extract.py +216 -0
- code_constraints/lint/__init__.py +10 -0
- code_constraints/lint/baseline.py +96 -0
- code_constraints/lint/config.py +239 -0
- code_constraints/lint/engine.py +179 -0
- code_constraints/lint/pipeline.py +108 -0
- code_constraints/lint/report.py +151 -0
- code_constraints/lint/rules/__init__.py +50 -0
- code_constraints/lint/rules/base.py +200 -0
- code_constraints/lint/rules/cyclic_package_dependencies.py +69 -0
- code_constraints/lint/rules/dangling_classes.py +98 -0
- code_constraints/lint/rules/forbidden_package_references.py +47 -0
- code_constraints/lint/rules/forbidden_references.py +48 -0
- code_constraints/lint/rules/frozen_members.py +67 -0
- code_constraints/lint/rules/frozen_rules.py +105 -0
- code_constraints/lint/rules/implementation_locks.py +156 -0
- code_constraints/lint/rules/layer_dependencies.py +92 -0
- code_constraints/lint/rules/max_class_fanout.py +41 -0
- code_constraints/lint/rules/no_new_classes.py +27 -0
- code_constraints/lint/rules/no_removed_classes.py +27 -0
- code_constraints/lint/rules/reference_architecture.py +111 -0
- code_constraints/lint/rules/subclass_naming.py +71 -0
- code_constraints/lint/rules/tag_conformance.py +76 -0
- code_constraints/lock/__init__.py +73 -0
- code_constraints/lock/engine.py +395 -0
- code_constraints/lock/model.py +235 -0
- code_constraints/lock/store.py +144 -0
- code_constraints/lua/__init__.py +5 -0
- code_constraints/lua/conformance.py +239 -0
- code_constraints/lua/fingerprint.py +252 -0
- code_constraints/lua/parser.py +500 -0
- code_constraints/lua/rules_extract.py +55 -0
- code_constraints/mcp/__init__.py +20 -0
- code_constraints/mcp/__main__.py +73 -0
- code_constraints/mcp/server.py +1203 -0
- code_constraints/odin/__init__.py +5 -0
- code_constraints/odin/conformance.py +244 -0
- code_constraints/odin/fingerprint.py +159 -0
- code_constraints/odin/parser.py +471 -0
- code_constraints/odin/rules_extract.py +38 -0
- code_constraints/python/__init__.py +3 -0
- code_constraints/python/activity.py +278 -0
- code_constraints/python/conformance.py +249 -0
- code_constraints/python/fingerprint.py +231 -0
- code_constraints/python/parser.py +330 -0
- code_constraints/python/rules_extract.py +83 -0
- code_constraints/python/sequence.py +257 -0
- code_constraints/reference/__init__.py +15 -0
- code_constraints/reference/compare.py +356 -0
- code_constraints/reference/report.py +38 -0
- code_constraints/svelte/__init__.py +3 -0
- code_constraints/svelte/parser.py +523 -0
- code_constraints/typescript/__init__.py +3 -0
- code_constraints/typescript/parser.py +590 -0
- code_constraints/waivers/__init__.py +89 -0
- code_constraints/waivers/collect.py +167 -0
- code_constraints/waivers/model.py +90 -0
- code_constraints/waivers/ops.py +150 -0
- code_constraints/waivers/review.py +156 -0
- code_constraints/waivers/store.py +300 -0
- code_constraints/web/__init__.py +0 -0
- code_constraints/web/_static/assets/index-3ivBsYY4.css +1 -0
- code_constraints/web/_static/assets/index-BTzTqGFp.js +9 -0
- code_constraints/web/_static/index.html +13 -0
- code_constraints/web/app.py +1076 -0
- code_constraints-0.1.0.dist-info/METADATA +663 -0
- code_constraints-0.1.0.dist-info/RECORD +116 -0
- code_constraints-0.1.0.dist-info/WHEEL +4 -0
- code_constraints-0.1.0.dist-info/entry_points.txt +3 -0
- code_constraints-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"""Diff two `Project` snapshots and produce a single annotated `Project`.
|
|
2
|
+
|
|
3
|
+
Matching is by stable identity:
|
|
4
|
+
- Packages and classes match on qualified name.
|
|
5
|
+
- Attributes / operations match on (name, type/signature) within the same class.
|
|
6
|
+
- Activities and sequences match on name.
|
|
7
|
+
|
|
8
|
+
Removed elements are *kept* in the output so renderers can show strikethrough.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from copy import deepcopy
|
|
14
|
+
|
|
15
|
+
from code_constraints.core.model import (
|
|
16
|
+
Activity,
|
|
17
|
+
ActivityEdge,
|
|
18
|
+
ActivityNode,
|
|
19
|
+
Attribute,
|
|
20
|
+
Class,
|
|
21
|
+
DiffStatus,
|
|
22
|
+
Operation,
|
|
23
|
+
Package,
|
|
24
|
+
Project,
|
|
25
|
+
Sequence,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def diff_projects(old: Project, new: Project) -> Project:
|
|
30
|
+
"""Return an annotated `Project` based on `new`, with removed elements
|
|
31
|
+
grafted in from `old` and `status` populated on every element.
|
|
32
|
+
"""
|
|
33
|
+
if old.source_language != new.source_language:
|
|
34
|
+
raise ValueError(
|
|
35
|
+
f"cannot diff across languages: {old.source_language} -> {new.source_language}"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
result = Project(source_language=new.source_language, root_path=new.root_path)
|
|
39
|
+
result.packages = _diff_package_lists(old.packages, new.packages)
|
|
40
|
+
result.activities = _diff_named_list(old.activities, new.activities, _diff_activity)
|
|
41
|
+
result.sequences = _diff_named_list(old.sequences, new.sequences, _diff_sequence)
|
|
42
|
+
return result
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ---------- packages ----------
|
|
46
|
+
|
|
47
|
+
def _diff_package_lists(
|
|
48
|
+
old: list[Package], new: list[Package]
|
|
49
|
+
) -> list[Package]:
|
|
50
|
+
old_by_qn = {p.qualified_name: p for p in old}
|
|
51
|
+
new_by_qn = {p.qualified_name: p for p in new}
|
|
52
|
+
out: list[Package] = []
|
|
53
|
+
|
|
54
|
+
for qn, new_pkg in new_by_qn.items():
|
|
55
|
+
if qn in old_by_qn:
|
|
56
|
+
out.append(_diff_package(old_by_qn[qn], new_pkg))
|
|
57
|
+
else:
|
|
58
|
+
out.append(_mark_subtree(new_pkg, DiffStatus.ADDED))
|
|
59
|
+
|
|
60
|
+
for qn, old_pkg in old_by_qn.items():
|
|
61
|
+
if qn not in new_by_qn:
|
|
62
|
+
out.append(_mark_subtree(old_pkg, DiffStatus.REMOVED))
|
|
63
|
+
|
|
64
|
+
return out
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _diff_package(old: Package, new: Package) -> Package:
|
|
68
|
+
merged = Package(name=new.name, qualified_name=new.qualified_name)
|
|
69
|
+
merged.classes = _diff_class_lists(old.classes, new.classes)
|
|
70
|
+
merged.sub_packages = _diff_package_lists(old.sub_packages, new.sub_packages)
|
|
71
|
+
merged.status = (
|
|
72
|
+
DiffStatus.CHANGED
|
|
73
|
+
if any(c.status != DiffStatus.UNCHANGED for c in merged.classes)
|
|
74
|
+
or any(p.status != DiffStatus.UNCHANGED for p in merged.sub_packages)
|
|
75
|
+
else DiffStatus.UNCHANGED
|
|
76
|
+
)
|
|
77
|
+
return merged
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _mark_subtree(pkg: Package, status: DiffStatus) -> Package:
|
|
81
|
+
pkg = deepcopy(pkg)
|
|
82
|
+
pkg.status = status
|
|
83
|
+
for cls in pkg.classes:
|
|
84
|
+
cls.status = status
|
|
85
|
+
for a in cls.attributes:
|
|
86
|
+
a.status = status
|
|
87
|
+
for o in cls.operations:
|
|
88
|
+
o.status = status
|
|
89
|
+
for sub in pkg.sub_packages:
|
|
90
|
+
_mark_subtree(sub, status)
|
|
91
|
+
return pkg
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# ---------- classes ----------
|
|
95
|
+
|
|
96
|
+
def _diff_class_lists(old: list[Class], new: list[Class]) -> list[Class]:
|
|
97
|
+
old_by_qn = {c.qualified_name: c for c in old}
|
|
98
|
+
new_by_qn = {c.qualified_name: c for c in new}
|
|
99
|
+
out: list[Class] = []
|
|
100
|
+
|
|
101
|
+
for qn, new_cls in new_by_qn.items():
|
|
102
|
+
if qn in old_by_qn:
|
|
103
|
+
out.append(_diff_class(old_by_qn[qn], new_cls))
|
|
104
|
+
else:
|
|
105
|
+
out.append(_mark_class(new_cls, DiffStatus.ADDED))
|
|
106
|
+
|
|
107
|
+
for qn, old_cls in old_by_qn.items():
|
|
108
|
+
if qn not in new_by_qn:
|
|
109
|
+
out.append(_mark_class(old_cls, DiffStatus.REMOVED))
|
|
110
|
+
|
|
111
|
+
return out
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _diff_class(old: Class, new: Class) -> Class:
|
|
115
|
+
merged = deepcopy(new)
|
|
116
|
+
merged.attributes = _diff_members(old.attributes, new.attributes)
|
|
117
|
+
merged.operations = _diff_members(old.operations, new.operations)
|
|
118
|
+
|
|
119
|
+
any_changed = any(a.status != DiffStatus.UNCHANGED for a in merged.attributes) or any(
|
|
120
|
+
o.status != DiffStatus.UNCHANGED for o in merged.operations
|
|
121
|
+
)
|
|
122
|
+
bases_changed = sorted(old.bases) != sorted(new.bases)
|
|
123
|
+
rules_changed = old.rules != new.rules
|
|
124
|
+
merged.status = (
|
|
125
|
+
DiffStatus.CHANGED
|
|
126
|
+
if any_changed or bases_changed or rules_changed
|
|
127
|
+
else DiffStatus.UNCHANGED
|
|
128
|
+
)
|
|
129
|
+
return merged
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _mark_class(cls: Class, status: DiffStatus) -> Class:
|
|
133
|
+
cls = deepcopy(cls)
|
|
134
|
+
cls.status = status
|
|
135
|
+
for a in cls.attributes:
|
|
136
|
+
a.status = status
|
|
137
|
+
for o in cls.operations:
|
|
138
|
+
o.status = status
|
|
139
|
+
return cls
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _diff_members(old_members, new_members):
|
|
143
|
+
"""Generic member differ for attributes and operations.
|
|
144
|
+
|
|
145
|
+
Match by signature so a renamed-but-same-signature member counts as the
|
|
146
|
+
same; a same-name-different-type attribute reports as changed (i.e. shows
|
|
147
|
+
up once as removed-old and once as added-new — we keep this explicit
|
|
148
|
+
rather than guessing rename semantics).
|
|
149
|
+
"""
|
|
150
|
+
old_by_sig = {m.signature(): m for m in old_members}
|
|
151
|
+
new_by_sig = {m.signature(): m for m in new_members}
|
|
152
|
+
out = []
|
|
153
|
+
|
|
154
|
+
for sig, new_m in new_by_sig.items():
|
|
155
|
+
if sig in old_by_sig:
|
|
156
|
+
m = deepcopy(new_m)
|
|
157
|
+
old_rules = getattr(old_by_sig[sig], "rules", [])
|
|
158
|
+
new_rules = getattr(new_m, "rules", [])
|
|
159
|
+
m.status = (
|
|
160
|
+
DiffStatus.CHANGED if old_rules != new_rules else DiffStatus.UNCHANGED
|
|
161
|
+
)
|
|
162
|
+
out.append(m)
|
|
163
|
+
else:
|
|
164
|
+
m = deepcopy(new_m)
|
|
165
|
+
m.status = DiffStatus.ADDED
|
|
166
|
+
out.append(m)
|
|
167
|
+
|
|
168
|
+
for sig, old_m in old_by_sig.items():
|
|
169
|
+
if sig not in new_by_sig:
|
|
170
|
+
m = deepcopy(old_m)
|
|
171
|
+
m.status = DiffStatus.REMOVED
|
|
172
|
+
out.append(m)
|
|
173
|
+
|
|
174
|
+
return out
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# ---------- activities / sequences ----------
|
|
178
|
+
|
|
179
|
+
def _diff_named_list(old_list, new_list, diff_one):
|
|
180
|
+
old_by_name = {x.name: x for x in old_list}
|
|
181
|
+
new_by_name = {x.name: x for x in new_list}
|
|
182
|
+
out = []
|
|
183
|
+
for name, new_item in new_by_name.items():
|
|
184
|
+
if name in old_by_name:
|
|
185
|
+
out.append(diff_one(old_by_name[name], new_item))
|
|
186
|
+
else:
|
|
187
|
+
item = deepcopy(new_item)
|
|
188
|
+
item.status = DiffStatus.ADDED
|
|
189
|
+
out.append(item)
|
|
190
|
+
for name, old_item in old_by_name.items():
|
|
191
|
+
if name not in new_by_name:
|
|
192
|
+
item = deepcopy(old_item)
|
|
193
|
+
item.status = DiffStatus.REMOVED
|
|
194
|
+
out.append(item)
|
|
195
|
+
return out
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _diff_activity(old: Activity, new: Activity) -> Activity:
|
|
199
|
+
"""Per-node + per-edge diff. Nodes match on (kind, label); edges match on
|
|
200
|
+
(source_kind, source_label, target_kind, target_label, guard) so renames of
|
|
201
|
+
auto-generated node ids don't ghost every edge. Removed nodes/edges are
|
|
202
|
+
appended to merged with status=REMOVED. The activity is CHANGED iff any
|
|
203
|
+
child element is non-UNCHANGED.
|
|
204
|
+
"""
|
|
205
|
+
merged = deepcopy(new)
|
|
206
|
+
|
|
207
|
+
old_nodes_by_key: dict[tuple[str, str], ActivityNode] = {}
|
|
208
|
+
for n in old.nodes:
|
|
209
|
+
old_nodes_by_key.setdefault((n.kind, n.label), n)
|
|
210
|
+
merged_nodes_by_key: dict[tuple[str, str], ActivityNode] = {}
|
|
211
|
+
for n in merged.nodes:
|
|
212
|
+
merged_nodes_by_key.setdefault((n.kind, n.label), n)
|
|
213
|
+
|
|
214
|
+
for n in merged.nodes:
|
|
215
|
+
key = (n.kind, n.label)
|
|
216
|
+
n.status = DiffStatus.UNCHANGED if key in old_nodes_by_key else DiffStatus.ADDED
|
|
217
|
+
|
|
218
|
+
for n in old.nodes:
|
|
219
|
+
key = (n.kind, n.label)
|
|
220
|
+
if key not in merged_nodes_by_key:
|
|
221
|
+
ghost = deepcopy(n)
|
|
222
|
+
ghost.status = DiffStatus.REMOVED
|
|
223
|
+
merged.nodes.append(ghost)
|
|
224
|
+
|
|
225
|
+
old_node_by_id = {n.id: n for n in old.nodes}
|
|
226
|
+
merged_node_by_id = {n.id: n for n in merged.nodes}
|
|
227
|
+
|
|
228
|
+
def edge_key(e: ActivityEdge, node_index: dict[str, ActivityNode]) -> tuple:
|
|
229
|
+
src = node_index.get(e.source)
|
|
230
|
+
tgt = node_index.get(e.target)
|
|
231
|
+
sk = (src.kind, src.label) if src else (None, e.source)
|
|
232
|
+
tk = (tgt.kind, tgt.label) if tgt else (None, e.target)
|
|
233
|
+
return (sk, tk, e.guard)
|
|
234
|
+
|
|
235
|
+
old_edge_keys = {edge_key(e, old_node_by_id) for e in old.edges}
|
|
236
|
+
new_edge_keys = {edge_key(e, merged_node_by_id) for e in merged.edges}
|
|
237
|
+
|
|
238
|
+
for e in merged.edges:
|
|
239
|
+
k = edge_key(e, merged_node_by_id)
|
|
240
|
+
e.status = DiffStatus.UNCHANGED if k in old_edge_keys else DiffStatus.ADDED
|
|
241
|
+
|
|
242
|
+
for e in old.edges:
|
|
243
|
+
if edge_key(e, old_node_by_id) not in new_edge_keys:
|
|
244
|
+
ghost = deepcopy(e)
|
|
245
|
+
ghost.status = DiffStatus.REMOVED
|
|
246
|
+
merged.edges.append(ghost)
|
|
247
|
+
|
|
248
|
+
any_changed = any(n.status != DiffStatus.UNCHANGED for n in merged.nodes) or any(
|
|
249
|
+
e.status != DiffStatus.UNCHANGED for e in merged.edges
|
|
250
|
+
)
|
|
251
|
+
merged.status = DiffStatus.CHANGED if any_changed else DiffStatus.UNCHANGED
|
|
252
|
+
return merged
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _diff_sequence(old: Sequence, new: Sequence) -> Sequence:
|
|
256
|
+
merged = deepcopy(new)
|
|
257
|
+
|
|
258
|
+
def _msg_key(m):
|
|
259
|
+
# Include guard + is_return so changing only the surrounding
|
|
260
|
+
# condition or swapping a call for a return is correctly diffed.
|
|
261
|
+
return (m.sender, m.receiver, m.label, m.guard, m.is_return)
|
|
262
|
+
|
|
263
|
+
def _frag_key(f):
|
|
264
|
+
return (f.kind, f.label, f.start_row, f.end_row)
|
|
265
|
+
|
|
266
|
+
new_msgs = {_msg_key(m) for m in new.messages}
|
|
267
|
+
old_msgs = {_msg_key(m) for m in old.messages}
|
|
268
|
+
new_lls = {ll.name for ll in new.lifelines}
|
|
269
|
+
old_lls = {ll.name for ll in old.lifelines}
|
|
270
|
+
new_frags = {_frag_key(f) for f in new.fragments}
|
|
271
|
+
old_frags = {_frag_key(f) for f in old.fragments}
|
|
272
|
+
|
|
273
|
+
for m in merged.messages:
|
|
274
|
+
m.status = DiffStatus.UNCHANGED if _msg_key(m) in old_msgs else DiffStatus.ADDED
|
|
275
|
+
for key in old_msgs - new_msgs:
|
|
276
|
+
old_m = next(m for m in old.messages if _msg_key(m) == key)
|
|
277
|
+
ghost = deepcopy(old_m)
|
|
278
|
+
ghost.status = DiffStatus.REMOVED
|
|
279
|
+
merged.messages.append(ghost)
|
|
280
|
+
|
|
281
|
+
for ll in merged.lifelines:
|
|
282
|
+
ll.status = DiffStatus.UNCHANGED if ll.name in old_lls else DiffStatus.ADDED
|
|
283
|
+
for ll_name in old_lls - new_lls:
|
|
284
|
+
old_ll = next(l for l in old.lifelines if l.name == ll_name)
|
|
285
|
+
ghost_ll = deepcopy(old_ll)
|
|
286
|
+
ghost_ll.status = DiffStatus.REMOVED
|
|
287
|
+
merged.lifelines.append(ghost_ll)
|
|
288
|
+
|
|
289
|
+
for f in merged.fragments:
|
|
290
|
+
f.status = DiffStatus.UNCHANGED if _frag_key(f) in old_frags else DiffStatus.ADDED
|
|
291
|
+
for key in old_frags - new_frags:
|
|
292
|
+
old_f = next(f for f in old.fragments if _frag_key(f) == key)
|
|
293
|
+
ghost_f = deepcopy(old_f)
|
|
294
|
+
ghost_f.status = DiffStatus.REMOVED
|
|
295
|
+
merged.fragments.append(ghost_f)
|
|
296
|
+
|
|
297
|
+
merged.status = (
|
|
298
|
+
DiffStatus.CHANGED
|
|
299
|
+
if (new_msgs != old_msgs or new_lls != old_lls or new_frags != old_frags)
|
|
300
|
+
else DiffStatus.UNCHANGED
|
|
301
|
+
)
|
|
302
|
+
return merged
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"""JSON <-> Project conversion for the editor bridge endpoints.
|
|
2
|
+
|
|
3
|
+
The editor in the browser holds a draft `Project` shaped like the Python
|
|
4
|
+
dataclasses; these helpers move it across the wire without growing a separate
|
|
5
|
+
schema. Field names are snake_case on both sides so the converters stay dumb.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import asdict
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from code_constraints.core.model import (
|
|
14
|
+
Activity,
|
|
15
|
+
ActivityEdge,
|
|
16
|
+
ActivityNode,
|
|
17
|
+
Association,
|
|
18
|
+
Attribute,
|
|
19
|
+
Class,
|
|
20
|
+
DiffStatus,
|
|
21
|
+
Fragment,
|
|
22
|
+
Layout,
|
|
23
|
+
Lifeline,
|
|
24
|
+
Message,
|
|
25
|
+
Operation,
|
|
26
|
+
Package,
|
|
27
|
+
Parameter,
|
|
28
|
+
Project,
|
|
29
|
+
RuleAnnotation,
|
|
30
|
+
Sequence,
|
|
31
|
+
SourceLocation,
|
|
32
|
+
Visibility,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def project_to_json(project: Project) -> dict[str, Any]:
|
|
37
|
+
"""Serialise a Project to a JSON-safe dict (enums become their .value)."""
|
|
38
|
+
return _coerce(asdict(project))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _coerce(obj: Any) -> Any:
|
|
42
|
+
if isinstance(obj, dict):
|
|
43
|
+
return {k: _coerce(v) for k, v in obj.items()}
|
|
44
|
+
if isinstance(obj, list):
|
|
45
|
+
return [_coerce(v) for v in obj]
|
|
46
|
+
if isinstance(obj, tuple):
|
|
47
|
+
return [_coerce(v) for v in obj]
|
|
48
|
+
if isinstance(obj, DiffStatus) or isinstance(obj, Visibility):
|
|
49
|
+
return obj.value
|
|
50
|
+
return obj
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def project_from_json(data: dict[str, Any]) -> Project:
|
|
54
|
+
"""Reconstruct a Project from the JSON dict shape produced by `project_to_json`.
|
|
55
|
+
|
|
56
|
+
Missing optional fields fall back to dataclass defaults. Unknown languages
|
|
57
|
+
default to 'python' to keep the editor permissive.
|
|
58
|
+
"""
|
|
59
|
+
lang = data.get("source_language", "python")
|
|
60
|
+
if lang not in (
|
|
61
|
+
"python", "csharp", "typescript", "svelte", "odin", "lua", "julia",
|
|
62
|
+
):
|
|
63
|
+
lang = "python"
|
|
64
|
+
project = Project(
|
|
65
|
+
source_language=lang, # type: ignore[arg-type]
|
|
66
|
+
root_path=data.get("root_path", "") or "",
|
|
67
|
+
)
|
|
68
|
+
project.packages = [_read_package(p) for p in data.get("packages", []) or []]
|
|
69
|
+
project.activities = [_read_activity(a) for a in data.get("activities", []) or []]
|
|
70
|
+
project.sequences = [_read_sequence(s) for s in data.get("sequences", []) or []]
|
|
71
|
+
project.associations = [_read_association(a) for a in data.get("associations", []) or []]
|
|
72
|
+
return project
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _status(raw: Any) -> DiffStatus:
|
|
76
|
+
try:
|
|
77
|
+
return DiffStatus(raw) if raw else DiffStatus.UNCHANGED
|
|
78
|
+
except ValueError:
|
|
79
|
+
return DiffStatus.UNCHANGED
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _visibility(raw: Any) -> Visibility:
|
|
83
|
+
try:
|
|
84
|
+
return Visibility(raw) if raw else Visibility.PUBLIC
|
|
85
|
+
except ValueError:
|
|
86
|
+
return Visibility.PUBLIC
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _layout(raw: Any) -> Layout | None:
|
|
90
|
+
if not raw:
|
|
91
|
+
return None
|
|
92
|
+
return Layout(
|
|
93
|
+
x=float(raw.get("x", 0)),
|
|
94
|
+
y=float(raw.get("y", 0)),
|
|
95
|
+
width=float(raw.get("width", 0)),
|
|
96
|
+
height=float(raw.get("height", 0)),
|
|
97
|
+
collapsed=bool(raw.get("collapsed", False)),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _location(raw: Any) -> SourceLocation | None:
|
|
102
|
+
if not raw:
|
|
103
|
+
return None
|
|
104
|
+
return SourceLocation(
|
|
105
|
+
file=raw.get("file", ""),
|
|
106
|
+
start_line=int(raw.get("start_line", 0)),
|
|
107
|
+
end_line=int(raw.get("end_line", 0)),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _read_rules(raw: Any) -> list[RuleAnnotation]:
|
|
112
|
+
out: list[RuleAnnotation] = []
|
|
113
|
+
for r in raw or []:
|
|
114
|
+
if not isinstance(r, dict) or not r.get("name"):
|
|
115
|
+
continue
|
|
116
|
+
out.append(
|
|
117
|
+
RuleAnnotation(
|
|
118
|
+
name=str(r["name"]),
|
|
119
|
+
args=[str(a) for a in r.get("args", []) or []],
|
|
120
|
+
kwargs={str(k): str(v) for k, v in (r.get("kwargs", {}) or {}).items()},
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
return out
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _read_package(raw: dict[str, Any]) -> Package:
|
|
127
|
+
return Package(
|
|
128
|
+
name=raw.get("name", ""),
|
|
129
|
+
qualified_name=raw.get("qualified_name", raw.get("name", "")),
|
|
130
|
+
classes=[_read_class(c) for c in raw.get("classes", []) or []],
|
|
131
|
+
sub_packages=[_read_package(p) for p in raw.get("sub_packages", []) or []],
|
|
132
|
+
layout=_layout(raw.get("layout")),
|
|
133
|
+
description=raw.get("description") or None,
|
|
134
|
+
status=_status(raw.get("status")),
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _read_class(raw: dict[str, Any]) -> Class:
|
|
139
|
+
kind = raw.get("kind", "class")
|
|
140
|
+
if kind not in ("class", "interface", "abstract", "enum", "struct", "record", "static"):
|
|
141
|
+
kind = "class"
|
|
142
|
+
return Class(
|
|
143
|
+
name=raw.get("name", ""),
|
|
144
|
+
qualified_name=raw.get("qualified_name", raw.get("name", "")),
|
|
145
|
+
kind=kind, # type: ignore[arg-type]
|
|
146
|
+
attributes=[_read_attribute(a) for a in raw.get("attributes", []) or []],
|
|
147
|
+
operations=[_read_operation(o) for o in raw.get("operations", []) or []],
|
|
148
|
+
bases=list(raw.get("bases", []) or []),
|
|
149
|
+
dependencies=list(raw.get("dependencies", []) or []),
|
|
150
|
+
location=_location(raw.get("location")),
|
|
151
|
+
layout=_layout(raw.get("layout")),
|
|
152
|
+
description=raw.get("description") or None,
|
|
153
|
+
rules=_read_rules(raw.get("rules")),
|
|
154
|
+
status=_status(raw.get("status")),
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _read_attribute(raw: dict[str, Any]) -> Attribute:
|
|
159
|
+
return Attribute(
|
|
160
|
+
name=raw.get("name", ""),
|
|
161
|
+
type=raw.get("type", ""),
|
|
162
|
+
visibility=_visibility(raw.get("visibility")),
|
|
163
|
+
is_static=bool(raw.get("is_static", False)),
|
|
164
|
+
is_readonly=bool(raw.get("is_readonly", False)),
|
|
165
|
+
default=raw.get("default"),
|
|
166
|
+
description=raw.get("description") or None,
|
|
167
|
+
status=_status(raw.get("status")),
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _read_operation(raw: dict[str, Any]) -> Operation:
|
|
172
|
+
return Operation(
|
|
173
|
+
name=raw.get("name", ""),
|
|
174
|
+
parameters=[_read_parameter(p) for p in raw.get("parameters", []) or []],
|
|
175
|
+
return_type=raw.get("return_type", ""),
|
|
176
|
+
visibility=_visibility(raw.get("visibility")),
|
|
177
|
+
is_static=bool(raw.get("is_static", False)),
|
|
178
|
+
is_abstract=bool(raw.get("is_abstract", False)),
|
|
179
|
+
description=raw.get("description") or None,
|
|
180
|
+
rules=_read_rules(raw.get("rules")),
|
|
181
|
+
status=_status(raw.get("status")),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _read_parameter(raw: dict[str, Any]) -> Parameter:
|
|
186
|
+
return Parameter(
|
|
187
|
+
name=raw.get("name", ""),
|
|
188
|
+
type=raw.get("type", ""),
|
|
189
|
+
default=raw.get("default"),
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _read_association(raw: dict[str, Any]) -> Association:
|
|
194
|
+
return Association(
|
|
195
|
+
source=raw.get("source", ""),
|
|
196
|
+
target=raw.get("target", ""),
|
|
197
|
+
name=raw.get("name") or None,
|
|
198
|
+
source_multiplicity=raw.get("source_multiplicity") or None,
|
|
199
|
+
target_multiplicity=raw.get("target_multiplicity") or None,
|
|
200
|
+
source_role=raw.get("source_role") or None,
|
|
201
|
+
target_role=raw.get("target_role") or None,
|
|
202
|
+
status=_status(raw.get("status")),
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _read_activity(raw: dict[str, Any]) -> Activity:
|
|
207
|
+
gran = raw.get("granularity", "control-flow")
|
|
208
|
+
if gran not in ("control-flow", "statement", "calls"):
|
|
209
|
+
gran = "control-flow"
|
|
210
|
+
return Activity(
|
|
211
|
+
name=raw.get("name", ""),
|
|
212
|
+
nodes=[_read_activity_node(n) for n in raw.get("nodes", []) or []],
|
|
213
|
+
edges=[_read_activity_edge(e) for e in raw.get("edges", []) or []],
|
|
214
|
+
location=_location(raw.get("location")),
|
|
215
|
+
granularity=gran, # type: ignore[arg-type]
|
|
216
|
+
status=_status(raw.get("status")),
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _read_activity_node(raw: dict[str, Any]) -> ActivityNode:
|
|
221
|
+
kind = raw.get("kind", "action")
|
|
222
|
+
if kind not in ("initial", "final", "action", "decision", "merge", "fork", "join"):
|
|
223
|
+
kind = "action"
|
|
224
|
+
return ActivityNode(
|
|
225
|
+
id=raw.get("id", ""),
|
|
226
|
+
kind=kind, # type: ignore[arg-type]
|
|
227
|
+
label=raw.get("label", ""),
|
|
228
|
+
layout=_layout(raw.get("layout")),
|
|
229
|
+
status=_status(raw.get("status")),
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _read_activity_edge(raw: dict[str, Any]) -> ActivityEdge:
|
|
234
|
+
return ActivityEdge(
|
|
235
|
+
source=raw.get("source", ""),
|
|
236
|
+
target=raw.get("target", ""),
|
|
237
|
+
guard=raw.get("guard", ""),
|
|
238
|
+
status=_status(raw.get("status")),
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _read_sequence(raw: dict[str, Any]) -> Sequence:
|
|
243
|
+
return Sequence(
|
|
244
|
+
name=raw.get("name", ""),
|
|
245
|
+
lifelines=[
|
|
246
|
+
Lifeline(
|
|
247
|
+
name=l.get("name", ""),
|
|
248
|
+
represents=l.get("represents", ""),
|
|
249
|
+
column_x=l.get("column_x"),
|
|
250
|
+
status=_status(l.get("status")),
|
|
251
|
+
)
|
|
252
|
+
for l in raw.get("lifelines", []) or []
|
|
253
|
+
],
|
|
254
|
+
messages=[
|
|
255
|
+
Message(
|
|
256
|
+
sender=m.get("sender", ""),
|
|
257
|
+
receiver=m.get("receiver", ""),
|
|
258
|
+
label=m.get("label", ""),
|
|
259
|
+
is_return=bool(m.get("is_return", False)),
|
|
260
|
+
guard=m.get("guard", ""),
|
|
261
|
+
status=_status(m.get("status")),
|
|
262
|
+
)
|
|
263
|
+
for m in raw.get("messages", []) or []
|
|
264
|
+
],
|
|
265
|
+
fragments=[
|
|
266
|
+
Fragment(
|
|
267
|
+
kind=f.get("kind", "alt"),
|
|
268
|
+
label=f.get("label", ""),
|
|
269
|
+
start_row=int(f.get("start_row", 0)),
|
|
270
|
+
end_row=int(f.get("end_row", 0)),
|
|
271
|
+
status=_status(f.get("status")),
|
|
272
|
+
)
|
|
273
|
+
for f in raw.get("fragments", []) or []
|
|
274
|
+
],
|
|
275
|
+
location=_location(raw.get("location")),
|
|
276
|
+
status=_status(raw.get("status")),
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
__all__ = ["project_to_json", "project_from_json"]
|