metaobjects 0.15.13__tar.gz → 0.16.0__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.
Files changed (385) hide show
  1. {metaobjects-0.15.13 → metaobjects-0.16.0}/PKG-INFO +1 -1
  2. {metaobjects-0.15.13 → metaobjects-0.16.0}/pyproject.toml +1 -1
  3. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/codegen/KNOWN_GAPS.md +8 -0
  4. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/codegen/generators/entity_model.py +271 -27
  5. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/codegen/generators/router_generator.py +94 -9
  6. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/render/extract/extract.py +6 -0
  7. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/render/extract/types.py +31 -0
  8. metaobjects-0.16.0/tests/codegen/golden/extends/expected/BaseEntity.py +23 -0
  9. metaobjects-0.16.0/tests/codegen/golden/extends/expected/Program.py +27 -0
  10. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/codegen/golden/nested-array/expected/AuthorBrief.py +2 -2
  11. metaobjects-0.16.0/tests/codegen/golden/vanilla/expected/Subscriber.py +26 -0
  12. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/codegen/test_entity_model.py +128 -9
  13. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/codegen/test_extends_abstract_field_inheritance.py +6 -3
  14. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/codegen/test_inherit_without_restate_gate.py +8 -3
  15. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/codegen/test_inheritance_conformance.py +6 -2
  16. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/integration/api_contract_server.py +29 -0
  17. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/integration/generated_jsonb_app.py +3 -0
  18. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/integration/generated_m2m_app.py +7 -0
  19. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/integration/generated_router_app.py +6 -0
  20. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/integration/generated_tph_app.py +7 -0
  21. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/integration/test_generated_router_uuid_pk.py +3 -0
  22. metaobjects-0.15.13/tests/codegen/golden/extends/expected/BaseEntity.py +0 -11
  23. metaobjects-0.15.13/tests/codegen/golden/extends/expected/Program.py +0 -11
  24. metaobjects-0.15.13/tests/codegen/golden/vanilla/expected/Subscriber.py +0 -12
  25. {metaobjects-0.15.13 → metaobjects-0.16.0}/.gitignore +0 -0
  26. {metaobjects-0.15.13 → metaobjects-0.16.0}/LICENSE +0 -0
  27. {metaobjects-0.15.13 → metaobjects-0.16.0}/README.md +0 -0
  28. {metaobjects-0.15.13 → metaobjects-0.16.0}/hatch_build.py +0 -0
  29. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/__init__.py +0 -0
  30. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/agent_context/__init__.py +0 -0
  31. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/agent_context/scaffold.py +0 -0
  32. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/apidocs/__init__.py +0 -0
  33. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/apidocs/api_model.py +0 -0
  34. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/apidocs/builder.py +0 -0
  35. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/apidocs/naming.py +0 -0
  36. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/apidocs/paths.py +0 -0
  37. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/apidocs/renderer.py +0 -0
  38. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/attr_class_map.py +0 -0
  39. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/cli.py +0 -0
  40. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/codegen/__init__.py +0 -0
  41. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/codegen/config.py +0 -0
  42. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/codegen/constants.py +0 -0
  43. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/codegen/extract_delegate_emitter.py +0 -0
  44. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/codegen/extract_schema_emitter.py +0 -0
  45. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/codegen/format.py +0 -0
  46. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/codegen/fr010_field_mapping.py +0 -0
  47. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/codegen/generator.py +0 -0
  48. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/codegen/generator_registry.py +0 -0
  49. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/codegen/generators/__init__.py +0 -0
  50. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/codegen/generators/extractor_generator.py +0 -0
  51. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/codegen/generators/filter_allowlist_generator.py +0 -0
  52. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/codegen/generators/fr019_shared_enum.py +0 -0
  53. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/codegen/generators/m2m_codegen.py +0 -0
  54. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/codegen/generators/output_parser_generator.py +0 -0
  55. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/codegen/generators/output_prompt_generator.py +0 -0
  56. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/codegen/generators/payload_vo_generator.py +0 -0
  57. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/codegen/generators/render_helper_generator.py +0 -0
  58. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/codegen/generators/template_generator.py +0 -0
  59. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/codegen/generators/tph_plan.py +0 -0
  60. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/codegen/generators/trace_helper_generator.py +0 -0
  61. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/codegen/instance_artifacts.py +0 -0
  62. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/codegen/output_format_spec_emitter.py +0 -0
  63. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/codegen/overwrite_policy.py +0 -0
  64. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/codegen/runner.py +0 -0
  65. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/codegen/runtime/__init__.py +0 -0
  66. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/codegen/runtime/filter_parser.py +0 -0
  67. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/codegen/template_codegen/__init__.py +0 -0
  68. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/codegen/template_codegen/output_pattern.py +0 -0
  69. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/codegen/template_codegen/template_data.py +0 -0
  70. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/codegen/template_codegen/template_spec.py +0 -0
  71. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/codegen/type_map.py +0 -0
  72. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/core_types.py +0 -0
  73. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/datatype.py +0 -0
  74. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/documentation/__init__.py +0 -0
  75. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/documentation/doc_constants.py +0 -0
  76. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/documentation/doc_provider.py +0 -0
  77. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/documentation/doc_schema.py +0 -0
  78. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/errors.py +0 -0
  79. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/loader/__init__.py +0 -0
  80. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/loader/merge.py +0 -0
  81. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/loader/meta_data_loader.py +0 -0
  82. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/loader/registered_validation.py +0 -0
  83. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/loader/sources/__init__.py +0 -0
  84. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/loader/sources/directory_source.py +0 -0
  85. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/loader/sources/file_source.py +0 -0
  86. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/loader/sources/meta_data_source.py +0 -0
  87. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/loader/sources/uri_source.py +0 -0
  88. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/loader/validate_discriminator.py +0 -0
  89. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/loader/validate_field_readonly.py +0 -0
  90. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/loader/validate_source_parameter_ref.py +0 -0
  91. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/loader/validate_source_physical_names.py +0 -0
  92. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/loader/validation_passes.py +0 -0
  93. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/meta/__init__.py +0 -0
  94. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/meta/core/__init__.py +0 -0
  95. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/meta/core/attr/__init__.py +0 -0
  96. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/meta/core/attr/attr_constants.py +0 -0
  97. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/meta/core/attr/meta_attr.py +0 -0
  98. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/meta/core/field/__init__.py +0 -0
  99. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/meta/core/field/field_constants.py +0 -0
  100. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/meta/core/field/meta_field.py +0 -0
  101. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/meta/core/identity/__init__.py +0 -0
  102. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/meta/core/identity/identity_constants.py +0 -0
  103. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/meta/core/identity/meta_identity.py +0 -0
  104. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/meta/core/index/__init__.py +0 -0
  105. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/meta/core/index/index_constants.py +0 -0
  106. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/meta/core/index/meta_index.py +0 -0
  107. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/meta/core/object/__init__.py +0 -0
  108. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/meta/core/object/meta_object.py +0 -0
  109. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/meta/core/object/meta_object_aware.py +0 -0
  110. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/meta/core/object/object_class_registry.py +0 -0
  111. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/meta/core/object/object_constants.py +0 -0
  112. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/meta/core/object/object_extract.py +0 -0
  113. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/meta/core/object/value_object.py +0 -0
  114. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/meta/core/relationship/__init__.py +0 -0
  115. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/meta/core/relationship/derive_m2m_fields.py +0 -0
  116. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/meta/core/relationship/meta_relationship.py +0 -0
  117. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/meta/core/relationship/relationship_constants.py +0 -0
  118. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/meta/core/validator/__init__.py +0 -0
  119. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/meta/core/validator/validator_constants.py +0 -0
  120. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/meta/meta_data.py +0 -0
  121. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/meta/meta_root.py +0 -0
  122. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/meta/persistence/__init__.py +0 -0
  123. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/meta/persistence/db/__init__.py +0 -0
  124. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/meta/persistence/db/db_constants.py +0 -0
  125. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/meta/persistence/db/db_provider.py +0 -0
  126. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/meta/persistence/origin/__init__.py +0 -0
  127. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/meta/persistence/origin/meta_origin.py +0 -0
  128. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/meta/persistence/origin/origin_constants.py +0 -0
  129. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/meta/persistence/source/__init__.py +0 -0
  130. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/meta/persistence/source/meta_source.py +0 -0
  131. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/meta/persistence/source/source_constants.py +0 -0
  132. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/meta/presentation/__init__.py +0 -0
  133. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/meta/presentation/layout/__init__.py +0 -0
  134. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/meta/presentation/layout/layout_constants.py +0 -0
  135. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/meta/presentation/layout/meta_layout.py +0 -0
  136. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/meta/presentation/ui/__init__.py +0 -0
  137. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/meta/presentation/ui/ui_provider.py +0 -0
  138. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/meta/presentation/view/__init__.py +0 -0
  139. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/meta/presentation/view/meta_view.py +0 -0
  140. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/meta/presentation/view/view_constants.py +0 -0
  141. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/meta/provider_extends.py +0 -0
  142. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/meta/template/__init__.py +0 -0
  143. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/meta/template/meta_template.py +0 -0
  144. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/meta/template/prompt_provider.py +0 -0
  145. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/meta/template/template_constants.py +0 -0
  146. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/naming_refs.py +0 -0
  147. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/parser.py +0 -0
  148. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/parser_yaml.py +0 -0
  149. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/provider.py +0 -0
  150. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/py.typed +0 -0
  151. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/registry.py +0 -0
  152. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/registry_manifest.py +0 -0
  153. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/render/__init__.py +0 -0
  154. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/render/email_document.py +0 -0
  155. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/render/escapers.py +0 -0
  156. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/render/extract/KNOWN_GAPS.md +0 -0
  157. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/render/extract/__init__.py +0 -0
  158. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/render/extract/coerce.py +0 -0
  159. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/render/extract/extract_map.py +0 -0
  160. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/render/extract/json_forgiving_reader.py +0 -0
  161. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/render/extract/locate.py +0 -0
  162. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/render/extract/normalize.py +0 -0
  163. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/render/extract/strip.py +0 -0
  164. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/render/extract/xml_forgiving_reader.py +0 -0
  165. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/render/filesystem_provider.py +0 -0
  166. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/render/prompt/__init__.py +0 -0
  167. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/render/prompt/output_format_renderer.py +0 -0
  168. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/render/prompt/output_format_spec.py +0 -0
  169. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/render/prompt/prompt_field.py +0 -0
  170. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/render/prompt/prompt_overrides.py +0 -0
  171. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/render/prompt/prompt_style.py +0 -0
  172. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/render/renderer.py +0 -0
  173. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/render/verify.py +0 -0
  174. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/runtime/__init__.py +0 -0
  175. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/runtime/llm_recorder.py +0 -0
  176. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/runtime/n2m_resolver.py +0 -0
  177. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/runtime/object_manager.py +0 -0
  178. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/runtime/tph.py +0 -0
  179. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/serializer_json.py +0 -0
  180. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/shared/__init__.py +0 -0
  181. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/shared/base_types.py +0 -0
  182. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/shared/separators.py +0 -0
  183. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/shared/structural.py +0 -0
  184. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/source/__init__.py +0 -0
  185. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/source/error_source.py +0 -0
  186. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/source/json_path.py +0 -0
  187. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/source/semantic_diff.py +0 -0
  188. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/source/yaml_positions.py +0 -0
  189. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/spec_metamodel/__init__.py +0 -0
  190. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/spec_metamodel/attr.json +0 -0
  191. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/spec_metamodel/db.json +0 -0
  192. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/spec_metamodel/documentation.json +0 -0
  193. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/spec_metamodel/field.json +0 -0
  194. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/spec_metamodel/identity.json +0 -0
  195. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/spec_metamodel/index.json +0 -0
  196. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/spec_metamodel/layout.json +0 -0
  197. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/spec_metamodel/object.json +0 -0
  198. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/spec_metamodel/origin.json +0 -0
  199. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/spec_metamodel/prompt.json +0 -0
  200. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/spec_metamodel/relationship.json +0 -0
  201. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/spec_metamodel/source.json +0 -0
  202. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/spec_metamodel/template.json +0 -0
  203. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/spec_metamodel/ui.json +0 -0
  204. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/spec_metamodel/validator.json +0 -0
  205. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/spec_metamodel/view.json +0 -0
  206. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/super_resolve.py +0 -0
  207. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/validation_types.py +0 -0
  208. {metaobjects-0.15.13 → metaobjects-0.16.0}/src/metaobjects/yaml_desugar.py +0 -0
  209. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/__init__.py +0 -0
  210. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/codegen/__init__.py +0 -0
  211. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/codegen/golden/extends/meta.json +0 -0
  212. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/codegen/golden/nested-array/expected/PostBrief.py +0 -0
  213. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/codegen/golden/nested-array/meta.json +0 -0
  214. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/codegen/golden/scalars/expected/Metric.py +0 -0
  215. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/codegen/golden/scalars/expected/Report.py +0 -0
  216. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/codegen/golden/scalars/meta.json +0 -0
  217. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/codegen/golden/vanilla/meta.json +0 -0
  218. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/codegen/test_abstract_conformance.py +0 -0
  219. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/codegen/test_api_docs_builder.py +0 -0
  220. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/codegen/test_api_docs_paths.py +0 -0
  221. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/codegen/test_cli.py +0 -0
  222. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/codegen/test_cli_providers.py +0 -0
  223. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/codegen/test_cli_registry.py +0 -0
  224. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/codegen/test_cli_staleness_nudge.py +0 -0
  225. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/codegen/test_cli_verify_strict.py +0 -0
  226. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/codegen/test_cli_verify_subverbs.py +0 -0
  227. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/codegen/test_constants_config.py +0 -0
  228. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/codegen/test_enum_conformance.py +0 -0
  229. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/codegen/test_extractor_generator.py +0 -0
  230. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/codegen/test_filter_allowlist_generator.py +0 -0
  231. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/codegen/test_format.py +0 -0
  232. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/codegen/test_fr010_nested_object_prompt.py +0 -0
  233. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/codegen/test_fr010_output_codegen.py +0 -0
  234. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/codegen/test_fr019_shared_provided_conformance.py +0 -0
  235. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/codegen/test_generator.py +0 -0
  236. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/codegen/test_generator_extension_seams.py +0 -0
  237. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/codegen/test_golden.py +0 -0
  238. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/codegen/test_instance_artifacts.py +0 -0
  239. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/codegen/test_m2m_codegen.py +0 -0
  240. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/codegen/test_output_parser_generator.py +0 -0
  241. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/codegen/test_overwrite_policy.py +0 -0
  242. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/codegen/test_payload_vo_generator.py +0 -0
  243. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/codegen/test_projection_compile.py +0 -0
  244. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/codegen/test_render_helper_conformance.py +0 -0
  245. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/codegen/test_render_helper_generator.py +0 -0
  246. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/codegen/test_reverse_finders.py +0 -0
  247. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/codegen/test_router_generator.py +0 -0
  248. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/codegen/test_runner.py +0 -0
  249. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/codegen/test_template_data.py +0 -0
  250. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/codegen/test_template_generator.py +0 -0
  251. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/codegen/test_template_output_pattern.py +0 -0
  252. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/codegen/test_template_scope_helpers.py +0 -0
  253. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/codegen/test_template_scope_walk.py +0 -0
  254. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/codegen/test_template_spec.py +0 -0
  255. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/codegen/test_tph_codegen.py +0 -0
  256. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/codegen/test_trace_helper_generator.py +0 -0
  257. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/codegen/test_type_map.py +0 -0
  258. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/codegen/test_validation_conformance.py +0 -0
  259. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/conformance/__init__.py +0 -0
  260. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/conformance/capabilities.py +0 -0
  261. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/conformance/conformance-expected-failures.json +0 -0
  262. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/conformance/conformance_adapter.py +0 -0
  263. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/conformance/corpus.py +0 -0
  264. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/conformance/expected_failures.py +0 -0
  265. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/conformance/fixture_discovery.py +0 -0
  266. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/conformance/navigator.py +0 -0
  267. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/conformance/test_api_docs_cross_port_conformance.py +0 -0
  268. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/conformance/test_conformance.py +0 -0
  269. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/conformance/test_extract_conformance.py +0 -0
  270. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/conformance/test_extract_object_verdict.py +0 -0
  271. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/conformance/test_fr010_loader_attrs.py +0 -0
  272. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/conformance/test_fr011_attrs.py +0 -0
  273. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/conformance/test_generator_registry_conformance.py +0 -0
  274. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/conformance/test_object_model_conformance.py +0 -0
  275. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/conformance/test_provider_composition_conformance.py +0 -0
  276. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/conformance/test_registry_conformance.py +0 -0
  277. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/conformance/test_runner_hardfail.py +0 -0
  278. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/conformance/test_spec_metamodel_embed.py +0 -0
  279. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/conformance/test_strict_attr_load.py +0 -0
  280. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/conformance/test_template_codegen_conformance.py +0 -0
  281. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/conformance/test_template_generator_conformance.py +0 -0
  282. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/conformance/test_yaml_conformance.py +0 -0
  283. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/conformance/yaml-conformance-expected-failures.json +0 -0
  284. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/integration/__init__.py +0 -0
  285. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/integration/api_contract_assertions.py +0 -0
  286. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/integration/api_contract_jsonb_server.py +0 -0
  287. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/integration/api_contract_m2m_server.py +0 -0
  288. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/integration/meta_ai_trace.yaml +0 -0
  289. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/integration/normalization.py +0 -0
  290. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/integration/postgres_container.py +0 -0
  291. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/integration/query_runner.py +0 -0
  292. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/integration/scenarios.py +0 -0
  293. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/integration/test_api_contract.py +0 -0
  294. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/integration/test_api_contract_generated.py +0 -0
  295. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/integration/test_api_contract_jsonb.py +0 -0
  296. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/integration/test_api_contract_m2m.py +0 -0
  297. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/integration/test_api_contract_m2m_generated.py +0 -0
  298. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/integration/test_api_contract_tph_generated.py +0 -0
  299. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/integration/test_llm_call_trace.py +0 -0
  300. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/integration/test_normalization.py +0 -0
  301. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/integration/test_query_scenarios.py +0 -0
  302. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/integration/test_runtime_return_types.py +0 -0
  303. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/open_closed_proof_test.py +0 -0
  304. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/render/__init__.py +0 -0
  305. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/render/extract/__init__.py +0 -0
  306. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/render/extract/test_coerce.py +0 -0
  307. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/render/extract/test_extract.py +0 -0
  308. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/render/extract/test_extract_map.py +0 -0
  309. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/render/extract/test_json_forgiving_reader.py +0 -0
  310. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/render/extract/test_locate.py +0 -0
  311. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/render/extract/test_model.py +0 -0
  312. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/render/extract/test_normalize.py +0 -0
  313. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/render/extract/test_strip.py +0 -0
  314. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/render/extract/test_xml_forgiving_reader.py +0 -0
  315. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/render/prompt/__init__.py +0 -0
  316. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/render/prompt/test_output_format_renderer.py +0 -0
  317. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/render/test_email_document.py +0 -0
  318. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/render/test_filesystem_provider.py +0 -0
  319. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/render/test_output_format_renderer_nested.py +0 -0
  320. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/render/test_output_prompt_conformance.py +0 -0
  321. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/render/test_render_conformance.py +0 -0
  322. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/render/test_render_max_chars.py +0 -0
  323. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/render/test_verify.py +0 -0
  324. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/render/test_verify_conformance.py +0 -0
  325. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/source/__init__.py +0 -0
  326. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/source/test_fr5c_merge_attribution.py +0 -0
  327. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/source/test_fr5d_reference_resolution.py +0 -0
  328. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/source/test_fr5e_database_source_shape.py +0 -0
  329. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/source/test_json_path.py +0 -0
  330. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/source/test_semantic_diff.py +0 -0
  331. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/source/test_source_on_node.py +0 -0
  332. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/source/test_yaml_positions.py +0 -0
  333. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/test_api_docs_accuracy.py +0 -0
  334. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/unit/__init__.py +0 -0
  335. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/unit/test_agent_context_staleness.py +0 -0
  336. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/unit/test_capabilities.py +0 -0
  337. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/unit/test_common_attrs.py +0 -0
  338. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/unit/test_core_types.py +0 -0
  339. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/unit/test_effective_package.py +0 -0
  340. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/unit/test_errors.py +0 -0
  341. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/unit/test_field_enum.py +0 -0
  342. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/unit/test_field_map_validation.py +0 -0
  343. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/unit/test_field_uuid_dbcolumntype.py +0 -0
  344. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/unit/test_fr016_source_name_and_kind_aliases.py +0 -0
  345. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/unit/test_index_lookup.py +0 -0
  346. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/unit/test_llm_recorder.py +0 -0
  347. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/unit/test_loader.py +0 -0
  348. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/unit/test_loader_bom.py +0 -0
  349. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/unit/test_loader_class.py +0 -0
  350. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/unit/test_merge.py +0 -0
  351. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/unit/test_meta_attr.py +0 -0
  352. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/unit/test_meta_data.py +0 -0
  353. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/unit/test_meta_source.py +0 -0
  354. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/unit/test_module_shortcuts.py +0 -0
  355. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/unit/test_n2m_resolver.py +0 -0
  356. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/unit/test_n2m_resolver_inherited.py +0 -0
  357. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/unit/test_object_manager_uuid_coercion.py +0 -0
  358. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/unit/test_one_primary_source.py +0 -0
  359. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/unit/test_parser.py +0 -0
  360. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/unit/test_parser_yaml.py +0 -0
  361. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/unit/test_provider.py +0 -0
  362. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/unit/test_provider_extension.py +0 -0
  363. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/unit/test_registry.py +0 -0
  364. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/unit/test_registry_completeness.py +0 -0
  365. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/unit/test_registry_extend.py +0 -0
  366. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/unit/test_registry_sealed.py +0 -0
  367. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/unit/test_relationship_referential_actions.py +0 -0
  368. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/unit/test_resolution_key.py +0 -0
  369. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/unit/test_runtime_resolution_key_binding.py +0 -0
  370. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/unit/test_serializer.py +0 -0
  371. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/unit/test_shared_constants.py +0 -0
  372. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/unit/test_smoke.py +0 -0
  373. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/unit/test_sources.py +0 -0
  374. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/unit/test_strict_child_placement.py +0 -0
  375. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/unit/test_super_resolve.py +0 -0
  376. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/unit/test_template_toolcall.py +0 -0
  377. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/unit/test_template_wrong_subtype_attrs.py +0 -0
  378. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/unit/test_validation_attr_schema.py +0 -0
  379. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/unit/test_validation_filter_values.py +0 -0
  380. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/unit/test_validation_origin_paths.py +0 -0
  381. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/unit/test_validation_sort_field.py +0 -0
  382. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/unit/test_validation_warnings.py +0 -0
  383. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/unit/test_write_coercion_arrays.py +0 -0
  384. {metaobjects-0.15.13 → metaobjects-0.16.0}/tests/unit/test_yaml_desugar.py +0 -0
  385. {metaobjects-0.15.13 → metaobjects-0.16.0}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: metaobjects
