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.
Files changed (116) hide show
  1. code_constraints/__init__.py +1 -0
  2. code_constraints/cli/__init__.py +0 -0
  3. code_constraints/cli/__main__.py +1555 -0
  4. code_constraints/cli/_assets/agents/cdec-architect.md +468 -0
  5. code_constraints/cli/_assets/agents/oop-refactor-architect.md +317 -0
  6. code_constraints/cli/_assets/shims/csharp/CodeConstraintsRules.cs +94 -0
  7. code_constraints/cli/_assets/shims/julia/CdecRules.jl +129 -0
  8. code_constraints/cli/_assets/shims/lua/cdec_rules.lua +92 -0
  9. code_constraints/cli/_assets/shims/odin/cdec_rules.odin +67 -0
  10. code_constraints/cli/_assets/shims/python/cdec_rules.py +94 -0
  11. code_constraints/cli/_assets/skills/cdec-architecture-loop/SKILL.md +152 -0
  12. code_constraints/cli/depstamp.py +118 -0
  13. code_constraints/cli/detect.py +77 -0
  14. code_constraints/cli/interactive.py +304 -0
  15. code_constraints/cli/scaffold.py +602 -0
  16. code_constraints/cli/update.py +157 -0
  17. code_constraints/core/__init__.py +41 -0
  18. code_constraints/core/annotations.py +217 -0
  19. code_constraints/core/associations.py +134 -0
  20. code_constraints/core/diff.py +302 -0
  21. code_constraints/core/editor_io.py +280 -0
  22. code_constraints/core/graph_model.py +681 -0
  23. code_constraints/core/keys.py +105 -0
  24. code_constraints/core/model.py +294 -0
  25. code_constraints/core/model_io.py +65 -0
  26. code_constraints/core/receivers.py +34 -0
  27. code_constraints/core/rules.py +177 -0
  28. code_constraints/core/rulesdoc.py +208 -0
  29. code_constraints/core/tags.py +114 -0
  30. code_constraints/core/ts_fingerprint.py +88 -0
  31. code_constraints/core/xmi_reader.py +358 -0
  32. code_constraints/core/xmi_writer.py +373 -0
  33. code_constraints/csharp/__init__.py +3 -0
  34. code_constraints/csharp/activity.py +250 -0
  35. code_constraints/csharp/conformance.py +331 -0
  36. code_constraints/csharp/fingerprint.py +274 -0
  37. code_constraints/csharp/parser.py +436 -0
  38. code_constraints/csharp/rules_extract.py +78 -0
  39. code_constraints/csharp/sequence.py +295 -0
  40. code_constraints/enforce/__init__.py +15 -0
  41. code_constraints/enforce/engine.py +122 -0
  42. code_constraints/enforce/model.py +74 -0
  43. code_constraints/julia/__init__.py +5 -0
  44. code_constraints/julia/conformance.py +282 -0
  45. code_constraints/julia/fingerprint.py +226 -0
  46. code_constraints/julia/parser.py +523 -0
  47. code_constraints/julia/rules_extract.py +216 -0
  48. code_constraints/lint/__init__.py +10 -0
  49. code_constraints/lint/baseline.py +96 -0
  50. code_constraints/lint/config.py +239 -0
  51. code_constraints/lint/engine.py +179 -0
  52. code_constraints/lint/pipeline.py +108 -0
  53. code_constraints/lint/report.py +151 -0
  54. code_constraints/lint/rules/__init__.py +50 -0
  55. code_constraints/lint/rules/base.py +200 -0
  56. code_constraints/lint/rules/cyclic_package_dependencies.py +69 -0
  57. code_constraints/lint/rules/dangling_classes.py +98 -0
  58. code_constraints/lint/rules/forbidden_package_references.py +47 -0
  59. code_constraints/lint/rules/forbidden_references.py +48 -0
  60. code_constraints/lint/rules/frozen_members.py +67 -0
  61. code_constraints/lint/rules/frozen_rules.py +105 -0
  62. code_constraints/lint/rules/implementation_locks.py +156 -0
  63. code_constraints/lint/rules/layer_dependencies.py +92 -0
  64. code_constraints/lint/rules/max_class_fanout.py +41 -0
  65. code_constraints/lint/rules/no_new_classes.py +27 -0
  66. code_constraints/lint/rules/no_removed_classes.py +27 -0
  67. code_constraints/lint/rules/reference_architecture.py +111 -0
  68. code_constraints/lint/rules/subclass_naming.py +71 -0
  69. code_constraints/lint/rules/tag_conformance.py +76 -0
  70. code_constraints/lock/__init__.py +73 -0
  71. code_constraints/lock/engine.py +395 -0
  72. code_constraints/lock/model.py +235 -0
  73. code_constraints/lock/store.py +144 -0
  74. code_constraints/lua/__init__.py +5 -0
  75. code_constraints/lua/conformance.py +239 -0
  76. code_constraints/lua/fingerprint.py +252 -0
  77. code_constraints/lua/parser.py +500 -0
  78. code_constraints/lua/rules_extract.py +55 -0
  79. code_constraints/mcp/__init__.py +20 -0
  80. code_constraints/mcp/__main__.py +73 -0
  81. code_constraints/mcp/server.py +1203 -0
  82. code_constraints/odin/__init__.py +5 -0
  83. code_constraints/odin/conformance.py +244 -0
  84. code_constraints/odin/fingerprint.py +159 -0
  85. code_constraints/odin/parser.py +471 -0
  86. code_constraints/odin/rules_extract.py +38 -0
  87. code_constraints/python/__init__.py +3 -0
  88. code_constraints/python/activity.py +278 -0
  89. code_constraints/python/conformance.py +249 -0
  90. code_constraints/python/fingerprint.py +231 -0
  91. code_constraints/python/parser.py +330 -0
  92. code_constraints/python/rules_extract.py +83 -0
  93. code_constraints/python/sequence.py +257 -0
  94. code_constraints/reference/__init__.py +15 -0
  95. code_constraints/reference/compare.py +356 -0
  96. code_constraints/reference/report.py +38 -0
  97. code_constraints/svelte/__init__.py +3 -0
  98. code_constraints/svelte/parser.py +523 -0
  99. code_constraints/typescript/__init__.py +3 -0
  100. code_constraints/typescript/parser.py +590 -0
  101. code_constraints/waivers/__init__.py +89 -0
  102. code_constraints/waivers/collect.py +167 -0
  103. code_constraints/waivers/model.py +90 -0
  104. code_constraints/waivers/ops.py +150 -0
  105. code_constraints/waivers/review.py +156 -0
  106. code_constraints/waivers/store.py +300 -0
  107. code_constraints/web/__init__.py +0 -0
  108. code_constraints/web/_static/assets/index-3ivBsYY4.css +1 -0
  109. code_constraints/web/_static/assets/index-BTzTqGFp.js +9 -0
  110. code_constraints/web/_static/index.html +13 -0
  111. code_constraints/web/app.py +1076 -0
  112. code_constraints-0.1.0.dist-info/METADATA +663 -0
  113. code_constraints-0.1.0.dist-info/RECORD +116 -0
  114. code_constraints-0.1.0.dist-info/WHEEL +4 -0
  115. code_constraints-0.1.0.dist-info/entry_points.txt +3 -0
  116. 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"]