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,602 @@
1
+ """Reusable, non-interactive scaffolding primitives.
2
+
3
+ Shared by the `cdec init` subcommand and the interactive session
4
+ (`code_constraints.cli.interactive`). Everything here is pure file I/O + asset resolution —
5
+ no prompting, no `typer.Exit`. Callers decide how to surface results.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import importlib.resources
11
+ from pathlib import Path
12
+
13
+ from code_constraints.core.model import SUPPORTED_LANGUAGES
14
+ from code_constraints.core.rulesdoc import RULES_FILENAME
15
+ from code_constraints.lint.config import (
16
+ BASELINE_FILENAME,
17
+ CONFIG_FILENAME,
18
+ LOCKS_FILENAME,
19
+ REFERENCE_FILENAME,
20
+ )
21
+
22
+ SUPPORTED_LANGS = SUPPORTED_LANGUAGES
23
+
24
+ # Where a copied shim lands in the target project, by language. The file name
25
+ # matches the convention in examples/python_demo/cdec_rules.py — the shim sits at
26
+ # the project root so tagged code can `from cdec_rules import …` /
27
+ # `using CodeConstraints.Rules;`.
28
+ _SHIM_TARGETS = {
29
+ "python": ("shims/python/cdec_rules.py", "cdec_rules.py"),
30
+ "csharp": ("shims/csharp/CodeConstraintsRules.cs", "CodeConstraintsRules.cs"),
31
+ "julia": ("shims/julia/CdecRules.jl", "CdecRules.jl"),
32
+ # Lua and Odin carry tags in `@cdec` annotation comments rather than in a
33
+ # language construct, so their shims are vocabulary references plus runtime
34
+ # no-ops. They are still copied, so the tag list lives in the project.
35
+ "lua": ("shims/lua/cdec_rules.lua", "cdec_rules.lua"),
36
+ "odin": ("shims/odin/cdec_rules.odin", "cdec_rules.odin"),
37
+ # typescript / svelte have no shims yet.
38
+ }
39
+
40
+ # Claude assets deployed into target projects: agents plus the architecture-loop
41
+ # skill (the propose → review → lock workflow). Each entry is the destination
42
+ # path inside the user's project. The source copies live in the package at
43
+ # `cli/_assets/{agents,skills}/` — this repo does not track its own `.claude/`.
44
+ _AGENT_RELS = [
45
+ ".claude/agents/cdec-architect.md",
46
+ ".claude/agents/oop-refactor-architect.md",
47
+ ".claude/skills/cdec-architecture-loop/SKILL.md",
48
+ ]
49
+
50
+
51
+ class ScaffoldError(RuntimeError):
52
+ """Raised when an asset can't be located or a target already exists."""
53
+
54
+
55
+ # ---------- asset resolution ----------
56
+
57
+ def _repo_root() -> Path | None:
58
+ """Walk up from this file looking for the harness repo root (the dir that
59
+ holds both `pyproject.toml` and `shims/`). Present in editable installs."""
60
+ for parent in Path(__file__).resolve().parents:
61
+ if (parent / "pyproject.toml").is_file() and (parent / "shims").is_dir():
62
+ return parent
63
+ return None
64
+
65
+
66
+ def _asset(rel: str) -> Path:
67
+ """Resolve a bundled asset by its repo-relative path (e.g.
68
+ "shims/python/cdec_rules.py" or ".claude/agents/cdec-architect.md").
69
+
70
+ Shims prefer the repo-relative source-of-truth file (editable installs) and
71
+ fall back to the packaged copy under `code_constraints/cli/_assets/…`, where
72
+ `force-include` maps them in. The `.claude/…` rels have no repo-relative
73
+ original, so they always resolve to the packaged copy. Raises ScaffoldError
74
+ if missing.
75
+ """
76
+ # `.claude/…` rels are destination paths only — never read them back out of
77
+ # a repo root, or an editable install would prefer a developer's own
78
+ # untracked `.claude/` over the packaged source of truth.
79
+ root = None if rel.startswith(".claude/") else _repo_root()
80
+ if root is not None:
81
+ candidate = root / rel
82
+ if candidate.is_file():
83
+ return candidate
84
+
85
+ # Packaged copy: assets are flattened under code_constraints/cli/_assets/.
86
+ # shims/python/cdec_rules.py -> _assets/shims/python/cdec_rules.py
87
+ # .claude/agents/cdec-architect.md -> _assets/agents/cdec-architect.md
88
+ # .claude/skills/<name>/SKILL.md -> _assets/skills/<name>/SKILL.md
89
+ packaged_rel = rel
90
+ if rel.startswith(".claude/agents/"):
91
+ packaged_rel = "agents/" + rel.split("/")[-1]
92
+ elif rel.startswith(".claude/skills/"):
93
+ packaged_rel = rel.removeprefix(".claude/")
94
+ try:
95
+ base = importlib.resources.files("code_constraints.cli") / "_assets"
96
+ candidate = Path(str(base / packaged_rel))
97
+ if candidate.is_file():
98
+ return candidate
99
+ except (ModuleNotFoundError, FileNotFoundError):
100
+ pass
101
+
102
+ raise ScaffoldError(f"could not locate bundled asset: {rel}")
103
+
104
+
105
+ # ---------- .cdec/ scaffolding ----------
106
+
107
+ def init_cdec_config(
108
+ config_dir: Path, lang: str, source: Path, force: bool
109
+ ) -> list[Path]:
110
+ """Scaffold a `.cdec/` folder and snapshot a reference model from `source`.
111
+
112
+ One `rules.yaml` carries the settings and the rules; the exceptions granted
113
+ and the lock ledger are appended to its tool-managed tail as they are
114
+ earned. Returns the files written.
115
+
116
+ Raises ScaffoldError on an unsupported language, or if a target file exists
117
+ and `force` is False.
118
+ """
119
+ if lang not in SUPPORTED_LANGS:
120
+ raise ScaffoldError(f"unsupported language: {lang}")
121
+
122
+ config_dir.mkdir(parents=True, exist_ok=True)
123
+ written: list[Path] = []
124
+
125
+ files = [
126
+ (
127
+ config_dir / RULES_FILENAME,
128
+ _RULES_TEMPLATE.format(lang=lang, source=_posix(source)),
129
+ ),
130
+ (config_dir / "README.md", _README_TEMPLATE),
131
+ ]
132
+ for path, content in files:
133
+ if path.exists() and not force:
134
+ raise ScaffoldError(f"refusing to overwrite {path} (use force)")
135
+ path.write_text(content, encoding="utf-8")
136
+ written.append(path)
137
+
138
+ gitignore = config_dir / ".gitignore"
139
+ gitignore.write_text("cache/\n", encoding="utf-8")
140
+ written.append(gitignore)
141
+
142
+ # Snapshot a reference model up front. It is the baseline every diff-scope
143
+ # rule needs, and "where does reference.xmi come from" is the first question
144
+ # anybody asks — so scaffolding answers it rather than deferring it.
145
+ reference_path = config_dir / REFERENCE_FILENAME
146
+ if source.exists():
147
+ from code_constraints.core.xmi_writer import write_project
148
+
149
+ project = _parse_project(source, lang)
150
+ write_project(project, reference_path)
151
+ written.append(reference_path)
152
+
153
+ return written
154
+
155
+
156
+ def migrate_cdec_config(config_dir: Path) -> tuple[list[Path], list[Path]]:
157
+ """Fold `config.yaml` / `baseline.yaml` / `locks.yaml` into `rules.yaml`.
158
+
159
+ Returns (files written, files removed). The old files are deleted only once
160
+ their content has been read back out of the new one, so a failed migration
161
+ leaves the project exactly as it was.
162
+ """
163
+ import yaml
164
+
165
+ from code_constraints.core.rulesdoc import load_document, write_sections
166
+ from code_constraints.lock.store import load_locks, write_locks
167
+ from code_constraints.waivers.store import load_waivers, save_waivers
168
+
169
+ rules_path = config_dir / RULES_FILENAME
170
+ legacy_config = config_dir / CONFIG_FILENAME
171
+ legacy_baseline = config_dir / BASELINE_FILENAME
172
+ legacy_locks = config_dir / LOCKS_FILENAME
173
+ if not any(p.is_file() for p in (legacy_config, legacy_baseline, legacy_locks)):
174
+ return [], []
175
+
176
+ # 1. Settings, prepended as plain top-level keys above whatever rules.yaml
177
+ # already holds, so the rules and their comments are untouched.
178
+ if legacy_config.is_file():
179
+ with legacy_config.open(encoding="utf-8") as fh:
180
+ old = yaml.safe_load(fh) or {}
181
+ if not isinstance(old, dict):
182
+ raise ScaffoldError(f"{legacy_config}: top-level must be a mapping")
183
+ existing = load_document(rules_path) if rules_path.is_file() else {}
184
+ header = _migrated_settings_header(old, existing)
185
+ body = rules_path.read_text(encoding="utf-8") if rules_path.is_file() else _EMPTY_RULES
186
+ rules_path.parent.mkdir(parents=True, exist_ok=True)
187
+ rules_path.write_text(header + body, encoding="utf-8")
188
+ # A `lock:` settings block becomes an `implementation-locks` rule. Only
189
+ # a configured one is carried over: inventing a rule the project never
190
+ # opted into would turn a migration into a new gate.
191
+ lock_cfg = old.get("lock") or {}
192
+ if isinstance(lock_cfg, dict) and lock_cfg.get("targets"):
193
+ targets = ", ".join(f'"{t}"' for t in lock_cfg.get("targets") or [])
194
+ with rules_path.open("a", encoding="utf-8") as fh:
195
+ fh.write(
196
+ _MIGRATED_LOCK_RULE.format(
197
+ targets=f"[{targets}]",
198
+ include_docstrings=str(
199
+ bool(lock_cfg.get("include_docstrings", False))
200
+ ).lower(),
201
+ )
202
+ )
203
+
204
+ # 2. Exceptions and locks, read through the loaders that already understand
205
+ # both the old and the new shape, then written back in the new one.
206
+ store = load_waivers(config_dir)
207
+ entries = load_locks(config_dir)
208
+ if store.waivers:
209
+ save_waivers(config_dir, store)
210
+ if entries:
211
+ write_locks(config_dir, entries.values())
212
+
213
+ # 3. Only now that everything demonstrably landed, drop the old files.
214
+ reread = load_document(rules_path)
215
+ if store.waivers and not reread.get("exceptions"):
216
+ raise ScaffoldError(f"migration aborted: exceptions did not land in {rules_path}")
217
+ if entries and not reread.get("locks"):
218
+ raise ScaffoldError(f"migration aborted: locks did not land in {rules_path}")
219
+
220
+ removed: list[Path] = []
221
+ for path in (legacy_config, legacy_baseline, legacy_locks):
222
+ if path.is_file():
223
+ path.unlink()
224
+ removed.append(path)
225
+ return [rules_path], removed
226
+
227
+
228
+ def _migrated_settings_header(old: dict, existing: dict) -> str:
229
+ """Render a legacy config.yaml's settings as rules.yaml top-level keys.
230
+
231
+ Keys already present in rules.yaml win — that is the file the user has been
232
+ editing, so it is the more current statement of intent.
233
+ """
234
+ lines = ["# Project settings (migrated from config.yaml).\n"]
235
+ lines.append(f"language: {existing.get('language') or old.get('language') or 'python'}\n")
236
+ lines.append(f"source: {existing.get('source') or old.get('source') or '.'}\n")
237
+ reference = existing.get("reference")
238
+ if reference is None:
239
+ legacy_baseline = old.get("baseline")
240
+ if isinstance(legacy_baseline, dict):
241
+ reference = legacy_baseline.get("reference")
242
+ if reference:
243
+ lines.append(f"reference: {reference}\n")
244
+ output = existing.get("output") or old.get("output") or {}
245
+ if isinstance(output, dict) and (output.get("json") or output.get("log")):
246
+ lines.append("output:\n")
247
+ if output.get("json"):
248
+ lines.append(f" json: {output['json']}\n")
249
+ if output.get("log"):
250
+ lines.append(f" log: {output['log']}\n")
251
+ lines.append("\n")
252
+ return "".join(lines)
253
+
254
+
255
+ # ---------- copy helpers ----------
256
+
257
+ def copy_agents(project_root: Path, force: bool = False) -> list[Path]:
258
+ """Copy all code-constraints Claude agents into <project>/.claude/agents/.
259
+ Returns the destination paths written. Raises ScaffoldError if any target
260
+ exists and `force` is False."""
261
+ written: list[Path] = []
262
+ for rel in _AGENT_RELS:
263
+ src = _asset(rel)
264
+ dest = project_root / rel
265
+ if dest.exists() and not force:
266
+ raise ScaffoldError(f"refusing to overwrite {dest} (use --force)")
267
+ dest.parent.mkdir(parents=True, exist_ok=True)
268
+ dest.write_text(src.read_text(encoding="utf-8"), encoding="utf-8")
269
+ written.append(dest)
270
+ return written
271
+
272
+
273
+ def copy_architect_agent(project_root: Path, force: bool = False) -> Path:
274
+ """Compatibility shim — copies all agents and returns the first path."""
275
+ return copy_agents(project_root, force=force)[0]
276
+
277
+
278
+ def copy_shims(project_root: Path, lang: str, force: bool = False) -> list[Path]:
279
+ """Copy the language shim into the project root. Returns the destinations
280
+ written (empty list when the language has no shim). Raises ScaffoldError if a
281
+ target exists and `force` is False."""
282
+ entry = _SHIM_TARGETS.get(lang)
283
+ if entry is None:
284
+ return []
285
+ src_rel, dest_name = entry
286
+ src = _asset(src_rel)
287
+ dest = project_root / dest_name
288
+ if dest.exists() and not force:
289
+ raise ScaffoldError(f"refusing to overwrite {dest} (use force)")
290
+ dest.write_text(src.read_text(encoding="utf-8"), encoding="utf-8")
291
+ return [dest]
292
+
293
+
294
+ def has_shim(lang: str) -> bool:
295
+ return lang in _SHIM_TARGETS
296
+
297
+
298
+ # ---------- CI/CD script generation ----------
299
+
300
+ def write_ci_scripts(project_root: Path, lang: str, source: Path) -> list[Path]:
301
+ """Write `cdec-ci.bat` (Windows) and `cdec-ci.sh` (bash) into the project root.
302
+
303
+ Both are one line of real work: `cdec check` is the whole gate, and its exit
304
+ code is the whole answer."""
305
+ src = _posix(source)
306
+ bat = project_root / "cdec-ci.bat"
307
+ sh = project_root / "cdec-ci.sh"
308
+ bat.write_text(_CI_BAT_TEMPLATE.format(lang=lang, source=src), encoding="utf-8")
309
+ sh.write_text(_CI_SH_TEMPLATE.format(lang=lang, source=src), encoding="utf-8")
310
+ return [bat, sh]
311
+
312
+
313
+ # ---------- shared internals ----------
314
+
315
+ def _parse_project(path: Path, lang: str):
316
+ if lang == "python":
317
+ from code_constraints.python import parse_project
318
+
319
+ return parse_project(path)
320
+ if lang == "csharp":
321
+ from code_constraints.csharp import parse_project
322
+
323
+ return parse_project(path)
324
+ if lang == "typescript":
325
+ from code_constraints.typescript import parse_project
326
+
327
+ return parse_project(path)
328
+ if lang == "svelte":
329
+ from code_constraints.svelte import parse_project
330
+
331
+ return parse_project(path)
332
+ if lang == "odin":
333
+ from code_constraints.odin import parse_project
334
+
335
+ return parse_project(path)
336
+ if lang == "lua":
337
+ from code_constraints.lua import parse_project
338
+
339
+ return parse_project(path)
340
+ if lang == "julia":
341
+ from code_constraints.julia import parse_project
342
+
343
+ return parse_project(path)
344
+ raise ScaffoldError(f"unsupported language: {lang}")
345
+
346
+
347
+ def _posix(p: Path) -> str:
348
+ return str(p).replace("\\", "/")
349
+
350
+
351
+ _EMPTY_RULES = "rules: []\n"
352
+
353
+ _MIGRATED_LOCK_RULE = """
354
+ # Migrated from the `lock:` section of config.yaml. Locks are a rule now, so
355
+ # they are configured, reported and gated exactly like every other law here.
356
+ - id: frozen-implementations
357
+ type: implementation-locks
358
+ severity: error
359
+ include_docstrings: {include_docstrings}
360
+ targets: {targets}
361
+ """
362
+
363
+ _RULES_TEMPLATE = """\
364
+ # ============================================================================
365
+ # .cdec/rules.yaml — everything `cdec check` needs, in one committed file.
366
+ #
367
+ # settings what to check and where (below)
368
+ # rules: the laws that are enforced (below)
369
+ # exceptions: violations you accepted, and why (written by the tool)
370
+ # locks: digests of frozen implementations (written by the tool)
371
+ #
372
+ # The last two live in a marked section at the end of the file. The tool
373
+ # rewrites only that section, so every comment and message you write up here
374
+ # survives untouched.
375
+ #
376
+ # One command runs all of it:
377
+ #
378
+ # cdec check verify everything
379
+ # cdec check --automatic-exceptions reference snapshot .cdec/reference.xmi
380
+ # cdec check --automatic-exceptions rules grandfather today's violations
381
+ # cdec check --automatic-exceptions locks record digests for @locked code
382
+ # cdec exceptions allow V-1A2B3C4D --reason .. accept one issue, with a reason
383
+ # ============================================================================
384
+
385
+ language: {lang} # python | csharp | typescript | svelte | odin | lua | julia
386
+ source: {source} # the source tree `cdec check` parses
387
+
388
+ # The baseline every `scope: diff` rule compares against, and the model the
389
+ # `reference-architecture` rule gates on. `cdec init` snapshots it for you;
390
+ # re-snapshot with `cdec check --automatic-exceptions reference`.
391
+ reference: .cdec/reference.xmi
392
+
393
+ output:
394
+ json: null # optional default for --json-out
395
+ log: null # optional default for --log-out
396
+
397
+ # ---------------------------------------------------------------------------
398
+ # Rules. Every entry takes:
399
+ # id — stable identifier, and the heading violations are grouped under
400
+ # type — which rule to run (list below)
401
+ # severity — error | warning | off
402
+ # scope — diff | snapshot (defaults per rule type)
403
+ # message — what to print when it fires. Supports {{placeholders}} and YAML
404
+ # block scalars (`message: |`). USE IT: a rule that explains why
405
+ # it exists teaches; one that just says "violation" breeds
406
+ # resentment.
407
+ # ignore — list of qualified-name globs to exempt
408
+ #
409
+ # `cdec check` is opt-in — an empty list enforces nothing. Add laws one at a
410
+ # time, as the team agrees on them.
411
+ #
412
+ # --- model rules (read the parsed architecture) ---
413
+ # no-new-classes, no-removed-classes ....... structural drift (scope: diff)
414
+ # frozen-members ........................... a class's public shape (diff)
415
+ # frozen-rules ............................. the constraint tags themselves (diff)
416
+ # forbidden-references ..................... class A may not reference class B
417
+ # forbidden-package-references ............. package A may not reference package B
418
+ # no-cyclic-package-dependencies ........... no dependency cycles
419
+ # layer-dependencies ....................... @layer tags + an allowed-direction matrix
420
+ # subclass-naming .......................... subclasses of X must be named Y
421
+ # dangling-classes ......................... nothing references this class
422
+ # max-class-fanout ......................... a class references too many others
423
+ #
424
+ # --- source rules (re-read the code itself) ---
425
+ # tag-conformance .......................... the implementation obeys its
426
+ # @sealed / @immutable / @factory /
427
+ # @no_instantiation tags
428
+ # implementation-locks ..................... an @locked body may not change
429
+ # reference-architecture ................... no structural deviation from
430
+ # reference.xmi at all
431
+ #
432
+ # Placeholders available in `message`, by type:
433
+ # no-new-classes / no-removed-classes / dangling-classes:
434
+ # {{qualified_name}} (max-class-fanout adds {{fanout}}, {{limit}})
435
+ # frozen-members: {{qualified_name}}, {{member}}, {{kind}}, {{action}}
436
+ # forbidden-*-references: {{qualified_name}}, {{source}}, {{target}}
437
+ # subclass-naming: {{qualified_name}}, {{name}}, {{base}}, {{pattern}}
438
+ # no-cyclic-package-dependencies: {{cycle}}
439
+ # frozen-rules: {{qualified_name}}, {{rule}}, {{member}}, {{action}}
440
+ # layer-dependencies: {{qualified_name}}, {{source}}, {{target}},
441
+ # {{source_layer}}, {{target_layer}}
442
+ # tag-conformance: {{qualified_name}}, {{rule}}, {{detail}}, {{message}}
443
+ # implementation-locks: {{qualified_name}}, {{kind}}, {{message}}
444
+ # reference-architecture: {{qualified_name}}, {{category}}, {{member}}, {{message}}
445
+ #
446
+ # Every rule and every source tag is documented with options and worked
447
+ # pass/fail examples in docs/RULES_CATALOGUE.md.
448
+ # ---------------------------------------------------------------------------
449
+
450
+ rules: []
451
+
452
+ # Examples — uncomment and adapt:
453
+ #
454
+ # - id: domain-must-not-depend-on-ui
455
+ # type: forbidden-package-references
456
+ # severity: error
457
+ # from: ["myapp.domain.**"]
458
+ # to: ["myapp.ui.**"]
459
+ # message: |
460
+ # Layering violation: '{{source}}' must not depend on '{{target}}'.
461
+ # The domain is pure business rules; move the reference to whichever
462
+ # package owns the workflow.
463
+ #
464
+ # - id: no-new-classes
465
+ # type: no-new-classes
466
+ # severity: error
467
+ # ignore: ["tests.**"]
468
+ #
469
+ # - id: layering
470
+ # type: layer-dependencies
471
+ # severity: error
472
+ # allow:
473
+ # ui: [domain]
474
+ # domain: [data]
475
+ # data: []
476
+ #
477
+ # - id: tags-must-be-honoured
478
+ # type: tag-conformance
479
+ # severity: error
480
+ #
481
+ # - id: frozen-implementations
482
+ # type: implementation-locks
483
+ # severity: error
484
+ #
485
+ # - id: public-shape-is-frozen
486
+ # type: reference-architecture
487
+ # severity: error
488
+ """
489
+
490
+ _README_TEMPLATE = """\
491
+ # `.cdec/` — architectural constraints for this project
492
+
493
+ Two files, and one command that reads them.
494
+
495
+ | File | What it is |
496
+ |---|---|
497
+ | `rules.yaml` | Settings, the rules enforced, the exceptions granted, and the digests of frozen implementations. Commit it. |
498
+ | `reference.xmi` | A snapshot of the architecture, used as the baseline for `scope: diff` rules and by the `reference-architecture` rule. Commit it. |
499
+ | `cache/` | Transient parse artefacts. Gitignored. |
500
+
501
+ ```
502
+ cdec check
503
+ ```
504
+
505
+ That is the whole gate. Architectural rules, source-tag conformance,
506
+ implementation locks and the reference gate are all rule types in `rules.yaml`,
507
+ so there is one command to run in CI and one exit code to read.
508
+
509
+ ## Accepting things
510
+
511
+ Every issue prints a stable key (`V-` a configured rule, `F-` tag conformance,
512
+ `L-` a lock, `R-` a reference deviation). The key is derived from what the issue
513
+ *is*, never from where it sits, so reformatting or moving code never invalidates
514
+ a decision you recorded.
515
+
516
+ ```
517
+ cdec exceptions allow V-1A2B3C4D --reason "agreed in ARCH-42"
518
+ ```
519
+
520
+ For a batch, save the report, mark the lines you accept with `[ALLOW]` (or
521
+ `[ALLOW: reason]`), and apply the file:
522
+
523
+ ```
524
+ cdec check --log-out check.log
525
+ cdec exceptions patch --file check.log
526
+ ```
527
+
528
+ `cdec exceptions list` shows what is accepted and why; `remove KEY` withdraws
529
+ it; `prune` drops exceptions whose issue no longer occurs — worth running
530
+ periodically, since a stale one pre-approves the next violation just the same.
531
+
532
+ To grandfather everything at once when adopting a rule on an existing codebase:
533
+
534
+ ```
535
+ cdec check --automatic-exceptions rules
536
+ ```
537
+
538
+ ## Locks are the exception to exceptions
539
+
540
+ `L-` issues cannot be accepted through `cdec exceptions`. Tag a class or
541
+ function `@locked` (Python) / `[Locked]` (C#), add an `implementation-locks`
542
+ rule, and record the digest:
543
+
544
+ ```
545
+ cdec check --automatic-exceptions locks
546
+ ```
547
+
548
+ That is safe for anyone to run: without `--force` it can only *add* locks, never
549
+ erase the evidence that a frozen body changed. Accepting a change to locked code
550
+ is the privileged step:
551
+
552
+ ```
553
+ cdec check --automatic-exceptions locks --force
554
+ ```
555
+
556
+ It rewrites the `locks:` section, which is a reviewable diff. Put `rules.yaml`
557
+ behind a CODEOWNERS entry and re-baselining becomes a lead-only action that
558
+ always leaves a trail. To ship without re-baselining,
559
+ `cdec check --bypass-locks --bypass-reason "..."` prints an audit banner and
560
+ passes; reject bypassed runs in CI by checking `summary.bypassed` in
561
+ `--json-out`.
562
+
563
+ ## CI recipes
564
+
565
+ Pre-merge, against the target branch:
566
+
567
+ ```
568
+ cdec check --base-ref origin/main --json-out lint.json
569
+ ```
570
+
571
+ Against the committed reference:
572
+
573
+ ```
574
+ cdec check
575
+ ```
576
+
577
+ Either exits 1 on any violation at or above `--fail-on` severity (default:
578
+ `error`).
579
+ """
580
+
581
+ _CI_BAT_TEMPLATE = """\
582
+ @echo off
583
+ REM Architectural CI check. Generated by `cdec`.
584
+ REM One command: every rule in .cdec/rules.yaml, one exit code.
585
+ setlocal
586
+
587
+ python -m code_constraints.cli check --config .cdec --source {source}
588
+ if errorlevel 1 exit /b 1
589
+
590
+ echo code-constraints checks passed.
591
+ """
592
+
593
+ _CI_SH_TEMPLATE = """\
594
+ #!/usr/bin/env bash
595
+ # Architectural CI check. Generated by `cdec`.
596
+ # One command: every rule in .cdec/rules.yaml, one exit code.
597
+ set -euo pipefail
598
+
599
+ python -m code_constraints.cli check --config .cdec --source {source}
600
+
601
+ echo "code-constraints checks passed."
602
+ """