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,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}.[/]")
|