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,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])
|