3
- Version: 0.15.13
3
+ Version: 0.16.0
4
4
  Summary: Cross-language metadata standard: declare typed entities once, generate idiomatic drift-checked code across languages — Python port.
5
5
  Project-URL: Homepage, https://metaobjects.dev
6
6
  Project-URL: Repository, https://github.com/metaobjectsdev/metaobjects
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "metaobjects"
3
- version = "0.15.13"
3
+ version = "0.16.0"
4
4
  description = "Cross-language metadata standard: declare typed entities once, generate idiomatic drift-checked code across languages — Python port."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -32,6 +32,14 @@ The generated router takes `dto: dict[str, Any]` for `POST` / `PATCH` /
32
32
  `PUT` request bodies and returns `Any` for responses. The repository
33
33
  `Protocol` likewise uses `Any` for the row type.
34
34
 
35
+ **FR-036 update:** the router now DOES import the generated Pydantic models
36
+ (`from .<Entity> import <Entity>, <Entity>Patch`) and constructs them to enforce
37
+ field constraints over HTTP — POST validates `<Entity>(**dto)`, PATCH validates
38
+ the all-optional `<Entity>Patch(**dto)` (present values only) → `400 {"error":"validation"}`.
39
+ The repository seam still receives the original `dict` (preserving the FR-035
40
+ present-key tristate + wire values), so the strong-typing simplification below
41
+ remains for the repo/response types; only the validation import was added.
42
+
35
43
  This keeps the router module decoupled from sibling generated files
