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,279 @@
1
+ from __future__ import annotations
2
+
3
+ from modelable.parser.ir import (
4
+ AnnClassification,
5
+ AnnDeprecated,
6
+ AnnKey,
7
+ AnnOwner,
8
+ AnnPii,
9
+ AnnServer,
10
+ AnnWire,
11
+ ArrayType,
12
+ AutoProjectionDecl,
13
+ AutoProjectionTarget,
14
+ BindingDef,
15
+ ComputedMapping,
16
+ DecimalType,
17
+ DirectMapping,
18
+ DomainDef,
19
+ EnumType,
20
+ FieldDef,
21
+ FieldType,
22
+ GenerateTarget,
23
+ MapType,
24
+ MdlFile,
25
+ ModelVersion,
26
+ NamedType,
27
+ ObjectType,
28
+ PrimitiveType,
29
+ ProjectionField,
30
+ ProjectionVersion,
31
+ RefType,
32
+ VersionExact,
33
+ VersionMin,
34
+ VersionRange,
35
+ WorkspaceDef,
36
+ )
37
+ from modelable.parser.wire import render_wire_annotation
38
+
39
+
40
+ def render_mdl(mdl: MdlFile) -> str:
41
+ lines: list[str] = []
42
+ for domain in mdl.domains:
43
+ lines.extend(_render_domain(domain))
44
+ lines.append("")
45
+ for binding in mdl.bindings:
46
+ lines.extend(_render_binding(binding))
47
+ lines.append("")
48
+ if mdl.workspace is not None:
49
+ lines.extend(_render_workspace(mdl.workspace))
50
+ lines.append("")
51
+ while lines and lines[-1] == "":
52
+ lines.pop()
53
+ return "\n".join(lines) + ("\n" if lines else "")
54
+
55
+
56
+ def render_model_version(
57
+ domain_name: str, model_name: str, version: ModelVersion, owner: str | None = None, description: str | None = None
58
+ ) -> str:
59
+ return render_mdl(
60
+ MdlFile(
61
+ domains=[
62
+ DomainDef(
63
+ name=domain_name,
64
+ owner=owner,
65
+ description=description,
66
+ models={model_name: [version]},
67
+ )
68
+ ]
69
+ )
70
+ )
71
+
72
+
73
+ def render_projection_version(
74
+ domain_name: str,
75
+ projection_name: str,
76
+ version: ProjectionVersion,
77
+ owner: str | None = None,
78
+ description: str | None = None,
79
+ ) -> str:
80
+ return render_mdl(
81
+ MdlFile(
82
+ domains=[
83
+ DomainDef(
84
+ name=domain_name,
85
+ owner=owner,
86
+ description=description,
87
+ projections={projection_name: [version]},
88
+ )
89
+ ]
90
+ )
91
+ )
92
+
93
+
94
+ def _render_domain(domain: DomainDef) -> list[str]:
95
+ lines = [f"domain {domain.name} {{"]
96
+ if domain.owner:
97
+ lines.append(f' owner: "{domain.owner}"')
98
+ if domain.description:
99
+ lines.append(f' description: "{domain.description}"')
100
+ for model_name in sorted(domain.models):
101
+ for mv in domain.models[model_name]:
102
+ lines.extend(_indent(_render_model(model_name, mv), 2))
103
+ for projection_name in sorted(domain.projections):
104
+ for pv in domain.projections[projection_name]:
105
+ lines.extend(_indent(_render_projection(projection_name, pv), 2))
106
+ for decl in domain.auto_projections:
107
+ lines.extend(_indent(_render_auto_projection(decl), 2))
108
+ for target in domain.generate_targets:
109
+ lines.extend(_indent(_render_generate_target(target), 2))
110
+ lines.append("}")
111
+ return lines
112
+
113
+
114
+ def _render_model(model_name: str, version: ModelVersion) -> list[str]:
115
+ lines = [f"{version.model_kind.value} {model_name} @ {version.version} ({version.change_kind.value}) {{"]
116
+ for field in version.fields:
117
+ lines.append(_render_field(field, 2))
118
+ lines.append("}")
119
+ return lines
120
+
121
+
122
+ def _render_field(field: FieldDef, indent: int = 0) -> str:
123
+ prefix = " ".join(_render_annotations(field.annotations))
124
+ suffix = f"{field.name}{'?' if field.optional else ''}: {_render_type(field.type)}"
125
+ return _with_prefix(prefix, suffix, indent)
126
+
127
+
128
+ def _render_projection(projection_name: str, version: ProjectionVersion) -> list[str]:
129
+ lines = [f"projection {projection_name} @ {version.version}"]
130
+ lines.append(
131
+ f" from {version.source.model} @ {_render_version_spec(version.source.version)} as {version.source.alias}"
132
+ )
133
+ for join in version.joins:
134
+ lines.append(f" join {join.model} @ {_render_version_spec(join.version)} as {join.alias} on {join.on}")
135
+ if version.group_by:
136
+ lines.append(f" group by {', '.join(version.group_by)}")
137
+ lines.append("{")
138
+ for field in version.fields:
139
+ lines.append(_render_projection_field(field, 2))
140
+ lines.append("}")
141
+ return lines
142
+
143
+
144
+ def _render_projection_field(field: ProjectionField, indent: int = 0) -> str:
145
+ prefix = " ".join(_render_annotations(field.annotations))
146
+ if isinstance(field.mapping, DirectMapping):
147
+ body = f"{field.name} <- {field.mapping.source_alias}.{field.mapping.source_field}"
148
+ elif isinstance(field.mapping, ComputedMapping):
149
+ body = f"{field.name} = {field.mapping.expression}"
150
+ else:
151
+ body = field.name
152
+ return _with_prefix(prefix, body, indent)
153
+
154
+
155
+ def _render_auto_projection(decl: AutoProjectionDecl) -> list[str]:
156
+ lines = [f"auto projections {decl.model} @ {decl.version} {{"]
157
+ for target in decl.targets:
158
+ lines.append(_render_auto_target(target, 2))
159
+ lines.append("}")
160
+ return lines
161
+
162
+
163
+ def _render_auto_target(target: AutoProjectionTarget, indent: int = 0) -> str:
164
+ parts = [target.kind]
165
+ if target.excluded_fields or target.excluded_annotations:
166
+ exclusions = [
167
+ *target.excluded_fields,
168
+ *(_render_annotation_literal(ann) for ann in target.excluded_annotations),
169
+ ]
170
+ parts.append(f"exclude [{', '.join(exclusions)}]")
171
+ if target.operations:
172
+ parts.append(f"on [{', '.join(target.operations)}]")
173
+ return _join_tokens(parts, indent)
174
+
175
+
176
+ def _render_generate_target(target: GenerateTarget) -> str:
177
+ name = f"{target.name}({target.dialect})" if target.dialect else target.name
178
+ if target.output_path:
179
+ return f'{name} -> "{target.output_path}"'
180
+ return name
181
+
182
+
183
+ def _render_binding(binding: BindingDef) -> list[str]:
184
+ lines = [f"binding {binding.name} {{"]
185
+ lines.append(f" model: {binding.model} @ {binding.model_version}")
186
+ lines.append(f" adapter: {binding.adapter}")
187
+ if binding.table:
188
+ lines.append(f' table: "{binding.table}"')
189
+ for mapping in binding.field_mappings:
190
+ lines.append(f" {mapping.source} -> {mapping.target}")
191
+ lines.append("}")
192
+ return lines
193
+
194
+
195
+ def _render_workspace(workspace: WorkspaceDef) -> list[str]:
196
+ lines = ["workspace default {"]
197
+ if workspace.ai is not None:
198
+ lines.append(" ai {")
199
+ if workspace.ai.provider:
200
+ lines.append(f' provider: "{workspace.ai.provider}"')
201
+ if workspace.ai.model:
202
+ lines.append(f' model: "{workspace.ai.model}"')
203
+ lines.append(" }")
204
+ for target in workspace.generate_targets:
205
+ lines.append(f" {_render_generate_target(target)}")
206
+ lines.append("}")
207
+ return lines
208
+
209
+
210
+ def _render_type(field_type: FieldType) -> str:
211
+ if isinstance(field_type, PrimitiveType):
212
+ return field_type.kind
213
+ if isinstance(field_type, DecimalType):
214
+ return f"decimal({field_type.precision}, {field_type.scale})"
215
+ if isinstance(field_type, ArrayType):
216
+ return f"array<{_render_type(field_type.item)}>"
217
+ if isinstance(field_type, MapType):
218
+ return f"map<{_render_type(field_type.key)}, {_render_type(field_type.value)}>"
219
+ if isinstance(field_type, RefType):
220
+ return f"ref<{field_type.target}>"
221
+ if isinstance(field_type, EnumType):
222
+ return f"enum({', '.join(field_type.values)})"
223
+ if isinstance(field_type, ObjectType):
224
+ inner = " ".join(_render_field(field, 0) for field in field_type.fields)
225
+ return f"object {{ {inner} }}"
226
+ if isinstance(field_type, NamedType):
227
+ return field_type.name
228
+ return "string"
229
+
230
+
231
+ def _render_annotations(annotations) -> list[str]:
232
+ parts: list[str] = []
233
+ for ann in annotations:
234
+ parts.append(_render_annotation_literal(ann))
235
+ return parts
236
+
237
+
238
+ def _render_annotation_literal(annotation) -> str:
239
+ if isinstance(annotation, AnnKey):
240
+ return "@key"
241
+ if isinstance(annotation, AnnPii):
242
+ return "@pii"
243
+ if isinstance(annotation, AnnClassification):
244
+ return f'@classification("{annotation.level}")'
245
+ if isinstance(annotation, AnnDeprecated):
246
+ return f'@deprecated(replacedBy: "{annotation.replaced_by}")'
247
+ if isinstance(annotation, AnnOwner):
248
+ return f'@owner("{annotation.team}")'
249
+ if isinstance(annotation, AnnServer):
250
+ return "@server"
251
+ if isinstance(annotation, AnnWire):
252
+ return render_wire_annotation(annotation)
253
+ return "@unknown"
254
+
255
+
256
+ def _render_version_spec(version_spec) -> str:
257
+ if isinstance(version_spec, VersionExact):
258
+ return str(version_spec.version)
259
+ if isinstance(version_spec, VersionRange):
260
+ return f">={version_spec.min_inclusive}<{version_spec.max_exclusive}"
261
+ if isinstance(version_spec, VersionMin):
262
+ return f">={version_spec.min_inclusive}"
263
+ return "0"
264
+
265
+
266
+ def _join_tokens(parts: list[str], indent: int = 0) -> str:
267
+ text = " ".join(part for part in parts if part)
268
+ return f"{' ' * indent}{text}"
269
+
270
+
271
+ def _with_prefix(prefix: str, body: str, indent: int = 0) -> str:
272
+ if prefix:
273
+ return f"{' ' * indent}{prefix} {body}"
274
+ return f"{' ' * indent}{body}"
275
+
276
+
277
+ def _indent(lines: list[str], spaces: int) -> list[str]:
278
+ pad = " " * spaces
279
+ return [f"{pad}{line}" if line else line for line in lines]
@@ -0,0 +1,101 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ from typing import Literal, cast
6
+
7
+ from pydantic import BaseModel, Field
8
+
9
+ from modelable.llm.providers import LLMRequest
10
+
11
+
12
+ class UpdateChange(BaseModel):
13
+ kind: Literal[
14
+ "make_optional",
15
+ "make_required",
16
+ "rename_field",
17
+ "remove_field",
18
+ "change_type",
19
+ "add_field",
20
+ "change_source",
21
+ ]
22
+ field: str
23
+ new_name: str | None = None
24
+ type: str | None = None
25
+ source: str | None = None
26
+
27
+
28
+ class UpdatePlan(BaseModel):
29
+ target: str
30
+ target_kind: Literal["model", "projection"]
31
+ rationale: str | None = None
32
+ warnings: list[str] = Field(default_factory=list)
33
+ changes: list[UpdateChange] = Field(default_factory=list)
34
+
35
+
36
+ SYSTEM_PROMPT = """You edit Modelable .mdl definitions.
37
+ Return JSON only matching the supplied schema.
38
+ Do not include markdown fences, prose, or commentary.
39
+ Prefer the smallest set of changes that satisfies the instruction.
40
+ """
41
+
42
+ REPAIR_SYSTEM_PROMPT = """You edit Modelable .mdl definitions.
43
+ The previous response failed validation.
44
+ Return JSON only matching the supplied schema.
45
+ Do not include markdown fences, prose, or commentary.
46
+ Prefer the smallest set of changes that satisfies the instruction.
47
+ """
48
+
49
+
50
+ def build_update_request(*, ref: str, current_summary: str, current_text: str, instruction: str) -> LLMRequest:
51
+ user = (
52
+ f"Target reference: {ref}\n\n"
53
+ f"Current summary:\n{current_summary}\n\n"
54
+ f"Current .mdl:\n{current_text}\n\n"
55
+ f"Instruction:\n{instruction}\n"
56
+ )
57
+ return LLMRequest(
58
+ system=SYSTEM_PROMPT,
59
+ user=user,
60
+ temperature=0.1,
61
+ response_format="json",
62
+ schema=UpdatePlan.model_json_schema(),
63
+ )
64
+
65
+
66
+ def build_update_repair_request(
67
+ *,
68
+ ref: str,
69
+ current_summary: str,
70
+ current_text: str,
71
+ instruction: str,
72
+ validation_error: str,
73
+ ) -> LLMRequest:
74
+ user = (
75
+ f"Target reference: {ref}\n\n"
76
+ f"Current summary:\n{current_summary}\n\n"
77
+ f"Current .mdl:\n{current_text}\n\n"
78
+ f"Instruction:\n{instruction}\n\n"
79
+ f"Previous response validation error:\n{validation_error}\n\n"
80
+ "Return a corrected JSON object only."
81
+ )
82
+ return LLMRequest(
83
+ system=REPAIR_SYSTEM_PROMPT,
84
+ user=user,
85
+ temperature=0.05,
86
+ response_format="json",
87
+ schema=UpdatePlan.model_json_schema(),
88
+ )
89
+
90
+
91
+ def parse_update_plan(text: str) -> UpdatePlan:
92
+ payload = _extract_json(text)
93
+ return UpdatePlan.model_validate(payload)
94
+
95
+
96
+ def _extract_json(text: str) -> dict[str, object]:
97
+ stripped = text.strip()
98
+ if stripped.startswith("```"):
99
+ stripped = re.sub(r"^```(?:json)?\s*", "", stripped, flags=re.IGNORECASE)
100
+ stripped = re.sub(r"\s*```$", "", stripped)
101
+ return cast(dict[str, object], json.loads(stripped))
@@ -0,0 +1,10 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ def explain_validation_errors(errors: list[str]) -> str:
5
+ if not errors:
6
+ return "No validation errors found."
7
+ lines = ["Validation guidance:"]
8
+ for error in errors:
9
+ lines.append(f"- {error}")
10
+ return "\n".join(lines)
@@ -0,0 +1,3 @@
1
+ from modelable.lsp.server import server
2
+
3
+ __all__ = ["server"]
@@ -0,0 +1,4 @@
1
+ from modelable.lsp.server import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -0,0 +1,210 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+
5
+ from lsprotocol import types
6
+
7
+ from modelable.lsp.workspace import LspWorkspaceIndex
8
+
9
+ _DECL_PATTERN = re.compile(
10
+ r"^\s*(?P<kind>entity|aggregate|event|value|projection)\s+"
11
+ r"(?P<name>[A-Za-z_][A-Za-z0-9_]*)\s*@\s*(?P<version>\d+)"
12
+ )
13
+ _MODEL_WITHOUT_VERSION_PATTERN = re.compile(
14
+ r"^\s*(?P<kind>entity|aggregate|event|value)\s+(?P<name>[A-Za-z_][A-Za-z0-9_]*)\s*\{"
15
+ )
16
+ _FIELD_PATTERN = re.compile(r"^\s*(?:@[A-Za-z_][A-Za-z0-9_]*(?:\([^)]*\))?\s+)*(?P<name>[A-Za-z_][A-Za-z0-9_]*)\??\s*:")
17
+
18
+
19
+ def build_code_actions(
20
+ index: LspWorkspaceIndex,
21
+ uri: str,
22
+ line: int,
23
+ character: int,
24
+ diagnostics: list[types.Diagnostic],
25
+ ) -> list[types.CodeAction] | None:
26
+ source = index.documents.get(uri)
27
+ if source is None:
28
+ return None
29
+
30
+ if not source.text:
31
+ return None
32
+
33
+ if _has_parse_eof_diagnostic(diagnostics):
34
+ lines = source.text.splitlines()
35
+ if not lines:
36
+ return None
37
+
38
+ last_line_index = len(lines) - 1
39
+ last_character = len(lines[-1])
40
+ edit = types.WorkspaceEdit(
41
+ changes={
42
+ uri: [
43
+ types.TextEdit(
44
+ range=types.Range(
45
+ start=types.Position(line=last_line_index, character=last_character),
46
+ end=types.Position(line=last_line_index, character=last_character),
47
+ ),
48
+ new_text="\n}",
49
+ )
50
+ ]
51
+ }
52
+ )
53
+ return [
54
+ types.CodeAction(
55
+ title="Insert missing closing brace",
56
+ kind=types.CodeActionKind.QuickFix,
57
+ diagnostics=diagnostics,
58
+ is_preferred=True,
59
+ edit=edit,
60
+ )
61
+ ]
62
+
63
+ if _has_missing_key_diagnostic(diagnostics):
64
+ return _missing_key_action(source.text, uri, diagnostics)
65
+
66
+ if _has_missing_owner_diagnostic(diagnostics):
67
+ return _missing_owner_action(source.text, uri, diagnostics)
68
+
69
+ if _has_missing_version_diagnostic(diagnostics):
70
+ return _missing_version_action(source.text, uri, diagnostics)
71
+
72
+ return None
73
+
74
+
75
+ def _has_parse_eof_diagnostic(diagnostics: list[types.Diagnostic]) -> bool:
76
+ for diagnostic in diagnostics:
77
+ if diagnostic.code != "PARSE":
78
+ continue
79
+ if "Unexpected end-of-input" in diagnostic.message:
80
+ return True
81
+ return False
82
+
83
+
84
+ def _has_missing_key_diagnostic(diagnostics: list[types.Diagnostic]) -> bool:
85
+ return any(
86
+ diagnostic.code == "SEM" and "must have exactly one @key field" in diagnostic.message
87
+ for diagnostic in diagnostics
88
+ )
89
+
90
+
91
+ def _has_missing_owner_diagnostic(diagnostics: list[types.Diagnostic]) -> bool:
92
+ return any(
93
+ diagnostic.code == "SEM" and "must have an owner attribute" in diagnostic.message for diagnostic in diagnostics
94
+ )
95
+
96
+
97
+ def _has_missing_version_diagnostic(diagnostics: list[types.Diagnostic]) -> bool:
98
+ return any(
99
+ diagnostic.code == "SEM" and "must have a version header" in diagnostic.message for diagnostic in diagnostics
100
+ )
101
+
102
+
103
+ def _missing_key_action(text: str, uri: str, diagnostics: list[types.Diagnostic]) -> list[types.CodeAction] | None:
104
+ lines = text.splitlines()
105
+ in_model = False
106
+ for line_no, line in enumerate(lines):
107
+ if _DECL_PATTERN.match(line):
108
+ in_model = True
109
+ continue
110
+ if not in_model:
111
+ continue
112
+ field_match = _FIELD_PATTERN.match(line)
113
+ if field_match is None:
114
+ if line.strip() == "}":
115
+ in_model = False
116
+ continue
117
+
118
+ insert_at = field_match.start("name")
119
+ edit = types.WorkspaceEdit(
120
+ changes={
121
+ uri: [
122
+ types.TextEdit(
123
+ range=types.Range(
124
+ start=types.Position(line=line_no, character=insert_at),
125
+ end=types.Position(line=line_no, character=insert_at),
126
+ ),
127
+ new_text="@key ",
128
+ )
129
+ ]
130
+ }
131
+ )
132
+ return [
133
+ types.CodeAction(
134
+ title="Insert @key annotation",
135
+ kind=types.CodeActionKind.QuickFix,
136
+ diagnostics=diagnostics,
137
+ is_preferred=True,
138
+ edit=edit,
139
+ )
140
+ ]
141
+
142
+ return None
143
+
144
+
145
+ def _missing_owner_action(text: str, uri: str, diagnostics: list[types.Diagnostic]) -> list[types.CodeAction] | None:
146
+ lines = text.splitlines()
147
+ for line_no, line in enumerate(lines):
148
+ if "domain" in line and "{" in line:
149
+ # Insert owner right after the opening brace
150
+ indent = " "
151
+ if line_no + 1 < len(lines):
152
+ next_line = lines[line_no + 1]
153
+ if next_line.strip():
154
+ indent = next_line[: len(next_line) - len(next_line.lstrip())]
155
+
156
+ edit = types.WorkspaceEdit(
157
+ changes={
158
+ uri: [
159
+ types.TextEdit(
160
+ range=types.Range(
161
+ start=types.Position(line=line_no + 1, character=0),
162
+ end=types.Position(line=line_no + 1, character=0),
163
+ ),
164
+ new_text=f'{indent}owner: "required-team"\n',
165
+ )
166
+ ]
167
+ }
168
+ )
169
+ return [
170
+ types.CodeAction(
171
+ title='Insert owner: "required-team"',
172
+ kind=types.CodeActionKind.QuickFix,
173
+ diagnostics=diagnostics,
174
+ is_preferred=True,
175
+ edit=edit,
176
+ )
177
+ ]
178
+ return None
179
+
180
+
181
+ def _missing_version_action(text: str, uri: str, diagnostics: list[types.Diagnostic]) -> list[types.CodeAction] | None:
182
+ lines = text.splitlines()
183
+ for line_no, line in enumerate(lines):
184
+ match = _MODEL_WITHOUT_VERSION_PATTERN.match(line)
185
+ if match:
186
+ # Insert @ 1 (additive) before the brace
187
+ opening_brace_at = line.find("{")
188
+ edit = types.WorkspaceEdit(
189
+ changes={
190
+ uri: [
191
+ types.TextEdit(
192
+ range=types.Range(
193
+ start=types.Position(line=line_no, character=opening_brace_at),
194
+ end=types.Position(line=line_no, character=opening_brace_at),
195
+ ),
196
+ new_text="@ 1 (additive) ",
197
+ )
198
+ ]
199
+ }
200
+ )
201
+ return [
202
+ types.CodeAction(
203
+ title="Insert @ 1 (additive)",
204
+ kind=types.CodeActionKind.QuickFix,
205
+ diagnostics=diagnostics,
206
+ is_preferred=True,
207
+ edit=edit,
208
+ )
209
+ ]
210
+ return None