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.
- modelable/__init__.py +1 -0
- modelable/__main__.py +3 -0
- modelable/_pydantic_py314_compat.py +31 -0
- modelable/cli.py +41 -0
- modelable/commands/__init__.py +1 -0
- modelable/commands/apicurio.py +84 -0
- modelable/commands/codegen.py +241 -0
- modelable/commands/common.py +43 -0
- modelable/commands/compile.py +237 -0
- modelable/commands/create.py +164 -0
- modelable/commands/diff.py +82 -0
- modelable/commands/graph.py +53 -0
- modelable/commands/llm.py +564 -0
- modelable/commands/lsp.py +15 -0
- modelable/commands/runtime.py +37 -0
- modelable/commands/scenario.py +104 -0
- modelable/commands/spec.py +197 -0
- modelable/commands/workspace.py +240 -0
- modelable/compat/__init__.py +11 -0
- modelable/compat/checker.py +179 -0
- modelable/compat/diff.py +169 -0
- modelable/compiler/__init__.py +3 -0
- modelable/compiler/compiler.py +19 -0
- modelable/compiler/workspace.py +346 -0
- modelable/diagnostics/__init__.py +3 -0
- modelable/diagnostics/model.py +27 -0
- modelable/emitters/__init__.py +0 -0
- modelable/emitters/base.py +22 -0
- modelable/emitters/csharp.py +245 -0
- modelable/emitters/dbt_yaml.py +290 -0
- modelable/emitters/diagnostics.py +25 -0
- modelable/emitters/fhir.py +694 -0
- modelable/emitters/fhir_validator.py +36 -0
- modelable/emitters/go.py +334 -0
- modelable/emitters/java.py +264 -0
- modelable/emitters/json_schema.py +458 -0
- modelable/emitters/markdown.py +252 -0
- modelable/emitters/odcs.py +355 -0
- modelable/emitters/openlineage.py +315 -0
- modelable/emitters/openmetadata.py +258 -0
- modelable/emitters/python.py +282 -0
- modelable/emitters/rust.py +643 -0
- modelable/emitters/shapes.py +261 -0
- modelable/emitters/sql.py +266 -0
- modelable/emitters/targets.py +141 -0
- modelable/emitters/typescript.py +352 -0
- modelable/expressions/__init__.py +0 -0
- modelable/expressions/cel.py +547 -0
- modelable/governance/__init__.py +3 -0
- modelable/governance/checker.py +271 -0
- modelable/governance/por.py +46 -0
- modelable/grammar/__init__.py +1 -0
- modelable/grammar/modelable.lark +257 -0
- modelable/graph/__init__.py +5 -0
- modelable/graph/export.py +442 -0
- modelable/llm/__init__.py +43 -0
- modelable/llm/chat.py +255 -0
- modelable/llm/config.py +87 -0
- modelable/llm/context.py +194 -0
- modelable/llm/engine.py +976 -0
- modelable/llm/importers.py +1077 -0
- modelable/llm/provenance.py +84 -0
- modelable/llm/providers.py +182 -0
- modelable/llm/qa.py +126 -0
- modelable/llm/recommendations.py +33 -0
- modelable/llm/redaction.py +19 -0
- modelable/llm/render.py +279 -0
- modelable/llm/update_plan.py +101 -0
- modelable/llm/validation_help.py +10 -0
- modelable/lsp/__init__.py +3 -0
- modelable/lsp/__main__.py +4 -0
- modelable/lsp/code_actions.py +210 -0
- modelable/lsp/completion.py +480 -0
- modelable/lsp/definition.py +343 -0
- modelable/lsp/diagnostics.py +31 -0
- modelable/lsp/document_symbols.py +197 -0
- modelable/lsp/federation.py +261 -0
- modelable/lsp/folding.py +33 -0
- modelable/lsp/formatting.py +64 -0
- modelable/lsp/highlight.py +30 -0
- modelable/lsp/hover.py +370 -0
- modelable/lsp/inlay_hints.py +158 -0
- modelable/lsp/references.py +511 -0
- modelable/lsp/rename.py +564 -0
- modelable/lsp/semantic_tokens.py +412 -0
- modelable/lsp/server.py +370 -0
- modelable/lsp/workspace.py +83 -0
- modelable/lsp/workspace_symbols.py +104 -0
- modelable/parser/__init__.py +94 -0
- modelable/parser/ir.py +451 -0
- modelable/parser/parse.py +47 -0
- modelable/parser/transformer.py +798 -0
- modelable/parser/wire.py +68 -0
- modelable/planner/__init__.py +0 -0
- modelable/planner/lineage.py +91 -0
- modelable/planner/planner.py +134 -0
- modelable/planner/plans.py +122 -0
- modelable/py.typed +0 -0
- modelable/registry/__init__.py +9 -0
- modelable/registry/apicurio.py +166 -0
- modelable/registry/base.py +18 -0
- modelable/registry/factory.py +18 -0
- modelable/registry/index.py +419 -0
- modelable/registry/local.py +26 -0
- modelable/registry/oci.py +22 -0
- modelable/registry/resolver.py +213 -0
- modelable/registry/schema.sql +119 -0
- modelable/registry/signature.py +26 -0
- modelable/release.py +125 -0
- modelable/runtime/__init__.py +5 -0
- modelable/runtime/adapter/__init__.py +17 -0
- modelable/runtime/adapter/base.py +18 -0
- modelable/runtime/adapter/postgres.py +82 -0
- modelable/specs/__init__.py +23 -0
- modelable/specs/tracking.py +220 -0
- modelable/validation/__init__.py +3 -0
- modelable/validation/semantic.py +659 -0
- modelable-1.0.0.dist-info/METADATA +61 -0
- modelable-1.0.0.dist-info/RECORD +122 -0
- modelable-1.0.0.dist-info/WHEEL +4 -0
- modelable-1.0.0.dist-info/entry_points.txt +2 -0
- 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
|
+
]
|