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,590 @@
1
+ """Parse a TypeScript project (a directory of .ts / .tsx files) into a
2
+ `code_constraints.core.Project`.
3
+
4
+ Uses `tree_sitter` with the `tree_sitter_typescript` grammar. Like the C# parser
5
+ this is a syntactic parse — there is no type-resolution across files, so an
6
+ inheritance / type reference that goes through an aliased import will appear as
7
+ the local alias rather than the source's qualified name.
8
+
9
+ Packages are derived from the directory layout (mirrors the Python parser);
10
+ file-level `namespace X { ... }` (TypeScript's `internal_module`) further nests
11
+ classes inside the directory-derived package.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from pathlib import Path
17
+
18
+ import tree_sitter_typescript
19
+ from tree_sitter import Language, Node, Parser
20
+
21
+ from code_constraints.core.model import (
22
+ Attribute,
23
+ Class,
24
+ ClassKind,
25
+ Operation,
26
+ Package,
27
+ Parameter,
28
+ Project,
29
+ SourceLocation,
30
+ Visibility,
31
+ )
32
+
33
+ _LANG_TS = Language(tree_sitter_typescript.language_typescript())
34
+ _LANG_TSX = Language(tree_sitter_typescript.language_tsx())
35
+ _PARSER_TS = Parser(_LANG_TS)
36
+ _PARSER_TSX = Parser(_LANG_TSX)
37
+
38
+ _SKIP_DIR_NAMES = {
39
+ "node_modules", ".git", "dist", "build", "out", ".next", ".svelte-kit",
40
+ ".turbo", ".cache", "coverage",
41
+ }
42
+
43
+ _TS_SUFFIXES = (".ts", ".tsx", ".mts", ".cts")
44
+
45
+
46
+ def parse_project(root: str | Path) -> Project:
47
+ root_path = Path(root).resolve()
48
+ if not root_path.is_dir():
49
+ raise ValueError(f"not a directory: {root_path}")
50
+
51
+ project = Project(source_language="typescript", root_path=str(root_path))
52
+ package_index: dict[str, Package] = {}
53
+
54
+ for ts_file in sorted(_iter_ts_files(root_path)):
55
+ _parse_file(ts_file, root_path, project, package_index)
56
+
57
+ return project
58
+
59
+
60
+ def _iter_ts_files(root_path: Path):
61
+ for suf in _TS_SUFFIXES:
62
+ for f in root_path.rglob(f"*{suf}"):
63
+ if _should_skip(f):
64
+ continue
65
+ # Skip declaration files: they restate types defined elsewhere and
66
+ # would double-count classes when parsed alongside their .ts pair.
67
+ if f.name.endswith(".d.ts"):
68
+ continue
69
+ yield f
70
+
71
+
72
+ def _should_skip(p: Path) -> bool:
73
+ return any(part in _SKIP_DIR_NAMES for part in p.parts)
74
+
75
+
76
+ def _parse_file(
77
+ file: Path,
78
+ root: Path,
79
+ project: Project,
80
+ package_index: dict[str, Package],
81
+ ) -> None:
82
+ source_bytes = file.read_bytes()
83
+ parser = _PARSER_TSX if file.suffix == ".tsx" else _PARSER_TS
84
+ tree = parser.parse(source_bytes)
85
+ rel = file.relative_to(root)
86
+ file_str = rel.as_posix()
87
+ package_qn = _qualified_package_name(rel)
88
+ _ensure_package(project, package_qn, package_index)
89
+
90
+ parse_module_body(
91
+ tree.root_node,
92
+ source_bytes,
93
+ package_qn=package_qn,
94
+ project=project,
95
+ package_index=package_index,
96
+ file_str=file_str,
97
+ )
98
+
99
+
100
+ def parse_module_body(
101
+ root: Node,
102
+ source: bytes,
103
+ *,
104
+ package_qn: str,
105
+ project: Project,
106
+ package_index: dict[str, Package],
107
+ file_str: str,
108
+ ) -> None:
109
+ """Walk the top-level of a parsed TS source and ingest its declarations.
110
+
111
+ Factored out of `_parse_file` so the Svelte parser can run it on the
112
+ contents of a `<script>` block (which parse as a `program` with the same
113
+ grammar) without re-implementing the dispatch logic.
114
+ """
115
+ for child in root.named_children:
116
+ _visit_top_level(child, package_qn, project, package_index, source, file_str)
117
+
118
+
119
+ _CLASS_LIKE_TYPES = {
120
+ "class_declaration",
121
+ "abstract_class_declaration",
122
+ "interface_declaration",
123
+ "enum_declaration",
124
+ "type_alias_declaration",
125
+ }
126
+
127
+
128
+ def _visit_top_level(
129
+ node: Node,
130
+ package_qn: str,
131
+ project: Project,
132
+ package_index: dict[str, Package],
133
+ source: bytes,
134
+ file_str: str,
135
+ ) -> None:
136
+ # Unwrap `export class ...` / `export default class ...`.
137
+ if node.type == "export_statement":
138
+ for c in node.named_children:
139
+ _visit_top_level(c, package_qn, project, package_index, source, file_str)
140
+ return
141
+
142
+ # `expression_statement` shows up around `namespace X { ... }`.
143
+ if node.type == "expression_statement" and node.named_child_count == 1:
144
+ _visit_top_level(
145
+ node.named_children[0], package_qn, project, package_index, source, file_str
146
+ )
147
+ return
148
+
149
+ if node.type == "internal_module":
150
+ _visit_namespace(node, package_qn, project, package_index, source, file_str)
151
+ return
152
+
153
+ if node.type in _CLASS_LIKE_TYPES:
154
+ _ingest_class(node, package_qn, project, package_index, source, file_str)
155
+ return
156
+
157
+
158
+ def _visit_namespace(
159
+ node: Node,
160
+ parent_qn: str,
161
+ project: Project,
162
+ package_index: dict[str, Package],
163
+ source: bytes,
164
+ file_str: str,
165
+ ) -> None:
166
+ # The name child is either `identifier` or `nested_identifier`
167
+ # (`namespace Foo.Bar { ... }`).
168
+ name_node = next(
169
+ (c for c in node.named_children
170
+ if c.type in ("identifier", "nested_identifier")),
171
+ None,
172
+ )
173
+ name = _text(name_node, source) if name_node else "anon"
174
+ qn = name if parent_qn in ("", "__root__") else f"{parent_qn}.{name}"
175
+ _ensure_package(project, qn, package_index)
176
+
177
+ body = next(
178
+ (c for c in node.named_children if c.type == "statement_block"), None
179
+ )
180
+ if body is None:
181
+ return
182
+ for child in body.named_children:
183
+ _visit_top_level(child, qn, project, package_index, source, file_str)
184
+
185
+
186
+ def _ensure_package(
187
+ project: Project, qualified_name: str, index: dict[str, Package]
188
+ ) -> Package:
189
+ if qualified_name in index:
190
+ return index[qualified_name]
191
+ if qualified_name == "__root__":
192
+ pkg = Package(name="__root__", qualified_name="__root__")
193
+ project.packages.append(pkg)
194
+ index[qualified_name] = pkg
195
+ return pkg
196
+ parts = qualified_name.split(".")
197
+ if len(parts) == 1:
198
+ pkg = Package(name=parts[0], qualified_name=qualified_name)
199
+ project.packages.append(pkg)
200
+ else:
201
+ parent = _ensure_package(project, ".".join(parts[:-1]), index)
202
+ pkg = Package(name=parts[-1], qualified_name=qualified_name)
203
+ parent.sub_packages.append(pkg)
204
+ index[qualified_name] = pkg
205
+ return pkg
206
+
207
+
208
+ def _qualified_package_name(rel_file: Path) -> str:
209
+ parts = list(rel_file.parts[:-1])
210
+ if not parts:
211
+ return "__root__"
212
+ return ".".join(parts)
213
+
214
+
215
+ # ---------- class / member extraction ----------
216
+
217
+
218
+ def _ingest_class(
219
+ node: Node,
220
+ package_qn: str,
221
+ project: Project,
222
+ package_index: dict[str, Package],
223
+ source: bytes,
224
+ file_str: str,
225
+ ) -> None:
226
+ name_node = node.child_by_field_name("name") or _first_named_of_type(
227
+ node, "type_identifier"
228
+ )
229
+ if name_node is None:
230
+ return
231
+ name = _text(name_node, source)
232
+ qn = (
233
+ f"{package_qn}.{name}"
234
+ if package_qn and package_qn != "__root__"
235
+ else name
236
+ )
237
+
238
+ cls = Class(
239
+ name=name,
240
+ qualified_name=qn,
241
+ kind=_kind_for(node, source),
242
+ bases=_bases(node, source),
243
+ location=SourceLocation(
244
+ file=file_str,
245
+ start_line=node.start_point[0] + 1,
246
+ end_line=node.end_point[0] + 1,
247
+ ),
248
+ description=_jsdoc_for(node, source),
249
+ )
250
+
251
+ if node.type == "type_alias_declaration":
252
+ # A type alias has no members; the `value` child is the aliased type
253
+ # expression. We surface the alias text as a synthetic attribute so
254
+ # the viewer shows what the alias resolves to.
255
+ value = node.child_by_field_name("value")
256
+ if value is not None:
257
+ cls.attributes.append(
258
+ Attribute(name="_alias", type=_text(value, source))
259
+ )
260
+ else:
261
+ body = _class_body(node)
262
+ if body is not None:
263
+ for member in body.named_children:
264
+ _ingest_member(member, cls, source)
265
+
266
+ pkg = _ensure_package(project, package_qn, package_index)
267
+ pkg.classes.append(cls)
268
+
269
+
270
+ def _class_body(node: Node) -> Node | None:
271
+ for c in node.named_children:
272
+ if c.type in ("class_body", "interface_body", "enum_body", "object_type"):
273
+ return c
274
+ return node.child_by_field_name("body")
275
+
276
+
277
+ def _kind_for(node: Node, source: bytes) -> ClassKind:
278
+ if node.type == "interface_declaration":
279
+ return "interface"
280
+ if node.type == "enum_declaration":
281
+ return "enum"
282
+ if node.type == "type_alias_declaration":
283
+ return "interface" # closest UML equivalent — a named type contract
284
+ if node.type == "abstract_class_declaration":
285
+ return "abstract"
286
+ return "class"
287
+
288
+
289
+ def _bases(node: Node, source: bytes) -> list[str]:
290
+ """Collect `extends` + `implements` targets.
291
+
292
+ Class heritage on `class_declaration` / `abstract_class_declaration` lives
293
+ inside a `class_heritage` child holding `extends_clause` and/or
294
+ `implements_clause`. Interfaces use an `extends_type_clause` directly on the
295
+ `interface_declaration`.
296
+ """
297
+ out: list[str] = []
298
+ for child in node.named_children:
299
+ if child.type == "class_heritage":
300
+ for sub in child.named_children:
301
+ if sub.type == "extends_clause":
302
+ out.extend(_clause_types(sub, source))
303
+ elif sub.type == "implements_clause":
304
+ out.extend(_clause_types(sub, source))
305
+ elif child.type in ("extends_clause", "extends_type_clause", "implements_clause"):
306
+ out.extend(_clause_types(child, source))
307
+ return out
308
+
309
+
310
+ def _clause_types(clause: Node, source: bytes) -> list[str]:
311
+ types: list[str] = []
312
+ for c in clause.named_children:
313
+ # Skip the `extends` / `implements` keyword token (unnamed).
314
+ text = _text(c, source).strip()
315
+ if text:
316
+ types.append(text)
317
+ return types
318
+
319
+
320
+ def _ingest_member(node: Node, cls: Class, source: bytes) -> None:
321
+ t = node.type
322
+ if t == "public_field_definition":
323
+ cls.attributes.append(_attribute_from_field(node, source))
324
+ elif t == "property_signature":
325
+ cls.attributes.append(_attribute_from_property_sig(node, source))
326
+ elif t in ("method_definition", "method_signature", "abstract_method_signature"):
327
+ op = _operation_from_method(node, source)
328
+ if op.name == "constructor":
329
+ # TS allows `constructor(public name: string)` parameter properties.
330
+ for p in op.parameters:
331
+ # A parameter property is a constructor parameter that carried
332
+ # an `accessibility_modifier` or `readonly` — encoded in the
333
+ # parsed parameter via the leading marker in its name. We
334
+ # capture this directly from the tree below.
335
+ pass
336
+ # Re-walk the formal parameters for parameter properties.
337
+ params_node = node.child_by_field_name("parameters")
338
+ if params_node is not None:
339
+ for p in params_node.named_children:
340
+ attr = _attribute_from_parameter_property(p, source)
341
+ if attr is not None:
342
+ cls.attributes.append(attr)
343
+ cls.operations.append(op)
344
+ elif t == "enum_assignment" or t == "property_identifier":
345
+ # Enum body entries are bare `property_identifier`s (no value) or
346
+ # `enum_assignment` (with an initializer).
347
+ if t == "property_identifier":
348
+ cls.attributes.append(
349
+ Attribute(
350
+ name=_text(node, source),
351
+ type=cls.name,
352
+ visibility=Visibility.PUBLIC,
353
+ is_static=True,
354
+ )
355
+ )
356
+ else:
357
+ name_node = node.child_by_field_name("name")
358
+ value_node = node.child_by_field_name("value")
359
+ if name_node is not None:
360
+ cls.attributes.append(
361
+ Attribute(
362
+ name=_text(name_node, source),
363
+ type=cls.name,
364
+ visibility=Visibility.PUBLIC,
365
+ is_static=True,
366
+ default=_text(value_node, source) if value_node else None,
367
+ )
368
+ )
369
+
370
+
371
+ def _attribute_from_field(node: Node, source: bytes) -> Attribute:
372
+ name_node = node.child_by_field_name("name") or _first_named_of_type(
373
+ node, "property_identifier"
374
+ )
375
+ name = _text(name_node, source) if name_node else ""
376
+ type_node = _first_named_of_type(node, "type_annotation")
377
+ type_text = _strip_type_annotation(_text(type_node, source)) if type_node else ""
378
+ value_node = node.child_by_field_name("value")
379
+ default = _text(value_node, source) if value_node else None
380
+
381
+ vis = _visibility_for_member(node, name, source)
382
+ is_static = _has_keyword(node, "static")
383
+ is_readonly = _has_keyword(node, "readonly")
384
+ return Attribute(
385
+ name=name,
386
+ type=type_text,
387
+ visibility=vis,
388
+ is_static=is_static,
389
+ is_readonly=is_readonly,
390
+ default=default,
391
+ )
392
+
393
+
394
+ def _attribute_from_property_sig(node: Node, source: bytes) -> Attribute:
395
+ name_node = node.child_by_field_name("name") or _first_named_of_type(
396
+ node, "property_identifier"
397
+ )
398
+ name = _text(name_node, source) if name_node else ""
399
+ type_node = _first_named_of_type(node, "type_annotation")
400
+ type_text = _strip_type_annotation(_text(type_node, source)) if type_node else ""
401
+ return Attribute(
402
+ name=name,
403
+ type=type_text,
404
+ visibility=_visibility_for_member(node, name, source),
405
+ is_readonly=_has_keyword(node, "readonly"),
406
+ )
407
+
408
+
409
+ def _attribute_from_parameter_property(param: Node, source: bytes) -> Attribute | None:
410
+ """Capture `constructor(public name: string)` parameter properties.
411
+
412
+ Such parameters carry an `accessibility_modifier` or `readonly` keyword;
413
+ everything else is a plain parameter we should NOT mirror as an attribute.
414
+ """
415
+ has_marker = False
416
+ for c in param.children:
417
+ if c.type == "accessibility_modifier":
418
+ has_marker = True
419
+ if c.type == "readonly":
420
+ has_marker = True
421
+ if not has_marker:
422
+ return None
423
+ if param.type not in ("required_parameter", "optional_parameter"):
424
+ return None
425
+ name_node = next(
426
+ (c for c in param.named_children if c.type in ("identifier", "shorthand_property_identifier_pattern")),
427
+ None,
428
+ )
429
+ name = _text(name_node, source) if name_node else ""
430
+ type_node = _first_named_of_type(param, "type_annotation")
431
+ type_text = _strip_type_annotation(_text(type_node, source)) if type_node else ""
432
+ return Attribute(
433
+ name=name,
434
+ type=type_text,
435
+ visibility=_visibility_for_member(param, name, source),
436
+ is_readonly=_has_keyword(param, "readonly"),
437
+ )
438
+
439
+
440
+ def _operation_from_method(node: Node, source: bytes) -> Operation:
441
+ name_node = node.child_by_field_name("name") or _first_named_of_type(
442
+ node, "property_identifier"
443
+ )
444
+ name = _text(name_node, source) if name_node else ""
445
+ return_node = _first_named_of_type(node, "type_annotation")
446
+ return_type = (
447
+ _strip_type_annotation(_text(return_node, source)) if return_node else ""
448
+ )
449
+
450
+ params: list[Parameter] = []
451
+ params_node = node.child_by_field_name("parameters")
452
+ if params_node is not None:
453
+ for p in params_node.named_children:
454
+ if p.type not in ("required_parameter", "optional_parameter"):
455
+ continue
456
+ params.append(_parameter_from(p, source))
457
+
458
+ return Operation(
459
+ name=name,
460
+ parameters=params,
461
+ return_type=return_type,
462
+ visibility=_visibility_for_member(node, name, source),
463
+ is_static=_has_keyword(node, "static"),
464
+ is_abstract=node.type == "abstract_method_signature" or _has_keyword(node, "abstract"),
465
+ description=_jsdoc_for(node, source),
466
+ )
467
+
468
+
469
+ def _parameter_from(p: Node, source: bytes) -> Parameter:
470
+ name_node = next(
471
+ (c for c in p.named_children
472
+ if c.type in ("identifier", "shorthand_property_identifier_pattern",
473
+ "object_pattern", "array_pattern")),
474
+ None,
475
+ )
476
+ name = _text(name_node, source) if name_node else ""
477
+ type_node = _first_named_of_type(p, "type_annotation")
478
+ type_text = _strip_type_annotation(_text(type_node, source)) if type_node else ""
479
+ # Default value is the named child after the type annotation.
480
+ default = None
481
+ saw_type = type_node is None
482
+ for c in p.named_children:
483
+ if c is type_node:
484
+ saw_type = True
485
+ continue
486
+ if c is name_node:
487
+ continue
488
+ if saw_type and c.type not in ("type_annotation", "accessibility_modifier"):
489
+ default = _text(c, source)
490
+ break
491
+ return Parameter(name=name, type=type_text, default=default)
492
+
493
+
494
+ # ---------- modifier helpers ----------
495
+
496
+
497
+ def _visibility_for_member(node: Node, name: str, source: bytes) -> Visibility:
498
+ for c in node.children:
499
+ if c.type == "accessibility_modifier":
500
+ text = _text(c, source).strip()
501
+ if text == "private":
502
+ return Visibility.PRIVATE
503
+ if text == "protected":
504
+ return Visibility.PROTECTED
505
+ if text == "public":
506
+ return Visibility.PUBLIC
507
+ # TS convention: leading `#` is a private field; leading `_` is "by
508
+ # convention" protected.
509
+ if name.startswith("#"):
510
+ return Visibility.PRIVATE
511
+ if name.startswith("_"):
512
+ return Visibility.PROTECTED
513
+ return Visibility.PUBLIC
514
+
515
+
516
+ def _has_keyword(node: Node, keyword: str) -> bool:
517
+ """True if `node` has a direct (unnamed or named) child whose token text
518
+ matches `keyword`."""
519
+ for c in node.children:
520
+ if c.type == keyword:
521
+ return True
522
+ return False
523
+
524
+
525
+ # ---------- text helpers ----------
526
+
527
+
528
+ def _text(node: Node | None, source: bytes) -> str:
529
+ if node is None:
530
+ return ""
531
+ return source[node.start_byte : node.end_byte].decode("utf-8", errors="replace")
532
+
533
+
534
+ def _strip_type_annotation(text: str) -> str:
535
+ """Turn `: Foo<Bar>` into `Foo<Bar>` and trim whitespace."""
536
+ text = text.strip()
537
+ if text.startswith(":"):
538
+ text = text[1:].strip()
539
+ return text
540
+
541
+
542
+ def _first_named_of_type(node: Node, type_name: str) -> Node | None:
543
+ for c in node.named_children:
544
+ if c.type == type_name:
545
+ return c
546
+ return None
547
+
548
+
549
+ def _jsdoc_for(node: Node, source: bytes) -> str | None:
550
+ """Read a `/** ... */` JSDoc block immediately above `node`.
551
+
552
+ Class / interface declarations are commonly wrapped in an
553
+ `export_statement` whose own `prev_sibling` is the comment we want, so we
554
+ step up to the export wrapper when the immediate previous sibling is the
555
+ `export` keyword.
556
+ """
557
+ start = node
558
+ if (
559
+ node.parent is not None
560
+ and node.parent.type == "export_statement"
561
+ and node.prev_sibling is not None
562
+ and node.prev_sibling.type == "export"
563
+ ):
564
+ start = node.parent
565
+
566
+ sib = start.prev_sibling
567
+ # Skip past unnamed token siblings (semicolons, keywords) to find the
568
+ # nearest comment OR named declaration. We only accept the comment if we
569
+ # encounter it before any other declaration.
570
+ while sib is not None and sib.type != "comment" and not sib.is_named:
571
+ sib = sib.prev_sibling
572
+ if sib is None or sib.type != "comment":
573
+ return None
574
+ raw = _text(sib, source).strip()
575
+ if not raw.startswith("/**"):
576
+ return None
577
+ # Strip /** ... */ and per-line " * " prefixes.
578
+ body = raw[3:]
579
+ if body.endswith("*/"):
580
+ body = body[:-2]
581
+ lines = []
582
+ for line in body.splitlines():
583
+ stripped = line.strip()
584
+ if stripped.startswith("*"):
585
+ stripped = stripped[1:].strip()
586
+ if stripped.startswith("@"):
587
+ break # stop at first @param/@returns tag
588
+ lines.append(stripped)
589
+ joined = " ".join(s for s in lines if s)
590
+ return joined or None
@@ -0,0 +1,89 @@
1
+ """Waivers — the review loop that lets an enforced codebase keep evolving.
2
+
3
+ Rules that only ever say "no" get switched off. The point of this package is the
4
+ other half of the loop: a violation is reported with a stable key, a human or an
5
+ agent decides it is acceptable, and that decision is recorded in
6
+ the `exceptions:` section of `.cdec/rules.yaml` with a reason — sitting next to
7
+ the rule it exempts, reviewable in the diff, and revocable.
8
+
9
+ cdec check # every issue prints its key
10
+ cdec exceptions review --out r.txt # one line per issue, ready to mark
11
+ …mark lines [ALLOW]…
12
+ cdec exceptions patch --file r.txt # apply exactly those decisions
13
+ cdec exceptions allow V-1A2B3C4D # or name one directly
14
+
15
+ Locks (Engine C) are pointedly excluded — see `model.NotWaivable`.
16
+ """
17
+
18
+ from code_constraints.core.keys import (
19
+ KEY_RE,
20
+ engine_of,
21
+ find_keys,
22
+ is_key,
23
+ make_key,
24
+ normalize_key,
25
+ )
26
+ from code_constraints.waivers.collect import CollectOptions, Collected, collect_issues
27
+ from code_constraints.waivers.model import Issue, NotWaivable
28
+ from code_constraints.waivers.ops import (
29
+ ApplyResult,
30
+ allow_keys,
31
+ apply_decisions,
32
+ prune,
33
+ remove_keys,
34
+ )
35
+ from code_constraints.waivers.review import (
36
+ Decision,
37
+ ReviewDecisions,
38
+ parse_review,
39
+ render_issue_line,
40
+ render_review,
41
+ )
42
+ from code_constraints.waivers.store import (
43
+ BASELINE_FILENAME,
44
+ EXCEPTIONS_SECTION,
45
+ WAIVABLE_ENGINES,
46
+ Waiver,
47
+ WaiverFileError,
48
+ WaiverStore,
49
+ default_actor,
50
+ ledger_paths,
51
+ load_waivers,
52
+ now_stamp,
53
+ save_waivers,
54
+ )
55
+
56
+ __all__ = [
57
+ "ApplyResult",
58
+ "BASELINE_FILENAME",
59
+ "EXCEPTIONS_SECTION",
60
+ "CollectOptions",
61
+ "Collected",
62
+ "Decision",
63
+ "Issue",
64
+ "KEY_RE",
65
+ "NotWaivable",
66
+ "ReviewDecisions",
67
+ "WAIVABLE_ENGINES",
68
+ "Waiver",
69
+ "WaiverFileError",
70
+ "WaiverStore",
71
+ "allow_keys",
72
+ "apply_decisions",
73
+ "collect_issues",
74
+ "default_actor",
75
+ "engine_of",
76
+ "find_keys",
77
+ "is_key",
78
+ "ledger_paths",
79
+ "load_waivers",
80
+ "make_key",
81
+ "normalize_key",
82
+ "now_stamp",
83
+ "parse_review",
84
+ "prune",
85
+ "remove_keys",
86
+ "render_issue_line",
87
+ "render_review",
88
+ "save_waivers",
89
+ ]