modelable 1.0.0__tar.gz → 1.0.2__tar.gz

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 (219) hide show
  1. {modelable-1.0.0 → modelable-1.0.2}/.gitignore +1 -0
  2. {modelable-1.0.0 → modelable-1.0.2}/PKG-INFO +1 -1
  3. {modelable-1.0.0 → modelable-1.0.2}/pyproject.toml +1 -1
  4. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/commands/spec.py +36 -16
  5. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/emitters/rust.py +241 -18
  6. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/emitters/typescript.py +38 -6
  7. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/grammar/modelable.lark +5 -1
  8. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/parser/transformer.py +3 -0
  9. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/specs/tracking.py +39 -14
  10. {modelable-1.0.0 → modelable-1.0.2}/tests/test_cli.py +2 -1
  11. modelable-1.0.2/tests/test_conformance_fixture.py +102 -0
  12. {modelable-1.0.0 → modelable-1.0.2}/tests/test_emit_rust.py +284 -4
  13. {modelable-1.0.0 → modelable-1.0.2}/tests/test_emit_typescript.py +84 -0
  14. {modelable-1.0.0 → modelable-1.0.2}/tests/test_release_metadata.py +3 -1
  15. {modelable-1.0.0 → modelable-1.0.2}/tests/test_repository_release.py +23 -2
  16. {modelable-1.0.0 → modelable-1.0.2}/tests/test_spec_tracking.py +97 -1
  17. {modelable-1.0.0 → modelable-1.0.2}/uv.lock +1 -1
  18. {modelable-1.0.0 → modelable-1.0.2}/.python-version +0 -0
  19. {modelable-1.0.0 → modelable-1.0.2}/LICENSE +0 -0
  20. {modelable-1.0.0 → modelable-1.0.2}/README.md +0 -0
  21. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/__init__.py +0 -0
  22. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/__main__.py +0 -0
  23. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/_pydantic_py314_compat.py +0 -0
  24. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/cli.py +0 -0
  25. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/commands/__init__.py +0 -0
  26. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/commands/apicurio.py +0 -0
  27. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/commands/codegen.py +0 -0
  28. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/commands/common.py +0 -0
  29. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/commands/compile.py +0 -0
  30. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/commands/create.py +0 -0
  31. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/commands/diff.py +0 -0
  32. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/commands/graph.py +0 -0
  33. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/commands/llm.py +0 -0
  34. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/commands/lsp.py +0 -0
  35. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/commands/runtime.py +0 -0
  36. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/commands/scenario.py +0 -0
  37. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/commands/workspace.py +0 -0
  38. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/compat/__init__.py +0 -0
  39. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/compat/checker.py +0 -0
  40. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/compat/diff.py +0 -0
  41. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/compiler/__init__.py +0 -0
  42. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/compiler/compiler.py +0 -0
  43. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/compiler/workspace.py +0 -0
  44. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/diagnostics/__init__.py +0 -0
  45. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/diagnostics/model.py +0 -0
  46. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/emitters/__init__.py +0 -0
  47. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/emitters/base.py +0 -0
  48. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/emitters/csharp.py +0 -0
  49. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/emitters/dbt_yaml.py +0 -0
  50. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/emitters/diagnostics.py +0 -0
  51. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/emitters/fhir.py +0 -0
  52. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/emitters/fhir_validator.py +0 -0
  53. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/emitters/go.py +0 -0
  54. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/emitters/java.py +0 -0
  55. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/emitters/json_schema.py +0 -0
  56. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/emitters/markdown.py +0 -0
  57. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/emitters/odcs.py +0 -0
  58. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/emitters/openlineage.py +0 -0
  59. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/emitters/openmetadata.py +0 -0
  60. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/emitters/python.py +0 -0
  61. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/emitters/shapes.py +0 -0
  62. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/emitters/sql.py +0 -0
  63. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/emitters/targets.py +0 -0
  64. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/expressions/__init__.py +0 -0
  65. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/expressions/cel.py +0 -0
  66. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/governance/__init__.py +0 -0
  67. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/governance/checker.py +0 -0
  68. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/governance/por.py +0 -0
  69. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/grammar/__init__.py +0 -0
  70. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/graph/__init__.py +0 -0
  71. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/graph/export.py +0 -0
  72. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/llm/__init__.py +0 -0
  73. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/llm/chat.py +0 -0
  74. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/llm/config.py +0 -0
  75. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/llm/context.py +0 -0
  76. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/llm/engine.py +0 -0
  77. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/llm/importers.py +0 -0
  78. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/llm/provenance.py +0 -0
  79. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/llm/providers.py +0 -0
  80. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/llm/qa.py +0 -0
  81. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/llm/recommendations.py +0 -0
  82. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/llm/redaction.py +0 -0
  83. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/llm/render.py +0 -0
  84. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/llm/update_plan.py +0 -0
  85. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/llm/validation_help.py +0 -0
  86. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/lsp/__init__.py +0 -0
  87. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/lsp/__main__.py +0 -0
  88. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/lsp/code_actions.py +0 -0
  89. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/lsp/completion.py +0 -0
  90. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/lsp/definition.py +0 -0
  91. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/lsp/diagnostics.py +0 -0
  92. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/lsp/document_symbols.py +0 -0
  93. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/lsp/federation.py +0 -0
  94. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/lsp/folding.py +0 -0
  95. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/lsp/formatting.py +0 -0
  96. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/lsp/highlight.py +0 -0
  97. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/lsp/hover.py +0 -0
  98. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/lsp/inlay_hints.py +0 -0
  99. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/lsp/references.py +0 -0
  100. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/lsp/rename.py +0 -0
  101. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/lsp/semantic_tokens.py +0 -0
  102. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/lsp/server.py +0 -0
  103. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/lsp/workspace.py +0 -0
  104. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/lsp/workspace_symbols.py +0 -0
  105. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/parser/__init__.py +0 -0
  106. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/parser/ir.py +0 -0
  107. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/parser/parse.py +0 -0
  108. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/parser/wire.py +0 -0
  109. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/planner/__init__.py +0 -0
  110. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/planner/lineage.py +0 -0
  111. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/planner/planner.py +0 -0
  112. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/planner/plans.py +0 -0
  113. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/py.typed +0 -0
  114. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/registry/__init__.py +0 -0
  115. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/registry/apicurio.py +0 -0
  116. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/registry/base.py +0 -0
  117. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/registry/factory.py +0 -0
  118. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/registry/index.py +0 -0
  119. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/registry/local.py +0 -0
  120. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/registry/oci.py +0 -0
  121. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/registry/resolver.py +0 -0
  122. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/registry/schema.sql +0 -0
  123. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/registry/signature.py +0 -0
  124. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/release.py +0 -0
  125. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/runtime/__init__.py +0 -0
  126. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/runtime/adapter/__init__.py +0 -0
  127. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/runtime/adapter/base.py +0 -0
  128. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/runtime/adapter/postgres.py +0 -0
  129. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/specs/__init__.py +0 -0
  130. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/validation/__init__.py +0 -0
  131. {modelable-1.0.0 → modelable-1.0.2}/src/modelable/validation/semantic.py +0 -0
  132. {modelable-1.0.0 → modelable-1.0.2}/tests/conftest.py +0 -0
  133. {modelable-1.0.0 → modelable-1.0.2}/tests/fixtures/auto_projection_complex.mdl +0 -0
  134. {modelable-1.0.0 → modelable-1.0.2}/tests/fixtures/billing_projection.mdl +0 -0
  135. {modelable-1.0.0 → modelable-1.0.2}/tests/fixtures/breaking_changes.mdl +0 -0
  136. {modelable-1.0.0 → modelable-1.0.2}/tests/fixtures/cel_error_cases.mdl +0 -0
  137. {modelable-1.0.0 → modelable-1.0.2}/tests/fixtures/customer.mdl +0 -0
  138. {modelable-1.0.0 → modelable-1.0.2}/tests/fixtures/federation_multi_registry/consumer.mdl +0 -0
  139. {modelable-1.0.0 → modelable-1.0.2}/tests/fixtures/federation_multi_registry/provider.mdl +0 -0
  140. {modelable-1.0.0 → modelable-1.0.2}/tests/fixtures/federation_multi_registry/workspace.mdl +0 -0
  141. {modelable-1.0.0 → modelable-1.0.2}/tests/fixtures/fhir_patient_profile.mdl +0 -0
  142. {modelable-1.0.0 → modelable-1.0.2}/tests/fixtures/governance_export_model.mdl +0 -0
  143. {modelable-1.0.0 → modelable-1.0.2}/tests/fixtures/materialized_projection_chain.mdl +0 -0
  144. {modelable-1.0.0 → modelable-1.0.2}/tests/fixtures/missing_header.mdl +0 -0
  145. {modelable-1.0.0 → modelable-1.0.2}/tests/fixtures/multi_domain_joins.mdl +0 -0
  146. {modelable-1.0.0 → modelable-1.0.2}/tests/fixtures/multi_language_target.mdl +0 -0
  147. {modelable-1.0.0 → modelable-1.0.2}/tests/fixtures/no_owner.mdl +0 -0
  148. {modelable-1.0.0 → modelable-1.0.2}/tests/fixtures/pii_governance.mdl +0 -0
  149. {modelable-1.0.0 → modelable-1.0.2}/tests/fixtures/projection_of_projection.mdl +0 -0
  150. {modelable-1.0.0 → modelable-1.0.2}/tests/fixtures/sql_and_dbt_targets.mdl +0 -0
  151. {modelable-1.0.0 → modelable-1.0.2}/tests/helpers.py +0 -0
  152. {modelable-1.0.0 → modelable-1.0.2}/tests/test_apicurio_registry.py +0 -0
  153. {modelable-1.0.0 → modelable-1.0.2}/tests/test_apicurio_testcontainers.py +0 -0
  154. {modelable-1.0.0 → modelable-1.0.2}/tests/test_auto_projection.py +0 -0
  155. {modelable-1.0.0 → modelable-1.0.2}/tests/test_cel_validation.py +0 -0
  156. {modelable-1.0.0 → modelable-1.0.2}/tests/test_cli_create.py +0 -0
  157. {modelable-1.0.0 → modelable-1.0.2}/tests/test_cli_inspect.py +0 -0
  158. {modelable-1.0.0 → modelable-1.0.2}/tests/test_codegen_docker_smoke.py +0 -0
  159. {modelable-1.0.0 → modelable-1.0.2}/tests/test_codegen_targets.py +0 -0
  160. {modelable-1.0.0 → modelable-1.0.2}/tests/test_compatibility.py +0 -0
  161. {modelable-1.0.0 → modelable-1.0.2}/tests/test_compiler.py +0 -0
  162. {modelable-1.0.0 → modelable-1.0.2}/tests/test_diagnostics.py +0 -0
  163. {modelable-1.0.0 → modelable-1.0.2}/tests/test_emit_csharp.py +0 -0
  164. {modelable-1.0.0 → modelable-1.0.2}/tests/test_emit_dbt_yaml.py +0 -0
  165. {modelable-1.0.0 → modelable-1.0.2}/tests/test_emit_fhir.py +0 -0
  166. {modelable-1.0.0 → modelable-1.0.2}/tests/test_emit_go.py +0 -0
  167. {modelable-1.0.0 → modelable-1.0.2}/tests/test_emit_java.py +0 -0
  168. {modelable-1.0.0 → modelable-1.0.2}/tests/test_emit_json_schema.py +0 -0
  169. {modelable-1.0.0 → modelable-1.0.2}/tests/test_emit_markdown.py +0 -0
  170. {modelable-1.0.0 → modelable-1.0.2}/tests/test_emit_odcs.py +0 -0
  171. {modelable-1.0.0 → modelable-1.0.2}/tests/test_emit_openlineage.py +0 -0
  172. {modelable-1.0.0 → modelable-1.0.2}/tests/test_emit_openmetadata.py +0 -0
  173. {modelable-1.0.0 → modelable-1.0.2}/tests/test_emit_python.py +0 -0
  174. {modelable-1.0.0 → modelable-1.0.2}/tests/test_emit_sql.py +0 -0
  175. {modelable-1.0.0 → modelable-1.0.2}/tests/test_fhir_validator.py +0 -0
  176. {modelable-1.0.0 → modelable-1.0.2}/tests/test_fixtures_regression.py +0 -0
  177. {modelable-1.0.0 → modelable-1.0.2}/tests/test_governance.py +0 -0
  178. {modelable-1.0.0 → modelable-1.0.2}/tests/test_grammar.py +0 -0
  179. {modelable-1.0.0 → modelable-1.0.2}/tests/test_graph_export.py +0 -0
  180. {modelable-1.0.0 → modelable-1.0.2}/tests/test_impact_analysis.py +0 -0
  181. {modelable-1.0.0 → modelable-1.0.2}/tests/test_lineage.py +0 -0
  182. {modelable-1.0.0 → modelable-1.0.2}/tests/test_llm_features.py +0 -0
  183. {modelable-1.0.0 → modelable-1.0.2}/tests/test_llm_provider_integration.py +0 -0
  184. {modelable-1.0.0 → modelable-1.0.2}/tests/test_lsp_code_actions.py +0 -0
  185. {modelable-1.0.0 → modelable-1.0.2}/tests/test_lsp_completion.py +0 -0
  186. {modelable-1.0.0 → modelable-1.0.2}/tests/test_lsp_completion_mirrors.py +0 -0
  187. {modelable-1.0.0 → modelable-1.0.2}/tests/test_lsp_definition.py +0 -0
  188. {modelable-1.0.0 → modelable-1.0.2}/tests/test_lsp_diagnostics.py +0 -0
  189. {modelable-1.0.0 → modelable-1.0.2}/tests/test_lsp_document_symbols.py +0 -0
  190. {modelable-1.0.0 → modelable-1.0.2}/tests/test_lsp_features.py +0 -0
  191. {modelable-1.0.0 → modelable-1.0.2}/tests/test_lsp_federation_diagnostics.py +0 -0
  192. {modelable-1.0.0 → modelable-1.0.2}/tests/test_lsp_folding.py +0 -0
  193. {modelable-1.0.0 → modelable-1.0.2}/tests/test_lsp_formatting.py +0 -0
  194. {modelable-1.0.0 → modelable-1.0.2}/tests/test_lsp_highlight.py +0 -0
  195. {modelable-1.0.0 → modelable-1.0.2}/tests/test_lsp_hover.py +0 -0
  196. {modelable-1.0.0 → modelable-1.0.2}/tests/test_lsp_inlay_hints.py +0 -0
  197. {modelable-1.0.0 → modelable-1.0.2}/tests/test_lsp_integration.py +0 -0
  198. {modelable-1.0.0 → modelable-1.0.2}/tests/test_lsp_references.py +0 -0
  199. {modelable-1.0.0 → modelable-1.0.2}/tests/test_lsp_rename.py +0 -0
  200. {modelable-1.0.0 → modelable-1.0.2}/tests/test_lsp_semantic_tokens.py +0 -0
  201. {modelable-1.0.0 → modelable-1.0.2}/tests/test_lsp_server.py +0 -0
  202. {modelable-1.0.0 → modelable-1.0.2}/tests/test_lsp_workspace.py +0 -0
  203. {modelable-1.0.0 → modelable-1.0.2}/tests/test_lsp_workspace_symbols.py +0 -0
  204. {modelable-1.0.0 → modelable-1.0.2}/tests/test_openmetadata_testcontainers.py +0 -0
  205. {modelable-1.0.0 → modelable-1.0.2}/tests/test_planner.py +0 -0
  206. {modelable-1.0.0 → modelable-1.0.2}/tests/test_por.py +0 -0
  207. {modelable-1.0.0 → modelable-1.0.2}/tests/test_postgres_adapter.py +0 -0
  208. {modelable-1.0.0 → modelable-1.0.2}/tests/test_py314_compat.py +0 -0
  209. {modelable-1.0.0 → modelable-1.0.2}/tests/test_registry.py +0 -0
  210. {modelable-1.0.0 → modelable-1.0.2}/tests/test_registry_index.py +0 -0
  211. {modelable-1.0.0 → modelable-1.0.2}/tests/test_release_workflow.py +0 -0
  212. {modelable-1.0.0 → modelable-1.0.2}/tests/test_resolver.py +0 -0
  213. {modelable-1.0.0 → modelable-1.0.2}/tests/test_samples.py +0 -0
  214. {modelable-1.0.0 → modelable-1.0.2}/tests/test_semantic.py +0 -0
  215. {modelable-1.0.0 → modelable-1.0.2}/tests/test_serialization_hints.py +0 -0
  216. {modelable-1.0.0 → modelable-1.0.2}/tests/test_transformer.py +0 -0
  217. {modelable-1.0.0 → modelable-1.0.2}/tests/test_validate_surface_detection.py +0 -0
  218. {modelable-1.0.0 → modelable-1.0.2}/tests/test_version_range_resolution.py +0 -0
  219. {modelable-1.0.0 → modelable-1.0.2}/tests/test_workspace.py +0 -0
