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,523 @@
1
+ """Parse a Svelte 5 project (a directory of .svelte + .ts files) into a
2
+ `code_constraints.core.Project`.
3
+
4
+ Each `.svelte` file is modelled as a single UML class — the component itself.
5
+ The script block (between `<script>` and `</script>`, including `lang="ts"`
6
+ and `context="module"` variants) is fed to the TypeScript parser, with the
7
+ following Svelte-specific twists:
8
+
9
+ - Top-level `let` / `const` declarations inside the script become attributes
10
+ of the component class. `let foo = $state(...)`, `let { ... } = $props()`,
11
+ and `let bar = $derived(...)` are all recognised — the rune helpers are
12
+ captured in the attribute's default so it's obvious from the diagram what
13
+ kind of reactive value it is.
14
+ - Top-level `function` declarations become operations of the component class.
15
+ - Any *class* / *interface* / *enum* / *type alias* defined inside the script
16
+ block flows through to the package as a regular TypeScript class.
17
+ - Imports are tracked so a component reference in the markup
18
+ (`<Foo prop={x} />`) can be resolved to the imported component's qualified
19
+ name. Resolved component dependencies are surfaced as attributes whose type
20
+ matches the imported component's class name — that way the existing
21
+ `resolve_association` machinery wires up the association edges without any
22
+ Svelte-specific code in the renderer.
23
+
24
+ Plain `.ts` files alongside `.svelte` files are also parsed (using
25
+ `code_constraints.typescript.parser`) so a component's helper module shows up too.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import re
31
+ from pathlib import Path
32
+
33
+ from tree_sitter import Node
34
+
35
+ from code_constraints.core.model import (
36
+ Attribute,
37
+ Class,
38
+ Operation,
39
+ Package,
40
+ Parameter,
41
+ Project,
42
+ SourceLocation,
43
+ Visibility,
44
+ )
45
+ from code_constraints.typescript.parser import (
46
+ _PARSER_TS,
47
+ _ensure_package,
48
+ _qualified_package_name,
49
+ _strip_type_annotation,
50
+ _text,
51
+ parse_module_body,
52
+ )
53
+ from code_constraints.typescript.parser import _parameter_from as _ts_parameter_from
54
+ from code_constraints.typescript import parse_project as _ts_parse_project # noqa: F401 (kept for clarity)
55
+ from code_constraints.typescript.parser import _iter_ts_files, _should_skip
56
+
57
+
58
+ _SCRIPT_RE = re.compile(
59
+ r"<script\b([^>]*)>(.*?)</script\s*>", re.DOTALL | re.IGNORECASE
60
+ )
61
+ _STYLE_RE = re.compile(r"<style\b[^>]*>.*?</style\s*>", re.DOTALL | re.IGNORECASE)
62
+ _COMMENT_RE = re.compile(r"<!--.*?-->", re.DOTALL)
63
+ # Component tag: opening `<` then a capital letter and JS-identifier chars,
64
+ # then either whitespace, a slash, or the closing `>`.
65
+ _COMPONENT_TAG_RE = re.compile(r"<([A-Z][A-Za-z0-9_$.]*)\b")
66
+
67
+ # `Foo.svelte` is the convention; `index.svelte` -> use the parent directory
68
+ # name as the component name.
69
+ _SVELTE_SUFFIX = ".svelte"
70
+
71
+
72
+ def parse_project(root: str | Path) -> Project:
73
+ root_path = Path(root).resolve()
74
+ if not root_path.is_dir():
75
+ raise ValueError(f"not a directory: {root_path}")
76
+
77
+ project = Project(source_language="svelte", root_path=str(root_path))
78
+ package_index: dict[str, Package] = {}
79
+
80
+ # First, parse every plain .ts / .tsx file so component imports can
81
+ # resolve to project classes regardless of file order.
82
+ for ts_file in sorted(_iter_ts_files(root_path)):
83
+ _parse_ts_file(ts_file, root_path, project, package_index)
84
+
85
+ # Then ingest .svelte files.
86
+ for svelte_file in sorted(root_path.rglob(f"*{_SVELTE_SUFFIX}")):
87
+ if _should_skip(svelte_file):
88
+ continue
89
+ _parse_svelte_file(svelte_file, root_path, project, package_index)
90
+
91
+ return project
92
+
93
+
94
+ # ---------- plain .ts files ----------
95
+
96
+
97
+ def _parse_ts_file(
98
+ file: Path,
99
+ root: Path,
100
+ project: Project,
101
+ package_index: dict[str, Package],
102
+ ) -> None:
103
+ source_bytes = file.read_bytes()
104
+ tree = _PARSER_TS.parse(source_bytes)
105
+ rel = file.relative_to(root)
106
+ file_str = rel.as_posix()
107
+ package_qn = _qualified_package_name(rel)
108
+ _ensure_package(project, package_qn, package_index)
109
+ parse_module_body(
110
+ tree.root_node,
111
+ source_bytes,
112
+ package_qn=package_qn,
113
+ project=project,
114
+ package_index=package_index,
115
+ file_str=file_str,
116
+ )
117
+
118
+
119
+ # ---------- .svelte files ----------
120
+
121
+
122
+ def _parse_svelte_file(
123
+ file: Path,
124
+ root: Path,
125
+ project: Project,
126
+ package_index: dict[str, Package],
127
+ ) -> None:
128
+ rel = file.relative_to(root)
129
+ file_str = rel.as_posix()
130
+ package_qn = _qualified_package_name(rel)
131
+ pkg = _ensure_package(project, package_qn, package_index)
132
+
133
+ text = file.read_text(encoding="utf-8", errors="replace")
134
+ component_name = _component_name_from(file)
135
+ component_qn = (
136
+ f"{package_qn}.{component_name}"
137
+ if package_qn and package_qn != "__root__"
138
+ else component_name
139
+ )
140
+
141
+ component = Class(
142
+ name=component_name,
143
+ qualified_name=component_qn,
144
+ kind="class",
145
+ location=SourceLocation(
146
+ file=file_str,
147
+ start_line=1,
148
+ end_line=len(text.splitlines()) or 1,
149
+ ),
150
+ )
151
+
152
+ # Strip styles + comments so markup scanning isn't fooled by tag-like
153
+ # content inside them.
154
+ markup_text = _STYLE_RE.sub("", text)
155
+ markup_text = _COMMENT_RE.sub("", markup_text)
156
+
157
+ imports: dict[str, str] = {} # local-name -> module path string
158
+ component_description: str | None = None
159
+
160
+ for script_match in _SCRIPT_RE.finditer(markup_text):
161
+ script_body = script_match.group(2)
162
+ # Pad with newlines so script tree-sitter line numbers don't end up
163
+ # confusingly close to the markup ones if we ever surface them.
164
+ script_bytes = script_body.encode("utf-8")
165
+ tree = _PARSER_TS.parse(script_bytes)
166
+ desc = _ingest_script(
167
+ tree.root_node,
168
+ script_bytes,
169
+ component=component,
170
+ package_qn=package_qn,
171
+ project=project,
172
+ package_index=package_index,
173
+ imports=imports,
174
+ file_str=file_str,
175
+ )
176
+ if desc and component_description is None:
177
+ component_description = desc
178
+
179
+ # Now scan markup for component references and turn each unique one
180
+ # whose import we resolved into an attribute (so `resolve_association`
181
+ # picks it up as a dependency).
182
+ markup_only = _SCRIPT_RE.sub("", markup_text)
183
+ referenced: list[str] = []
184
+ seen_refs: set[str] = set()
185
+ for m in _COMPONENT_TAG_RE.finditer(markup_only):
186
+ name = m.group(1)
187
+ # If the import was `import * as Util from "..."`, the user writes
188
+ # `<Util.Foo />`. Track the head identifier in that case.
189
+ head = name.split(".")[0]
190
+ if head not in imports:
191
+ continue
192
+ if name in seen_refs:
193
+ continue
194
+ seen_refs.add(name)
195
+ referenced.append(name)
196
+
197
+ for ref_name in referenced:
198
+ head = ref_name.split(".")[0]
199
+ target_module = imports[head]
200
+ target_qn = _resolve_component_qn(target_module, file, root, ref_name)
201
+ # Surface the dependency as an attribute. `resolve_association`
202
+ # matches by the trailing identifier so a value like
203
+ # `target_qn = "app.components.Foo"` will edge-link to that class
204
+ # if it exists in the project.
205
+ component.attributes.append(
206
+ Attribute(
207
+ name=_attr_name_for_ref(ref_name),
208
+ type=target_qn,
209
+ visibility=Visibility.PRIVATE,
210
+ )
211
+ )
212
+
213
+ if component_description is not None:
214
+ component.description = component_description
215
+
216
+ pkg.classes.append(component)
217
+
218
+
219
+ def _component_name_from(file: Path) -> str:
220
+ stem = file.stem
221
+ if stem.lower() in ("index", "+page", "+layout"):
222
+ # Fall back to the parent directory name for SvelteKit route files /
223
+ # barrel-style `index.svelte`s.
224
+ return file.parent.name or stem
225
+ return stem
226
+
227
+
228
+ def _resolve_component_qn(
229
+ module_path: str, importer: Path, root: Path, ref_name: str
230
+ ) -> str:
231
+ """Resolve a `from "..."` module path to a project-qualified class name.
232
+
233
+ Returns the bare component name if the import is to a node_modules
234
+ package or otherwise unresolvable — the renderer will then just show it
235
+ as an unattached label, which is exactly right for external components.
236
+ """
237
+ # `ref_name` may be "Foo" or "Util.Foo" — the trailing component is the
238
+ # class we're after.
239
+ short_name = ref_name.split(".")[-1]
240
+ if not module_path.startswith(".") and not module_path.startswith("/"):
241
+ return short_name
242
+ target = (importer.parent / module_path).resolve()
243
+ candidates = [
244
+ target,
245
+ target.with_suffix(".svelte"),
246
+ target.with_suffix(".ts"),
247
+ target.with_suffix(".tsx"),
248
+ target / f"{short_name}.svelte",
249
+ target / "index.svelte",
250
+ target / "index.ts",
251
+ ]
252
+ for c in candidates:
253
+ if c.exists() and c.is_file():
254
+ try:
255
+ rel = c.relative_to(root)
256
+ except ValueError:
257
+ continue
258
+ package_qn = _qualified_package_name(rel)
259
+ if c.suffix == _SVELTE_SUFFIX:
260
+ stem = c.stem
261
+ if stem.lower() in ("index", "+page", "+layout"):
262
+ stem = c.parent.name or stem
263
+ if package_qn and package_qn != "__root__":
264
+ return f"{package_qn}.{stem}"
265
+ return stem
266
+ # Plain .ts: a re-export barrel. Best effort: assume the short
267
+ # name matches a class declared in that file.
268
+ if package_qn and package_qn != "__root__":
269
+ return f"{package_qn}.{short_name}"
270
+ return short_name
271
+ return short_name
272
+
273
+
274
+ def _attr_name_for_ref(ref_name: str) -> str:
275
+ """Turn a component tag like `Foo` or `Util.Foo` into a stable attribute
276
+ slot name. Prefixed with `_` so the visibility heuristic still flags it
277
+ as protected/internal."""
278
+ safe = ref_name.replace(".", "_")
279
+ return f"_{safe[0].lower()}{safe[1:]}" if safe else "_ref"
280
+
281
+
282
+ # ---------- script block ingestion ----------
283
+
284
+
285
+ def _ingest_script(
286
+ root: Node,
287
+ source: bytes,
288
+ *,
289
+ component: Class,
290
+ package_qn: str,
291
+ project: Project,
292
+ package_index: dict[str, Package],
293
+ imports: dict[str, str],
294
+ file_str: str,
295
+ ) -> str | None:
296
+ """Walk a parsed `<script>` body. Returns the first JSDoc block found
297
+ above a top-level declaration, used as the component description."""
298
+ first_doc: str | None = None
299
+ for node in root.named_children:
300
+ unwrapped = node
301
+ if node.type == "export_statement" and node.named_child_count == 1:
302
+ unwrapped = node.named_children[0]
303
+
304
+ if unwrapped.type == "import_statement":
305
+ _record_import(unwrapped, source, imports)
306
+ continue
307
+
308
+ if unwrapped.type == "lexical_declaration":
309
+ for attr in _attributes_from_lexical(unwrapped, source):
310
+ # De-dupe against any same-named attribute already on the
311
+ # component (e.g. from a prior script block).
312
+ if not any(a.name == attr.name for a in component.attributes):
313
+ component.attributes.append(attr)
314
+ continue
315
+
316
+ if unwrapped.type == "function_declaration":
317
+ component.operations.append(_operation_from_function(unwrapped, source))
318
+ continue
319
+
320
+ if unwrapped.type in {
321
+ "class_declaration",
322
+ "abstract_class_declaration",
323
+ "interface_declaration",
324
+ "enum_declaration",
325
+ "type_alias_declaration",
326
+ "internal_module",
327
+ }:
328
+ # Let the TS parser ingest these as regular classes.
329
+ parse_module_body(
330
+ _Wrap([unwrapped]), # type: ignore[arg-type]
331
+ source,
332
+ package_qn=package_qn,
333
+ project=project,
334
+ package_index=package_index,
335
+ file_str=file_str,
336
+ )
337
+ continue
338
+
339
+ return first_doc
340
+
341
+
342
+ class _Wrap:
343
+ """Minimal Node-shaped wrapper letting us reuse `parse_module_body` for
344
+ a hand-picked list of top-level declarations.
345
+
346
+ `parse_module_body` only touches `.named_children`, so we expose just that.
347
+ """
348
+
349
+ def __init__(self, children: list[Node]) -> None:
350
+ self.named_children = children
351
+
352
+
353
+ def _record_import(node: Node, source: bytes, imports: dict[str, str]) -> None:
354
+ """Populate `imports` with every binding introduced by an import_statement."""
355
+ # The module path lives in a `string` child of the import_statement.
356
+ module_node = next((c for c in node.named_children if c.type == "string"), None)
357
+ if module_node is None:
358
+ return
359
+ module_path = _string_value(module_node, source)
360
+
361
+ clause = next(
362
+ (c for c in node.named_children if c.type == "import_clause"), None
363
+ )
364
+ if clause is None:
365
+ return
366
+ for child in clause.named_children:
367
+ if child.type == "identifier":
368
+ imports[_text(child, source)] = module_path
369
+ elif child.type == "namespace_import":
370
+ for c in child.named_children:
371
+ if c.type == "identifier":
372
+ imports[_text(c, source)] = module_path
373
+ elif child.type == "named_imports":
374
+ for spec in child.named_children:
375
+ if spec.type != "import_specifier":
376
+ continue
377
+ idents = [c for c in spec.named_children if c.type == "identifier"]
378
+ # `import { Foo as Bar }` → idents are [Foo, Bar]; we want Bar
379
+ # as the binding. `import { Foo }` → idents = [Foo].
380
+ if idents:
381
+ imports[_text(idents[-1], source)] = module_path
382
+
383
+
384
+ def _string_value(node: Node, source: bytes) -> str:
385
+ """Extract the literal text of a `string` node (drops the surrounding
386
+ quotes; does not handle escape sequences)."""
387
+ for c in node.named_children:
388
+ if c.type == "string_fragment":
389
+ return _text(c, source)
390
+ raw = _text(node, source)
391
+ if len(raw) >= 2 and raw[0] in ("'", '"', "`"):
392
+ return raw[1:-1]
393
+ return raw
394
+
395
+
396
+ def _attributes_from_lexical(node: Node, source: bytes) -> list[Attribute]:
397
+ """One `let`/`const` declaration can introduce multiple variables (object
398
+ destructuring); return one Attribute per leaf binding."""
399
+ out: list[Attribute] = []
400
+ for declarator in node.named_children:
401
+ if declarator.type != "variable_declarator":
402
+ continue
403
+ name_node = declarator.child_by_field_name("name") or _first_named_child(
404
+ declarator
405
+ )
406
+ if name_node is None:
407
+ continue
408
+ type_node = next(
409
+ (c for c in declarator.named_children if c.type == "type_annotation"),
410
+ None,
411
+ )
412
+ type_text = (
413
+ _strip_type_annotation(_text(type_node, source)) if type_node else ""
414
+ )
415
+ # Find the initializer (named child that isn't the name or type).
416
+ init = None
417
+ for c in declarator.named_children:
418
+ if c is name_node or c is type_node:
419
+ continue
420
+ init = c
421
+ if name_node.type in ("object_pattern", "array_pattern"):
422
+ for sub in _flatten_pattern(name_node, source):
423
+ out.append(
424
+ Attribute(
425
+ name=sub,
426
+ type=_rune_type_hint(init, source) or "",
427
+ visibility=Visibility.PUBLIC,
428
+ default=_rune_default(init, source),
429
+ )
430
+ )
431
+ else:
432
+ out.append(
433
+ Attribute(
434
+ name=_text(name_node, source),
435
+ type=type_text,
436
+ visibility=Visibility.PUBLIC,
437
+ default=_rune_default(init, source),
438
+ )
439
+ )
440
+ return out
441
+
442
+
443
+ def _flatten_pattern(pattern: Node, source: bytes) -> list[str]:
444
+ """Pull binding names out of an object_pattern / array_pattern."""
445
+ out: list[str] = []
446
+ for child in pattern.named_children:
447
+ if child.type == "shorthand_property_identifier_pattern":
448
+ out.append(_text(child, source))
449
+ elif child.type == "identifier":
450
+ out.append(_text(child, source))
451
+ elif child.type == "object_assignment_pattern":
452
+ # `{ foo = default }` — first named child is the binding.
453
+ first = _first_named_child(child)
454
+ if first is not None:
455
+ out.append(_text(first, source))
456
+ elif child.type == "pair_pattern":
457
+ # `{ foo: bar }` — `bar` is the binding name.
458
+ for c in child.named_children:
459
+ if c.type in ("identifier", "shorthand_property_identifier_pattern"):
460
+ out.append(_text(c, source))
461
+ elif child.type in ("object_pattern", "array_pattern"):
462
+ out.extend(_flatten_pattern(child, source))
463
+ return out
464
+
465
+
466
+ _RUNE_NAMES = ("$state", "$derived", "$props", "$bindable", "$effect")
467
+
468
+
469
+ def _rune_default(init: Node | None, source: bytes) -> str | None:
470
+ """Surface rune helpers in the default field so the diagram label makes
471
+ it obvious that an attribute is reactive."""
472
+ if init is None:
473
+ return None
474
+ text = _text(init, source).strip()
475
+ if not text:
476
+ return None
477
+ # Truncate long initializers — keep just enough to be a useful hint.
478
+ if len(text) > 60:
479
+ text = text[:60] + "…"
480
+ return text
481
+
482
+
483
+ def _rune_type_hint(init: Node | None, source: bytes) -> str | None:
484
+ """If a destructured binding came from `$props()`, mark its type as
485
+ `Props` so the diagram hints at the source."""
486
+ if init is None:
487
+ return None
488
+ raw = _text(init, source).strip()
489
+ for rune in _RUNE_NAMES:
490
+ if raw.startswith(rune + "("):
491
+ return rune
492
+ return None
493
+
494
+
495
+ def _operation_from_function(node: Node, source: bytes) -> Operation:
496
+ name_node = node.child_by_field_name("name")
497
+ name = _text(name_node, source) if name_node else ""
498
+ return_node = next(
499
+ (c for c in node.named_children if c.type == "type_annotation"),
500
+ None,
501
+ )
502
+ return_type = (
503
+ _strip_type_annotation(_text(return_node, source)) if return_node else ""
504
+ )
505
+ params: list[Parameter] = []
506
+ params_node = node.child_by_field_name("parameters")
507
+ if params_node is not None:
508
+ for p in params_node.named_children:
509
+ if p.type not in ("required_parameter", "optional_parameter"):
510
+ continue
511
+ params.append(_ts_parameter_from(p, source))
512
+ return Operation(
513
+ name=name,
514
+ parameters=params,
515
+ return_type=return_type,
516
+ visibility=Visibility.PUBLIC,
517
+ is_static=False,
518
+ is_abstract=False,
519
+ )
520
+
521
+
522
+ def _first_named_child(node: Node) -> Node | None:
523
+ return node.named_children[0] if node.named_child_count else None
@@ -0,0 +1,3 @@
1
+ from code_constraints.typescript.parser import parse_project
2
+
3
+ __all__ = ["parse_project"]