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
+ from dataclasses import asdict
4
+ from pathlib import Path
5
+
6
+ import click
7
+ from rich.console import Console
8
+
9
+ from modelable.commands.diff import run_diff
10
+ from modelable.compiler.workspace import load_workspace
11
+ from modelable.llm.chat import ChatState, chat_turn
12
+ from modelable.llm.config import resolve_llm_config
13
+ from modelable.llm.context import build_workspace_summary
14
+ from modelable.llm.engine import (
15
+ answer_model_question_cli,
16
+ attach_external_version,
17
+ describe_path_or_ref,
18
+ explain_validation,
19
+ generate_entity_from_prompt,
20
+ import_definition,
21
+ recommend_cli,
22
+ render_attach_audit_summary,
23
+ render_update_audit_summary,
24
+ render_write_audit_summary,
25
+ suggest_projection,
26
+ transform_ref_to_target,
27
+ update_definition,
28
+ validate_generated_text,
29
+ )
30
+ from modelable.llm.provenance import (
31
+ AttachmentRecord,
32
+ build_write_provenance,
33
+ write_attachment_record,
34
+ write_provenance_sidecar,
35
+ )
36
+ from modelable.llm.providers import build_provider
37
+
38
+ console = Console()
39
+
40
+
41
+ def register_llm_commands(cli_group: click.Group) -> None:
42
+ cli_group.add_command(describe)
43
+ cli_group.add_command(generate)
44
+ cli_group.add_command(import_model)
45
+ cli_group.add_command(diff)
46
+ cli_group.add_command(update)
47
+ cli_group.add_command(attach)
48
+ cli_group.add_command(transform)
49
+ cli_group.add_command(suggest_projection_cmd)
50
+ cli_group.add_command(ask)
51
+ cli_group.add_command(recommend)
52
+ cli_group.add_command(explain)
53
+ cli_group.add_command(chat)
54
+
55
+
56
+ @click.command()
57
+ @click.argument("target", required=False)
58
+ @click.option("--path", "path", type=click.Path(exists=True, path_type=Path), default=None)
59
+ def describe(target: str | None, path: Path | None) -> None:
60
+ """Describe a model, projection, or workspace."""
61
+ if target is None and path is None:
62
+ raise click.UsageError("provide a model ref or --path")
63
+ console.print(describe_path_or_ref(path=path, ref=target))
64
+
65
+
66
+ def _detect_format(path: Path) -> str | None:
67
+ import json
68
+
69
+ import yaml
70
+
71
+ suffix = path.suffix.lower()
72
+ if suffix in {".yml", ".yaml"}:
73
+ try:
74
+ content = path.read_text(encoding="utf-8")
75
+ doc = yaml.safe_load(content)
76
+ if isinstance(doc, dict) and ("models" in doc or "sources" in doc):
77
+ return "dbt"
78
+ if isinstance(doc, dict) and (
79
+ doc.get("kind") == "DataContract"
80
+ or "schema" in doc
81
+ or "schemas" in doc
82
+ or "dataContractSpecification" in doc
83
+ ):
84
+ return "odcs"
85
+ except Exception:
86
+ pass
87
+ return None
88
+ if suffix == ".json":
89
+ try:
90
+ content = path.read_text(encoding="utf-8")
91
+ doc = json.loads(content)
92
+ if isinstance(doc, dict):
93
+ if doc.get("resourceType") == "StructureDefinition":
94
+ return "fhir"
95
+ if "nodes" in doc:
96
+ return "dbt"
97
+ if "title" in doc and "type" in doc:
98
+ return "json-schema"
99
+ if "openapi" in doc or "swagger" in doc:
100
+ return "openapi"
101
+ if isinstance(doc.get("type"), str) and "fields" in doc:
102
+ return "avro"
103
+ if isinstance(doc.get("type"), dict) and doc["type"].get("type") == "record":
104
+ return "avro"
105
+ except Exception:
106
+ pass
107
+ if suffix == ".avsc":
108
+ try:
109
+ content = path.read_text(encoding="utf-8")
110
+ doc = json.loads(content)
111
+ if isinstance(doc, dict) and isinstance(doc.get("type"), str) and "fields" in doc:
112
+ return "avro"
113
+ except Exception:
114
+ pass
115
+ if suffix == ".proto":
116
+ try:
117
+ content = path.read_text(encoding="utf-8")
118
+ if "message " in content:
119
+ return "protobuf"
120
+ except Exception:
121
+ pass
122
+ if suffix in {".sql", ".ddl"}:
123
+ try:
124
+ content = path.read_text(encoding="utf-8")
125
+ upper = content.upper()
126
+ if "CREATE TABLE" in upper or "CREATE VIEW" in upper:
127
+ return "sql"
128
+ except Exception:
129
+ pass
130
+ return None
131
+
132
+
133
+ @click.command()
134
+ @click.option("--from", "source", required=True, help="Natural language or source file path.")
135
+ @click.option("--format", "source_format", default=None, help="Source format for import paths.")
136
+ @click.option("--domain", "domain_name", default=None, help="Override the output domain.")
137
+ @click.option("--name", "model_name", default=None, help="Override the output model name.")
138
+ @click.option("--output", "output", type=click.Path(path_type=Path), default=None)
139
+ def generate(
140
+ source: str, source_format: str | None, domain_name: str | None, model_name: str | None, output: Path | None
141
+ ) -> None:
142
+ """Generate a draft entity definition."""
143
+ source_path = Path(source)
144
+ source_descriptor = f"path={source_path}"
145
+ try:
146
+ if source_path.exists():
147
+ detected = source_format or _detect_format(source_path)
148
+ if detected is not None:
149
+ text = import_definition(source_path, detected, domain_name=domain_name, source_name=model_name)
150
+ source_format = detected
151
+ else:
152
+ text = source_path.read_text(encoding="utf-8")
153
+ else:
154
+ if source_format is not None:
155
+ text = import_definition(source, source_format, domain_name=domain_name, source_name=model_name)
156
+ source_descriptor = "inline"
157
+ else:
158
+ text = generate_entity_from_prompt(source, domain_name=domain_name, model_name=model_name)
159
+ source_descriptor = "prompt"
160
+ except ValueError as exc:
161
+ raise click.ClickException(str(exc)) from exc
162
+ except (UnicodeDecodeError, OSError) as exc:
163
+ raise click.ClickException(f"Could not read source: {exc}") from exc
164
+
165
+ _mdl, errors = validate_generated_text(text)
166
+ if errors:
167
+ for error in errors:
168
+ console.print(f"[red]ERROR[/red] {error}")
169
+ raise click.ClickException("generated output failed validation")
170
+
171
+ rendered = text if text.endswith("\n") else text + "\n"
172
+ if output is not None:
173
+ output.write_text(rendered, encoding="utf-8")
174
+ write_provenance_sidecar(
175
+ output,
176
+ build_write_provenance(
177
+ command="generate",
178
+ artifact_path=output,
179
+ provider="local",
180
+ model="modelable-local",
181
+ validation_status="passed",
182
+ diagnostics_repaired=0,
183
+ inputs={
184
+ "source": source_descriptor,
185
+ "format": source_format or "prompt",
186
+ },
187
+ ),
188
+ )
189
+ console.print(f"[green]OK[/green] wrote {output}")
190
+ console.print(
191
+ render_write_audit_summary(
192
+ provider="local",
193
+ model="modelable-local",
194
+ validation_status="passed",
195
+ files_written=str(output),
196
+ inputs=f"{source_descriptor} format={source_format or 'prompt'}",
197
+ diagnostics_repaired=0,
198
+ )
199
+ )
200
+ else:
201
+ console.print(rendered.rstrip())
202
+
203
+
204
+ @click.command(name="import")
205
+ @click.argument("source", type=click.Path(exists=True, path_type=Path))
206
+ @click.option(
207
+ "--format", "source_format", required=True, help="json-schema, openapi, avro, protobuf, sql, dbt, fhir, odcs"
208
+ )
209
+ @click.option("--domain", "domain_name", default=None)
210
+ @click.option("--output", "output", type=click.Path(path_type=Path), default=None)
211
+ def import_model(source: Path, source_format: str, domain_name: str | None, output: Path | None) -> None:
212
+ """Import a schema or DDL file into Modelable text."""
213
+ try:
214
+ text = import_definition(source, source_format, domain_name=domain_name)
215
+ except ValueError as exc:
216
+ raise click.ClickException(str(exc)) from exc
217
+ except (UnicodeDecodeError, OSError) as exc:
218
+ raise click.ClickException(f"Could not read source: {exc}") from exc
219
+ _mdl, errors = validate_generated_text(text)
220
+ if errors:
221
+ for error in errors:
222
+ console.print(f"[red]ERROR[/red] {error}")
223
+ raise click.ClickException("imported definition failed validation")
224
+ if output is not None:
225
+ output.write_text(text, encoding="utf-8")
226
+ write_provenance_sidecar(
227
+ output,
228
+ build_write_provenance(
229
+ command="import",
230
+ artifact_path=output,
231
+ provider="local",
232
+ model="modelable-local",
233
+ validation_status="passed",
234
+ diagnostics_repaired=0,
235
+ inputs={
236
+ "path": str(source),
237
+ "format": source_format,
238
+ "domain": domain_name or "",
239
+ },
240
+ ),
241
+ )
242
+ console.print(f"[green]OK[/green] wrote {output}")
243
+ console.print(
244
+ render_write_audit_summary(
245
+ provider="local",
246
+ model="modelable-local",
247
+ validation_status="passed",
248
+ files_written=str(output),
249
+ inputs=f"path={source} format={source_format}",
250
+ diagnostics_repaired=0,
251
+ )
252
+ )
253
+ else:
254
+ console.print(text.rstrip())
255
+
256
+
257
+ @click.command()
258
+ @click.argument("from_ref")
259
+ @click.argument("to_ref")
260
+ @click.option("--path", "path", type=click.Path(exists=True, path_type=Path), required=True)
261
+ def diff(from_ref: str, to_ref: str, path: Path) -> None:
262
+ """Compare two published model versions."""
263
+ run_diff(from_ref, to_ref, path)
264
+
265
+
266
+ @click.command()
267
+ @click.argument("ref")
268
+ @click.argument("instruction", nargs=-1)
269
+ @click.option("--path", "path", type=click.Path(exists=True, path_type=Path), required=True)
270
+ @click.option("--output", "output", type=click.Path(path_type=Path), default=None)
271
+ @click.option("--preview", is_flag=True, help="Show the edit diff without writing changes.")
272
+ @click.option("--provider", "provider", default=None, help="Provider name, for example ollama or anthropic.")
273
+ @click.option("--model", "model", default=None, help="Model identifier.")
274
+ @click.option("--base-url", "base_url", default=None, help="Provider base URL.")
275
+ def update(
276
+ ref: str,
277
+ instruction: tuple[str, ...],
278
+ path: Path,
279
+ output: Path | None,
280
+ preview: bool,
281
+ provider: str | None,
282
+ model: str | None,
283
+ base_url: str | None,
284
+ ) -> None:
285
+ """Apply a natural-language update to an existing model version."""
286
+ workspace = load_workspace(path)
287
+ llm_config = resolve_llm_config(
288
+ flag_provider=provider,
289
+ flag_model=model,
290
+ flag_base_url=base_url,
291
+ workspace=workspace.mdl.workspace,
292
+ )
293
+ llm_provider = build_provider(llm_config.provider, model=llm_config.model, base_url=llm_config.base_url)
294
+ try:
295
+ result = update_definition(
296
+ path,
297
+ ref,
298
+ " ".join(instruction),
299
+ output=output,
300
+ write=not preview,
301
+ provider=llm_provider,
302
+ llm_config=llm_config,
303
+ )
304
+ except ValueError as exc:
305
+ raise click.ClickException(str(exc)) from exc
306
+ if preview:
307
+ from modelable.llm.chat import _render_update_preview
308
+
309
+ console.print(_render_update_preview(result))
310
+ else:
311
+ write_provenance_sidecar(
312
+ result.path,
313
+ build_write_provenance(
314
+ command="update",
315
+ artifact_path=result.path,
316
+ provider=result.provider,
317
+ model=result.model,
318
+ validation_status="passed",
319
+ diagnostics_repaired=result.diagnostics_repaired,
320
+ inputs={
321
+ "ref": result.ref,
322
+ "source_path": str(result.source_path),
323
+ },
324
+ ),
325
+ )
326
+ console.print(result.content.rstrip())
327
+ for warning in result.warnings:
328
+ console.print(f"[yellow]WARN[/yellow] {warning}")
329
+ if not preview:
330
+ console.print(render_update_audit_summary(result))
331
+
332
+
333
+ @click.command()
334
+ @click.argument("ref")
335
+ @click.option(
336
+ "--source",
337
+ "source",
338
+ type=click.Path(exists=True, path_type=Path),
339
+ required=True,
340
+ help="External dbt schema.yml or FHIR StructureDefinition JSON file.",
341
+ )
342
+ @click.option("--source-format", "source_format", type=click.Choice(["dbt", "fhir", "odcs"]), required=True)
343
+ @click.option(
344
+ "--source-name",
345
+ "source_name",
346
+ default=None,
347
+ help="dbt model name or FHIR resource name to match, if the source defines multiple.",
348
+ )
349
+ @click.option("--path", "path", type=click.Path(exists=True, path_type=Path), required=True)
350
+ @click.option("--output", "output", type=click.Path(path_type=Path), default=None)
351
+ @click.option("--preview", is_flag=True, help="Show the new version diff without writing changes.")
352
+ def attach(
353
+ ref: str,
354
+ source: Path,
355
+ source_format: str,
356
+ source_name: str | None,
357
+ path: Path,
358
+ output: Path | None,
359
+ preview: bool,
360
+ ) -> None:
361
+ """Attach a model version to an external dbt or FHIR source and record drift as a new version."""
362
+ try:
363
+ result = attach_external_version(
364
+ path, ref, source, source_format, source_name=source_name, output=output, write=not preview
365
+ )
366
+ except ValueError as exc:
367
+ raise click.ClickException(str(exc)) from exc
368
+
369
+ for warning in result.warnings:
370
+ console.print(f"[yellow]WARN[/yellow] {warning}")
371
+
372
+ if not result.attached:
373
+ console.print(
374
+ f"[green]OK[/green] {ref} already matches {result.source_descriptor} "
375
+ f"({result.source_format}); no new version created"
376
+ )
377
+ return
378
+
379
+ if preview:
380
+ from modelable.llm.chat import _render_update_preview
381
+
382
+ console.print(_render_update_preview(result))
383
+ return
384
+
385
+ write_attachment_record(
386
+ result.path,
387
+ AttachmentRecord(
388
+ ref=result.ref,
389
+ source_format=result.source_format,
390
+ source_name=result.source_name,
391
+ source_path=result.source_descriptor,
392
+ source_hash=result.source_hash,
393
+ from_version=result.from_version,
394
+ to_version=result.to_version,
395
+ change_kind=result.change_kind,
396
+ changes=[asdict(change) for change in result.changes],
397
+ ),
398
+ )
399
+ console.print(result.content.rstrip())
400
+ console.print(
401
+ f"[green]OK[/green] attached {ref} to {result.source_descriptor} "
402
+ f"({result.source_format}); new version {result.to_version} ({result.change_kind})"
403
+ )
404
+ console.print(render_attach_audit_summary(result))
405
+
406
+
407
+ @click.command()
408
+ @click.argument("ref")
409
+ @click.option("--path", "path", type=click.Path(exists=True, path_type=Path), required=True)
410
+ @click.option(
411
+ "--to",
412
+ "target",
413
+ type=click.Choice(["json-schema", "markdown", "typescript", "csharp", "java", "python", "rust", "go", "dbt-yaml"]),
414
+ required=True,
415
+ )
416
+ @click.option("--out", "output", type=click.Path(path_type=Path), default=None)
417
+ @click.option("--explain", is_flag=True, help="Show a mapping explanation alongside the emitted artifact.")
418
+ def transform(ref: str, path: Path, target: str, output: Path | None, explain: bool) -> None:
419
+ """Transform a model or projection into another artifact format."""
420
+ result = transform_ref_to_target(path, ref, target)
421
+ if output is not None:
422
+ output.write_text(result.content, encoding="utf-8")
423
+ write_provenance_sidecar(
424
+ output,
425
+ build_write_provenance(
426
+ command="transform",
427
+ artifact_path=output,
428
+ provider="local",
429
+ model="modelable-local",
430
+ validation_status="passed",
431
+ diagnostics_repaired=0,
432
+ inputs={
433
+ "ref": ref,
434
+ "target": target,
435
+ "path": str(path),
436
+ },
437
+ ),
438
+ )
439
+ console.print(f"[green]OK[/green] wrote {output}")
440
+ console.print(
441
+ render_write_audit_summary(
442
+ provider="local",
443
+ model="modelable-local",
444
+ validation_status="passed",
445
+ files_written=str(output),
446
+ inputs=f"ref={ref} target={target} path={path}",
447
+ diagnostics_repaired=0,
448
+ )
449
+ )
450
+ if explain and result.explanation is not None:
451
+ console.print(result.explanation)
452
+ console.print(result.content)
453
+ for warning in result.warnings:
454
+ console.print(f"[yellow]WARN[/yellow] {warning}")
455
+
456
+
457
+ @click.command(name="suggest-projection")
458
+ @click.option("--path", "path", type=click.Path(exists=True, path_type=Path), required=True)
459
+ @click.option("--source", "source_ref", required=True)
460
+ @click.option("--consumer", "consumer_domain", required=True)
461
+ @click.option("--output", "output", type=click.Path(path_type=Path), default=None)
462
+ def suggest_projection_cmd(path: Path, source_ref: str, consumer_domain: str, output: Path | None) -> None:
463
+ """Suggest a projection for a consuming domain."""
464
+ text = suggest_projection(path, source_ref, consumer_domain)
465
+ _mdl, errors = validate_generated_text(text)
466
+ if errors:
467
+ for error in errors:
468
+ console.print(f"[red]ERROR[/red] {error}")
469
+ raise click.ClickException("suggested projection failed validation")
470
+ if output is not None:
471
+ output.write_text(text, encoding="utf-8")
472
+ write_provenance_sidecar(
473
+ output,
474
+ build_write_provenance(
475
+ command="suggest-projection",
476
+ artifact_path=output,
477
+ provider="local",
478
+ model="modelable-local",
479
+ validation_status="passed",
480
+ diagnostics_repaired=0,
481
+ inputs={
482
+ "path": str(path),
483
+ "source": source_ref,
484
+ "consumer": consumer_domain,
485
+ },
486
+ ),
487
+ )
488
+ console.print(f"[green]OK[/green] wrote {output}")
489
+ console.print(
490
+ render_write_audit_summary(
491
+ provider="local",
492
+ model="modelable-local",
493
+ validation_status="passed",
494
+ files_written=str(output),
495
+ inputs=f"path={path} source={source_ref} consumer={consumer_domain}",
496
+ diagnostics_repaired=0,
497
+ )
498
+ )
499
+ else:
500
+ console.print(text.rstrip())
501
+
502
+
503
+ @click.command()
504
+ @click.argument("question")
505
+ @click.option("--path", "path", type=click.Path(exists=True, path_type=Path), required=True)
506
+ def ask(question: str, path: Path) -> None:
507
+ """Answer a question about the current model workspace."""
508
+ console.print(answer_model_question_cli(path, question))
509
+
510
+
511
+ @click.command()
512
+ @click.option("--path", "path", type=click.Path(exists=True, path_type=Path), required=True)
513
+ @click.option("--ref", "ref", default=None)
514
+ @click.option("--consumer", "consumer", default=None)
515
+ def recommend(path: Path, ref: str | None, consumer: str | None) -> None:
516
+ """Provide a recommendation for a model or projection."""
517
+ console.print(recommend_cli(path, ref=ref, consumer=consumer))
518
+
519
+
520
+ @click.command()
521
+ @click.option("--path", "path", type=click.Path(exists=True, path_type=Path), required=True)
522
+ def explain(path: Path) -> None:
523
+ """Explain current validation errors."""
524
+ console.print(explain_validation(path))
525
+
526
+
527
+ @click.command()
528
+ @click.option("--path", "path", type=click.Path(exists=True, path_type=Path), required=True)
529
+ @click.option("--ref", "ref", default=None, help="Optional model or projection ref to focus the chat.")
530
+ @click.option("--message", "message", default=None, help="Send a single message and exit.")
531
+ @click.option("--provider", "provider", default=None, help="Provider name, for example ollama or anthropic.")
532
+ @click.option("--model", "model", default=None, help="Model identifier.")
533
+ @click.option("--base-url", "base_url", default=None, help="Provider base URL.")
534
+ def chat(
535
+ path: Path, ref: str | None, message: str | None, provider: str | None, model: str | None, base_url: str | None
536
+ ) -> None:
537
+ """Chat with a model about the current workspace."""
538
+ workspace = load_workspace(path)
539
+ workspace_summary = build_workspace_summary(workspace)
540
+ config = resolve_llm_config(
541
+ flag_provider=provider,
542
+ flag_model=model,
543
+ flag_base_url=base_url,
544
+ workspace=workspace.mdl.workspace,
545
+ )
546
+ llm_provider = build_provider(config.provider, model=config.model, base_url=config.base_url)
547
+ state = ChatState(ref=ref, workspace_summary=workspace_summary)
548
+
549
+ if message is not None:
550
+ console.print(chat_turn(workspace, message, path=path, state=state, provider=llm_provider))
551
+ return
552
+
553
+ console.print("Modelable chat. Type /exit to quit.")
554
+ while True:
555
+ try:
556
+ user_message = click.prompt("you", prompt_suffix="> ", default="", show_default=False)
557
+ except EOFError, click.Abort:
558
+ break
559
+ if not user_message.strip():
560
+ continue
561
+ response = chat_turn(workspace, user_message, path=path, state=state, provider=llm_provider)
562
+ if response == "/exit":
563
+ break
564
+ console.print(f"assistant> {response}")
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ import click
4
+
5
+ from modelable.lsp.server import main as run_lsp
6
+
7
+
8
+ def register_lsp_commands(cli_group: click.Group) -> None:
9
+ cli_group.add_command(lsp)
10
+
11
+
12
+ @click.command()
13
+ def lsp() -> None:
14
+ """Start the Modelable language server over stdio."""
15
+ run_lsp()
@@ -0,0 +1,37 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ import click
7
+
8
+ from modelable.commands.common import console
9
+ from modelable.runtime.adapter import get_adapter
10
+
11
+
12
+ def register_runtime_commands(cli_group: click.Group) -> None:
13
+ cli_group.add_command(runtime)
14
+
15
+
16
+ @click.group()
17
+ def runtime() -> None:
18
+ """Manage runtime adapter operations."""
19
+ pass
20
+
21
+
22
+ @runtime.command()
23
+ @click.option("--adapter", required=True, help="Adapter type (e.g., postgres)")
24
+ @click.option("--config", required=True, type=click.Path(exists=True), help="Path to adapter configuration JSON")
25
+ def bootstrap(adapter: str, config: str) -> None:
26
+ """Initialize a runtime adapter environment."""
27
+ try:
28
+ adapter_instance = get_adapter(adapter)
29
+ config_path = Path(config)
30
+ with open(config_path) as f:
31
+ config_data = json.load(f)
32
+
33
+ adapter_instance.bootstrap(config_data)
34
+ console.print(f"[green]OK[/green] Bootstrapped {adapter}.")
35
+ except Exception as exc:
36
+ console.print(f"[red]ERROR[/red] {exc}")
37
+ raise click.ClickException(str(exc)) from exc