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,304 @@
1
+ """The modern interactive `cdec` session.
2
+
3
+ Launched when `cdec` is run with no subcommand. Detects whether the current
4
+ folder is an initialized harness project (presence of `.cdec/`), walks the user
5
+ through onboarding if not, then offers a menu of the common actions.
6
+
7
+ Built on `rich` (styled output) + `questionary` (arrow-key prompts). All the
8
+ real work lives in `code_constraints.cli.scaffold` and the existing subcommands; this module
9
+ is purely the conversational shell.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import subprocess
15
+ import sys
16
+ from pathlib import Path
17
+ from shutil import which
18
+ from urllib.parse import quote
19
+
20
+ import questionary
21
+ from rich.console import Console
22
+ from rich.panel import Panel
23
+ from rich.text import Text
24
+
25
+ from code_constraints.cli.detect import detect_language
26
+ from code_constraints.cli.scaffold import (
27
+ SUPPORTED_LANGS,
28
+ ScaffoldError,
29
+ copy_agents,
30
+ copy_shims,
31
+ has_shim,
32
+ init_cdec_config,
33
+ write_ci_scripts,
34
+ )
35
+
36
+ console = Console()
37
+
38
+
39
+ def run_interactive(path: Path) -> None:
40
+ """Entry point for a bare `cdec` invocation rooted at `path`."""
41
+ if not sys.stdin.isatty():
42
+ console.print(
43
+ "[yellow]cdec[/] needs an interactive terminal. "
44
+ "Run a subcommand instead, e.g. [bold]cdec --help[/], "
45
+ "[bold]cdec serve parse .[/], or [bold]cdec check[/]."
46
+ )
47
+ return
48
+
49
+ _banner(path)
50
+
51
+ cdec_dir = path / ".cdec"
52
+ if not cdec_dir.is_dir():
53
+ if not _onboard(path):
54
+ return # user bailed out of onboarding
55
+ else:
56
+ console.print(
57
+ f"[green]✓[/] Found an initialized project at [bold]{path}[/] "
58
+ "([dim].cdec/ present[/])."
59
+ )
60
+
61
+ _main_menu(path)
62
+
63
+
64
+ # ---------- presentation ----------
65
+
66
+ def _banner(path: Path) -> None:
67
+ title = Text("code-constraints", style="bold cyan")
68
+ body = Text.assemble(
69
+ ("Interactive session\n", "dim"),
70
+ ("Working directory: ", "dim"),
71
+ (str(path), "bold"),
72
+ )
73
+ console.print(Panel(body, title=title, border_style="cyan", expand=False))
74
+
75
+
76
+ def _ask_language(path: Path) -> str | None:
77
+ detected = detect_language(path)
78
+ if detected:
79
+ console.print(f"[dim]Detected language:[/] [bold]{detected}[/]")
80
+ choice = questionary.select(
81
+ "Which language is this project?",
82
+ choices=list(SUPPORTED_LANGS),
83
+ default=detected if detected in SUPPORTED_LANGS else None,
84
+ ).ask()
85
+ return choice # None if the user cancels (Ctrl+C)
86
+
87
+
88
+ # ---------- onboarding ----------
89
+
90
+ def _onboard(path: Path) -> bool:
91
+ """Walk the user through first-time setup. Returns True to continue to the
92
+ menu, False if the user cancelled out entirely."""
93
+ console.print(
94
+ "\n[bold]This folder isn't a code-constraints project yet.[/] "
95
+ "Let's get it set up.\n"
96
+ )
97
+ if not questionary.confirm("Initialize a code-constraints project here?", default=True).ask():
98
+ console.print("[yellow]Skipped setup.[/] Run [bold]cdec[/] again when ready.")
99
+ return False
100
+
101
+ lang = _ask_language(path)
102
+ if lang is None:
103
+ return False
104
+
105
+ # 1. Scaffold .cdec/
106
+ try:
107
+ written = init_cdec_config(path / ".cdec", lang, Path("."), force=False)
108
+ except ScaffoldError as exc:
109
+ console.print(f"[red]Could not scaffold .cdec/:[/] {exc}")
110
+ return False
111
+ console.print(f"[green]✓[/] Scaffolded [bold].cdec/[/] ({len(written)} files).")
112
+
113
+ # 2. Claude agents
114
+ if questionary.confirm(
115
+ "Copy code-constraints Claude agents into .claude/agents/?", default=True
116
+ ).ask():
117
+ _try_copy_agents(path, force=False)
118
+
119
+ # 3. Shims
120
+ if has_shim(lang):
121
+ if questionary.confirm(
122
+ f"Copy the {lang} rule shims into the project?", default=True
123
+ ).ask():
124
+ _try_copy_shims(path, lang, force=False)
125
+ else:
126
+ console.print(f"[dim]No rule shims available for {lang} yet — skipping.[/]")
127
+
128
+ console.print("\n[green bold]Setup complete![/]\n")
129
+ return True
130
+
131
+
132
+ # ---------- main menu ----------
133
+
134
+ _LAUNCH = "🌐 Launch the web app (interactive diagrams)"
135
+ _CHECKS = "🔍 Run the architectural checks (cdec check)"
136
+ _TESTS = "🧪 Run the project test suite"
137
+ _CI = "📦 Generate CI/CD scripts (Windows + Linux)"
138
+ _UPDATE = "🔄 Update project assets (agents, shims)"
139
+ _EXIT = "❌ Exit"
140
+
141
+
142
+ def _main_menu(path: Path) -> None:
143
+ lang = _resolve_language(path)
144
+ while True:
145
+ action = questionary.select(
146
+ "What would you like to do?",
147
+ choices=[_LAUNCH, _CHECKS, _TESTS, _CI, _UPDATE, _EXIT],
148
+ ).ask()
149
+
150
+ if action is None or action == _EXIT:
151
+ console.print("[dim]Bye.[/]")
152
+ return
153
+
154
+ try:
155
+ if action == _LAUNCH:
156
+ _launch_web(path, lang)
157
+ elif action == _CHECKS:
158
+ _run_checks(path, lang)
159
+ elif action == _TESTS:
160
+ _run_tests(path, lang)
161
+ elif action == _CI:
162
+ _generate_ci(path, lang)
163
+ elif action == _UPDATE:
164
+ _update_assets(path, lang)
165
+ except KeyboardInterrupt:
166
+ console.print("\n[yellow]Interrupted — back to the menu.[/]")
167
+ except Exception as exc: # keep the session alive on any action failure
168
+ console.print(f"[red]Action failed:[/] {exc}")
169
+
170
+ console.print() # spacer before the menu repeats
171
+
172
+
173
+ def _resolve_language(path: Path) -> str:
174
+ """Prefer the language recorded in .cdec/rules.yaml, fall back to detection,
175
+ then to 'python'."""
176
+ try:
177
+ from code_constraints.lint.config import load_project_config
178
+
179
+ return load_project_config(path / ".cdec").language
180
+ except Exception:
181
+ return detect_language(path) or "python"
182
+
183
+
184
+ # ---------- actions ----------
185
+
186
+ def _launch_web(path: Path, lang: str) -> None:
187
+ host, port = "127.0.0.1", 8765
188
+ url = f"http://{host}:{port}/?path={quote(str(path.resolve()))}&lang={lang}"
189
+ console.print(
190
+ f"[green]Starting the web app[/] at [bold]{url}[/]\n"
191
+ "[dim]Press Ctrl+C to stop and return to the menu.[/]"
192
+ )
193
+ from code_constraints.cli.__main__ import _run_server
194
+
195
+ try:
196
+ _run_server(host, port, open_url=url)
197
+ except KeyboardInterrupt:
198
+ console.print("\n[yellow]Server stopped.[/]")
199
+
200
+
201
+ def _run_checks(path: Path, lang: str) -> None:
202
+ """One command, every rule. `cdec check` is the whole gate."""
203
+ console.print("[bold]Running the architectural checks…[/]")
204
+ _run(
205
+ [sys.executable, "-m", "code_constraints.cli", "check", "--config", ".cdec", "--source", "."],
206
+ path,
207
+ )
208
+ console.print(
209
+ "\n[dim]To accept a reported issue, quote its key: "
210
+ "[bold]cdec exceptions allow V-XXXXXXXX --reason \"why\"[/]. "
211
+ "For a batch: [bold]cdec check --log-out check.log[/], mark lines [ALLOW], "
212
+ "[bold]cdec exceptions patch --file check.log[/].\n"
213
+ "To grandfather everything on an existing codebase: "
214
+ "[bold]cdec check --automatic-exceptions rules[/].[/]"
215
+ )
216
+
217
+
218
+ def _run_tests(path: Path, lang: str) -> None:
219
+ if lang in ("python",):
220
+ console.print("[bold]Running pytest…[/]")
221
+ _run([sys.executable, "-m", "pytest"], path)
222
+ elif lang == "csharp":
223
+ if which("dotnet") is None:
224
+ console.print("[yellow]`dotnet` not found on PATH — cannot run C# tests.[/]")
225
+ return
226
+ console.print("[bold]Running dotnet test…[/]")
227
+ _run(["dotnet", "test"], path)
228
+ else:
229
+ # typescript / svelte: lean on npm if a package.json is present.
230
+ if (path / "package.json").is_file() and which("npm") is not None:
231
+ console.print("[bold]Running npm test…[/]")
232
+ _run(["npm", "test"], path)
233
+ else:
234
+ console.print(
235
+ f"[yellow]No known test runner for {lang} in this folder "
236
+ "(looked for package.json + npm).[/]"
237
+ )
238
+
239
+
240
+ def _generate_ci(path: Path, lang: str) -> None:
241
+ written = write_ci_scripts(path, lang, Path("."))
242
+ for p in written:
243
+ console.print(f"[green]✓[/] wrote [bold]{p.name}[/]")
244
+ console.print(
245
+ "[dim]Both scripts run `cdec check` and exit non-zero on any violation "
246
+ "— drop them into your CI pipeline.[/]"
247
+ )
248
+
249
+
250
+ def _update_assets(path: Path, lang: str) -> None:
251
+ """Update (or install) Claude agents and shims from the currently-installed
252
+ code-constraints version. Safe to run after upgrading — overwrites only the
253
+ harness-owned files, never your .cdec/ config or source code."""
254
+ console.print(
255
+ "[dim]Updates Claude agents and language shims to the version bundled "
256
+ "with this installation. Run after upgrading code-constraints.[/]\n"
257
+ )
258
+
259
+ agents_dest = path / ".claude" / "agents"
260
+ agents_exist = agents_dest.is_dir() and any(agents_dest.iterdir())
261
+ agents_label = "Update" if agents_exist else "Install"
262
+ if questionary.confirm(
263
+ f"{agents_label} code-constraints Claude agents in .claude/agents/?", default=True
264
+ ).ask():
265
+ _try_copy_agents(path, force=True)
266
+
267
+ if has_shim(lang):
268
+ shim_exists = (path / "cdec_rules.py").is_file() or (path / "CodeConstraintsRules.cs").is_file()
269
+ shim_label = "Update" if shim_exists else "Install"
270
+ if questionary.confirm(
271
+ f"{shim_label} the {lang} rule shims?", default=True
272
+ ).ask():
273
+ _try_copy_shims(path, lang, force=True)
274
+ else:
275
+ console.print(f"[dim]No rule shims for {lang} — skipping.[/]")
276
+
277
+
278
+ # ---------- copy wrappers (shared by onboarding + recopy) ----------
279
+
280
+ def _try_copy_agents(path: Path, force: bool) -> None:
281
+ try:
282
+ dests = copy_agents(path, force=force)
283
+ for dest in dests:
284
+ console.print(f"[green]✓[/] Copied agent → [bold]{dest}[/]")
285
+ except ScaffoldError as exc:
286
+ console.print(f"[yellow]Agents not copied:[/] {exc}")
287
+
288
+
289
+ def _try_copy_shims(path: Path, lang: str, force: bool) -> None:
290
+ try:
291
+ dests = copy_shims(path, lang, force=force)
292
+ for dest in dests:
293
+ console.print(f"[green]✓[/] Copied shim → [bold]{dest}[/]")
294
+ except ScaffoldError as exc:
295
+ console.print(f"[yellow]Shim not copied:[/] {exc}")
296
+
297
+
298
+ # ---------- subprocess helper ----------
299
+
300
+ def _run(cmd: list[str], cwd: Path) -> None:
301
+ """Run a subprocess, streaming its output, without raising on non-zero."""
302
+ result = subprocess.run(cmd, cwd=str(cwd))
303
+ if result.returncode != 0:
304
+ console.print(f"[yellow]Command exited with code {result.returncode}.[/]")