modelable 1.0.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.

Potentially problematic release.


This version of modelable might be problematic. Click here for more details.

Files changed (122) hide show
  1. modelable/__init__.py +1 -0
  2. modelable/__main__.py +3 -0
  3. modelable/_pydantic_py314_compat.py +31 -0
  4. modelable/cli.py +41 -0
  5. modelable/commands/__init__.py +1 -0
  6. modelable/commands/apicurio.py +84 -0
  7. modelable/commands/codegen.py +241 -0
  8. modelable/commands/common.py +43 -0
  9. modelable/commands/compile.py +237 -0
  10. modelable/commands/create.py +164 -0
  11. modelable/commands/diff.py +82 -0
  12. modelable/commands/graph.py +53 -0
  13. modelable/commands/llm.py +564 -0
  14. modelable/commands/lsp.py +15 -0
  15. modelable/commands/runtime.py +37 -0
  16. modelable/commands/scenario.py +104 -0
  17. modelable/commands/spec.py +197 -0
  18. modelable/commands/workspace.py +240 -0
  19. modelable/compat/__init__.py +11 -0
  20. modelable/compat/checker.py +179 -0
  21. modelable/compat/diff.py +169 -0
  22. modelable/compiler/__init__.py +3 -0
  23. modelable/compiler/compiler.py +19 -0
  24. modelable/compiler/workspace.py +346 -0
  25. modelable/diagnostics/__init__.py +3 -0
  26. modelable/diagnostics/model.py +27 -0
  27. modelable/emitters/__init__.py +0 -0
  28. modelable/emitters/base.py +22 -0
  29. modelable/emitters/csharp.py +245 -0
  30. modelable/emitters/dbt_yaml.py +290 -0
  31. modelable/emitters/diagnostics.py +25 -0
  32. modelable/emitters/fhir.py +694 -0
  33. modelable/emitters/fhir_validator.py +36 -0
  34. modelable/emitters/go.py +334 -0
  35. modelable/emitters/java.py +264 -0
  36. modelable/emitters/json_schema.py +458 -0
  37. modelable/emitters/markdown.py +252 -0
  38. modelable/emitters/odcs.py +355 -0
  39. modelable/emitters/openlineage.py +315 -0
  40. modelable/emitters/openmetadata.py +258 -0
  41. modelable/emitters/python.py +282 -0
  42. modelable/emitters/rust.py +643 -0
  43. modelable/emitters/shapes.py +261 -0
  44. modelable/emitters/sql.py +266 -0
  45. modelable/emitters/targets.py +141 -0
  46. modelable/emitters/typescript.py +352 -0
  47. modelable/expressions/__init__.py +0 -0
  48. modelable/expressions/cel.py +547 -0
  49. modelable/governance/__init__.py +3 -0
  50. modelable/governance/checker.py +271 -0
  51. modelable/governance/por.py +46 -0
  52. modelable/grammar/__init__.py +1 -0
  53. modelable/grammar/modelable.lark +257 -0
  54. modelable/graph/__init__.py +5 -0
  55. modelable/graph/export.py +442 -0
  56. modelable/llm/__init__.py +43 -0
  57. modelable/llm/chat.py +255 -0
  58. modelable/llm/config.py +87 -0
  59. modelable/llm/context.py +194 -0
  60. modelable/llm/engine.py +976 -0
  61. modelable/llm/importers.py +1077 -0
  62. modelable/llm/provenance.py +84 -0
  63. modelable/llm/providers.py +182 -0
  64. modelable/llm/qa.py +126 -0
  65. modelable/llm/recommendations.py +33 -0
  66. modelable/llm/redaction.py +19 -0
  67. modelable/llm/render.py +279 -0
  68. modelable/llm/update_plan.py +101 -0
  69. modelable/llm/validation_help.py +10 -0
  70. modelable/lsp/__init__.py +3 -0
  71. modelable/lsp/__main__.py +4 -0
  72. modelable/lsp/code_actions.py +210 -0
  73. modelable/lsp/completion.py +480 -0
  74. modelable/lsp/definition.py +343 -0
  75. modelable/lsp/diagnostics.py +31 -0
  76. modelable/lsp/document_symbols.py +197 -0
  77. modelable/lsp/federation.py +261 -0
  78. modelable/lsp/folding.py +33 -0
  79. modelable/lsp/formatting.py +64 -0
  80. modelable/lsp/highlight.py +30 -0
  81. modelable/lsp/hover.py +370 -0
  82. modelable/lsp/inlay_hints.py +158 -0
  83. modelable/lsp/references.py +511 -0
  84. modelable/lsp/rename.py +564 -0
  85. modelable/lsp/semantic_tokens.py +412 -0
  86. modelable/lsp/server.py +370 -0
  87. modelable/lsp/workspace.py +83 -0
  88. modelable/lsp/workspace_symbols.py +104 -0
  89. modelable/parser/__init__.py +94 -0
  90. modelable/parser/ir.py +451 -0
  91. modelable/parser/parse.py +47 -0
  92. modelable/parser/transformer.py +798 -0
  93. modelable/parser/wire.py +68 -0
  94. modelable/planner/__init__.py +0 -0
  95. modelable/planner/lineage.py +91 -0
  96. modelable/planner/planner.py +134 -0
  97. modelable/planner/plans.py +122 -0
  98. modelable/py.typed +0 -0
  99. modelable/registry/__init__.py +9 -0
  100. modelable/registry/apicurio.py +166 -0
  101. modelable/registry/base.py +18 -0
  102. modelable/registry/factory.py +18 -0
  103. modelable/registry/index.py +419 -0
  104. modelable/registry/local.py +26 -0
  105. modelable/registry/oci.py +22 -0
  106. modelable/registry/resolver.py +213 -0
  107. modelable/registry/schema.sql +119 -0
  108. modelable/registry/signature.py +26 -0
  109. modelable/release.py +125 -0
  110. modelable/runtime/__init__.py +5 -0
  111. modelable/runtime/adapter/__init__.py +17 -0
  112. modelable/runtime/adapter/base.py +18 -0
  113. modelable/runtime/adapter/postgres.py +82 -0
  114. modelable/specs/__init__.py +23 -0
  115. modelable/specs/tracking.py +220 -0
  116. modelable/validation/__init__.py +3 -0
  117. modelable/validation/semantic.py +659 -0
  118. modelable-1.0.0.dist-info/METADATA +61 -0
  119. modelable-1.0.0.dist-info/RECORD +122 -0
  120. modelable-1.0.0.dist-info/WHEEL +4 -0
  121. modelable-1.0.0.dist-info/entry_points.txt +2 -0
  122. modelable-1.0.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,564 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from dataclasses import dataclass
