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,295 @@
1
+ """Build a UML `Sequence` from a tagged C# region.
2
+
3
+ Mirrors the semantics of the Python parser — see that module for the
4
+ overall design (post-order calls, paired returns, fragments per if-branch,
5
+ self-return for `return X` at the end). This module just adapts the same
6
+ rules to tree-sitter's C# grammar.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from tree_sitter import Node, Tree
12
+
13
+ from code_constraints.core.model import Fragment, Lifeline, Message, Sequence, SourceLocation
14
+ from code_constraints.core.tags import TagInstance
15
+
16
+
17
+ def build_sequence_from_tag(
18
+ tag: TagInstance, tree: Tree, source: bytes, *, file: str
19
+ ) -> Sequence | None:
20
+ if not tag.name:
21
+ return None
22
+ root = tag.attributes.get("root", "this")
23
+ stmts = _statements_in_range(tree.root_node, tag.start_line, tag.end_line)
24
+ if not stmts:
25
+ return None
26
+
27
+ state = _State(root=root, source=source)
28
+ _emit_messages(stmts, [], state)
29
+
30
+ return Sequence(
31
+ name=tag.name,
32
+ lifelines=list(state.lifelines.values()),
33
+ messages=state.messages,
34
+ fragments=state.fragments,
35
+ location=SourceLocation(file=file, start_line=tag.start_line, end_line=tag.end_line),
36
+ )
37
+
38
+
39
+ class _State:
40
+ def __init__(self, root: str, source: bytes) -> None:
41
+ self.root = root
42
+ self.source = source
43
+ self.lifelines: dict[str, Lifeline] = {root: Lifeline(name=root, represents=root)}
44
+ self.messages: list[Message] = []
45
+ self.fragments: list[Fragment] = []
46
+
47
+ def ensure_lifeline(self, name: str) -> None:
48
+ if name not in self.lifelines:
49
+ self.lifelines[name] = Lifeline(name=name, represents=name)
50
+
51
+
52
+ _BODY_HOLDERS = {
53
+ "method_declaration",
54
+ "constructor_declaration",
55
+ "local_function_statement",
56
+ "destructor_declaration",
57
+ }
58
+
59
+
60
+ def _statements_in_range(root: Node, start: int, end: int) -> list[Node]:
61
+ best: list[Node] | None = None
62
+ best_start = -1
63
+ stack: list[Node] = [root]
64
+ while stack:
65
+ node = stack.pop()
66
+ if node.type in _BODY_HOLDERS:
67
+ body = node.child_by_field_name("body")
68
+ if body is not None and body.type == "block":
69
+ node_def_line = node.start_point[0] + 1
70
+ body_end_line = body.end_point[0] + 1
71
+ if node_def_line < start and body_end_line >= start:
72
+ stmts = [
73
+ c
74
+ for c in body.named_children
75
+ if c.type != "comment"
76
+ and start < (c.start_point[0] + 1) < end
77
+ ]
78
+ if stmts and node_def_line > best_start:
79
+ best = stmts
80
+ best_start = node_def_line
81
+ for c in node.children:
82
+ stack.append(c)
83
+ return best or []
84
+
85
+
86
+ def _emit_messages(
87
+ stmts: list[Node],
88
+ active_guards: list[str],
89
+ state: _State,
90
+ ) -> None:
91
+ guard_str = " and ".join(active_guards)
92
+ for stmt in stmts:
93
+ if stmt.type == "if_statement":
94
+ _emit_if(stmt, active_guards, state)
95
+ elif stmt.type == "return_statement":
96
+ value_node = _return_value(stmt)
97
+ _emit_calls_in(value_node, guard_str, state, return_label="")
98
+ value_text = _text(value_node, state.source).strip() if value_node else ""
99
+ label = f"return {value_text}" if value_text else "return"
100
+ state.messages.append(
101
+ Message(
102
+ sender=state.root,
103
+ receiver=state.root,
104
+ label=label,
105
+ is_return=True,
106
+ guard=guard_str,
107
+ )
108
+ )
109
+ elif stmt.type == "local_declaration_statement":
110
+ # Walk variable_declarator(s); the initializer is the last named
111
+ # child of the declarator. We pair the OUTERMOST call's return
112
+ # with the variable name.
113
+ for declarator, init_node in _iter_declarators(stmt):
114
+ name = ""
115
+ name_node = declarator.child_by_field_name("name")
116
+ if name_node is not None:
117
+ name = _text(name_node, state.source).strip()
118
+ _emit_calls_in(init_node, guard_str, state, return_label=name)
119
+ elif stmt.type == "expression_statement":
120
+ inner = stmt.named_child(0) if stmt.named_child_count else None
121
+ # If it's an assignment_expression and the RHS is a single
122
+ # invocation, treat it like a local declaration so the return
123
+ # gets labeled with the LHS target name.
124
+ if inner is not None and inner.type == "assignment_expression":
125
+ left = inner.child_by_field_name("left")
126
+ right = inner.child_by_field_name("right")
127
+ target = _text(left, state.source).strip() if left else ""
128
+ _emit_calls_in(right, guard_str, state, return_label=target)
129
+ else:
130
+ _emit_calls_in(stmt, guard_str, state, return_label="")
131
+ else:
132
+ _emit_calls_in(stmt, guard_str, state, return_label="")
133
+
134
+
135
+ def _iter_declarators(local_decl: Node):
136
+ """Yield (declarator, initializer_expr) pairs in `local_declaration_statement`."""
137
+ var_decl = None
138
+ for c in local_decl.named_children:
139
+ if c.type == "variable_declaration":
140
+ var_decl = c
141
+ break
142
+ if var_decl is None:
143
+ return
144
+ for declarator in var_decl.named_children:
145
+ if declarator.type != "variable_declarator":
146
+ continue
147
+ # The initializer expression is the named child that isn't `name`.
148
+ name_node = declarator.child_by_field_name("name")
149
+ init = None
150
+ for c in declarator.named_children:
151
+ if c is name_node:
152
+ continue
153
+ init = c
154
+ yield declarator, init
155
+
156
+
157
+ def _return_value(return_stmt: Node) -> Node | None:
158
+ """Get the optional expression child of a `return_statement`."""
159
+ for c in return_stmt.named_children:
160
+ if c.type != "comment":
161
+ return c
162
+ return None
163
+
164
+
165
+ def _emit_if(stmt: Node, active_guards: list[str], state: _State) -> None:
166
+ cond_node = stmt.child_by_field_name("condition")
167
+ cond_text = _text(cond_node, state.source).strip() if cond_node else ""
168
+
169
+ # Calls inside the condition execute unconditionally (relative to the
170
+ # enclosing context) — keep current guards.
171
+ if cond_node is not None:
172
+ _emit_calls_in(cond_node, " and ".join(active_guards), state, return_label="")
173
+
174
+ consequence = stmt.child_by_field_name("consequence")
175
+ has_else = stmt.child_by_field_name("alternative") is not None
176
+ if consequence is not None:
177
+ body_start = len(state.messages)
178
+ _emit_messages(
179
+ _branch_stmts(consequence),
180
+ active_guards + [cond_text] if cond_text else active_guards,
181
+ state,
182
+ )
183
+ body_end = len(state.messages) - 1
184
+ if body_end >= body_start:
185
+ state.fragments.append(
186
+ Fragment(
187
+ kind="alt" if has_else else "opt",
188
+ label=cond_text,
189
+ start_row=body_start,
190
+ end_row=body_end,
191
+ )
192
+ )
193
+
194
+ alternative = stmt.child_by_field_name("alternative")
195
+ if alternative is not None:
196
+ neg = f"!({cond_text})" if cond_text else ""
197
+ else_start = len(state.messages)
198
+ _emit_messages(
199
+ _branch_stmts(alternative),
200
+ active_guards + [neg] if neg else active_guards,
201
+ state,
202
+ )
203
+ else_end = len(state.messages) - 1
204
+ if else_end >= else_start:
205
+ state.fragments.append(
206
+ Fragment(
207
+ kind="alt",
208
+ label=f"else ({cond_text} is false)",
209
+ start_row=else_start,
210
+ end_row=else_end,
211
+ )
212
+ )
213
+
214
+
215
+ def _branch_stmts(node: Node) -> list[Node]:
216
+ if node.type == "block":
217
+ return [c for c in node.named_children if c.type != "comment"]
218
+ return [node]
219
+
220
+
221
+ def _emit_calls_in(
222
+ node: Node | None,
223
+ guard: str,
224
+ state: _State,
225
+ *,
226
+ return_label: str,
227
+ ) -> None:
228
+ if node is None:
229
+ return
230
+ invs = _invocations_post_order(node)
231
+ if not invs:
232
+ return
233
+ last_idx = len(invs) - 1
234
+ for i, inv in enumerate(invs):
235
+ rl = return_label if i == last_idx else ""
236
+ _emit_call(inv, state.root, guard, state, return_label=rl)
237
+
238
+
239
+ def _invocations_post_order(node: Node) -> list[Node]:
240
+ out: list[Node] = []
241
+
242
+ def visit(n: Node) -> None:
243
+ for c in n.children:
244
+ visit(c)
245
+ if n.type == "invocation_expression":
246
+ out.append(n)
247
+
248
+ visit(node)
249
+ return out
250
+
251
+
252
+ def _emit_call(
253
+ inv: Node,
254
+ root: str,
255
+ guard: str,
256
+ state: _State,
257
+ *,
258
+ return_label: str,
259
+ ) -> None:
260
+ sender, receiver, label = _resolve_invocation(inv, state.source, root)
261
+ state.ensure_lifeline(receiver)
262
+ state.ensure_lifeline(sender)
263
+ state.messages.append(
264
+ Message(sender=sender, receiver=receiver, label=label, guard=guard)
265
+ )
266
+ state.messages.append(
267
+ Message(
268
+ sender=receiver,
269
+ receiver=sender,
270
+ label=return_label,
271
+ is_return=True,
272
+ guard=guard,
273
+ )
274
+ )
275
+
276
+
277
+ def _resolve_invocation(
278
+ inv: Node, source: bytes, root: str
279
+ ) -> tuple[str, str, str]:
280
+ func = inv.child_by_field_name("function")
281
+ if func is None:
282
+ return root, "module", "()"
283
+ if func.type == "member_access_expression":
284
+ expr = func.child_by_field_name("expression")
285
+ name = func.child_by_field_name("name")
286
+ receiver = _text(expr, source) if expr else "obj"
287
+ label = (_text(name, source) if name else "method") + "()"
288
+ return root, receiver, label
289
+ return root, "module", _text(func, source) + "()"
290
+
291
+
292
+ def _text(node: Node | None, source: bytes) -> str:
293
+ if node is None:
294
+ return ""
295
+ return source[node.start_byte : node.end_byte].decode("utf-8", errors="replace")
@@ -0,0 +1,15 @@
1
+ """Implementation-conformance engine (Engine B).
2
+
3
+ Answers "does the code actually obey the constraint tags written on it" by
4
+ re-parsing the source and inspecting method bodies. Driven by the
5
+ `tag-conformance` rule type in `.cdec/rules.yaml`, which is what `cdec check`
6
+ runs; this package stays decoupled from `code_constraints.lint`, and the rule
7
+ is a thin adapter over `engine.enforce`.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from code_constraints.enforce.engine import enforce
13
+ from code_constraints.enforce.model import Finding, findings_to_json, format_findings
14
+
15
+ __all__ = ["enforce", "Finding", "format_findings", "findings_to_json"]
@@ -0,0 +1,122 @@
1
+ """Dispatcher for the implementation-conformance engine (Engine B).
2
+
3
+ This re-parses the current source independently and inspects method bodies. It
4
+ never consults the reference model or the diff metadata (that is Engine A).
5
+ Findings are filtered against the exceptions ledger by the *caller* (the
6
+ `tag-conformance` rule), so the engine itself stays a pure function of the
7
+ source.
8
+
9
+ Per-language body analysis (`no-instantiation`, `factory`, `immutable`) lives in
10
+ the language packages' `conformance` modules. The `sealed` check is structural
11
+ and cross-file, so it's evaluated here against the parsed `Project` model.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from pathlib import Path
17
+
18
+ from code_constraints.core.model import Project
19
+ from code_constraints.enforce.model import Finding
20
+
21
+ SEALED_RULE = "sealed"
22
+
23
+
24
+ def enforce(root: str | Path, lang: str) -> list[Finding]:
25
+ root_path = Path(root).resolve()
26
+ project = _parse(root_path, lang)
27
+ findings: list[Finding] = []
28
+ findings.extend(_check_sealed(project))
29
+ findings.extend(_analyze_bodies(root_path, lang, project))
30
+ return findings
31
+
32
+
33
+ def _check_sealed(project: Project) -> list[Finding]:
34
+ """A `@sealed` class may not be subclassed.
35
+
36
+ Tree-sitter / ast give us textual base names (not resolved qnames), so match
37
+ a sealed class by short name against every other class's base list.
38
+ """
39
+ sealed_names: dict[str, object] = {
40
+ cls.name: cls
41
+ for cls in project.iter_classes()
42
+ if any(r.name == SEALED_RULE for r in cls.rules)
43
+ }
44
+ if not sealed_names:
45
+ return []
46
+ findings: list[Finding] = []
47
+ for cls in project.iter_classes():
48
+ for base in cls.bases:
49
+ short = base.rsplit(".", 1)[-1]
50
+ if short in sealed_names and short != cls.name:
51
+ loc = cls.location
52
+ findings.append(
53
+ Finding(
54
+ rule=SEALED_RULE,
55
+ qualified_name=cls.qualified_name,
56
+ message=(
57
+ f"'{cls.qualified_name}' subclasses sealed class "
58
+ f"'{short}'; sealed types may not be subclassed."
59
+ ),
60
+ detail=f"base:{short}",
61
+ file=loc.file if loc else "",
62
+ line=loc.start_line if loc else 0,
63
+ )
64
+ )
65
+ return findings
66
+
67
+
68
+ def _analyze_bodies(root: Path, lang: str, project: Project) -> list[Finding]:
69
+ if lang == "python":
70
+ from code_constraints.python.conformance import analyze
71
+
72
+ return analyze(root, project)
73
+ if lang == "csharp":
74
+ from code_constraints.csharp.conformance import analyze
75
+
76
+ return analyze(root, project)
77
+ if lang == "odin":
78
+ from code_constraints.odin.conformance import analyze
79
+
80
+ return analyze(root, project)
81
+ if lang == "lua":
82
+ from code_constraints.lua.conformance import analyze
83
+
84
+ return analyze(root, project)
85
+ if lang == "julia":
86
+ from code_constraints.julia.conformance import analyze
87
+
88
+ return analyze(root, project)
89
+ # Other languages have no body analyzer yet.
90
+ return []
91
+
92
+
93
+ def _parse(root: Path, lang: str) -> Project:
94
+ if lang == "python":
95
+ from code_constraints.python import parse_project
96
+
97
+ return parse_project(root)
98
+ if lang == "csharp":
99
+ from code_constraints.csharp import parse_project
100
+
101
+ return parse_project(root)
102
+ if lang == "typescript":
103
+ from code_constraints.typescript import parse_project
104
+
105
+ return parse_project(root)
106
+ if lang == "svelte":
107
+ from code_constraints.svelte import parse_project
108
+
109
+ return parse_project(root)
110
+ if lang == "odin":
111
+ from code_constraints.odin import parse_project
112
+
113
+ return parse_project(root)
114
+ if lang == "lua":
115
+ from code_constraints.lua import parse_project
116
+
117
+ return parse_project(root)
118
+ if lang == "julia":
119
+ from code_constraints.julia import parse_project
120
+
121
+ return parse_project(root)
122
+ raise ValueError(f"unsupported language: {lang}")
@@ -0,0 +1,74 @@
1
+ """Result types for the conformance engine (Engine B).
2
+
3
+ Deliberately separate from `code_constraints.lint.Violation`: the two engines
4
+ are decoupled by design. Engine A answers "did the architectural intent drift
5
+ over time"; Engine B answers "does the code actually obey the tag right now" by
6
+ inspecting method bodies. They share the rule *catalog* and the review-key
7
+ scheme (`code_constraints.core.keys`) so one exceptions list can cover both —
8
+ nothing else. Both are reached through rule types in `.cdec/rules.yaml`.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from dataclasses import dataclass
14
+
15
+ from code_constraints.core.keys import make_key
16
+
17
+
18
+ @dataclass
19
+ class Finding:
20
+ """A single conformance violation found by a body analyzer."""
21
+
22
+ rule: str # catalog id, e.g. "no-instantiation"
23
+ qualified_name: str # the element the violation is attributed to
24
+ message: str
25
+ file: str = ""
26
+ line: int = 0
27
+ # Stable discriminator between two findings of the same rule on the same
28
+ # element — e.g. "settle->Invoice" for a construction, "rename.total" for a
29
+ # field reassignment. Deliberately free of line numbers so the review key
30
+ # survives edits elsewhere in the file. Every producer sets it; an empty
31
+ # detail just means the rule can only fire once per element.
32
+ detail: str = ""
33
+
34
+ def fingerprint(self) -> tuple[str, str, str, int]:
35
+ return (self.rule, self.qualified_name, self.file, self.line)
36
+
37
+ def key(self) -> str:
38
+ """Stable review key (`F-…`) — see `code_constraints.core.keys`."""
39
+ return make_key("enforce", self.rule, self.qualified_name, self.detail)
40
+
41
+
42
+ def format_findings(findings: list[Finding], *, suppressed: int = 0) -> str:
43
+ """Render findings as a human-readable report.
44
+
45
+ Each line leads with the review key so the output can be saved, marked up,
46
+ and fed back through `cdec exceptions patch`.
47
+ """
48
+ if not findings:
49
+ text = "no conformance violations.\n"
50
+ if suppressed:
51
+ text += f"({suppressed} finding(s) silenced by an exception.)\n"
52
+ return text
53
+ lines = [f"{len(findings)} conformance violation(s):"]
54
+ for f in sorted(findings, key=lambda x: (x.file, x.line, x.rule)):
55
+ loc = f"{f.file}:{f.line}" if f.file else f.qualified_name
56
+ lines.append(f" - [{f.key()}] [{f.rule}] {loc}: {f.message}")
57
+ if suppressed:
58
+ lines.append(f"({suppressed} finding(s) silenced by an exception.)")
59
+ return "\n".join(lines) + "\n"
60
+
61
+
62
+ def findings_to_json(findings: list[Finding]) -> list[dict]:
63
+ return [
64
+ {
65
+ "key": f.key(),
66
+ "rule": f.rule,
67
+ "qualifiedName": f.qualified_name,
68
+ "detail": f.detail,
69
+ "message": f.message,
70
+ "file": f.file,
71
+ "line": f.line,
72
+ }
73
+ for f in findings
74
+ ]
@@ -0,0 +1,5 @@
1
+ """Julia language support: UML parsing, rule recognition, conformance and locks."""
2
+
3
+ from code_constraints.julia.parser import parse_project
4
+
5
+ __all__ = ["parse_project"]