36
44
  (the entity-model generator emits one `@dataclass`/Pydantic model per
37
45
  entity in a separate file — wiring router→entity-model would require
@@ -5,7 +5,14 @@ import re
5
5
 
6
6
  from metaobjects.meta.core.field.meta_field import MetaField
7
7
  from metaobjects.meta.core.object.meta_object import MetaObject
8
+ from metaobjects.meta.core.object.object_constants import OBJECT_SUBTYPE_ENTITY
8
9
  from metaobjects.meta.core.field import field_constants as fc
10
+ from metaobjects.meta.core.identity.identity_constants import (
11
+ GENERATION_INCREMENT,
12
+ GENERATION_UUID,
13
+ IDENTITY_ATTR_FIELDS,
14
+ IDENTITY_ATTR_GENERATION,
15
+ )
9
16
  from metaobjects.meta.core.validator import validator_constants as vc
10
17
  from metaobjects.shared.base_types import TYPE_VALIDATOR
11
18
  from metaobjects.shared.separators import PACKAGE_SEP
@@ -98,7 +105,10 @@ def _validator_constraints(field: MetaField) -> dict[str, object]:
98
105
  max_len = _first_attr(field, vc.VALIDATOR_SUBTYPE_LENGTH, vc.VALIDATOR_ATTR_MAX)
99
106
  field_max = field.attrs().get(fc.FIELD_ATTR_MAX_LENGTH)
100
107
  if _is_int(field_max):
101
- max_len = field_max
108
+ # FR-036 A3 — strictest-wins: when BOTH a validator.length @max and a field
109
+ # @maxLength cap a string, the smaller bound governs (never the field attr
110
+ # unconditionally, which could loosen a tighter validator).
111
+ max_len = field_max if max_len is None else min(max_len, field_max)
102
112
  if min_len is not None:
103
113
  kwargs["min_length"] = min_len
104
114
  if max_len is not None:
@@ -127,7 +137,11 @@ def _validator_constraints(field: MetaField) -> dict[str, object]:
127
137
  # located via the resolving field.children() in _validators().
128
138
  pattern = v.attr(vc.VALIDATOR_ATTR_PATTERN)
129
139
  if isinstance(pattern, str):
130
- kwargs["pattern"] = pattern
140
+ # FR-036 Pin 2 — a validator.regex is a FULL match. Pydantic v2 `pattern=`
141
+ # is search/partial (un-anchored), so wrap the authored expression in an
142
+ # anchored non-capturing group so alternation still spans the whole value
143
+ # (a redundant anchor on an already-anchored pattern matches identically).
144
+ kwargs["pattern"] = f"^(?:{pattern})$"
131
145
  break
132
146
 
133
147
  # ADR-0036/0037 Wave 3 — @stringFormat: hostname -> a codegen-owned canonical
@@ -137,60 +151,87 @@ def _validator_constraints(field: MetaField) -> dict[str, object]:
137
151
  if field.attrs().get(fc.FIELD_ATTR_STRING_FORMAT) == fc.STRING_FORMAT_HOSTNAME:
138
152
  kwargs["pattern"] = _HOSTNAME_REGEX
139
153
 
154
+ # FR-036 Pin 1 — a @required non-array string is non-empty: reject null / "" but
155
+ # ACCEPT whitespace-only. Emit an implicit min_length of 1, keeping the stricter
156
+ # floor if a validator.length @min already set one.
157
+ if (
158
+ field.sub_type == fc.FIELD_SUBTYPE_STRING
159
+ and not field_is_array(field)
160
+ and field.attrs().get(fc.FIELD_ATTR_REQUIRED) is True
161
+ ):
162
+ existing_min = kwargs.get("min_length")
163
+ kwargs["min_length"] = max(1, existing_min) if isinstance(existing_min, int) else 1
164
+
140
165
  return kwargs
141
166
 
142
167
 
143
- def _field_line(field: MetaField, imports: set[str], config: GenConfig) -> tuple[str, bool]:
144
- """Return (source line, uses_field). Collects required imports into *imports*."""
145
- # FR-019: a field resolving to a package-level shared enum is typed by the materialized
146
- # standalone class (imported from the shared `enums` module) or — when @provided — an
147
- # external module from per-port config; inline enums keep their `Literal[...]` (py_type_for).
168
+ # Constraint kwargs are emitted in this fixed order so generated output is deterministic.
169
+ _CONSTRAINT_ORDER = ("pattern", "ge", "le", "min_length", "max_length")
170
+
171
+
172
+ def _constraint_parts(constraints: dict[str, object]) -> list[str]:
173
+ """The ``Field(...)`` constraint kwargs rendered in the stable, deterministic order."""
174
+ return [f"{k}={constraints[k]!r}" for k in _CONSTRAINT_ORDER if k in constraints]
175
+
176
+
177
+ def _type_expr_for_field(
178
+ field: MetaField, imports: set[str], config: GenConfig
179
+ ) -> tuple[str, str | None]:
180
+ """The Python annotation text for *field* (arrays wrapped in ``list[...]``) plus the
181
+ shared-enum class name when the field resolves to one (used to render an enum
182
+ default). Collects any required imports into *imports*. Shared by the create-model
183
+ and patch-model emitters so both carry byte-identical typing.
184
+
185
+ FR-019: a field resolving to a package-level shared enum is typed by the materialized
186
+ standalone class (imported from the shared ``enums`` module) or — when @provided — an
187
+ external module; inline enums keep their ``Literal[...]`` (py_type_for)."""
148
188
  shared = (
149
189
  fr019.shared_enum_for_field(field)
150
190
  if field.sub_type == fc.FIELD_SUBTYPE_ENUM
151
191
  else None
152
192
  )
193
+ enum_type_name: str | None = None
153
194
  if shared is not None:
154
195
  type_name, import_line = fr019.enum_type_and_import(shared, config)
155
196
  imports.add(import_line)
197
+ enum_type_name = type_name
156
198
  type_expr = f"list[{type_name}]" if field_is_array(field) else type_name
157
199
  else:
158
200
  pt = py_type_for(field)
159
201
  imports.update(pt.imports)
160
202
  type_expr = pt.expr
161
- # ADR-0036/0037 Wave 3 — @stringFormat: email narrows a plain string to a
162
- # validated email. Bind Pydantic's idiomatic EmailStr (the Python analogue of
163
- # Zod .email()); hostname stays a plain str with a codegen-owned canonical
164
- # pattern= (handled in _validator_constraints). The field stays a string.
203
+ # ADR-0036/0037 Wave 3 — @stringFormat: email narrows a plain string to a validated
204
+ # email (Pydantic EmailStr, the Python analogue of Zod .email()); hostname stays a
205
+ # plain str with a codegen-owned canonical pattern= (in _validator_constraints).
165
206
  if (
166
207
  field.sub_type == fc.FIELD_SUBTYPE_STRING
167
208
  and field.attrs().get(fc.FIELD_ATTR_STRING_FORMAT) == fc.STRING_FORMAT_EMAIL
168
209
  ):
169
- base_expr = "EmailStr"
170
210
  imports.add("from pydantic import EmailStr")
171
- type_expr = f"list[{base_expr}]" if field_is_array(field) else base_expr
211
+ type_expr = "list[EmailStr]" if field_is_array(field) else "EmailStr"
172
212
  if field.sub_type in (fc.FIELD_SUBTYPE_OBJECT, fc.FIELD_SUBTYPE_MAP):
173
- # A field.object (-> VO) and a field.map with @objectRef (-> dict[str, VO])
174
- # both reference a value-object by name; import it so the annotation
175
- # py_type_for emitted resolves.
213
+ # A field.object (-> VO) and a field.map with @objectRef (-> dict[str, VO]) both
214
+ # reference a value-object by name; import it so the emitted annotation resolves.
215
+ # @objectRef is FQN-expanded at load; the generated VOs live flat in one package,
216
+ # so import by the bare class name.
176
217
  ref = field.attrs().get(fc.FIELD_ATTR_OBJECT_REF)
177
218
  if ref:
178
- # @objectRef is FQN-expanded at load time; the generated VOs live
179
- # flat in one package, so import by the bare class name.
180
219
  ref_name = str(ref).split("::")[-1]
181
220
  imports.add(f"from .{ref_name} import {ref_name}")
221
+ return type_expr, enum_type_name
222
+
223
+
224
+ def _field_line(field: MetaField, imports: set[str], config: GenConfig) -> tuple[str, bool]:
225
+ """Return (source line, uses_field). Collects required imports into *imports*."""
226
+ type_expr, enum_type_name = _type_expr_for_field(field, imports, config)
182
227
  required = field.attrs().get(fc.FIELD_ATTR_REQUIRED) is True
183
228
  default_raw = field.attrs().get(fc.FIELD_ATTR_DEFAULT)
184
229
  # A server-side expression default (now(), gen_random_uuid(), CURRENT_TIMESTAMP)
185
230
  # is filled by the DB — the Python model carries no default for it (the field keeps
186
231
  # its required/optional shape). Only literal defaults become Pydantic defaults.
187
232
  has_default = default_raw is not None and not _is_sql_expr_default(default_raw)
188
- enum_type_name = type_name if shared is not None else None
189
233
 
190
- constraints = _validator_constraints(field)
191
- # Emit kwargs in a stable order so generated output is deterministic.
192
- _order = ["pattern", "ge", "le", "min_length", "max_length"]
193
- parts = [f"{k}={constraints[k]!r}" for k in _order if k in constraints]
234
+ parts = _constraint_parts(_validator_constraints(field))
194
235
  uses_field = bool(parts)
195
236
 
196
237
  # @default — render an enum member (Type.MEMBER, UPPER_CASE) or a Python literal.
@@ -226,6 +267,106 @@ def _field_line(field: MetaField, imports: set[str], config: GenConfig) -> tuple
226
267
  return f" {py_name}: {annotation}{assignment}", uses_field
227
268
 
228
269
 
270
+ def _primary_identity_field_names(entity: MetaObject) -> list[str]:
271
+ """The field names in the entity's primary identity (``identity.primary @fields``),
272
+ normalized to a list. Empty when there is no primary identity. Mirrors the TS
273
+ ``primaryIdentityFieldNames``."""
274
+ primary = entity.primary_identity()
275
+ if primary is None:
276
+ return []
277
+ # ADR-0039: resolving (identity attr) — an identity.primary may be inherited via extends.
278
+ fields = primary.get_meta_attr(IDENTITY_ATTR_FIELDS)
279
+ if isinstance(fields, (list, tuple)):
280
+ return [str(f) for f in fields]
281
+ if isinstance(fields, str) and fields:
282
+ return [s.strip() for s in fields.split(",") if s.strip()]
283
+ return []
284
+
285
+
286
+ def _auto_gen_pk_field_names(entity: MetaObject) -> set[str]:
287
+ """Primary-key field names that the SERVER generates (``identity.primary
288
+ @generation: increment|uuid``) — excluded from the create-validation shape because
289
+ the caller never supplies them. An ``assigned`` (or ungenerated) PK is NOT excluded
290
+ (the caller provides it). Mirrors the TS ``autoGenPkFieldNames``."""
291
+ primary = entity.primary_identity()
292
+ if primary is None:
293
+ return set()
294
+ # ADR-0039: resolving (identity attr).
295
+ generation = primary.get_meta_attr(IDENTITY_ATTR_GENERATION)
296
+ if generation not in (GENERATION_INCREMENT, GENERATION_UUID):
297
+ return set()
298
+ return set(_primary_identity_field_names(entity))
299
+
300
+
301
+ def _create_field_line(
302
+ field: MetaField, imports: set[str], config: GenConfig
303
+ ) -> tuple[str, bool]:
304
+ """A CREATE-model field line (FR-036): keyed by the WIRE name (``field.name``) and
305
+ carrying the same per-field constraints as the read model. A ``@required`` field
306
+ that is SERVER-FILLED — it has ANY ``@default`` (a literal OR a SQL expression such
307
+ as ``now()`` / ``gen_random_uuid()`` / ``CURRENT_TIMESTAMP``) or an ``@autoSet`` — is
308
+ emitted OPTIONAL, because the DB / trigger provides it and a POST that omits it must
309
+ not 400 (mirrors the TS InsertSchema ``fieldWillBeOptional``). A literal ``@default``
310
+ is preserved as the Python default; a SQL-expression default / ``@autoSet`` carries
311
+ no Python default (the server fills it). Returns (source line, uses_field)."""
312
+ type_expr, enum_type_name = _type_expr_for_field(field, imports, config)
313
+ required = field.attrs().get(fc.FIELD_ATTR_REQUIRED) is True
314
+ default_raw = field.attrs().get(fc.FIELD_ATTR_DEFAULT)
315
+ literal_default = default_raw is not None and not _is_sql_expr_default(default_raw)
316
+ auto_set = field.attrs().get(fc.FIELD_ATTR_AUTO_SET)
317
+ is_auto_set = auto_set in (fc.AUTO_SET_ON_CREATE, fc.AUTO_SET_ON_UPDATE)
318
+ # A field is required in the CREATE shape only when @required AND not server-filled:
319
+ # ANY @default (literal or SQL-expr) or @autoSet means the DB fills it, so the wire
320
+ # body may omit it (the FR-036 #1 regression fix).
321
+ server_filled = default_raw is not None or is_auto_set
322
+ create_required = required and not server_filled
323
+
324
+ parts = _constraint_parts(_validator_constraints(field))
325
+ uses_field = bool(parts)
326
+
327
+ py_name = field.name # FR-036 #3 — validation models key by the WIRE name (field.name)
328
+
329
+ if literal_default:
330
+ # Preserve the literal @default (enum member or Python literal) exactly as the
331
+ # read model does — the caller may omit the field and the model fills the default.
332
+ if enum_type_name is not None and not field_is_array(field):
333
+ default_expr = f"{enum_type_name}.{str(default_raw).upper()}"
334
+ else:
335
+ default_expr = repr(default_raw)
336
+ annotation = type_expr
337
+ assignment = (
338
+ f" = Field(default={default_expr}, {', '.join(parts)})"
339
+ if uses_field
340
+ else f" = {default_expr}"
341
+ )
342
+ elif create_required:
343
+ annotation = type_expr
344
+ assignment = f" = Field({', '.join(parts)})" if uses_field else ""
345
+ else:
346
+ annotation = f"{type_expr} | None"
347
+ assignment = (
348
+ f" = Field(default=None, {', '.join(parts)})" if uses_field else " = None"
349
+ )
350
+
351
+ return f" {py_name}: {annotation}{assignment}", uses_field
352
+
353
+
354
+ def _patch_field_line(
355
+ field: MetaField, imports: set[str], config: GenConfig
356
+ ) -> tuple[str, bool]:
357
+ """A PATCH-model field line: the field is made Optional (default ``None``) yet
358
+ carries the SAME per-field constraints as the create model, so a present, non-null
359
+ value is validated with the create rules (FR-036) while an absent field is left
360
+ untouched. A ``@default`` is never emitted (a patch does not default a column).
361
+ Returns (source line, uses_field)."""
362
+ type_expr, _enum = _type_expr_for_field(field, imports, config)
363
+ parts = _constraint_parts(_validator_constraints(field))
364
+ py_name = field.name # FR-036 #3 — validation models key by the WIRE name (field.name)
365
+ if parts:
366
+ return f" {py_name}: {type_expr} | None = Field(default=None, {', '.join(parts)})", True
367
+ return f" {py_name}: {type_expr} | None = None", False
368
+
369
+
229
370
  def _effective_fqn(entity: MetaObject) -> str:
230
371
  """`package::name` via the canonical :meth:`MetaData.resolution_key` — own
231
372
  package, else the file-default package (captured at parse), else the nearest
@@ -295,6 +436,76 @@ class EntityModelGenerator:
295
436
  lines.append(f" {d.relation_name}: list[{element}] = []")
296
437
  return lines, uses_field
297
438
 
439
+ def _emit_create_lines(
440
+ self,
441
+ entity: MetaObject,
442
+ imports: set[str],
443
+ config: GenConfig | None = None,
444
+ ) -> tuple[list[str], bool]:
445
+ """The ``<Name>Create`` body — the CREATE-validation shape the generated router
446
+ validates a POST body against (FR-036). Restates every EFFECTIVE field (own +
447
+ inherited via ``extends``) EXCEPT the ones the server owns: an auto-generated PK
448
+ (``identity.primary @generation``; FR-036 #2) and any ``@readOnly`` field
449
+ (FR-013) are omitted, and a ``@required`` server-filled field (``@default`` /
450
+ ``@autoSet``; FR-036 #1) is made optional. Keyed by the wire name; a TPH
451
+ subtype's discriminator is pinned to its ``Literal`` value (the route injects it
452
+ from the URL, so it stays optional with a default). M:N navigations are omitted
453
+ (a create body targets columns, not relations). Returns ``(lines, uses_field)``.
454
+ Override to add/transform create-model lines."""
455
+ cfg = config if config is not None else GenConfig(out_dir="")
456
+ auto_gen_pk = _auto_gen_pk_field_names(entity)
457
+ binding = tph_subtype_binding(entity)
458
+ disc_field = binding[0] if binding is not None else None
459
+ uses_field = False
460
+ lines: list[str] = []
461
+ # RESOLVING (fields()): the flat Create model subclasses BaseModel (not the entity
462
+ # base), so it restates inherited fields too — unlike the read model's own_fields
463
+ # subclass-emit.
464
+ for f in entity.fields():
465
+ if disc_field is not None and f.name == disc_field:
466
+ continue # pinned below (prepended), never as a normal field line
467
+ if f.name in auto_gen_pk:
468
+ continue # FR-036 #2 — a server-generated PK is never in the create body
469
+ if f.attrs().get(fc.FIELD_ATTR_READ_ONLY) is True:
470
+ continue # FR-013 — a read-only column is DB/owner-written, not create input
471
+ line, used = _create_field_line(f, imports, cfg)
472
+ uses_field = uses_field or used
473
+ lines.append(line)
474
+ # FR-017 TPH: pin the inherited discriminator to this subtype's value (with a
475
+ # default so the URL-injected create body may omit it) — mirrors the read model.
476
+ if binding is not None:
477
+ disc_name, disc_value = binding
478
+ lines = [f' {disc_name}: Literal["{disc_value}"] = "{disc_value}"', *lines]
479
+ imports.add("from typing import Literal")
480
+ return lines, uses_field
481
+
482
+ def _emit_patch_lines(
483
+ self,
484
+ entity: MetaObject,
485
+ imports: set[str],
486
+ config: GenConfig | None = None,
487
+ ) -> tuple[list[str], bool]:
488
+ """The ``<Name>Patch`` body: every EFFECTIVE field (own + inherited via
489
+ ``extends``) made Optional while retaining its create-model constraints, so the
490
+ generated PATCH handler can validate present, non-null values without
491
+ bean-validating absent ones (FR-036). The primary-key field(s) are excluded
492
+ (route-authoritative; FR-036 #4) and M:N navigations are omitted (a patch
493
+ targets columns, not relations). Returns ``(lines, uses_field)``. Override to
494
+ add/transform patch-model lines."""
495
+ cfg = config if config is not None else GenConfig(out_dir="")
496
+ pk_fields = set(_primary_identity_field_names(entity))
497
+ uses_field = False
498
+ lines: list[str] = []
499
+ # RESOLVING (fields()): the flat Patch model is self-contained (subclasses
500
+ # BaseModel, not the entity base), so it restates inherited fields too.
501
+ for f in entity.fields():
502
+ if f.name in pk_fields:
503
+ continue # FR-036 #4 — the PK is route-authoritative, never patched
504
+ line, used = _patch_field_line(f, imports, cfg)
505
+ uses_field = uses_field or used
506
+ lines.append(line)
507
+ return lines, uses_field
508
+
298
509
  def render_entity_model(
299
510
  self,
300
511
  entity: MetaObject,
@@ -333,11 +544,29 @@ class EntityModelGenerator:
333
544
  imports.add("from typing import Literal")
334
545
  body = lines if lines else [" pass"]
335
546
 
336
- # Import only the pydantic names actually referenced.
547
+ # FR-036 an object.entity also emits two flat validation models the generated
548
+ # router binds: a ``<Name>Create`` (POST-body shape: auto-gen PK / @readOnly
549
+ # omitted, @default/@autoSet optional, other @required required) and an
550
+ # all-optional ``<Name>Patch`` (PATCH-body shape: PK excluded). Both key by the
551
+ # WIRE name and carry the field constraints, so present values are validated with
552
+ # the create rules. Value objects / projections are never routed, so they emit
553
+ # neither (only the read model). See findings #1–#4.
554
+ emit_validation_models = entity.sub_type == OBJECT_SUBTYPE_ENTITY
555
+ create_lines: list[str] = []
556
+ create_uses_field = False
557
+ patch_lines: list[str] = []
558
+ patch_uses_field = False
559
+ if emit_validation_models:
560
+ create_lines, create_uses_field = self._emit_create_lines(entity, imports, config)
561
+ patch_lines, patch_uses_field = self._emit_patch_lines(entity, imports, config)
562
+
563
+ # Import only the pydantic names actually referenced. The main model imports
564
+ # BaseModel only when it has no super, but the flat Create/Patch models always
565
+ # subclass BaseModel, so their presence forces the import regardless.
337
566
  pyd_names: list[str] = []
338
- if entity.super_data is None:
567
+ if entity.super_data is None or emit_validation_models:
339
568
  pyd_names.append("BaseModel")
340
- if uses_field:
569
+ if uses_field or create_uses_field or patch_uses_field:
341
570
  pyd_names.append("Field")
342
571
 
343
572
  parts: list[str] = [
@@ -350,7 +579,22 @@ class EntityModelGenerator:
350
579
  parts += [*extra_imports, ""]
351
580
  if pyd_names:
352
581
  parts += [f"from pydantic import {', '.join(pyd_names)}", ""]
353
- parts += ["", self._emit_class_header(entity, base_class), *body, ""]
582
+ parts += ["", self._emit_class_header(entity, base_class), *body]
583
+ if emit_validation_models:
584
+ parts += [
585
+ "",
586
+ "",
587
+ f"class {entity.name}Create(BaseModel):",
588
+ ' """GENERATED — CREATE input: auto-gen PK / @readOnly omitted; '
589
+ '@default/@autoSet optional; present values validated (FR-036)."""',
590
+ *create_lines,
591
+ "",
592
+ "",
593
+ f"class {entity.name}Patch(BaseModel):",
594
+ ' """GENERATED — PATCH input: all fields optional (PK excluded); present values validated (FR-036)."""',
595
+ *patch_lines,
596
+ ]
597
+ parts += [""]
354
598
  return "\n".join(parts)
355
599
 
356
600
  def _render_shared_enums_module(self, entities: list[MetaObject]) -> EmittedFile | None:
@@ -39,6 +39,7 @@ from metaobjects.apidocs.naming import reverse_finder_fn, reverse_finder_in_fn
39
39
  from metaobjects.apidocs.naming import snake_case as _snake_case
40
40
  from metaobjects.codegen.constants import generated_header
41
41
  from metaobjects.codegen.format import ruff_format
42
+ from metaobjects.codegen.fr010_field_mapping import is_required
42
43
  from metaobjects.codegen.generator import EmittedFile, GenContext, Generator, per_entity
43
44
  from metaobjects.codegen.generators.m2m_codegen import (
44
45
  M2mDescriptor,
@@ -89,6 +90,24 @@ def _scalar_fields(entity: MetaObject) -> list[MetaField]:
89
90
  return [f for f in entity.fields() if f.sub_type != fc.FIELD_SUBTYPE_OBJECT]
90
91
 
91
92
 
93
+ def _required_field_names(entity: MetaObject) -> list[str]:
94
+ """Every @required field name — scalar AND object/jsonb (unlike
95
+ _scalar_fields, which drops object fields for the sort allowlist). FR-035
96
+ PATCH-2 guards present-null on any of these, and a @required jsonb column can
97
+ be nulled just like a scalar one."""
98
+ return [f.name for f in entity.fields() if is_required(f)]
99
+
100
+
101
+ def _py_set_literal(names: list[str], *, frozen: bool = False) -> str:
102
+ """A Python set/frozenset literal from field names, matching the generated
103
+ allowlist idiom (one quoted name per line). Empty → ``frozenset()`` / ``set()``."""
104
+ empty = "frozenset()" if frozen else "set()"
105
+ if not names:
106
+ return empty
107
+ body = "{\n" + "".join(f' "{name}",\n' for name in names) + "}"
108
+ return f"frozenset({body})" if frozen else body
109
+
110
+
92
111
  def _pk_py_type(entity: MetaObject) -> PyType:
93
112
  """The Python type of the entity's primary-key path/id parameter, derived
94
113
  from the PK field's declared subtype via ``type_map.py_type_for`` — the same
@@ -238,10 +257,14 @@ class RouterGenerator:
238
257
  repo_class: str,
239
258
  fields_const: str,
240
259
  ops_const: str,
260
+ model_name: str,
261
+ patch_model: str,
241
262
  ) -> list[str]:
242
263
  """One CRUD route handler block, dispatched by *name*
243
264
  (``list`` / ``get`` / ``create`` / ``update`` / ``delete``). Override to
244
- change a handler's body / decorators / response shape."""
265
+ change a handler's body / decorators / response shape. *model_name* /
266
+ *patch_model* are the generated create + all-optional PATCH Pydantic models
267
+ used to run field constraints on POST / PATCH (FR-036)."""
245
268
  if name == "list":
246
269
  return [
247
270
  '@router.get("")',
@@ -289,6 +312,12 @@ class RouterGenerator:
289
312
  " dto: dict[str, Any],",
290
313
  f" repo: Annotated[{repo_class}, Depends(get_repository)],",
291
314
  ") -> Any:",
315
+ " # FR-036: run the generated field constraints (length / pattern /",
316
+ " # numeric bounds) before persisting; a violation is the cross-port 400.",
317
+ " try:",
318
+ f" {model_name}(**dto)",
319
+ " except ValidationError:",
320
+ ' return JSONResponse(status_code=400, content={"error": "validation"})',
292
321
  " return repo.create(dto)",
293
322
  ]
294
323
  if name == "update":
@@ -300,6 +329,18 @@ class RouterGenerator:
300
329
  " dto: dict[str, Any],",
301
330
  f" repo: Annotated[{repo_class}, Depends(get_repository)],",
302
331
  ") -> Any:",
332
+ " # FR-035 PATCH-2: an explicit null on a @required field is a 400 —",
333
+ " # a present null on a NON-required field falls through and clears it,",
334
+ " # and an OMITTED required field is untouched (never a 400).",
335
+ " for _k in _REQUIRED_FIELDS:",
336
+ " if _k in dto and dto[_k] is None:",
337
+ ' return JSONResponse(status_code=400, content={"error": "validation"})',
338
+ " # FR-036: validate PRESENT, non-null values with the create rules (the",
339
+ " # all-optional patch model never fires required-checks on ABSENT fields).",
340
+ " try:",
341
+ f" {patch_model}(**dto)",
342
+ " except ValidationError:",
343
+ ' return JSONResponse(status_code=400, content={"error": "validation"})',
303
344
  f" saved = repo.update({pk_param}, dto)",
304
345
  " if saved is None:",
305
346
  ' return JSONResponse(status_code=404, content={"error": "not_found"})',
@@ -385,9 +426,18 @@ class RouterGenerator:
385
426
  if f.name not in seen:
386
427
  seen.add(f.name)
387
428
  sort_fields.append(f.name)
388
- sort_set_body = "set()" if not sort_fields else (
389
- "{\n" + "".join(f' "{name}",\n' for name in sort_fields) + "}"
390
- )
429
+ sort_set_body = _py_set_literal(sort_fields)
430
+ # FR-035 PATCH-2: @required fields across the base AND every subtype — an
431
+ # explicit null on any of these is a 400 (the per-subtype update handlers
432
+ # guard against it before the repo call). Union, stable order.
433
+ required_names: list[str] = _required_field_names(entity)
434
+ req_seen = set(required_names)
435
+ for st in plan.subtypes:
436
+ for name in _required_field_names(st.entity):
437
+ if name not in req_seen:
438
+ req_seen.add(name)
439
+ required_names.append(name)
440
+ required_set_body = _py_set_literal(required_names, frozen=True)
391
441
 
392
442
  h = generated_header(short_name, _effective_fqn(entity)).rstrip()
393
443
  parts: list[str] = []
@@ -406,7 +456,7 @@ class RouterGenerator:
406
456
  parts.append("")
407
457
  parts.append("from fastapi import APIRouter, Depends, Query, Request, status")
408
458
  parts.append("from fastapi.responses import JSONResponse")
409
- parts.append("from pydantic import BaseModel")
459
+ parts.append("from pydantic import BaseModel, ValidationError")
410
460
  parts.append("")
411
461
  parts.append("from metaobjects.codegen.runtime.filter_parser import (")
412
462
  parts.append(" FilterPredicate,")
@@ -414,6 +464,12 @@ class RouterGenerator:
414
464
  parts.append(")")
415
465
  parts.append("")
416
466
  parts.append(f"from .{allowlist_module} import {fields_const}, {ops_const}")
467
+ # FR-036: each concrete subtype's CREATE + all-optional PATCH validation models
468
+ # drive the field constraints run on the per-subtype POST / PATCH (sibling
469
+ # modules). These are the wire-shaped validation models, NOT the read model.
470
+ for st in plan.subtypes:
471
+ sub = st.entity.name
472
+ parts.append(f"from .{sub} import {sub}Create, {sub}Patch")
417
473
  parts.append("")
418
474
  parts.append(f'router = APIRouter(prefix="/api/{plural}", tags=["{plural}"])')
419
475
  parts.append("")
@@ -427,6 +483,9 @@ class RouterGenerator:
427
483
  parts.append(f"_SORT_ALLOWLIST: set[str] = {sort_set_body}")
428
484
  parts.append("")
429
485
  parts.append("")
486
+ parts.append(f"_REQUIRED_FIELDS: frozenset[str] = {required_set_body}")
487
+ parts.append("")
488
+ parts.append("")
430
489
  parts.append("def _parse_sort(raw: str) -> _SortClause | None:")
431
490
  parts.append(' """Parse `field:asc|desc`; return None for malformed / disallowed input."""')
432
491
  parts.append(' parts = raw.split(":", 1)')
@@ -480,6 +539,7 @@ class RouterGenerator:
480
539
  for st in plan.subtypes:
481
540
  seg = st.route_segment
482
541
  val = st.value
542
+ sub = st.entity.name # the subtype's generated create + PATCH model classes
483
543
  sfx = st.route_segment # handler-name suffix = URL segment (e.g. "bridge"), matches the route
484
544
  parts.extend(list_sig(f"list_{plural}_{sfx}", f"/{seg}"))
485
545
  parts.extend(self._emit_tph_list_body(f'"{val}"', fields_const, ops_const))
@@ -490,6 +550,11 @@ class RouterGenerator:
490
550
  parts.append(" dto: dict[str, Any],")
491
551
  parts.append(f" repo: Annotated[{repo_class}, Depends(get_repository)],")
492
552
  parts.append(") -> Any:")
553
+ parts.append(" # FR-036: run the subtype's field constraints before persisting.")
554
+ parts.append(" try:")
555
+ parts.append(f" {sub}Create(**dto)")
556
+ parts.append(" except ValidationError:")
557
+ parts.append(' return JSONResponse(status_code=400, content={"error": "validation"})')
493
558
  parts.append(f' return repo.create("{val}", dto)')
494
559
  parts.append("")
495
560
  parts.append("")
@@ -511,6 +576,15 @@ class RouterGenerator:
511
576
  parts.append(" dto: dict[str, Any],")
512
577
  parts.append(f" repo: Annotated[{repo_class}, Depends(get_repository)],")
513
578
  parts.append(") -> Any:")
579
+ parts.append(" # FR-035 PATCH-2: an explicit null on a @required field is a 400.")
580
+ parts.append(" for _k in _REQUIRED_FIELDS:")
581
+ parts.append(" if _k in dto and dto[_k] is None:")
582
+ parts.append(' return JSONResponse(status_code=400, content={"error": "validation"})')
583
+ parts.append(" # FR-036: validate PRESENT, non-null values with the subtype's create rules.")
584
+ parts.append(" try:")
585
+ parts.append(f" {sub}Patch(**dto)")
586
+ parts.append(" except ValidationError:")
587
+ parts.append(' return JSONResponse(status_code=400, content={"error": "validation"})')
514
588
  parts.append(f' saved = repo.update("{val}", {pk_param}, dto)')
515
589
  parts.append(" if saved is None:")
516
590
  parts.append(' return JSONResponse(status_code=404, content={"error": "not_found"})')
@@ -600,9 +674,10 @@ class RouterGenerator:
600
674
  ops_const = f"{upper}_FILTER_OPS_BY_FIELD"
601
675
  allowlist_module = f"{snake}_filter_allowlist"
602
676
 
603
- sort_set_body = "set()" if not sort_fields else (
604
- "{\n" + "".join(f' "{name}",\n' for name in sort_fields) + "}"
605
- )
677
+ sort_set_body = _py_set_literal(sort_fields)
678
+ # FR-035 PATCH-2: an explicit null on a @required field (scalar or jsonb)
679
+ # is a 400 — the update handler guards these before the repo call.
680
+ required_set_body = _py_set_literal(_required_field_names(entity), frozen=True)
606
681
 
607
682
  parts: list[str] = []
608
683
  parts.append(
@@ -619,7 +694,7 @@ class RouterGenerator:
619
694
  parts.append("")
620
695
  parts.append("from fastapi import APIRouter, Depends, Query, Request, status")
621
696
  parts.append("from fastapi.responses import JSONResponse")
622
- parts.append("from pydantic import BaseModel")
697
+ parts.append("from pydantic import BaseModel, ValidationError")
623
698
  parts.append("")
624
699
  parts.append("from metaobjects.codegen.runtime.filter_parser import (")
625
700
  parts.append(" FilterPredicate,")
@@ -627,6 +702,11 @@ class RouterGenerator:
627
702
  parts.append(")")
628
703
  parts.append("")
629
704
  parts.append(f"from .{allowlist_module} import {fields_const}, {ops_const}")
705
+ # FR-036: the generated CREATE + all-optional PATCH validation models drive the
706
+ # field constraints run on POST / PATCH. These are the WIRE-shaped validation
707
+ # models (auto-gen PK / @readOnly omitted from create; PK omitted from patch;
708
+ # both keyed by field.name), NOT the read/entity-shape model.
709
+ parts.append(f"from .{short_name} import {short_name}Create, {short_name}Patch")
630
710
  parts.append("")
631
711
  parts.append(f'router = APIRouter(prefix="/api/{plural}", tags=["{plural}"])')
632
712
  parts.append("")
@@ -642,6 +722,9 @@ class RouterGenerator:
642
722
  parts.append(f"_SORT_ALLOWLIST: set[str] = {sort_set_body}")
643
723
  parts.append("")
644
724
  parts.append("")
725
+ parts.append(f"_REQUIRED_FIELDS: frozenset[str] = {required_set_body}")
726
+ parts.append("")
727
+ parts.append("")
645
728
  parts.append('def _parse_sort(raw: str) -> _SortClause | None:')
646
729
  parts.append(' """Parse `field:asc|desc`; return None for malformed / disallowed input."""')
647
730
  parts.append(' parts = raw.split(":", 1)')
@@ -672,6 +755,8 @@ class RouterGenerator:
672
755
  repo_class=repo_class,
673
756
  fields_const=fields_const,
674
757
  ops_const=ops_const,
758
+ model_name=f"{short_name}Create",
759
+ patch_model=f"{short_name}Patch",
675
760
  )
676
761
  for i, hname in enumerate(("list", "get", "create", "update", "delete")):
677
762
  if i > 0:
@@ -92,6 +92,12 @@ def _extract(
92
92
  data[f.name] = coerced
93
93
  report.add_coercion(Coercion(path, "", f.default_value, "default"))
94
94
  report.set(path, FieldExtraction.DEFAULTED)
95
+ # A default SATISFIES @required, so this field never appears in
96
+ # lost_required() — which is what the generated guards key on. Record
97
+ # it separately so "the document did not answer a required field"
98
+ # stays askable.
99
+ if f.required:
100
+ report.mark_defaulted_required(path)
95
101
  continue
96
102
  report.set(
97
103
  path,