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,67 @@
1
+ /*
2
+ No-op architectural-rule tags for code-constraints (Odin).
3
+
4
+ Odin's `@(...)` attributes are a closed set — the compiler rejects any attribute
5
+ it does not know ("unknown attribute"), so a no-op `@(cdec_sealed)` would fail to
6
+ build. Tags are therefore written as namespaced annotation comments placed
7
+ directly above the declaration, in the slot an attribute would occupy. The
8
+ `@cdec` prefix is the namespace, so an unrelated comment can never false-match.
9
+
10
+ //@cdec sealed
11
+ //@cdec layer("domain")
12
+ Invoice :: struct {
13
+ id: string,
14
+ total: f64,
15
+ }
16
+
17
+ //@cdec no_instantiation(allow = ["Builder"])
18
+ summarise :: proc(inv: ^Invoice) -> string { ... }
19
+
20
+ //@cdec locked(reason = "agreed settlement sequence")
21
+ settle :: proc(inv: ^Invoice) -> f64 { ... }
22
+
23
+ Annotation arguments use call syntax: positional (`layer("domain")`) or named
24
+ with `=` (`locked(reason = "why", owner = "ann")`). Lists use brackets:
25
+ `no_instantiation(allow = ["Builder"])`. A bare tag needs no parentheses.
26
+
27
+ Odin has no methods-inside-structs, so code-constraints models a procedure as an
28
+ *operation of* a struct when its first parameter is that struct or a pointer to
29
+ it (`proc(inv: ^Invoice, ...)`). Tag such a procedure to constrain that operation.
30
+
31
+ Enforcement happens out-of-band via `cdec check` (drift), `cdec enforce`
32
+ (implementation conformance) and `cdec lock` (implementation freeze). The
33
+ annotations do nothing at compile time or runtime.
34
+
35
+ This file additionally declares each tag as a no-op procedure, for code that
36
+ prefers an explicit call to a comment, and as the canonical in-repo reference for
37
+ the tag vocabulary. The parser reads the annotation comments, not these calls.
38
+ */
39
+ package cdec_rules
40
+
41
+ // Forbid constructing objects in the tagged struct's operations or procedure
42
+ // body. Option: `allow` — type names that may still be constructed.
43
+ no_instantiation :: proc(subject: rawptr = nil) {}
44
+
45
+ // Declare the tagged procedure free of side effects. Body analysis is deferred;
46
+ // the tag is still captured, visualised and frozen against removal.
47
+ no_side_effects :: proc(subject: rawptr = nil) {}
48
+
49
+ // Forbid embedding the tagged struct as a `using` base (no subtyping).
50
+ sealed :: proc(subject: rawptr = nil) {}
51
+
52
+ // Forbid reassigning the tagged struct's fields after construction.
53
+ immutable :: proc(subject: rawptr = nil) {}
54
+
55
+ // Mark the tagged struct/procedure as the designated constructor of the types in
56
+ // `creates`; constructing those types elsewhere is forbidden.
57
+ factory :: proc(subject: rawptr = nil) {}
58
+
59
+ // Freeze the tagged implementation. `cdec lock set` records the element's
60
+ // normalised AST digest in `.cdec/locks.yaml`; any later semantic change to the
61
+ // body — or removal of the tag — fails `cdec lock check`. Reformatting, moving
62
+ // the declaration, and editing comments do not trip the lock.
63
+ // Options: `reason`, `owner`.
64
+ locked :: proc(subject: rawptr = nil) {}
65
+
66
+ // Assign the struct to an architectural layer for dependency-direction checks.
67
+ layer :: proc(subject: rawptr = nil) {}
@@ -0,0 +1,94 @@
1
+ """No-op architectural-rule decorators for code-constraints.
2
+
3
+ Import these to tag classes and methods with architectural constraints. They do
4
+ nothing at runtime — they exist so tagged code still imports/runs, and so the
5
+ UML parser can recognise the tags (it only treats a decorator as a rule when its
6
+ base name was imported from this module). Enforcement happens out-of-band via
7
+ `cdec check` (drift) and `cdec enforce` (implementation conformance).
8
+
9
+ from cdec_rules import no_instantiation, factory, sealed, immutable, layer, locked
10
+
11
+ @sealed
12
+ @layer("domain")
13
+ class Order: ...
14
+
15
+ class OrderService:
16
+ @no_instantiation(allow=["list", "dict"])
17
+ def total(self): ...
18
+
19
+ @locked(reason="agreed settlement sequence")
20
+ def settle(self): ...
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ from typing import Any, Callable, TypeVar
26
+
27
+ _T = TypeVar("_T")
28
+
29
+
30
+ def _passthrough(obj: _T) -> _T:
31
+ return obj
32
+
33
+
34
+ def _decorator_factory(*_args: Any, **_kwargs: Any) -> Callable[[_T], _T]:
35
+ return _passthrough
36
+
37
+
38
+ def no_instantiation(*args: Any, **kwargs: Any):
39
+ """Forbid constructing objects in the tagged class/method body.
40
+
41
+ `allow` (list[str]): type names that may still be instantiated (e.g.
42
+ collections like "list", "dict")."""
43
+ # Support both bare `@no_instantiation` and `@no_instantiation(allow=[...])`.
44
+ if len(args) == 1 and not kwargs and callable(args[0]):
45
+ return args[0]
46
+ return _passthrough
47
+
48
+
49
+ def no_side_effects(*args: Any, **kwargs: Any):
50
+ """Declare the tagged operation free of side effects."""
51
+ if len(args) == 1 and not kwargs and callable(args[0]):
52
+ return args[0]
53
+ return _passthrough
54
+
55
+
56
+ def factory(*args: Any, **kwargs: Any):
57
+ """Mark the tagged class/method as the designated factory for the types in
58
+ `creates` (list[str]); constructing those types elsewhere is forbidden."""
59
+ if len(args) == 1 and not kwargs and callable(args[0]):
60
+ return args[0]
61
+ return _passthrough
62
+
63
+
64
+ def layer(*args: Any, **kwargs: Any):
65
+ """Assign the class to an architectural layer, e.g. `@layer("domain")`."""
66
+ if len(args) == 1 and not kwargs and callable(args[0]):
67
+ return args[0]
68
+ return _passthrough
69
+
70
+
71
+ def locked(*args: Any, **kwargs: Any):
72
+ """Freeze the tagged class/function implementation.
73
+
74
+ The element's normalised AST is digested and recorded in `.cdec/locks.yaml`
75
+ by `cdec lock set`. Any later semantic change to the body — or removal of
76
+ this tag — fails `cdec lock check` (and `cdec check`, which runs it). Moving
77
+ the element around the file, reformatting it, or editing comments does not
78
+ trip the lock: the digest is computed from the AST, not the source text.
79
+
80
+ Optional metadata: `reason` (why it's frozen) and `owner` (who to ask).
81
+ Both are recorded in the lockfile and echoed in violation messages."""
82
+ if len(args) == 1 and not kwargs and callable(args[0]):
83
+ return args[0]
84
+ return _passthrough
85
+
86
+
87
+ def sealed(obj: _T) -> _T:
88
+ """Forbid subclassing the tagged class."""
89
+ return obj
90
+
91
+
92
+ def immutable(obj: _T) -> _T:
93
+ """Forbid reassigning the tagged class's fields after construction."""
94
+ return obj
@@ -0,0 +1,152 @@
1
+ ---
2
+ name: cdec-architecture-loop
3
+ description: Discuss and iterate architectural decisions with code-constraints — author a JSON model of the target architecture, show it in the browser diffed against the code with `cdec propose`, refine it through conversation (the open tab refreshes on every push), then lock it with `cdec reference set` to constrain development, and accept deliberate exceptions afterwards with `cdec exceptions allow`. Use whenever the user wants to plan, review, restructure, or agree on class/package architecture, discuss a refactor's target shape, compare a design against the current code, update the reference architecture, or decide what to do when `cdec check` / the `tag-conformance` rule blocks a change they want to keep.
4
+ ---
5
+
6
+ # UML architecture discussion loop
7
+
8
+ You are driving an interactive architecture review with the **code-constraints** tool. The
9
+ human looks at a live diagram in their browser; you edit a JSON model file and push
10
+ updates. Never paste raw XMI (or the whole JSON) into the chat as the primary review
11
+ medium — the diagram diff *is* the review medium; chat is for reasoning and decisions.
12
+
13
+ ## The loop
14
+
15
+ ```bash
16
+ # 0. Nothing to start manually — `cdec propose` reuses a running `cdec serve`
17
+ # (port 8765) or boots one itself.
18
+
19
+ # 1. Get an editable model (pick ONE starting point):
20
+ cdec parse <src> --lang <lang> --out target.json # start from the code as-is
21
+ cdec convert .cdec/reference.xmi target.json # start from the locked design
22
+
23
+ # 2. Edit target.json to express the proposed architecture (see shape below).
24
+
25
+ # 3. Push for review, pre-filtered to what's under discussion:
26
+ cdec propose target.json --focus pkg.ClassA,pkg.ClassB
27
+ # green = code still needs to grow this; red = proposal removes this.
28
+ # --against reference → diff vs the locked design instead of the code
29
+ # --against none → render the proposal standalone (greenfield)
30
+ # --no-browser → push without opening a tab (tab already open)
31
+
32
+ # 4. Discuss → edit target.json → `cdec propose` again. The open tab refreshes
33
+ # in place (it polls; positions and filters survive). Repeat until agreed.
34
+
35
+ # 5. Lock the agreed design (ONLY after explicit approval from the human):
36
+ cdec reference set target.json # writes .cdec/reference.xmi
37
+
38
+ # 6. Development is now constrained:
39
+ cdec check # exit 1 on structural deviation (CI gate)
40
+ cdec check # drift rules (frozen tags, layers, forbidden refs)
41
+ cdec check # every rule: drift, tags, locks, reference
42
+
43
+ # 7. When a constraint blocks something the design intends to allow:
44
+ cdec exceptions allow V-1A2B3C4D --reason "why this is acceptable"
45
+ ```
46
+
47
+ If `cdec` is not on PATH, use `.venv/Scripts/python.exe -m code_constraints.cli <command>`.
48
+
49
+ ## When the constraints block a legitimate change
50
+
51
+ A locked design that can only say *no* gets switched off. Step 7 is the release valve, and
52
+ picking the right one is itself an architectural decision — say which you chose and why:
53
+
54
+ | The blocked change is… | Do this |
55
+ |---|---|
56
+ | a deliberate change to the **architecture** | edit `target.json` → `cdec propose` → approval → `cdec reference set`. Back to the loop. |
57
+ | a **known, acceptable exception** | `cdec exceptions allow <key> --reason "…"` — the rule keeps protecting everything else. |
58
+ | evidence the **rule is wrong** | change it in `.cdec/rules.yaml`, and justify it. Rare, and never silent. |
59
+
60
+ Every issue leads with a stable key — `V-` (check), `F-` (enforce), `L-` (lock):
61
+
62
+ ```
63
+ - [V-DD3EA5B2] billing.LegacyGateway — billing/legacy.py:12: 'billing.LegacyGateway' is not allowed to reference 'ui.Panel'.
64
+ ```
65
+
66
+ Keys hash *what* an issue is, never where it sits, so they survive reformatting and repeat
67
+ run to run. For a batch: `cdec exceptions review --out review.txt`, mark lines `[ALLOW]` (or
68
+ `[ALLOW: reason]`), then `cdec exceptions patch --file review.txt`. A `cdec check --log-out`
69
+ file is patchable as-is. `cdec exceptions list` / `remove KEY` / `prune` manage what's been
70
+ accepted.
71
+
72
+ - **Always pass a reason.** The waiver is committed and read by a human.
73
+ - **`L-` keys are not waivable.** `cdec exceptions allow` refuses them and prints
74
+ `cdec check --automatic-exceptions locks --target … --force` — a frozen implementation changes only on the human's
75
+ explicit say-so, never on your initiative.
76
+ - Prefer a waiver over adding an `ignore:` glob or dropping a rule: one reviewable line
77
+ about one element, versus turning the rule off for everything it would have caught.
78
+
79
+ ## How to run the conversation
80
+
81
+ - **One artifact**: keep a single `target.json` on disk and evolve it across the whole
82
+ discussion. Don't fork variants unless the human asks to compare alternatives.
83
+ - **Small pushes, narrow focus**: push after each meaningful change and set `--focus`
84
+ to the 2–6 classes the current question is about, so the human isn't hunting
85
+ through the whole canvas. Change focus as the discussion moves.
86
+ - **Narrate the delta, not the model**: after each push, say in one or two sentences
87
+ what changed since the last push and what you want the human to look at.
88
+ - **Ask, don't lock**: `cdec reference set` is the commitment point — it changes what
89
+ every developer's `cdec check` / `cdec check` fails on. Run it only after the
90
+ human explicitly approves the *current* model (approval of an earlier iteration
91
+ doesn't carry over).
92
+ - After locking, offer the enforcement follow-ups: rule tags (`@layer`, `@sealed`,
93
+ `@immutable`, `@factory`, `@no_instantiation` — Python/C# only) and `.cdec/rules.yaml`
94
+ lint rules (`forbidden-package-references`, `no-cyclic-package-dependencies`, … —
95
+ all languages). Every rule and tag, with options and pass/fail examples, is catalogued in
96
+ `docs/RULES_CATALOGUE.md`.
97
+ - **Say how each new rule handles what already violates it.** Turning on a rule that fires
98
+ on twenty existing classes with no stated way through is how a team ends up deleting the
99
+ rule. Either fix them, waive them individually with reasons, or `cdec check
100
+ --automatic-exceptions rules` to grandfather the lot — but name the choice.
101
+
102
+ ## The JSON model shape
103
+
104
+ Same document the web editor uses; snake_case; produce a valid skeleton with
105
+ `cdec parse … --out x.json` and edit it rather than writing from scratch when possible.
106
+
107
+ ```json
108
+ {
109
+ "source_language": "python",
110
+ "packages": [
111
+ {
112
+ "name": "billing",
113
+ "qualified_name": "billing",
114
+ "classes": [
115
+ {
116
+ "name": "Invoice",
117
+ "qualified_name": "billing.Invoice",
118
+ "kind": "class",
119
+ "attributes": [{ "name": "total", "type": "float" }],
120
+ "operations": [
121
+ { "name": "pay", "parameters": [], "return_type": "bool" }
122
+ ],
123
+ "bases": ["billing.Document"],
124
+ "description": "why this class exists"
125
+ }
126
+ ],
127
+ "sub_packages": []
128
+ }
129
+ ],
130
+ "associations": []
131
+ }
132
+ ```
133
+
134
+ Notes:
135
+ - `kind` is one of `class|interface|abstract|enum|struct|record|static`; `bases` holds
136
+ qualified names and draws inheritance edges.
137
+ - An attribute `type` naming a project class draws an association edge automatically
138
+ (collection wrappers like `list[X]` / `List<X>` are unwrapped).
139
+ - Omitted optional fields default sensibly; unknown classes referenced in `bases`
140
+ render as external placeholder nodes.
141
+ - `.json` and `.xmi` are interchangeable in every command; `cdec convert` translates.
142
+
143
+ ## Pitfalls
144
+
145
+ - The viewer runs on **port 8765** (8000 is reserved by Windows http.sys on this machine).
146
+ - Repeated `cdec propose` pushes for the same `(source path, lang)` hit the same
147
+ project, which is what makes the open tab refresh — don't vary `--source` between
148
+ iterations of one discussion.
149
+ - Diff orientation: the proposal is the NEW side. If the human expects "what would I
150
+ have to delete", remind them red = present in code, absent from proposal.
151
+ - For TypeScript/Svelte, rule *tags* don't exist — encode constraints as package-level
152
+ lint rules in `.cdec/rules.yaml` instead, and say so.
@@ -0,0 +1,118 @@
1
+ """Dependency fingerprinting for the install/update flows.
2
+
3
+ Each heavy install step (pip install, npm install, npm run build) hashes the
4
+ files that determine its output and stores that hash in a *stamp file*. On the
5
+ next run the step is skipped when the freshly-computed hash matches the stamp —
6
+ so re-running `cdec update`, the standalone installers, or `scripts/bootstrap.*`
7
+ no longer reinstalls everything when nothing relevant has changed.
8
+
9
+ This module is intentionally pure-stdlib so it can be used two ways:
10
+
11
+ * imported by ``code_constraints.cli.update`` (``from code_constraints.cli import depstamp``), and
12
+ * run by file path from the shell installers
13
+ (``python /path/to/depstamp.py check --stamp <file> <inputs...>``) — which
14
+ works even before the package is pip-installed, i.e. on a first install.
15
+
16
+ A directory passed as an input is expanded to every file beneath it (sorted),
17
+ so a source tree like ``frontend/src`` produces a stable, content-addressed
18
+ hash that changes whenever any file under it is added, removed, or edited.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import argparse
24
+ import hashlib
25
+ import sys
26
+ from pathlib import Path
27
+
28
+ # Bumped if the hashing scheme ever changes, to invalidate old stamps.
29
+ _SCHEME = "cdec-depstamp-v1"
30
+
31
+
32
+ def _iter_files(path: Path):
33
+ """Yield the files contributing to the fingerprint for ``path``.
34
+
35
+ A file yields itself; a directory yields every file beneath it (recursively).
36
+ Missing paths yield nothing — a manifest that doesn't exist simply doesn't
37
+ contribute, which keeps the hash defined even on a partial checkout.
38
+ """
39
+ if path.is_dir():
40
+ yield from (p for p in sorted(path.rglob("*")) if p.is_file())
41
+ elif path.is_file():
42
+ yield path
43
+
44
+
45
+ def fingerprint(inputs: list[Path], *, base: Path | None = None) -> str:
46
+ """Return a SHA256 hex digest over the contents of ``inputs``.
47
+
48
+ The digest folds in each contributing file's path (relative to ``base`` when
49
+ given, so the hash is stable regardless of the absolute checkout location)
50
+ and its bytes. File order is normalised by sorting, so the result depends
51
+ only on the set of files and their contents.
52
+ """
53
+ h = hashlib.sha256()
54
+ h.update(_SCHEME.encode())
55
+
56
+ entries: list[tuple[str, Path]] = []
57
+ for raw in inputs:
58
+ for f in _iter_files(raw):
59
+ try:
60
+ rel = f.relative_to(base) if base else f
61
+ except ValueError:
62
+ rel = f
63
+ entries.append((rel.as_posix(), f))
64
+
65
+ for rel_str, f in sorted(entries, key=lambda e: e[0]):
66
+ h.update(rel_str.encode())
67
+ h.update(b"\0")
68
+ h.update(f.read_bytes())
69
+ h.update(b"\0")
70
+
71
+ return h.hexdigest()
72
+
73
+
74
+ def is_changed(stamp: Path, inputs: list[Path], *, base: Path | None = None) -> bool:
75
+ """True if the step should run: stamp missing, unreadable, or hash differs."""
76
+ if not stamp.is_file():
77
+ return True
78
+ try:
79
+ previous = stamp.read_text(encoding="utf-8").strip()
80
+ except OSError:
81
+ return True
82
+ return previous != fingerprint(inputs, base=base)
83
+
84
+
85
+ def write_stamp(stamp: Path, inputs: list[Path], *, base: Path | None = None) -> None:
86
+ """Record the current fingerprint of ``inputs`` into ``stamp``."""
87
+ stamp.parent.mkdir(parents=True, exist_ok=True)
88
+ stamp.write_text(fingerprint(inputs, base=base), encoding="utf-8")
89
+
90
+
91
+ def _main(argv: list[str] | None = None) -> int:
92
+ parser = argparse.ArgumentParser(description="Dependency fingerprint stamps.")
93
+ sub = parser.add_subparsers(dest="command", required=True)
94
+
95
+ for name in ("check", "write"):
96
+ p = sub.add_parser(name)
97
+ p.add_argument("--stamp", required=True, type=Path, help="Stamp file path.")
98
+ p.add_argument(
99
+ "--base",
100
+ type=Path,
101
+ default=None,
102
+ help="Base dir to relativise input paths against (for stable hashing).",
103
+ )
104
+ p.add_argument("inputs", nargs="+", type=Path, help="Files/dirs to fingerprint.")
105
+
106
+ args = parser.parse_args(argv)
107
+
108
+ if args.command == "check":
109
+ # Exit 0 = changed (run the step); exit 1 = unchanged (skip). This maps
110
+ # onto shell `if` truthiness so callers can write `if python ... check`.
111
+ return 0 if is_changed(args.stamp, args.inputs, base=args.base) else 1
112
+
113
+ write_stamp(args.stamp, args.inputs, base=args.base)
114
+ return 0
115
+
116
+
117
+ if __name__ == "__main__":
118
+ sys.exit(_main())
@@ -0,0 +1,77 @@
1
+ """Heuristic source-language detection for `cdec serve parse`.
2
+
3
+ When the user doesn't pass `--lang`, we guess the project's language from the
4
+ mix of file extensions in the tree. The rule (per spec):
5
+
6
+ 1. ANY `.svelte` file present -> "svelte" (Svelte projects also carry .ts,
7
+ so .svelte wins over typescript).
8
+ 2. otherwise the language with the most files among python/.py,
9
+ csharp/.cs, typescript/.ts|.tsx, odin/.odin, lua/.lua, julia/.jl.
10
+ 3. nothing recognised -> None (the caller asks for --lang).
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import os
16
+ from pathlib import Path
17
+ from typing import Optional
18
+
19
+ # Directories never worth walking for language detection.
20
+ IGNORE_DIRS = frozenset(
21
+ {
22
+ ".git",
23
+ ".venv",
24
+ "venv",
25
+ "node_modules",
26
+ "dist",
27
+ "build",
28
+ ".svelte-kit",
29
+ "obj",
30
+ "bin",
31
+ "__pycache__",
32
+ ".cdec_cache",
33
+ }
34
+ )
35
+
36
+ # Extension -> language. `.svelte` is handled as an override, not via counting.
37
+ EXT_TO_LANG = {
38
+ ".py": "python",
39
+ ".cs": "csharp",
40
+ ".ts": "typescript",
41
+ ".tsx": "typescript",
42
+ ".odin": "odin",
43
+ ".lua": "lua",
44
+ ".jl": "julia",
45
+ }
46
+
47
+ # Deterministic tie-break order when counts are equal.
48
+ _PRIORITY = ("python", "csharp", "typescript", "odin", "lua", "julia")
49
+
50
+
51
+ def detect_language(root: Path) -> Optional[str]:
52
+ """Return the detected language for the tree at `root`, or None."""
53
+ counts: dict[str, int] = {}
54
+ saw_svelte = False
55
+
56
+ for dirpath, dirnames, filenames in os.walk(root):
57
+ # Prune ignored directories in place so os.walk doesn't descend.
58
+ dirnames[:] = [d for d in dirnames if d not in IGNORE_DIRS]
59
+ for name in filenames:
60
+ ext = os.path.splitext(name)[1].lower()
61
+ if ext == ".svelte":
62
+ saw_svelte = True
63
+ continue
64
+ lang = EXT_TO_LANG.get(ext)
65
+ if lang is not None:
66
+ counts[lang] = counts.get(lang, 0) + 1
67
+
68
+ if saw_svelte:
69
+ return "svelte"
70
+ if not counts:
71
+ return None
72
+ best = max(counts.values())
73
+ for lang in _PRIORITY:
74
+ if counts.get(lang, 0) == best:
75
+ return lang
76
+ # Fallback (shouldn't happen given _PRIORITY covers EXT_TO_LANG values).
77
+ return max(counts, key=lambda k: counts[k])