@@ -5,6 +5,7 @@ target/
5
5
  node_modules/
6
6
  dist/
7
7
  .modelable/
8
+ site/
8
9
 
9
10
  # Helm fetched chart dependencies — regenerated by `helm dependency update`
10
11
  charts/*/charts/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modelable
3
- Version: 1.0.0
3
+ Version: 1.0.2
4
4
  Summary: Compiler and language server for versioned, domain-owned data models
5
5
  Project-URL: Homepage, https://github.com/ktjn/modelable
6
6
  Project-URL: Documentation, https://github.com/ktjn/modelable/tree/main/docs
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "modelable"
7
- version = "1.0.0"
7
+ version = "1.0.2"
8
8
  description = "Compiler and language server for versioned, domain-owned data models"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import json
4
4
  import os
5
+ import time
5
6
  from dataclasses import asdict
6
7
  from pathlib import Path
7
8
 
@@ -70,25 +71,44 @@ def add(
70
71
  @click.option("--path", "workspace_path", type=click.Path(exists=True, path_type=Path), default=Path("."))
71
72
  @click.option("--json", "json_output", is_flag=True, help="Print machine-readable JSON.")
72
73
  @click.option("--fail-on", default="", help="Comma-separated statuses that should return a non-zero exit code.")
73
- def status(workspace_path: Path, json_output: bool, fail_on: str) -> None:
74
+ @click.option("--poll-interval", default=None, type=float, help="Re-check every N seconds (useful in CI watch mode).")
75
+ @click.option("--max-polls", default=None, type=int, help="Stop after this many checks when --poll-interval is set.")
76
+ def status(
77
+ workspace_path: Path,
78
+ json_output: bool,
79
+ fail_on: str,
80
+ poll_interval: float | None,
81
+ max_polls: int | None,
82
+ ) -> None:
74
83
  """Report drift status for tracked external specifications."""
75
84
  token = os.getenv("MODELABLE_SPEC_TOKEN")
76
- entries = select_specs(workspace_path, None)
77
- evaluations = [evaluate_spec(workspace_path, entry, write=False, token=token) for entry in entries]
78
- payload = {"specs": [evaluation.as_status_dict() for evaluation in evaluations]}
79
- if json_output:
80
- click.echo(json.dumps(payload, indent=2, sort_keys=True))
81
- else:
82
- if not evaluations:
83
- console.print(f"[yellow]No tracked specs in {spec_config_path(workspace_path)}[/yellow]")
84
- for evaluation in evaluations:
85
- suffix = ""
86
- if evaluation.change_kind:
87
- suffix = f" ({evaluation.change_kind}, {evaluation.change_count} change(s))"
88
- if evaluation.error:
89
- suffix = f" ({evaluation.error})"
90
- console.print(f"{evaluation.entry.id}: {evaluation.status}{suffix}")
91
85
  fail_statuses = {item.strip() for item in fail_on.split(",") if item.strip()}
86
+ polls = 0
87
+
88
+ while True:
89
+ entries = select_specs(workspace_path, None)
90
+ evaluations = [evaluate_spec(workspace_path, entry, write=False, token=token) for entry in entries]
91
+ payload = {"specs": [evaluation.as_status_dict() for evaluation in evaluations]}
92
+ if json_output:
93
+ click.echo(json.dumps(payload, indent=2, sort_keys=True))
94
+ else:
95
+ if not evaluations:
96
+ console.print(f"[yellow]No tracked specs in {spec_config_path(workspace_path)}[/yellow]")
97
+ for evaluation in evaluations:
98
+ suffix = ""
99
+ if evaluation.change_kind:
100
+ suffix = f" ({evaluation.change_kind}, {evaluation.change_count} change(s))"
101
+ if evaluation.error:
102
+ suffix = f" ({evaluation.error})"
103
+ console.print(f"{evaluation.entry.id}: {evaluation.status}{suffix}")
104
+
105
+ polls += 1
106
+ if poll_interval is None:
107
+ break
108
+ if max_polls is not None and polls >= max_polls:
109
+ break
110
+ time.sleep(poll_interval)
111
+
92
112
  if fail_statuses and any(evaluation.status in fail_statuses for evaluation in evaluations):
93
113
  raise click.ClickException("tracked spec status matched --fail-on")
94
114
 
@@ -7,9 +7,18 @@ from pathlib import Path
7
7
 
8
8
  from modelable.compiler.workspace import Workspace
9
9
  from modelable.emitters.base import EmittedArtifact, compute_content_hash
10
- from modelable.emitters.diagnostics import type_loss
10
+ from modelable.emitters.diagnostics import missing_metadata, type_loss
11
11
  from modelable.emitters.shapes import TypeShape
12
- from modelable.parser.ir import DirectMapping, DomainDef, MdlFile, ModelVersion, ProjectionVersion
12
+ from modelable.parser.ir import (
13
+ ArrayType,
14
+ DirectMapping,
15
+ DomainDef,
16
+ MapType,
17
+ MdlFile,
18
+ ModelVersion,
19
+ NamedType,
20
+ ProjectionVersion,
21
+ )
13
22
  from modelable.registry.resolver import resolve_model_ref
14
23
 
15
24
 
@@ -22,15 +31,89 @@ class _FieldSpec:
22
31
  serde_attrs: list[str] = dc_field(default_factory=list)
23
32
 
24
33
 
34
+ def _append_cross_enum_from_impls(
35
+ artifacts: list[EmittedArtifact],
36
+ enum_registry: dict[str, dict],
37
+ ) -> list[EmittedArtifact]:
38
+ """For each pair of enum types with identical variants in the same domain,
39
+ append From impl blocks into projection files without manual match arms.
40
+ """
41
+ # Group by domain: domain -> [(artifact_id, module_name, enums_dict, kind)]
42
+ by_domain: dict[str, list[tuple[str, str, dict[str, list[str]], str]]] = {}
43
+ for art_id, info in enum_registry.items():
44
+ domain = info["domain"]
45
+ by_domain.setdefault(domain, []).append((art_id, info["module_name"], info["enums"], info["kind"]))
46
+
47
+ # Build: frozenset(raw_variants) -> [(artifact_id, module_name, enum_type_name)]
48
+ # Per domain — From impls only work within the same Rust module tree (super:: path).
49
+ extra: dict[str, list[str]] = {} # artifact_id -> lines to append
50
+ for _domain, entries in by_domain.items():
51
+ variant_map: dict[frozenset, list[tuple[str, str, str, str]]] = {}
52
+ for art_id, module_name, enums, kind in entries:
53
+ for enum_type_name, raw_variants in enums.items():
54
+ key = frozenset(raw_variants)
55
+ variant_map.setdefault(key, []).append((art_id, module_name, enum_type_name, kind))
56
+
57
+ for variant_set, enum_list in variant_map.items():
58
+ if len(enum_list) < 2:
59
+ continue
60
+ sorted_variants = sorted(variant_set)
61
+ # For each ordered pair (src → tgt): put From<src> for tgt in tgt's file.
62
+ for src_art_id, src_module, src_enum, _src_kind in enum_list:
63
+ for tgt_art_id, _tgt_module, tgt_enum, tgt_kind in enum_list:
64
+ if src_art_id == tgt_art_id and src_enum == tgt_enum:
65
+ continue
66
+ if tgt_kind == "model":
67
+ continue
68
+ lines: list[str] = [
69
+ "",
70
+ f"use super::{src_module}::{src_enum};",
71
+ f"impl From<{src_enum}> for {tgt_enum} {{",
72
+ f" fn from(src: {src_enum}) -> Self {{",
73
+ " match src {",
74
+ ]
75
+ for raw_v in sorted_variants:
76
+ member = _enum_member_name(raw_v)
77
+ lines.append(f" {src_enum}::{member} => {tgt_enum}::{member},")
78
+ lines += [" }", " }", "}"]
79
+ extra.setdefault(tgt_art_id, []).extend(lines)
80
+
81
+ if not extra:
82
+ return artifacts
83
+
84
+ result: list[EmittedArtifact] = []
85
+ for artifact in artifacts:
86
+ appendage = extra.get(artifact.artifact_id)
87
+ if appendage:
88
+ new_content = artifact.content.rstrip("\n") + "\n" + "\n".join(appendage) + "\n"
89
+ result.append(
90
+ EmittedArtifact(
91
+ target=artifact.target,
92
+ ref=artifact.ref,
93
+ artifact_id=artifact.artifact_id,
94
+ path=artifact.path,
95
+ content=new_content,
96
+ content_hash=compute_content_hash(new_content),
97
+ warnings=artifact.warnings,
98
+ )
99
+ )
100
+ else:
101
+ result.append(artifact)
102
+ return result
103
+
104
+
25
105
  def emit_rust(workspace: Workspace, out_dir: Path) -> list[EmittedArtifact]:
26
106
  """Emit Rust source files for every model and projection version."""
27
107
  postgres_sources = _adapter_bound_sources(workspace.mdl, "postgres")
28
108
  clickhouse_sources = _adapter_bound_sources(workspace.mdl, "clickhouse")
109
+ enum_registry: dict[str, dict] = {}
29
110
  artifacts: list[EmittedArtifact] = []
30
111
  for domain in workspace.mdl.domains:
31
112
  for model_name, versions in domain.models.items():
32
113
  for version in versions:
33
- artifacts.append(_emit_model(domain, model_name, version, out_dir))
114
+ artifacts.append(
115
+ _emit_model(domain, model_name, version, out_dir, enum_registry=enum_registry, mdl=workspace.mdl)
116
+ )
34
117
  for projection_name, versions in domain.projections.items():
35
118
  for version in versions:
36
119
  source = version.source.model
@@ -43,9 +126,10 @@ def emit_rust(workspace: Workspace, out_dir: Path) -> list[EmittedArtifact]:
43
126
  workspace.mdl,
44
127
  sqlx_fromrow=source in postgres_sources,
45
128
  clickhouse_row=source in clickhouse_sources,
129
+ enum_registry=enum_registry,
46
130
  )
47
131
  )
48
- return artifacts
132
+ return _append_cross_enum_from_impls(artifacts, enum_registry)
49
133
 
50
134
 
51
135
  def _adapter_bound_sources(mdl: MdlFile, adapter_type: str) -> set[str]:
@@ -86,18 +170,87 @@ def _stable_type_name(domain: str, name: str, version: int) -> str:
86
170
  return f"{_pascalize(domain)}{_pascalize(name)}V{version}"
87
171
 
88
172
 
89
- def _emit_model(domain: DomainDef, model_name: str, version: ModelVersion, out_dir: Path) -> EmittedArtifact:
173
+ def _collect_named_type_refs(field_type, result: set) -> None:
174
+ """Recursively collect NamedType names from a field type."""
175
+ if isinstance(field_type, NamedType):
176
+ result.add(field_type.name)
177
+ elif isinstance(field_type, ArrayType):
178
+ _collect_named_type_refs(field_type.item, result)
179
+ elif isinstance(field_type, MapType):
180
+ _collect_named_type_refs(field_type.key, result)
181
+ _collect_named_type_refs(field_type.value, result)
182
+
183
+
184
+ def _resolve_named_type_map(named_refs: set, mdl: MdlFile | None) -> tuple[dict[str, str], list[str]]:
185
+ """Resolve NamedType references to Rust type names from the workspace.
186
+
187
+ Returns (name -> rust_type_name, list of use statements).
188
+ """
189
+ if not named_refs or mdl is None:
190
+ return {}, []
191
+ resolved_map: dict[str, str] = {}
192
+ use_statements: list[str] = []
193
+ for name in named_refs:
194
+ for domain in mdl.domains:
195
+ if name in domain.models:
196
+ versions = domain.models[name]
197
+ if versions:
198
+ latest = versions[-1]
199
+ rust_name = _stable_type_name(domain.name, name, latest.version)
200
+ module = _snake_case(rust_name)
201
+ resolved_map[name] = rust_name
202
+ use_statements.append(f"use super::{module}::{rust_name};")
203
+ break
204
+ return resolved_map, use_statements
205
+
206
+
207
+ def _emit_model(
208
+ domain: DomainDef,
209
+ model_name: str,
210
+ version: ModelVersion,
211
+ out_dir: Path,
212
+ *,
213
+ enum_registry: dict[str, dict] | None = None,
214
+ mdl: MdlFile | None = None,
215
+ ) -> EmittedArtifact:
90
216
  artifact_id = _artifact_id(domain.name, model_name, version.version)
91
217
  type_name = _stable_type_name(domain.name, model_name, version.version)
92
218
  nested_definitions: dict[str, list[str]] = {}
219
+ local_enum_info: dict[str, list[str]] = {}
220
+
221
+ # Resolve NamedType references from the workspace
222
+ named_refs: set[str] = set()
223
+ for field in version.fields:
224
+ _collect_named_type_refs(field.type, named_refs)
225
+ named_type_map, use_statements = _resolve_named_type_map(named_refs, mdl)
226
+
93
227
  field_specs = _field_specs_from_model_fields(
94
- version.fields, owner_type=type_name, path=[], definitions=nested_definitions
228
+ version.fields,
229
+ owner_type=type_name,
230
+ path=[],
231
+ definitions=nested_definitions,
232
+ enum_info=local_enum_info,
233
+ named_type_map=named_type_map,
95
234
  )
235
+ if enum_registry is not None:
236
+ enum_registry[artifact_id] = {
237
+ "enums": local_enum_info,
238
+ "module_name": _snake_case(type_name),
239
+ "domain": domain.name,
240
+ "kind": "model",
241
+ }
242
+
243
+ warnings: list[str] = []
244
+ for field in version.fields:
245
+ if isinstance(field.type, NamedType) and field.type.name not in named_type_map:
246
+ warnings.append(missing_metadata(f"{domain.name}.{model_name}.{field.name}"))
96
247
 
97
248
  needs_serde_with = _any_needs_serde_with(field_specs)
98
249
  needs_uuid = _any_needs_uuid(field_specs)
99
250
  needs_serde_json = _any_needs_serde_json(field_specs)
100
- lines = _header_lines(serde_with=needs_serde_with, uuid=needs_uuid, serde_json=needs_serde_json)
251
+ lines = _header_lines(
252
+ serde_with=needs_serde_with, uuid=needs_uuid, serde_json=needs_serde_json, extra_uses=use_statements
253
+ )
101
254
  lines.extend(_render_struct_definition(type_name, field_specs))
102
255
  lines.extend(_render_nested_definitions(nested_definitions))
103
256
 
@@ -109,7 +262,7 @@ def _emit_model(domain: DomainDef, model_name: str, version: ModelVersion, out_d
109
262
  path=out_dir / _module_path(domain.name, type_name),
110
263
  content=text,
111
264
  content_hash=compute_content_hash(text),
112
- warnings=[],
265
+ warnings=warnings,
113
266
  )
114
267
 
115
268
 
@@ -122,10 +275,12 @@ def _emit_projection(
122
275
  *,
123
276
  sqlx_fromrow: bool = False,
124
277
  clickhouse_row: bool = False,
278
+ enum_registry: dict[str, dict] | None = None,
125
279
  ) -> EmittedArtifact:
126
280
  artifact_id = _artifact_id(domain.name, projection_name, version.version)
127
281
  type_name = _stable_type_name(domain.name, projection_name, version.version)
128
282
  nested_definitions: dict[str, list[str]] = {}
283
+ local_enum_info: dict[str, list[str]] = {}
129
284
 
130
285
  field_specs: list[_FieldSpec] = []
131
286
  warnings: list[str] = []
@@ -136,16 +291,24 @@ def _emit_projection(
136
291
  field_specs.append(_FieldSpec(index=index, name=field.name, annotation="String", optional=False))
137
292
  continue
138
293
  wire = _resolve_merged_projection_wire(field, version, mdl)
139
- annotation = _shape_annotation(
140
- field_shape,
141
- owner_type=type_name,
142
- path=[field.name],
143
- definitions=nested_definitions,
144
- rust_hint=wire.get("rust"),
145
- clickhouse_hint=wire.get("clickhouse"),
146
- )
294
+ if clickhouse_row and field_shape.kind == "enum":
295
+ # clickhouse-rs 0.15 panics on serialize_unit_variant for typed enums;
296
+ # force String for all ClickHouse-bound enum fields.
297
+ annotation = "String"
298
+ else:
299
+ annotation = _shape_annotation(
300
+ field_shape,
301
+ owner_type=type_name,
302
+ path=[field.name],
303
+ definitions=nested_definitions,
304
+ rust_hint=wire.get("rust"),
305
+ clickhouse_hint=wire.get("clickhouse"),
306
+ enum_info=local_enum_info,
307
+ )
147
308
  optional = field_shape.optional or field_shape.nullable
148
309
  serde_attrs = _serde_attrs_for_field(wire, field_shape, clickhouse=clickhouse_row)
310
+ if field_shape.optional and not clickhouse_row:
311
+ serde_attrs = ['#[serde(skip_serializing_if = "Option::is_none")]', *serde_attrs]
149
312
  field_specs.append(
150
313
  _FieldSpec(index=index, name=field.name, annotation=annotation, optional=optional, serde_attrs=serde_attrs)
151
314
  )
@@ -172,7 +335,19 @@ def _emit_projection(
172
335
  _render_struct_definition(type_name, field_specs, extra_derives=extra_derives, storage_gated=storage_gated)
173
336
  )
174
337
  lines.extend(_render_nested_definitions(nested_definitions))
175
- lines.extend(_emit_from_impl(type_name, domain.name, version, mdl, storage_gated=storage_gated))
338
+ lines.extend(
339
+ _emit_from_impl(
340
+ type_name, domain.name, version, mdl, storage_gated=storage_gated, clickhouse_row=clickhouse_row
341
+ )
342
+ )
343
+
344
+ if enum_registry is not None:
345
+ enum_registry[artifact_id] = {
346
+ "enums": local_enum_info,
347
+ "module_name": _snake_case(type_name),
348
+ "domain": domain.name,
349
+ "kind": "projection",
350
+ }
176
351
 
177
352
  text = "\n".join(lines) + "\n"
178
353
  return EmittedArtifact(
@@ -218,6 +393,7 @@ def _emit_from_impl(
218
393
  mdl: MdlFile,
219
394
  *,
220
395
  storage_gated: bool = False,
396
+ clickhouse_row: bool = False,
221
397
  ) -> list[str]:
222
398
  """Emit impl From<SourceModel> for Projection.
223
399
 
@@ -250,6 +426,16 @@ def _emit_from_impl(
250
426
  if storage_gated:
251
427
  lines.append('#[cfg(feature = "storage")]')
252
428
  lines.append(f"use super::{src_module}::{src_type_name};")
429
+ if clickhouse_row:
430
+ for proj_field in version.fields:
431
+ if not isinstance(proj_field.mapping, DirectMapping):
432
+ continue
433
+ field_shape = _resolve_projection_field_shape(proj_field, version, mdl)
434
+ if field_shape is not None and field_shape.kind == "enum":
435
+ enum_type = _nested_type_name(src_type_name, [proj_field.mapping.source_field])
436
+ if storage_gated:
437
+ lines.append('#[cfg(feature = "storage")]')
438
+ lines.append(f"use super::{src_module}::{enum_type};")
253
439
  if storage_gated:
254
440
  lines.append('#[cfg(feature = "storage")]')
255
441
  lines.append(f"impl From<{src_type_name}> for {proj_type_name} {{")
@@ -261,7 +447,15 @@ def _emit_from_impl(
261
447
  if isinstance(proj_field.mapping, DirectMapping):
262
448
  src_rust_name = _field_name(proj_field.mapping.source_field)
263
449
  field_shape = _resolve_projection_field_shape(proj_field, version, mdl)
264
- if _projection_field_is_json_passthrough_to_string(proj_field, version, mdl):
450
+ if clickhouse_row and field_shape is not None and field_shape.kind == "enum":
451
+ # ClickHouse-bound enum fields are stored as String; generate explicit match.
452
+ src_enum_type = _nested_type_name(src_type_name, [proj_field.mapping.source_field])
453
+ lines.append(f" {rust_name}: match src.{src_rust_name} {{")
454
+ for raw_v in field_shape.enum_values:
455
+ member = _enum_member_name(raw_v)
456
+ lines.append(f' {src_enum_type}::{member} => "{raw_v}".to_string(),')
457
+ lines.append(" },")
458
+ elif _projection_field_is_json_passthrough_to_string(proj_field, version, mdl):
265
459
  lines.append(
266
460
  f" {rust_name}: serde_json::to_string(&src.{src_rust_name}).unwrap_or_default(),"
267
461
  )
@@ -333,6 +527,7 @@ def _header_lines(
333
527
  clickhouse: bool = False,
334
528
  uuid: bool = False,
335
529
  serde_json: bool = False,
530
+ extra_uses: list[str] | None = None,
336
531
  ) -> list[str]:
337
532
  lines = [
338
533
  "// @generated by Modelable",
@@ -349,6 +544,9 @@ def _header_lines(
349
544
  lines.insert(1, "// requires: uuid (https://docs.rs/uuid)")
350
545
  if serde_json:
351
546
  lines.insert(1, "// requires: serde_json (https://docs.rs/serde_json)")
547
+ if extra_uses:
548
+ # Insert use statements just before the trailing empty string
549
+ lines[-1:-1] = extra_uses
352
550
  return lines
353
551
 
354
552
 
@@ -437,6 +635,8 @@ def _field_specs_from_model_fields(
437
635
  owner_type: str,
438
636
  path: list[str],
439
637
  definitions: dict[str, list[str]],
638
+ enum_info: dict[str, list[str]] | None = None,
639
+ named_type_map: dict[str, str] | None = None,
440
640
  ) -> list[_FieldSpec]:
441
641
  specs: list[_FieldSpec] = []
442
642
  for index, field in enumerate(fields):
@@ -448,6 +648,8 @@ def _field_specs_from_model_fields(
448
648
  path=[*path, field.name],
449
649
  definitions=definitions,
450
650
  rust_hint=wire.get("rust"),
651
+ enum_info=enum_info,
652
+ named_type_map=named_type_map,
451
653
  )
452
654
  is_optional = shape.optional or shape.nullable
453
655
  serde_attrs = _serde_attrs_for_field(wire, shape)
@@ -462,6 +664,10 @@ def _field_specs_from_model_fields(
462
664
  definitions=definitions,
463
665
  rust_hint=wire.get("rust"),
464
666
  )
667
+ elif shape.optional:
668
+ # Omittable field: skip during serialization when None.
669
+ # Nullable-only fields must always be serialized (as null), so no skip attr.
670
+ serde_attrs = ['#[serde(skip_serializing_if = "Option::is_none")]', *serde_attrs]
465
671
  specs.append(
466
672
  _FieldSpec(
467
673
  index=index, name=field.name, annotation=annotation, optional=is_optional, serde_attrs=serde_attrs
@@ -476,6 +682,7 @@ def _field_specs_from_object_fields(
476
682
  owner_type: str,
477
683
  path: list[str],
478
684
  definitions: dict[str, list[str]],
685
+ enum_info: dict[str, list[str]] | None = None,
479
686
  ) -> list[_FieldSpec]:
480
687
  specs: list[_FieldSpec] = []
481
688
  for index, field in enumerate(fields):
@@ -486,6 +693,7 @@ def _field_specs_from_object_fields(
486
693
  path=[*path, field.name],
487
694
  definitions=definitions,
488
695
  rust_hint=wire.get("rust"),
696
+ enum_info=enum_info,
489
697
  )
490
698
  default_none = field.optional or field.shape.optional or field.shape.nullable
491
699
  serde_attrs = _serde_attrs_for_field(wire, field.shape)
@@ -505,6 +713,8 @@ def _shape_annotation(
505
713
  definitions: dict[str, list[str]],
506
714
  rust_hint=None,
507
715
  clickhouse_hint=None,
716
+ enum_info: dict[str, list[str]] | None = None,
717
+ named_type_map: dict[str, str] | None = None,
508
718
  ) -> str:
509
719
  base = _shape_base_annotation(
510
720
  shape,
@@ -513,6 +723,8 @@ def _shape_annotation(
513
723
  definitions=definitions,
514
724
  rust_hint=rust_hint,
515
725
  clickhouse_hint=clickhouse_hint,
726
+ enum_info=enum_info,
727
+ named_type_map=named_type_map,
516
728
  )
517
729
  if shape.optional or shape.nullable:
518
730
  return f"Option<{base}>"
@@ -527,6 +739,8 @@ def _shape_base_annotation(
527
739
  definitions: dict[str, list[str]],
528
740
  rust_hint=None,
529
741
  clickhouse_hint=None,
742
+ enum_info: dict[str, list[str]] | None = None,
743
+ named_type_map: dict[str, str] | None = None,
530
744
  ) -> str:
531
745
  clickhouse_string = clickhouse_hint is not None and getattr(clickhouse_hint, "encoding", None) == "string"
532
746
  if shape.kind == "primitive":
@@ -545,6 +759,8 @@ def _shape_base_annotation(
545
759
  path=[*path, "Item"],
546
760
  definitions=definitions,
547
761
  rust_hint=rust_hint,
762
+ enum_info=enum_info,
763
+ named_type_map=named_type_map,
548
764
  )
549
765
  return f"Vec<{element_type}>"
550
766
  if shape.kind == "map":
@@ -556,6 +772,8 @@ def _shape_base_annotation(
556
772
  owner_type=owner_type,
557
773
  path=[*path, "Value"],
558
774
  definitions=definitions,
775
+ enum_info=enum_info,
776
+ named_type_map=named_type_map,
559
777
  )
560
778
  return f"HashMap<String, {value_type}>"
561
779
  if shape.kind == "ref":
@@ -564,8 +782,12 @@ def _shape_base_annotation(
564
782
  enum_type_name = _nested_type_name(owner_type, path)
565
783
  if enum_type_name not in definitions:
566
784
  definitions[enum_type_name] = _render_enum_definition(enum_type_name, list(shape.enum_values))
785
+ if enum_info is not None and enum_type_name not in enum_info:
786
+ enum_info[enum_type_name] = list(shape.enum_values)
567
787
  return enum_type_name
568
788
  if shape.kind == "named":
789
+ if named_type_map is not None and shape.ref in named_type_map:
790
+ return named_type_map[shape.ref]
569
791
  return _pascalize(shape.ref or "Named")
570
792
  if shape.kind == "object":
571
793
  type_name = _nested_type_name(owner_type, path)
@@ -577,6 +799,7 @@ def _shape_base_annotation(
577
799
  owner_type=owner_type,
578
800
  path=path,
579
801
  definitions=definitions,
802
+ enum_info=enum_info,
580
803
  ),
581
804
  )
582
805
  return type_name
@@ -92,15 +92,39 @@ def _collect_ref_imports(field_type, mdl, resolved_refs: dict[str, str]) -> None
92
92
  _collect_ref_imports(f.type, mdl, resolved_refs)
93
93
 
94
94
 
95
+ def _collect_named_imports(field_type, mdl, named_imports: dict[str, tuple[str, str]]) -> None:
96
+ """Recursively resolve NamedType refs to (exported_alias, artifact_id) in named_imports."""
97
+ if isinstance(field_type, NamedType):
98
+ name = field_type.name
99
+ if name not in named_imports and mdl is not None:
100
+ for domain in mdl.domains:
101
+ if name in domain.models:
102
+ versions = domain.models[name]
103
+ if versions:
104
+ latest = max(versions, key=lambda v: v.version)
105
+ aid = _artifact_id(domain.name, name, latest.version)
106
+ named_imports[name] = (name, aid)
107
+ return
108
+ elif isinstance(field_type, ArrayType):
109
+ _collect_named_imports(field_type.item, mdl, named_imports)
110
+ elif isinstance(field_type, MapType):
111
+ _collect_named_imports(field_type.value, mdl, named_imports)
112
+ elif isinstance(field_type, ObjectType):
113
+ for f in field_type.fields:
114
+ _collect_named_imports(f.type, mdl, named_imports)
115
+
116
+
95
117
  def _emit_model(domain: DomainDef, model_name: str, version: ModelVersion, out_dir: Path, mdl=None) -> EmittedArtifact:
96
118
  artifact_id = _artifact_id(domain.name, model_name, version.version)
97
119
  interface_name = _stable_interface_name(domain.name, model_name, version.version)
98
120
 
99
121
  # Resolve ref<X> fields to stable interface names; collect imports.
100
122
  resolved_refs: dict[str, str] = {} # ref target → stable interface name
123
+ named_imports: dict[str, tuple[str, str]] = {} # bare name → (stable iface name, artifact_id)
101
124
  if mdl is not None:
102
125
  for field in version.fields:
103
126
  _collect_ref_imports(field.type, mdl, resolved_refs)
127
+ _collect_named_imports(field.type, mdl, named_imports)
104
128
 
105
129
  declaration_json_wire = version.wire_targets().get("json")
106
130
  field_case = declaration_json_wire.field_case if declaration_json_wire is not None else None
@@ -109,6 +133,9 @@ def _emit_model(domain: DomainDef, model_name: str, version: ModelVersion, out_d
109
133
  for iface in sorted(set(resolved_refs.values())):
110
134
  iface_id = _iface_to_artifact_id(iface)
111
135
  import_lines.append(f'import type {{ {iface} }} from "./{iface_id}";')
136
+ for name in sorted(named_imports):
137
+ iface, aid = named_imports[name]
138
+ import_lines.append(f'import type {{ {iface} }} from "./{aid}";')
112
139
 
113
140
  meta_lines = _metadata_lines(
114
141
  _domain_metadata_entries(
@@ -123,16 +150,16 @@ def _emit_model(domain: DomainDef, model_name: str, version: ModelVersion, out_d
123
150
  body_lines.append(f"export interface {interface_name} {{")
124
151
  warnings: list[str] = []
125
152
  for field in version.fields:
126
- if isinstance(field.type, NamedType):
153
+ if isinstance(field.type, NamedType) and field.type.name not in named_imports:
127
154
  warnings.append(missing_metadata(f"{domain.name}.{model_name}.{field.name}"))
128
155
  field_name = _apply_case(field.name, field_case) if field_case else field.name
129
156
  body_lines.append(
130
- f" {field_name}{'?' if field.optional else ''}: {_type_to_ts(field.type, wire_targets=field.wire_targets(), resolved_refs=resolved_refs)};"
157
+ f" {field_name}{'?' if field.optional else ''}: {_type_to_ts(field.type, wire_targets=field.wire_targets(), resolved_refs=resolved_refs, named_imports=named_imports)};"
131
158
  )
132
159
  body_lines.append("}")
133
160
  body_lines.append(f"export type {model_name} = {interface_name};")
134
161
 
135
- all_lines = import_lines + ([""] if import_lines else []) + meta_lines + body_lines
162
+ all_lines = meta_lines + ([*import_lines, ""] if import_lines else []) + body_lines
136
163
  content = "\n".join(all_lines) + "\n"
137
164
  return EmittedArtifact(
138
165
  target="typescript",
@@ -286,6 +313,7 @@ def _type_to_ts(
286
313
  *,
287
314
  wire_targets: dict[str, object] | None = None,
288
315
  resolved_refs: dict[str, str] | None = None,
316
+ named_imports: dict[str, tuple[str, str]] | None = None,
289
317
  ) -> str:
290
318
  json_wire = None
291
319
  if wire_targets is not None:
@@ -314,12 +342,14 @@ def _type_to_ts(
314
342
  if isinstance(field_type, DecimalType):
315
343
  return "string"
316
344
  if isinstance(field_type, ArrayType):
317
- item_ts = _type_to_ts(field_type.item)
345
+ item_ts = _type_to_ts(field_type.item, resolved_refs=resolved_refs, named_imports=named_imports)
318
346
  if isinstance(field_type.item, EnumType):
319
347
  return f"({item_ts})[]"
320
348
  return f"{item_ts}[]"
321
349
  if isinstance(field_type, MapType):
322
- return f"Record<string, {_type_to_ts(field_type.value)}>"
350
+ return (
351
+ f"Record<string, {_type_to_ts(field_type.value, resolved_refs=resolved_refs, named_imports=named_imports)}>"
352
+ )
323
353
  if isinstance(field_type, RefType):
324
354
  if resolved_refs and field_type.target in resolved_refs:
325
355
  return resolved_refs[field_type.target]
@@ -339,11 +369,13 @@ def _type_to_ts(
339
369
  return values or "string"
340
370
  if isinstance(field_type, ObjectType):
341
371
  inner = "; ".join(
342
- f"{field.name}{'?' if field.optional else ''}: {_type_to_ts(field.type, wire_targets=field.wire_targets(), resolved_refs=resolved_refs)}"
372
+ f"{field.name}{'?' if field.optional else ''}: {_type_to_ts(field.type, wire_targets=field.wire_targets(), resolved_refs=resolved_refs, named_imports=named_imports)}"
343
373
  for field in field_type.fields
344
374
  )
345
375
  return f"{{ {inner} }}"
346
376
  if isinstance(field_type, NamedType):
377
+ if named_imports and field_type.name in named_imports:
378
+ return named_imports[field_type.name][0]
347
379
  return field_type.name
348
380
  if field_type is None:
349
381
  return "unknown"
@@ -96,7 +96,11 @@ primitive_type: "string" -> pt_string
96
96
  | "json" -> pt_json
97
97
 
98
98
  decimal_type: "decimal" "(" INT "," INT ")"
99
- enum_type: "enum" "(" IDENT ("," IDENT)* ")"
99
+ enum_type: "enum" "(" enum_member ("," enum_member)* ")"
100
+ enum_member: IDENT
101
+ | NUMERIC_IDENT
102
+
103
+ NUMERIC_IDENT: /[0-9][A-Za-z0-9_-]*/
100
104
  array_type: "array" "<" type_expr ">"
101
105
  map_type: "map" "<" type_expr "," type_expr ">"
102
106
  ref_type: "ref" "<" dotted_ref ">"