5
+
6
+ from lsprotocol import types
7
+
8
+ from modelable.lsp.workspace import LspWorkspaceIndex
9
+ from modelable.registry.resolver import resolve_model_ref
10
+
11
+ _QUALIFIED_REF_PATTERN = re.compile(
12
+ r"(?P<domain>[A-Za-z_][A-Za-z0-9_]*)\.(?P<name>[A-Za-z_][A-Za-z0-9_]*)\s*@\s*(?P<version>\d+)"
13
+ )
14
+ _FIELD_REF_PATTERN = re.compile(r"(?P<alias>[A-Za-z_][A-Za-z0-9_]*)\.(?P<field>[A-Za-z_][A-Za-z0-9_]*)")
15
+ _DECL_PATTERN = re.compile(
16
+ r"^\s*(?P<kind>entity|aggregate|event|value|projection)\s+"
17
+ r"(?P<name>[A-Za-z_][A-Za-z0-9_]*)\s*@\s*(?P<version>\d+)"
18
+ )
19
+ _DOMAIN_PATTERN = re.compile(r'^\s*domain\s+(?:"(?P<quoted>[^"]+)"|(?P<name>[A-Za-z_][A-Za-z0-9_]*))')
20
+ _MODEL_FIELD_PATTERN = re.compile(
21
+ r"^\s*(?:@[A-Za-z_][A-Za-z0-9_]*(?:\([^)]*\))?\s+)*(?P<name>[A-Za-z_][A-Za-z0-9_]*)\??\s*:"
22
+ )
23
+ _PROJECTION_FIELD_PATTERN = re.compile(
24
+ r"^\s*(?:@[A-Za-z_][A-Za-z0-9_]*(?:\([^)]*\))?\s+)*(?P<name>[A-Za-z_][A-Za-z0-9_]*)\s*(?:<-|=)"
25
+ )
26
+ _WORD_PATTERN = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
27
+ _MODEL_DECL_KINDS = {"entity", "aggregate", "event", "value"}
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class _Target:
32
+ kind: str
33
+ domain: str
34
+ name: str
35
+ version: int
36
+ field_name: str | None
37
+ range: types.Range
38
+
39
+
40
+ def build_prepare_rename(
41
+ index: LspWorkspaceIndex,
42
+ uri: str,
43
+ line: int,
44
+ character: int,
45
+ ) -> types.Range | None:
46
+ target = _target_at(index, uri, line, character)
47
+ if target is None:
48
+ return None
49
+ return target.range
50
+
51
+
52
+ def build_rename(
53
+ index: LspWorkspaceIndex,
54
+ uri: str,
55
+ line: int,
56
+ character: int,
57
+ new_name: str,
58
+ ) -> types.WorkspaceEdit | None:
59
+ source = index.documents.get(uri)
60
+ workspace = index.workspace
61
+ if source is None or workspace is None:
62
+ return None
63
+
64
+ target = _target_at(index, uri, line, character)
65
+ if target is None:
66
+ return None
67
+
68
+ changes: dict[str, list[types.TextEdit]] = {}
69
+
70
+ if target.kind in {"model_decl", "projection_decl"}:
71
+ _add_decl_renames(changes, workspace, target, new_name)
72
+ elif target.kind == "model_field":
73
+ _add_model_field_renames(changes, workspace, target, new_name)
74
+ elif target.kind == "projection_field":
75
+ _add_projection_field_renames(changes, workspace, target, new_name)
76
+ else:
77
+ return None
78
+
79
+ if not changes:
80
+ return None
81
+ return types.WorkspaceEdit(changes=changes)
82
+
83
+
84
+ def _target_at(index: LspWorkspaceIndex, uri: str, line: int, character: int) -> _Target | None:
85
+ source = index.documents.get(uri)
86
+ workspace = index.workspace
87
+ if source is None or workspace is None:
88
+ return None
89
+ lines = source.text.splitlines()
90
+ if line < 0 or line >= len(lines):
91
+ return None
92
+ text_line = lines[line]
93
+
94
+ for match in _QUALIFIED_REF_PATTERN.finditer(text_line):
95
+ if not _contains(match.start(), match.end(), character):
96
+ continue
97
+ domain = match.group("domain")
98
+ name = match.group("name")
99
+ version = int(match.group("version"))
100
+ domain_def = next((d for d in workspace.mdl.domains if d.name == domain), None)
101
+ if domain_def is None:
102
+ return None
103
+ if name in domain_def.models:
104
+ return _Target(
105
+ kind="model_decl",
106
+ domain=domain,
107
+ name=name,
108
+ version=version,
109
+ field_name=None,
110
+ range=types.Range(
111
+ start=types.Position(line=line, character=match.start("name")),
112
+ end=types.Position(line=line, character=match.end("name")),
113
+ ),
114
+ )
115
+ if name in domain_def.projections:
116
+ return _Target(
117
+ kind="projection_decl",
118
+ domain=domain,
119
+ name=name,
120
+ version=version,
121
+ field_name=None,
122
+ range=types.Range(
123
+ start=types.Position(line=line, character=match.start("name")),
124
+ end=types.Position(line=line, character=match.end("name")),
125
+ ),
126
+ )
127
+ return None
128
+
129
+ for match in _FIELD_REF_PATTERN.finditer(text_line):
130
+ if not _contains(match.start(), match.end(), character):
131
+ continue
132
+ scope = _current_scope(source.text, line)
133
+ if scope is None:
134
+ return None
135
+ domain_name, kind, name, version = scope
136
+ if kind == "model":
137
+ return _Target(
138
+ kind="model_field",
139
+ domain=domain_name,
140
+ name=name,
141
+ version=version,
142
+ field_name=match.group("field"),
143
+ range=types.Range(
144
+ start=types.Position(line=line, character=match.start("field")),
145
+ end=types.Position(line=line, character=match.end("field")),
146
+ ),
147
+ )
148
+ alias_map = _projection_aliases(workspace, domain_name, name, version)
149
+ target_model = alias_map.get(match.group("alias"))
150
+ if target_model is not None and match.group("field"):
151
+ return _Target(
152
+ kind="model_field",
153
+ domain=target_model[0],
154
+ name=target_model[1],
155
+ version=target_model[2],
156
+ field_name=match.group("field"),
157
+ range=types.Range(
158
+ start=types.Position(line=line, character=match.start("field")),
159
+ end=types.Position(line=line, character=match.end("field")),
160
+ ),
161
+ )
162
+ return _Target(
163
+ kind="projection_field",
164
+ domain=domain_name,
165
+ name=name,
166
+ version=version,
167
+ field_name=match.group("field"),
168
+ range=types.Range(
169
+ start=types.Position(line=line, character=match.start("field")),
170
+ end=types.Position(line=line, character=match.end("field")),
171
+ ),
172
+ )
173
+
174
+ word = _word_at(text_line, character)
175
+ if word is None:
176
+ return None
177
+
178
+ scope = _current_scope(source.text, line)
179
+ if scope is None:
180
+ return None
181
+ domain_name, kind, name, version = scope
182
+
183
+ if word == name:
184
+ return _Target(
185
+ kind="model_decl" if kind == "model" else "projection_decl",
186
+ domain=domain_name,
187
+ name=name,
188
+ version=version,
189
+ field_name=None,
190
+ range=types.Range(
191
+ start=types.Position(line=line, character=_word_start(text_line, word)),
192
+ end=types.Position(line=line, character=_word_end(text_line, word)),
193
+ ),
194
+ )
195
+
196
+ if kind == "model":
197
+ if _is_field_name(workspace, domain_name, name, version, word):
198
+ return _Target(
199
+ kind="model_field",
200
+ domain=domain_name,
201
+ name=name,
202
+ version=version,
203
+ field_name=word,
204
+ range=types.Range(
205
+ start=types.Position(line=line, character=_word_start(text_line, word)),
206
+ end=types.Position(line=line, character=_word_end(text_line, word)),
207
+ ),
208
+ )
209
+ else:
210
+ if _is_projection_field_name(workspace, domain_name, name, version, word):
211
+ return _Target(
212
+ kind="projection_field",
213
+ domain=domain_name,
214
+ name=name,
215
+ version=version,
216
+ field_name=word,
217
+ range=types.Range(
218
+ start=types.Position(line=line, character=_word_start(text_line, word)),
219
+ end=types.Position(line=line, character=_word_end(text_line, word)),
220
+ ),
221
+ )
222
+ return None
223
+
224
+
225
+ def _add_decl_renames(
226
+ changes: dict[str, list[types.TextEdit]],
227
+ workspace,
228
+ target: _Target,
229
+ new_name: str,
230
+ ) -> None:
231
+ decl_kind = "model" if target.kind == "model_decl" else "projection"
232
+ declaration = _find_decl_location(workspace, target.domain, decl_kind, target.name, target.version)
233
+ if declaration is not None:
234
+ changes.setdefault(declaration.uri, []).append(types.TextEdit(range=declaration.range, new_text=new_name))
235
+
236
+ old_ref = f"{target.domain}.{target.name}@{target.version}"
237
+ for source in workspace.sources:
238
+ edits = changes.setdefault(source.uri, [])
239
+ lines = source.text.splitlines()
240
+ for line_no, line in enumerate(lines):
241
+ for match in _QUALIFIED_REF_PATTERN.finditer(line):
242
+ ref = f"{match.group('domain')}.{match.group('name')}@{match.group('version')}"
243
+ if ref != old_ref:
244
+ continue
245
+ edits.append(
246
+ types.TextEdit(
247
+ range=types.Range(
248
+ start=types.Position(line=line_no, character=match.start("name")),
249
+ end=types.Position(line=line_no, character=match.end("name")),
250
+ ),
251
+ new_text=new_name,
252
+ )
253
+ )
254
+
255
+
256
+ def _add_model_field_renames(
257
+ changes: dict[str, list[types.TextEdit]],
258
+ workspace,
259
+ target: _Target,
260
+ new_name: str,
261
+ ) -> None:
262
+ declaration = _find_source_field_location(
263
+ workspace,
264
+ target.domain,
265
+ target.name,
266
+ target.version,
267
+ target.field_name or "",
268
+ )
269
+ if declaration is not None:
270
+ changes.setdefault(declaration.uri, []).append(types.TextEdit(range=declaration.range, new_text=new_name))
271
+
272
+ for source in workspace.sources:
273
+ lines = source.text.splitlines()
274
+ alias_map: dict[str, tuple[str, str, int]] = {}
275
+ current_domain: str | None = None
276
+ current_projection: tuple[str, int] | None = None
277
+ for line_no, line in enumerate(lines):
278
+ domain_match = _DOMAIN_PATTERN.match(line)
279
+ if domain_match:
280
+ current_domain = domain_match.group("quoted") or domain_match.group("name")
281
+ current_projection = None
282
+ alias_map = {}
283
+ continue
284
+
285
+ decl_match = _DECL_PATTERN.match(line)
286
+ if decl_match and current_domain is not None:
287
+ if decl_match.group("kind") == "projection":
288
+ current_projection = (decl_match.group("name"), int(decl_match.group("version")))
289
+ alias_map = _projection_aliases(workspace, current_domain, *current_projection)
290
+ else:
291
+ current_projection = None
292
+ alias_map = {}
293
+ continue
294
+
295
+ if current_projection is None:
296
+ continue
297
+
298
+ for match in _FIELD_REF_PATTERN.finditer(line):
299
+ target_model = alias_map.get(match.group("alias"))
300
+ if target_model is None:
301
+ continue
302
+ if target_model != (target.domain, target.name, target.version):
303
+ continue
304
+ if match.group("field") != target.field_name:
305
+ continue
306
+ changes.setdefault(source.uri, []).append(
307
+ types.TextEdit(
308
+ range=types.Range(
309
+ start=types.Position(line=line_no, character=match.start("field")),
310
+ end=types.Position(line=line_no, character=match.end("field")),
311
+ ),
312
+ new_text=new_name,
313
+ )
314
+ )
315
+
316
+
317
+ def _add_projection_field_renames(
318
+ changes: dict[str, list[types.TextEdit]],
319
+ workspace,
320
+ target: _Target,
321
+ new_name: str,
322
+ ) -> None:
323
+ declaration = _find_source_field_location(
324
+ workspace,
325
+ target.domain,
326
+ target.name,
327
+ target.version,
328
+ target.field_name or "",
329
+ )
330
+ if declaration is not None:
331
+ changes.setdefault(declaration.uri, []).append(types.TextEdit(range=declaration.range, new_text=new_name))
332
+
333
+ for source in workspace.sources:
334
+ lines = source.text.splitlines()
335
+ alias_map: dict[str, tuple[str, str, int]] = {}
336
+ current_domain: str | None = None
337
+ current_projection: tuple[str, int] | None = None
338
+ for line_no, line in enumerate(lines):
339
+ domain_match = _DOMAIN_PATTERN.match(line)
340
+ if domain_match:
341
+ current_domain = domain_match.group("quoted") or domain_match.group("name")
342
+ current_projection = None
343
+ alias_map = {}
344
+ continue
345
+
346
+ decl_match = _DECL_PATTERN.match(line)
347
+ if decl_match and current_domain is not None:
348
+ if decl_match.group("kind") == "projection":
349
+ current_projection = (decl_match.group("name"), int(decl_match.group("version")))
350
+ alias_map = _projection_aliases(workspace, current_domain, *current_projection)
351
+ else:
352
+ current_projection = None
353
+ alias_map = {}
354
+ continue
355
+
356
+ if current_projection is None:
357
+ continue
358
+
359
+ for match in _FIELD_REF_PATTERN.finditer(line):
360
+ target_model = alias_map.get(match.group("alias"))
361
+ if target_model is None:
362
+ continue
363
+ if target_model != (target.domain, target.name, target.version):
364
+ continue
365
+ if match.group("field") != target.field_name:
366
+ continue
367
+ changes.setdefault(source.uri, []).append(
368
+ types.TextEdit(
369
+ range=types.Range(
370
+ start=types.Position(line=line_no, character=match.start("field")),
371
+ end=types.Position(line=line_no, character=match.end("field")),
372
+ ),
373
+ new_text=new_name,
374
+ )
375
+ )
376
+
377
+
378
+ def _projection_aliases(
379
+ workspace,
380
+ domain_name: str,
381
+ projection_name: str,
382
+ version: int,
383
+ ) -> dict[str, tuple[str, str, int]]:
384
+ domain = next((item for item in workspace.mdl.domains if item.name == domain_name), None)
385
+ if domain is None:
386
+ return {}
387
+ versions = domain.projections.get(projection_name, [])
388
+ projection_version = next((item for item in versions if item.version == version), None)
389
+ if projection_version is None:
390
+ return {}
391
+
392
+ aliases: dict[str, tuple[str, str, int]] = {}
393
+ for source_ref in [projection_version.source, *projection_version.joins]:
394
+ try:
395
+ resolved = resolve_model_ref(workspace.mdl, source_ref.model, source_ref.version)
396
+ except LookupError:
397
+ continue
398
+ aliases[source_ref.alias] = (
399
+ resolved.domain_name,
400
+ resolved.model_name,
401
+ resolved.version.version,
402
+ )
403
+ return aliases
404
+
405
+
406
+ def _find_source_field_location(
407
+ workspace,
408
+ domain_name: str,
409
+ model_name: str,
410
+ version: int,
411
+ field_name: str,
412
+ ) -> types.Location | None:
413
+ for kind in ("model", "projection"):
414
+ location = _find_field_location(workspace, domain_name, kind, model_name, version, field_name)
415
+ if location is not None:
416
+ return location
417
+ return None
418
+
419
+
420
+ def _find_field_location(
421
+ workspace,
422
+ domain_name: str,
423
+ kind: str,
424
+ name: str,
425
+ version: int,
426
+ field_name: str,
427
+ ) -> types.Location | None:
428
+ for source in workspace.sources:
429
+ current_domain: str | None = None
430
+ active = False
431
+ lines = source.text.splitlines()
432
+ pattern = _MODEL_FIELD_PATTERN if kind == "model" else _PROJECTION_FIELD_PATTERN
433
+ for line_no, line in enumerate(lines):
434
+ domain_match = _DOMAIN_PATTERN.match(line)
435
+ if domain_match:
436
+ current_domain = domain_match.group("quoted") or domain_match.group("name")
437
+ active = False
438
+ continue
439
+ decl_match = _DECL_PATTERN.match(line)
440
+ if decl_match and current_domain == domain_name:
441
+ decl_kind = decl_match.group("kind")
442
+ active = (
443
+ (decl_kind in _MODEL_DECL_KINDS if kind == "model" else decl_kind == kind)
444
+ and decl_match.group("name") == name
445
+ and int(decl_match.group("version")) == version
446
+ )
447
+ continue
448
+ if not active:
449
+ continue
450
+ field_match = pattern.match(line)
451
+ if field_match and field_match.group("name") == field_name:
452
+ return types.Location(
453
+ uri=source.uri,
454
+ range=types.Range(
455
+ start=types.Position(line=line_no, character=field_match.start("name")),
456
+ end=types.Position(line=line_no, character=field_match.end("name")),
457
+ ),
458
+ )
459
+ if line.strip() == "}":
460
+ active = False
461
+ return None
462
+
463
+
464
+ def _find_decl_location(
465
+ workspace,
466
+ domain_name: str,
467
+ kind: str,
468
+ name: str,
469
+ version: int,
470
+ ) -> types.Location | None:
471
+ for source in workspace.sources:
472
+ current_domain: str | None = None
473
+ lines = source.text.splitlines()
474
+ for line_no, line in enumerate(lines):
475
+ domain_match = _DOMAIN_PATTERN.match(line)
476
+ if domain_match:
477
+ current_domain = domain_match.group("quoted") or domain_match.group("name")
478
+ continue
479
+ decl_match = _DECL_PATTERN.match(line)
480
+ if not decl_match or current_domain != domain_name:
481
+ continue
482
+ decl_kind = decl_match.group("kind")
483
+ if kind == "model":
484
+ if decl_kind not in _MODEL_DECL_KINDS:
485
+ continue
486
+ elif decl_kind != kind:
487
+ continue
488
+ if decl_match.group("name") != name:
489
+ continue
490
+ if int(decl_match.group("version")) != version:
491
+ continue
492
+ return types.Location(
493
+ uri=source.uri,
494
+ range=types.Range(
495
+ start=types.Position(line=line_no, character=decl_match.start("name")),
496
+ end=types.Position(line=line_no, character=decl_match.end("name")),
497
+ ),
498
+ )
499
+ return None
500
+
501
+
502
+ def _current_scope(text: str, line: int) -> tuple[str, str, str, int] | None:
503
+ lines = text.splitlines()
504
+ current_domain: str | None = None
505
+ current_kind: str | None = None
506
+ current_name: str | None = None
507
+ current_version: int | None = None
508
+ for item in lines[: line + 1]:
509
+ domain_match = _DOMAIN_PATTERN.match(item)
510
+ if domain_match:
511
+ current_domain = domain_match.group("quoted") or domain_match.group("name")
512
+ current_kind = None
513
+ current_name = None
514
+ current_version = None
515
+ continue
516
+ decl_match = _DECL_PATTERN.match(item)
517
+ if decl_match and current_domain is not None:
518
+ current_kind = "model" if decl_match.group("kind") != "projection" else "projection"
519
+ current_name = decl_match.group("name")
520
+ current_version = int(decl_match.group("version"))
521
+ if current_domain and current_kind and current_name and current_version is not None:
522
+ return current_domain, current_kind, current_name, current_version
523
+ return None
524
+
525
+
526
+ def _word_at(text_line: str, character: int) -> str | None:
527
+ for match in _WORD_PATTERN.finditer(text_line):
528
+ if _contains(match.start(), match.end(), character):
529
+ return match.group(0)
530
+ return None
531
+
532
+
533
+ def _word_start(text_line: str, word: str) -> int:
534
+ match = re.search(rf"\b{re.escape(word)}\b", text_line)
535
+ return match.start() if match is not None else 0
536
+
537
+
538
+ def _word_end(text_line: str, word: str) -> int:
539
+ match = re.search(rf"\b{re.escape(word)}\b", text_line)
540
+ return match.end() if match is not None else len(text_line)
541
+
542
+
543
+ def _is_field_name(
544
+ workspace,
545
+ domain_name: str,
546
+ model_name: str,
547
+ version: int,
548
+ field_name: str,
549
+ ) -> bool:
550
+ return _find_field_location(workspace, domain_name, "model", model_name, version, field_name) is not None
551
+
552
+
553
+ def _is_projection_field_name(
554
+ workspace,
555
+ domain_name: str,
556
+ projection_name: str,
557
+ version: int,
558
+ field_name: str,
559
+ ) -> bool:
560
+ return _find_field_location(workspace, domain_name, "projection", projection_name, version, field_name) is not None
561
+
562
+
563
+ def _contains(start: int, end: int, character: int) -> bool:
564
+ return start <= character <= max(end - 1, start)