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,41 @@
1
+ """Rule: cap the number of distinct outgoing class references on any class."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Iterable
6
+
7
+ from code_constraints.core.model import DiffStatus
8
+ from code_constraints.lint.rules import register
9
+ from code_constraints.lint.rules.base import Rule, RuleContext, Violation
10
+
11
+
12
+ @register("max-class-fanout")
13
+ class MaxClassFanout(Rule):
14
+ """Options:
15
+ limit: int, maximum allowed distinct outgoing references (default 10)
16
+ """
17
+
18
+ def check(self, ctx: RuleContext) -> Iterable[Violation]:
19
+ try:
20
+ limit = int(self.options.get("limit", 10))
21
+ except (TypeError, ValueError) as exc:
22
+ raise ValueError(f"max-class-fanout: 'limit' must be an integer: {exc}") from exc
23
+ for cls in ctx.project.iter_classes():
24
+ if cls.status == DiffStatus.REMOVED:
25
+ continue
26
+ if self.is_ignored(cls.qualified_name):
27
+ continue
28
+ if self.scope == "diff" and ctx.has_diff:
29
+ if cls.status not in (DiffStatus.ADDED, DiffStatus.CHANGED):
30
+ continue
31
+ fanout = len(ctx.outgoing_refs.get(cls.qualified_name, set()))
32
+ if fanout <= limit:
33
+ continue
34
+ yield self.emit(
35
+ qualified_name=cls.qualified_name,
36
+ message=self.message_for(
37
+ qualified_name=cls.qualified_name, fanout=fanout, limit=limit
38
+ )
39
+ or f"Class '{cls.qualified_name}' has fanout {fanout} (limit: {limit}).",
40
+ location=cls.location,
41
+ )
@@ -0,0 +1,27 @@
1
+ """Rule: forbid adding new classes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Iterable
6
+
7
+ from code_constraints.core.model import DiffStatus
8
+ from code_constraints.lint.rules import register
9
+ from code_constraints.lint.rules.base import Rule, RuleContext, Violation, changed_classes
10
+
11
+
12
+ @register("no-new-classes")
13
+ class NoNewClasses(Rule):
14
+ default_scope = "diff"
15
+
16
+ def check(self, ctx: RuleContext) -> Iterable[Violation]:
17
+ if not ctx.has_diff:
18
+ return
19
+ for cls in changed_classes(ctx, only_status=DiffStatus.ADDED):
20
+ if self.is_ignored(cls.qualified_name):
21
+ continue
22
+ yield self.emit(
23
+ qualified_name=cls.qualified_name,
24
+ message=self.message_for(qualified_name=cls.qualified_name)
25
+ or f"New class '{cls.qualified_name}' was added.",
26
+ location=cls.location,
27
+ )
@@ -0,0 +1,27 @@
1
+ """Rule: forbid removing classes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Iterable
6
+
7
+ from code_constraints.core.model import DiffStatus
8
+ from code_constraints.lint.rules import register
9
+ from code_constraints.lint.rules.base import Rule, RuleContext, Violation, changed_classes
10
+
11
+
12
+ @register("no-removed-classes")
13
+ class NoRemovedClasses(Rule):
14
+ default_scope = "diff"
15
+
16
+ def check(self, ctx: RuleContext) -> Iterable[Violation]:
17
+ if not ctx.has_diff:
18
+ return
19
+ for cls in changed_classes(ctx, only_status=DiffStatus.REMOVED):
20
+ if self.is_ignored(cls.qualified_name):
21
+ continue
22
+ yield self.emit(
23
+ qualified_name=cls.qualified_name,
24
+ message=self.message_for(qualified_name=cls.qualified_name)
25
+ or f"Class '{cls.qualified_name}' was removed.",
26
+ location=cls.location,
27
+ )
@@ -0,0 +1,111 @@
1
+ """Rule: the code must not deviate structurally from the reference model.
2
+
3
+ The adapter for the reference gate (`code_constraints.reference`). Where the
4
+ other rules are scalpels — each one enforcing exactly the law you wrote down —
5
+ this one is a wall: *any* structural difference from the committed
6
+ `.cdec/reference.xmi` fails.
7
+
8
+ It earns its place beside `frozen-members` because the two sit on different
9
+ comparison engines. The diff engine matches members by signature and only calls
10
+ a matched member changed when its *rule tags* differ, so it is blind to
11
+ visibility changes (`public` -> `private`), modifier changes (`static`,
12
+ `abstract`, `readonly`) and class-kind changes. The reference comparator walks
13
+ both models field by field and catches all of them. A pull request that flips a
14
+ public method to private passes `frozen-members` and fails this.
15
+
16
+ Generating the reference is `cdec check --automatic-exceptions reference`, which
17
+ is what `accept_current_state` below does: re-snapshot the source and commit the
18
+ new model in the same pull request, so the architectural delta is reviewable.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from pathlib import Path
24
+ from typing import Iterable
25
+
26
+ from code_constraints.lint.rules import register
27
+ from code_constraints.lint.rules.base import Rule, RuleContext, RuleSkipped, Violation
28
+
29
+
30
+ @register("reference-architecture")
31
+ class ReferenceArchitecture(Rule):
32
+ """Options:
33
+ reference: path to the reference model, relative to the project root
34
+ (default: the project's `reference:` setting, else
35
+ `.cdec/reference.xmi`).
36
+ categories: optional list[str] of deviation categories to enforce, e.g.
37
+ ["class-removed", "operation-changed"]. Default: all of them.
38
+ """
39
+
40
+ supports_auto_accept = True
41
+
42
+ def reference_path(self, ctx: RuleContext) -> Path:
43
+ configured = self.options.get("reference")
44
+ if configured:
45
+ base = ctx.config_dir.parent if ctx.config_dir else Path(".")
46
+ path = Path(str(configured))
47
+ return path if path.is_absolute() else (base / path)
48
+ if ctx.reference_path is not None:
49
+ return ctx.reference_path
50
+ raise RuleSkipped("no reference model configured")
51
+
52
+ def check(self, ctx: RuleContext) -> Iterable[Violation]:
53
+ path = self.reference_path(ctx)
54
+ if not path.is_file():
55
+ raise RuleSkipped(
56
+ f"reference model not found: {path}. Snapshot one with "
57
+ f"`cdec check --automatic-exceptions reference`."
58
+ )
59
+ from code_constraints.core.model_io import load_model
60
+ from code_constraints.reference import compare_to_reference
61
+
62
+ if ctx.source is None or not ctx.language:
63
+ raise RuleSkipped("no source tree resolved")
64
+
65
+ # The rule compares against the *current* parse, never the annotated
66
+ # diff project: a removed class is still present (marked REMOVED) in the
67
+ # annotated model, and the gate must see it as gone.
68
+ from code_constraints.lint.pipeline import parse_source
69
+
70
+ try:
71
+ deviations = compare_to_reference(
72
+ load_model(path), parse_source(ctx.source, ctx.language)
73
+ )
74
+ except ValueError as exc:
75
+ raise RuleSkipped(str(exc)) from exc
76
+
77
+ only = {str(c) for c in (self.options.get("categories") or [])}
78
+ for deviation in deviations:
79
+ if only and deviation.category not in only:
80
+ continue
81
+ if self.is_ignored(deviation.qualified_name):
82
+ continue
83
+ yield Violation(
84
+ rule_id=self.rule_id,
85
+ severity=self.severity,
86
+ qualified_name=deviation.qualified_name,
87
+ message=self.message_for(
88
+ qualified_name=deviation.qualified_name,
89
+ category=deviation.category,
90
+ member=deviation.member or "",
91
+ message=deviation.message,
92
+ )
93
+ or deviation.message,
94
+ signature=deviation.member or deviation.category,
95
+ key_engine="reference",
96
+ key_rule=deviation.category,
97
+ )
98
+
99
+ def accept_current_state(self, ctx: RuleContext, *, force: bool = False) -> list[str]:
100
+ """Re-snapshot the reference from the current source."""
101
+ if ctx.source is None or not ctx.language:
102
+ raise RuleSkipped("no source tree resolved")
103
+ from code_constraints.core.model_io import save_model
104
+ from code_constraints.lint.pipeline import parse_source
105
+
106
+ path = self.reference_path(ctx)
107
+ project = parse_source(ctx.source, ctx.language)
108
+ path.parent.mkdir(parents=True, exist_ok=True)
109
+ save_model(project, path)
110
+ n_classes = sum(1 for _ in project.iter_classes())
111
+ return [f" wrote {path} ({n_classes} class(es) snapshotted from {ctx.source})"]
@@ -0,0 +1,71 @@
1
+ """Rule: enforce a naming pattern on classes inheriting from a base/interface.
2
+
3
+ Example: every implementation of `IFactory` must end in `Factory`.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import re
9
+ from typing import Iterable
10
+
11
+ from code_constraints.core.model import DiffStatus
12
+ from code_constraints.lint.rules import register
13
+ from code_constraints.lint.rules.base import Rule, RuleContext, Violation, match_any_glob
14
+
15
+
16
+ @register("subclass-naming")
17
+ class SubclassNaming(Rule):
18
+ """Options:
19
+ base: glob pattern matched against entries in `Class.bases`
20
+ (e.g. "IFactory", "*.IFactory")
21
+ name_pattern: regex applied to the class name (full match)
22
+ """
23
+
24
+ def check(self, ctx: RuleContext) -> Iterable[Violation]:
25
+ base_pattern = self.options.get("base")
26
+ name_pattern = self.options.get("name_pattern")
27
+ if not base_pattern or not name_pattern:
28
+ return
29
+ try:
30
+ regex = re.compile(name_pattern)
31
+ except re.error as exc: # pragma: no cover - config-validation territory
32
+ raise ValueError(
33
+ f"subclass-naming: invalid name_pattern regex {name_pattern!r}: {exc}"
34
+ ) from exc
35
+
36
+ base_globs = base_pattern if isinstance(base_pattern, list) else [base_pattern]
37
+
38
+ for cls in ctx.project.iter_classes():
39
+ if cls.status == DiffStatus.REMOVED:
40
+ continue
41
+ if self.is_ignored(cls.qualified_name):
42
+ continue
43
+ if self.scope == "diff" and ctx.has_diff:
44
+ if cls.status not in (DiffStatus.ADDED, DiffStatus.CHANGED):
45
+ continue
46
+ # Match if any of the class's bases matches the configured base.
47
+ # We match both the literal base text and its short name (last
48
+ # dotted segment) so callers can write either "IFactory" or
49
+ # "myapp.factories.IFactory".
50
+ matched = False
51
+ for base in cls.bases:
52
+ short = base.split(".")[-1]
53
+ if match_any_glob(base, base_globs) or match_any_glob(short, base_globs):
54
+ matched = True
55
+ break
56
+ if not matched:
57
+ continue
58
+ if regex.fullmatch(cls.name):
59
+ continue
60
+ yield self.emit(
61
+ qualified_name=cls.qualified_name,
62
+ message=self.message_for(
63
+ qualified_name=cls.qualified_name,
64
+ name=cls.name,
65
+ base=base_pattern,
66
+ pattern=name_pattern,
67
+ )
68
+ or f"Class '{cls.qualified_name}' inherits from {base_pattern!r} "
69
+ f"but its name does not match /{name_pattern}/.",
70
+ location=cls.location,
71
+ )
@@ -0,0 +1,76 @@
1
+ """Rule: the implementation must honour the constraint tags written on it.
2
+
3
+ This is the adapter for the conformance engine (`code_constraints.enforce`,
4
+ "Engine B"). Every other rule in this package reads only the model; this one
5
+ re-parses the source and inspects method bodies, which is why it needs
6
+ `ctx.source` and `ctx.language`.
7
+
8
+ The engine stays an independent package with its own result type — the rule
9
+ imports it lazily and translates `Finding`s into `Violation`s. What the
10
+ `rules.yaml` entry buys is uniformity: tag conformance is now configured,
11
+ reported, ignored and excepted exactly like every other law in the file, instead
12
+ of through a second command with a second report and a second exit code.
13
+
14
+ Violations key under the `enforce` engine (`F-…`), not under the rules entry, so
15
+ renaming the entry never invalidates a granted exception.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from typing import Iterable
21
+
22
+ from code_constraints.core.model import SourceLocation
23
+ from code_constraints.lint.rules import register
24
+ from code_constraints.lint.rules.base import Rule, RuleContext, RuleSkipped, Violation
25
+
26
+ #: Languages whose parsers recognise constraint tags. The other supported
27
+ #: languages parse and diff fine but have no tag syntax, so a `tag-conformance`
28
+ #: rule there would report "clean" while checking nothing.
29
+ TAGGED_LANGUAGES = ("python", "csharp", "odin", "lua", "julia")
30
+
31
+
32
+ @register("tag-conformance")
33
+ class TagConformance(Rule):
34
+ """Options:
35
+ rules: optional list[str] of catalog ids to check (default: all of them),
36
+ e.g. ["no-instantiation", "factory"] to adopt one tag at a time.
37
+ """
38
+
39
+ def check(self, ctx: RuleContext) -> Iterable[Violation]:
40
+ if ctx.source is None or not ctx.language:
41
+ raise RuleSkipped("no source tree resolved")
42
+ if ctx.language not in TAGGED_LANGUAGES:
43
+ raise RuleSkipped(
44
+ f"{ctx.language} has no constraint-tag syntax; supported: "
45
+ f"{', '.join(TAGGED_LANGUAGES)}"
46
+ )
47
+ from code_constraints.enforce import enforce as run_enforce
48
+
49
+ only = {str(r) for r in (self.options.get("rules") or [])}
50
+ findings = run_enforce(ctx.source, ctx.language)
51
+ for finding in findings:
52
+ if only and finding.rule not in only:
53
+ continue
54
+ if self.is_ignored(finding.qualified_name):
55
+ continue
56
+ location = (
57
+ SourceLocation(file=finding.file, start_line=finding.line, end_line=finding.line)
58
+ if finding.file
59
+ else None
60
+ )
61
+ yield Violation(
62
+ rule_id=self.rule_id,
63
+ severity=self.severity,
64
+ qualified_name=finding.qualified_name,
65
+ message=self.message_for(
66
+ qualified_name=finding.qualified_name,
67
+ rule=finding.rule,
68
+ detail=finding.detail,
69
+ message=finding.message,
70
+ )
71
+ or finding.message,
72
+ location=location,
73
+ signature=finding.detail,
74
+ key_engine="enforce",
75
+ key_rule=finding.rule,
76
+ )
@@ -0,0 +1,73 @@
1
+ """The implementation-freeze engine (Engine C).
2
+
3
+ Freezes the *body* of a class or function so it cannot change without an
4
+ explicit, reviewable re-baseline. Identity is AST-derived, not line-based, so
5
+ code added above a locked element never trips it.
6
+
7
+ Driven by the `implementation-locks` rule type in `.cdec/rules.yaml`, which is
8
+ what `cdec check` runs. This package stays independent of the other engines:
9
+ the rule is a thin adapter over the entry points below.
10
+
11
+ Public surface:
12
+
13
+ from code_constraints.lock import (
14
+ LockOptions, check_locks, update_locks, collect_targets,
15
+ load_locks, write_locks, format_report,
16
+ )
17
+ """
18
+
19
+ from code_constraints.lock.engine import (
20
+ BYPASS_ENV,
21
+ BYPASS_REASON_ENV,
22
+ LockOptions,
23
+ UnsupportedLockLanguage,
24
+ UpdateResult,
25
+ check_locks,
26
+ collect_targets,
27
+ is_locked_target,
28
+ resolve_entries_for_removal,
29
+ update_locks,
30
+ )
31
+ from code_constraints.lock.model import (
32
+ LOCK_RULE,
33
+ LockEntry,
34
+ LockReport,
35
+ LockTarget,
36
+ LockViolation,
37
+ format_report,
38
+ match_any_glob,
39
+ report_to_json,
40
+ )
41
+ from code_constraints.lock.store import (
42
+ LOCKFILE_VERSION,
43
+ LOCKS_FILENAME,
44
+ LockfileError,
45
+ load_locks,
46
+ write_locks,
47
+ )
48
+
49
+ __all__ = [
50
+ "BYPASS_ENV",
51
+ "BYPASS_REASON_ENV",
52
+ "LOCKFILE_VERSION",
53
+ "LOCKS_FILENAME",
54
+ "LOCK_RULE",
55
+ "LockEntry",
56
+ "LockOptions",
57
+ "LockReport",
58
+ "LockTarget",
59
+ "LockViolation",
60
+ "LockfileError",
61
+ "UnsupportedLockLanguage",
62
+ "UpdateResult",
63
+ "check_locks",
64
+ "collect_targets",
65
+ "format_report",
66
+ "is_locked_target",
67
+ "load_locks",
68
+ "match_any_glob",
69
+ "report_to_json",
70
+ "resolve_entries_for_removal",
71
+ "update_locks",
72
+ "write_locks",
73
+ ]