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,442 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from collections.abc import Iterable
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from modelable.compiler.workspace import Workspace
9
+ from modelable.llm.context import parse_model_ref
10
+ from modelable.parser.ir import (
11
+ DirectMapping,
12
+ FieldDef,
13
+ ModelVersion,
14
+ ProjectionField,
15
+ ProjectionVersion,
16
+ )
17
+ from modelable.registry.resolver import resolve_model_ref
18
+
19
+ _NODE_KIND_ORDER = {
20
+ "domain": 0,
21
+ "model": 1,
22
+ "model_version": 2,
23
+ "field": 3,
24
+ "projection": 4,
25
+ "projection_version": 5,
26
+ "projection_field": 6,
27
+ }
28
+ _EDGE_KIND_ORDER = {
29
+ "owns": 0,
30
+ "version_of": 1,
31
+ "contains_field": 2,
32
+ "has_projection": 3,
33
+ "version_of_projection": 4,
34
+ "maps_to": 5,
35
+ }
36
+ _EDGE_GROUP_ORDER = {
37
+ ("owns", "domain"): 0,
38
+ ("version_of", "model"): 1,
39
+ ("contains_field", "model_version"): 2,
40
+ ("has_projection", "domain"): 3,
41
+ ("version_of_projection", "projection"): 4,
42
+ ("contains_field", "projection_version"): 5,
43
+ ("maps_to", "projection_field"): 6,
44
+ }
45
+
46
+
47
+ def build_graph_export(workspace: Workspace, focus: str | None = None) -> dict[str, Any]:
48
+ """Build a canonical JSON graph export for the normalized workspace."""
49
+ builder = _GraphBuilder()
50
+ for domain in workspace.mdl.domains:
51
+ _add_domain(builder, workspace, domain)
52
+
53
+ graph = {
54
+ "kind": "workspace_graph",
55
+ "nodes": _sorted_nodes(builder.nodes.values()),
56
+ "edges": _sorted_edges(builder.edges.values()),
57
+ }
58
+ if focus is None:
59
+ return graph
60
+
61
+ focus_ref = parse_model_ref(focus)
62
+ selected_ids = _select_focus_subgraph(builder, focus_ref)
63
+ return {
64
+ "kind": "workspace_graph",
65
+ "nodes": _sorted_nodes(node for node in builder.nodes.values() if node["id"] in selected_ids),
66
+ "edges": _sorted_edges(
67
+ edge for edge in builder.edges.values() if edge["source"] in selected_ids and edge["target"] in selected_ids
68
+ ),
69
+ }
70
+
71
+
72
+ def write_graph_export(graph: dict[str, Any], out_path: Path) -> None:
73
+ out_path.write_text(
74
+ json.dumps(graph, indent=2, sort_keys=True) + "\n",
75
+ encoding="utf-8",
76
+ )
77
+
78
+
79
+ class _GraphBuilder:
80
+ def __init__(self) -> None:
81
+ self.nodes: dict[str, dict[str, Any]] = {}
82
+ self.edges: dict[tuple[str, str, str], dict[str, Any]] = {}
83
+ self.parents: dict[str, tuple[str, ...]] = {}
84
+ self.edges_by_source: dict[str, list[dict[str, Any]]] = {}
85
+ self.edges_by_target: dict[str, list[dict[str, Any]]] = {}
86
+
87
+ def add_node(self, node: dict[str, Any], *, parents: Iterable[str] = ()) -> dict[str, Any]:
88
+ node_id = node["id"]
89
+ if node_id not in self.nodes:
90
+ self.nodes[node_id] = node
91
+ if node_id not in self.parents:
92
+ self.parents[node_id] = tuple(parents)
93
+ return self.nodes[node_id]
94
+
95
+ def add_edge(self, source: str, target: str, kind: str) -> dict[str, Any]:
96
+ key = (kind, source, target)
97
+ if key not in self.edges:
98
+ edge = {"kind": kind, "source": source, "target": target}
99
+ self.edges[key] = edge
100
+ self.edges_by_source.setdefault(source, []).append(edge)
101
+ self.edges_by_target.setdefault(target, []).append(edge)
102
+ return self.edges[key]
103
+
104
+
105
+ def _sorted_nodes(nodes: Iterable[dict[str, Any]]) -> list[dict[str, Any]]:
106
+ return sorted(nodes, key=lambda item: (_NODE_KIND_ORDER.get(item["kind"], 99), item["id"]))
107
+
108
+
109
+ def _sorted_edges(edges: Iterable[dict[str, Any]]) -> list[dict[str, Any]]:
110
+ return sorted(
111
+ edges,
112
+ key=_edge_sort_key,
113
+ )
114
+
115
+
116
+ def _edge_sort_key(item: dict[str, Any]) -> tuple[int, str, str]:
117
+ source_kind = item["source"].split(":", 1)[0]
118
+ rank = _EDGE_GROUP_ORDER.get((item["kind"], source_kind), _EDGE_KIND_ORDER.get(item["kind"], 99))
119
+ return (rank, item["source"], item["target"])
120
+
121
+
122
+ def _add_domain(builder: _GraphBuilder, workspace: Workspace, domain) -> None:
123
+ domain_id = f"domain:{domain.name}"
124
+ builder.add_node(
125
+ {
126
+ "id": domain_id,
127
+ "kind": "domain",
128
+ "label": domain.name,
129
+ "domain": domain.name,
130
+ "target_ref": domain.name,
131
+ }
132
+ )
133
+
134
+ for model_name, versions in domain.models.items():
135
+ _add_model(builder, domain_id, domain.name, model_name, versions)
136
+
137
+ for projection_name, versions in domain.projections.items():
138
+ _add_projection(builder, workspace, domain_id, domain.name, projection_name, versions)
139
+
140
+
141
+ def _add_model(
142
+ builder: _GraphBuilder,
143
+ domain_id: str,
144
+ domain_name: str,
145
+ model_name: str,
146
+ versions: list[ModelVersion],
147
+ ) -> None:
148
+ model_id = f"model:{domain_name}.{model_name}"
149
+ builder.add_node(
150
+ {
151
+ "id": model_id,
152
+ "kind": "model",
153
+ "label": model_name,
154
+ "domain": domain_name,
155
+ "name": model_name,
156
+ "target_ref": f"{domain_name}.{model_name}",
157
+ },
158
+ parents=(domain_id,),
159
+ )
160
+ builder.add_edge(domain_id, model_id, "owns")
161
+
162
+ for version in sorted(versions, key=lambda item: item.version):
163
+ _add_model_version(builder, model_id, domain_name, model_name, version)
164
+
165
+
166
+ def _add_model_version(
167
+ builder: _GraphBuilder,
168
+ model_id: str,
169
+ domain_name: str,
170
+ model_name: str,
171
+ version: ModelVersion,
172
+ ) -> None:
173
+ version_id = f"model_version:{domain_name}.{model_name}@{version.version}"
174
+ builder.add_node(
175
+ {
176
+ "id": version_id,
177
+ "kind": "model_version",
178
+ "label": f"{model_name}@{version.version}",
179
+ "domain": domain_name,
180
+ "name": model_name,
181
+ "version": version.version,
182
+ "change_kind": version.change_kind.value,
183
+ "model_kind": version.model_kind.value,
184
+ "target_ref": f"{domain_name}.{model_name}@{version.version}",
185
+ },
186
+ parents=(model_id,),
187
+ )
188
+ builder.add_edge(model_id, version_id, "version_of")
189
+
190
+ for field in version.fields:
191
+ _add_model_field(builder, version_id, domain_name, model_name, version.version, field)
192
+
193
+
194
+ def _add_model_field(
195
+ builder: _GraphBuilder,
196
+ version_id: str,
197
+ domain_name: str,
198
+ model_name: str,
199
+ version: int,
200
+ field: FieldDef,
201
+ ) -> None:
202
+ field_id = f"field:{domain_name}.{model_name}@{version}.{field.name}"
203
+ builder.add_node(
204
+ {
205
+ "id": field_id,
206
+ "kind": "field",
207
+ "label": field.name,
208
+ "domain": domain_name,
209
+ "name": model_name,
210
+ "version": version,
211
+ "field": field.name,
212
+ "optional": field.optional,
213
+ "target_ref": f"{domain_name}.{model_name}@{version}.{field.name}",
214
+ },
215
+ parents=(version_id,),
216
+ )
217
+ builder.add_edge(version_id, field_id, "contains_field")
218
+
219
+
220
+ def _add_projection(
221
+ builder: _GraphBuilder,
222
+ workspace: Workspace,
223
+ domain_id: str,
224
+ domain_name: str,
225
+ projection_name: str,
226
+ versions: list[ProjectionVersion],
227
+ ) -> None:
228
+ projection_id = f"projection:{domain_name}.{projection_name}"
229
+ builder.add_node(
230
+ {
231
+ "id": projection_id,
232
+ "kind": "projection",
233
+ "label": projection_name,
234
+ "domain": domain_name,
235
+ "name": projection_name,
236
+ "target_ref": f"{domain_name}.{projection_name}",
237
+ },
238
+ parents=(domain_id,),
239
+ )
240
+ builder.add_edge(domain_id, projection_id, "has_projection")
241
+
242
+ for version in sorted(versions, key=lambda item: item.version):
243
+ _add_projection_version(
244
+ builder,
245
+ workspace,
246
+ projection_id,
247
+ domain_name,
248
+ projection_name,
249
+ version,
250
+ )
251
+
252
+
253
+ def _add_projection_version(
254
+ builder: _GraphBuilder,
255
+ workspace: Workspace,
256
+ projection_id: str,
257
+ domain_name: str,
258
+ projection_name: str,
259
+ version: ProjectionVersion,
260
+ ) -> None:
261
+ version_id = f"projection_version:{domain_name}.{projection_name}@{version.version}"
262
+ source_ref = _resolve_version_ref(workspace, version.source.model, version.source.version)
263
+ builder.add_node(
264
+ {
265
+ "id": version_id,
266
+ "kind": "projection_version",
267
+ "label": f"{projection_name}@{version.version}",
268
+ "domain": domain_name,
269
+ "name": projection_name,
270
+ "version": version.version,
271
+ "source_ref": source_ref,
272
+ "target_ref": f"{domain_name}.{projection_name}@{version.version}",
273
+ },
274
+ parents=(projection_id,),
275
+ )
276
+ builder.add_edge(projection_id, version_id, "version_of_projection")
277
+
278
+ for field in version.fields:
279
+ _add_projection_field(
280
+ builder,
281
+ workspace,
282
+ version_id,
283
+ domain_name,
284
+ projection_name,
285
+ version,
286
+ field,
287
+ )
288
+
289
+
290
+ def _add_projection_field(
291
+ builder: _GraphBuilder,
292
+ workspace: Workspace,
293
+ version_id: str,
294
+ domain_name: str,
295
+ projection_name: str,
296
+ projection_version: ProjectionVersion,
297
+ field: ProjectionField,
298
+ ) -> None:
299
+ field_id = f"projection_field:{domain_name}.{projection_name}@{projection_version.version}.{field.name}"
300
+ node: dict[str, Any] = {
301
+ "id": field_id,
302
+ "kind": "projection_field",
303
+ "label": field.name,
304
+ "domain": domain_name,
305
+ "name": projection_name,
306
+ "version": projection_version.version,
307
+ "field": field.name,
308
+ "target_ref": f"{domain_name}.{projection_name}@{projection_version.version}.{field.name}",
309
+ }
310
+ if isinstance(field.mapping, DirectMapping):
311
+ node["mapping_kind"] = "direct"
312
+ source_ref = _resolve_direct_mapping_ref(
313
+ workspace,
314
+ projection_version,
315
+ field.mapping.source_alias,
316
+ field.mapping.source_field,
317
+ )
318
+ node["source_ref"] = source_ref
319
+ else:
320
+ node["mapping_kind"] = "computed"
321
+
322
+ builder.add_node(node, parents=(version_id,))
323
+ builder.add_edge(version_id, field_id, "contains_field")
324
+
325
+ if isinstance(field.mapping, DirectMapping):
326
+ source_field_id = source_ref
327
+ source_node_id = source_field_id.replace("source_ref:", "field:")
328
+ builder.add_edge(field_id, source_node_id, "maps_to")
329
+
330
+
331
+ def _resolve_version_ref(workspace: Workspace, model_ref: str, version_spec) -> str:
332
+ resolved = resolve_model_ref(workspace.mdl, model_ref, version_spec)
333
+ return f"{resolved.domain_name}.{resolved.model_name}@{resolved.version.version}"
334
+
335
+
336
+ def _resolve_direct_mapping_ref(
337
+ workspace: Workspace,
338
+ projection_version: ProjectionVersion,
339
+ source_alias: str,
340
+ source_field: str,
341
+ ) -> str:
342
+ source_model_ref = _alias_map(projection_version).get(source_alias)
343
+ if source_model_ref is None:
344
+ raise LookupError(f"unknown source alias '{source_alias}' in projection {projection_version.version}")
345
+ resolved = resolve_model_ref(workspace.mdl, source_model_ref.model, source_model_ref.version)
346
+ field_name = source_field
347
+ return f"source_ref:{resolved.domain_name}.{resolved.model_name}@{resolved.version.version}.{field_name}"
348
+
349
+
350
+ def _alias_map(projection_version: ProjectionVersion) -> dict[str, Any]:
351
+ aliases: dict[str, Any] = {projection_version.source.alias: projection_version.source}
352
+ for join in projection_version.joins:
353
+ aliases[join.alias] = join
354
+ return aliases
355
+
356
+
357
+ def _select_focus_subgraph(builder: _GraphBuilder, focus_ref) -> set[str]:
358
+ model_version_id = f"model_version:{focus_ref.domain}.{focus_ref.name}@{focus_ref.version}"
359
+ projection_version_id = f"projection_version:{focus_ref.domain}.{focus_ref.name}@{focus_ref.version}"
360
+ model_node_id = f"model:{focus_ref.domain}.{focus_ref.name}"
361
+ projection_node_id = f"projection:{focus_ref.domain}.{focus_ref.name}"
362
+
363
+ focus_kind: str | None = None
364
+ seed_ids: set[str] = set()
365
+ if model_version_id in builder.nodes:
366
+ focus_kind = "model"
367
+ seed_ids.update(
368
+ {
369
+ model_node_id,
370
+ model_version_id,
371
+ *(
372
+ node_id
373
+ for node_id, node in builder.nodes.items()
374
+ if node.get("kind") == "field"
375
+ and node.get("domain") == focus_ref.domain
376
+ and node.get("name") == focus_ref.name
377
+ and node.get("version") == focus_ref.version
378
+ ),
379
+ }
380
+ )
381
+ elif projection_version_id in builder.nodes:
382
+ focus_kind = "projection"
383
+ seed_ids.update(
384
+ {
385
+ projection_node_id,
386
+ projection_version_id,
387
+ *(
388
+ node_id
389
+ for node_id, node in builder.nodes.items()
390
+ if node.get("kind") == "projection_field"
391
+ and node.get("domain") == focus_ref.domain
392
+ and node.get("name") == focus_ref.name
393
+ and node.get("version") == focus_ref.version
394
+ ),
395
+ }
396
+ )
397
+ else:
398
+ raise LookupError(f"unknown model or projection {focus_ref.domain}.{focus_ref.name}@{focus_ref.version}")
399
+
400
+ selected = set(seed_ids)
401
+ changed = True
402
+ while changed:
403
+ changed = False
404
+
405
+ for child_id in list(selected):
406
+ for parent_id in builder.parents.get(child_id, ()):
407
+ if parent_id not in selected:
408
+ selected.add(parent_id)
409
+ changed = True
410
+
411
+ if focus_kind == "model":
412
+ selected.update(_projection_neighbors_for_model_focus(builder, selected))
413
+ else:
414
+ selected.update(_source_neighbors_for_projection_focus(builder, selected))
415
+
416
+ return selected
417
+
418
+
419
+ def _projection_neighbors_for_model_focus(builder: _GraphBuilder, selected_ids: set[str]) -> set[str]:
420
+ selected_fields = {node_id for node_id in selected_ids if builder.nodes[node_id]["kind"] == "field"}
421
+ related: set[str] = set()
422
+ for field_id in selected_fields:
423
+ for edge in builder.edges_by_target.get(field_id, ()):
424
+ if edge["kind"] != "maps_to":
425
+ continue
426
+ related.add(edge["source"])
427
+ related.add(edge["target"])
428
+ return related
429
+
430
+
431
+ def _source_neighbors_for_projection_focus(builder: _GraphBuilder, selected_ids: set[str]) -> set[str]:
432
+ selected_projection_fields = {
433
+ node_id for node_id in selected_ids if builder.nodes[node_id]["kind"] == "projection_field"
434
+ }
435
+ related: set[str] = set()
436
+ for field_id in selected_projection_fields:
437
+ for edge in builder.edges_by_source.get(field_id, ()):
438
+ if edge["kind"] != "maps_to":
439
+ continue
440
+ related.add(edge["source"])
441
+ related.add(edge["target"])
442
+ return related
@@ -0,0 +1,43 @@
1
+ from __future__ import annotations
2
+
3
+ from .chat import CHAT_SYSTEM_PROMPT, ChatState, chat_reply
4
+ from .config import LlmConfig, resolve_llm_config
5
+ from .context import (
6
+ build_model_summary,
7
+ build_projection_summary,
8
+ build_workspace_summary,
9
+ parse_model_ref,
10
+ )
11
+ from .providers import (
12
+ AnthropicProvider,
13
+ LLMProvider,
14
+ LLMRequest,
15
+ LLMResponse,
16
+ OllamaProvider,
17
+ build_provider,
18
+ )
19
+ from .redaction import redact_sensitive_values
20
+ from .update_plan import UpdateChange, UpdatePlan, build_update_request, parse_update_plan
21
+
22
+ __all__ = [
23
+ "CHAT_SYSTEM_PROMPT",
24
+ "AnthropicProvider",
25
+ "ChatState",
26
+ "LLMProvider",
27
+ "LLMRequest",
28
+ "LLMResponse",
29
+ "LlmConfig",
30
+ "OllamaProvider",
31
+ "UpdateChange",
32
+ "UpdatePlan",
33
+ "build_model_summary",
34
+ "build_projection_summary",
35
+ "build_provider",
36
+ "build_update_request",
37
+ "build_workspace_summary",
38
+ "chat_reply",
39
+ "parse_model_ref",
40
+ "parse_update_plan",
41
+ "redact_sensitive_values",
42
+ "resolve_llm_config",
43
+ ]