metaobjects 1.0.4__tar.gz → 1.0.5rc3__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 (462) hide show
  1. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/PKG-INFO +1 -1
  2. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/pyproject.toml +1 -1
  3. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/apidocs/naming.py +49 -16
  4. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/generators/m2m_codegen.py +25 -9
  5. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/generators/router_generator.py +213 -9
  6. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/library/embedded_library.py +1 -1
  7. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/requirement/requirement_constants.py +4 -2
  8. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/spec_metamodel/requirement.json +2 -2
  9. metaobjects-1.0.5rc3/tests/codegen/test_codegen_compile_conformance.py +156 -0
  10. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_m2m_codegen.py +20 -16
  11. metaobjects-1.0.5rc3/tests/codegen/test_route_path_naming.py +67 -0
  12. metaobjects-1.0.5rc3/tests/codegen/test_router_generator_m2m_target_tph.py +156 -0
  13. metaobjects-1.0.5rc3/tests/codegen/test_router_generator_tph_m2m.py +378 -0
  14. metaobjects-1.0.5rc3/tests/codegen/test_router_generator_tph_m2m_shadowed.py +205 -0
  15. metaobjects-1.0.5rc3/tests/integration/api_contract_m2m_server.py +424 -0
  16. metaobjects-1.0.5rc3/tests/integration/generated_m2m_app.py +562 -0
  17. metaobjects-1.0.5rc3/tests/integration/test_m2m_target_tph_generated.py +291 -0
  18. metaobjects-1.0.5rc3/tests/integration/test_tph_m2m_generated.py +420 -0
  19. metaobjects-1.0.5rc3/tests/integration/test_tph_m2m_shadowed_generated.py +358 -0
  20. metaobjects-1.0.4/tests/integration/api_contract_m2m_server.py +0 -220
  21. metaobjects-1.0.4/tests/integration/generated_m2m_app.py +0 -220
  22. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/.gitignore +0 -0
  23. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/LICENSE +0 -0
  24. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/README.md +0 -0
  25. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/hatch_build.py +0 -0
  26. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/scripts/generate_embedded_library.py +0 -0
  27. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/__init__.py +0 -0
  28. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/agent_context/__init__.py +0 -0
  29. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/agent_context/scaffold.py +0 -0
  30. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/apidocs/__init__.py +0 -0
  31. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/apidocs/api_model.py +0 -0
  32. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/apidocs/builder.py +0 -0
  33. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/apidocs/paths.py +0 -0
  34. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/apidocs/renderer.py +0 -0
  35. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/attr_class_map.py +0 -0
  36. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/cli.py +0 -0
  37. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/KNOWN_GAPS.md +0 -0
  38. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/__init__.py +0 -0
  39. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/collision_names.py +0 -0
  40. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/config.py +0 -0
  41. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/constants.py +0 -0
  42. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/extract_delegate_emitter.py +0 -0
  43. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/extract_schema_emitter.py +0 -0
  44. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/format.py +0 -0
  45. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/fr010_field_mapping.py +0 -0
  46. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/generator.py +0 -0
  47. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/generator_registry.py +0 -0
  48. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/generators/__init__.py +0 -0
  49. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/generators/entity_model.py +0 -0
  50. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/generators/extractor_generator.py +0 -0
  51. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/generators/filter_allowlist_generator.py +0 -0
  52. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/generators/find_inbound.py +0 -0
  53. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/generators/fr019_shared_enum.py +0 -0
  54. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/generators/names_generator.py +0 -0
  55. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/generators/output_parser_generator.py +0 -0
  56. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/generators/output_prompt_generator.py +0 -0
  57. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/generators/payload_vo_generator.py +0 -0
  58. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/generators/render_helper_generator.py +0 -0
  59. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/generators/template_generator.py +0 -0
  60. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/generators/tph_plan.py +0 -0
  61. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/generators/trace_helper_generator.py +0 -0
  62. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/instance_artifacts.py +0 -0
  63. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/metaobjects-config.schema.json +0 -0
  64. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/output_format_spec_emitter.py +0 -0
  65. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/overwrite_policy.py +0 -0
  66. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/project_config.py +0 -0
  67. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/runner.py +0 -0
  68. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/runtime/__init__.py +0 -0
  69. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/runtime/filter_parser.py +0 -0
  70. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/template_codegen/__init__.py +0 -0
  71. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/template_codegen/output_pattern.py +0 -0
  72. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/template_codegen/template_data.py +0 -0
  73. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/template_codegen/template_spec.py +0 -0
  74. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/type_map.py +0 -0
  75. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/config/__init__.py +0 -0
  76. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/config/dependencies.py +0 -0
  77. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/config/neutral_config.py +0 -0
  78. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/config/source_resolver.py +0 -0
  79. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/core_types.py +0 -0
  80. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/datatype.py +0 -0
  81. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/documentation/__init__.py +0 -0
  82. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/documentation/doc_constants.py +0 -0
  83. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/documentation/doc_provider.py +0 -0
  84. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/documentation/doc_schema.py +0 -0
  85. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/errors.py +0 -0
  86. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/library/__init__.py +0 -0
  87. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/library/library_sources.py +0 -0
  88. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/loader/__init__.py +0 -0
  89. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/loader/merge.py +0 -0
  90. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/loader/meta_data_loader.py +0 -0
  91. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/loader/registered_validation.py +0 -0
  92. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/loader/sources/__init__.py +0 -0
  93. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/loader/sources/directory_source.py +0 -0
  94. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/loader/sources/file_source.py +0 -0
  95. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/loader/sources/meta_data_source.py +0 -0
  96. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/loader/sources/uri_source.py +0 -0
  97. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/loader/validate_discriminator.py +0 -0
  98. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/loader/validate_enum_normalize_ambiguity.py +0 -0
  99. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/loader/validate_field_mutability.py +0 -0
  100. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/loader/validate_source_escapes.py +0 -0
  101. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/loader/validate_source_parameter_ref.py +0 -0
  102. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/loader/validate_source_physical_names.py +0 -0
  103. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/loader/validation_passes.py +0 -0
  104. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/__init__.py +0 -0
  105. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/__init__.py +0 -0
  106. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/attr/__init__.py +0 -0
  107. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/attr/attr_constants.py +0 -0
  108. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/attr/meta_attr.py +0 -0
  109. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/field/__init__.py +0 -0
  110. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/field/field_constants.py +0 -0
  111. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/field/meta_field.py +0 -0
  112. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/identity/__init__.py +0 -0
  113. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/identity/identity_constants.py +0 -0
  114. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/identity/meta_identity.py +0 -0
  115. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/index/__init__.py +0 -0
  116. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/index/index_constants.py +0 -0
  117. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/index/meta_index.py +0 -0
  118. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/object/__init__.py +0 -0
  119. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/object/meta_object.py +0 -0
  120. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/object/meta_object_aware.py +0 -0
  121. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/object/object_class_registry.py +0 -0
  122. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/object/object_constants.py +0 -0
  123. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/object/object_extract.py +0 -0
  124. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/object/value_object.py +0 -0
  125. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/relationship/__init__.py +0 -0
  126. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/relationship/derive_m2m_fields.py +0 -0
  127. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/relationship/meta_relationship.py +0 -0
  128. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/relationship/relationship_constants.py +0 -0
  129. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/relationship/relationship_references.py +0 -0
  130. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/requirement/__init__.py +0 -0
  131. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/requirement/meta_requirement.py +0 -0
  132. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/validator/__init__.py +0 -0
  133. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/validator/validator_constants.py +0 -0
  134. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/meta_data.py +0 -0
  135. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/meta_root.py +0 -0
  136. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/persistence/__init__.py +0 -0
  137. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/persistence/db/__init__.py +0 -0
  138. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/persistence/db/db_constants.py +0 -0
  139. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/persistence/db/db_provider.py +0 -0
  140. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/persistence/origin/__init__.py +0 -0
  141. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/persistence/origin/meta_origin.py +0 -0
  142. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/persistence/origin/origin_constants.py +0 -0
  143. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/persistence/source/__init__.py +0 -0
  144. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/persistence/source/meta_source.py +0 -0
  145. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/persistence/source/source_constants.py +0 -0
  146. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/presentation/__init__.py +0 -0
  147. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/presentation/layout/__init__.py +0 -0
  148. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/presentation/layout/layout_constants.py +0 -0
  149. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/presentation/layout/meta_layout.py +0 -0
  150. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/presentation/ui/__init__.py +0 -0
  151. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/presentation/ui/ui_provider.py +0 -0
  152. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/presentation/view/__init__.py +0 -0
  153. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/presentation/view/meta_view.py +0 -0
  154. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/presentation/view/view_constants.py +0 -0
  155. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/provider_extends.py +0 -0
  156. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/template/__init__.py +0 -0
  157. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/template/meta_template.py +0 -0
  158. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/template/prompt_provider.py +0 -0
  159. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/template/template_constants.py +0 -0
  160. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/naming.py +0 -0
  161. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/naming_refs.py +0 -0
  162. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/parser.py +0 -0
  163. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/parser_yaml.py +0 -0
  164. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/provider.py +0 -0
  165. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/py.typed +0 -0
  166. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/registry.py +0 -0
  167. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/registry_manifest.py +0 -0
  168. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/render/__init__.py +0 -0
  169. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/render/email_document.py +0 -0
  170. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/render/escapers.py +0 -0
  171. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/render/extract/KNOWN_GAPS.md +0 -0
  172. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/render/extract/__init__.py +0 -0
  173. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/render/extract/coerce.py +0 -0
  174. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/render/extract/extract.py +0 -0
  175. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/render/extract/extract_map.py +0 -0
  176. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/render/extract/json_forgiving_reader.py +0 -0
  177. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/render/extract/locate.py +0 -0
  178. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/render/extract/normalize.py +0 -0
  179. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/render/extract/strip.py +0 -0
  180. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/render/extract/types.py +0 -0
  181. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/render/extract/xml_forgiving_reader.py +0 -0
  182. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/render/filesystem_provider.py +0 -0
  183. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/render/payload_accessors.py +0 -0
  184. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/render/prompt/__init__.py +0 -0
  185. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/render/prompt/output_format_renderer.py +0 -0
  186. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/render/prompt/output_format_spec.py +0 -0
  187. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/render/prompt/prompt_field.py +0 -0
  188. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/render/prompt/prompt_overrides.py +0 -0
  189. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/render/prompt/prompt_style.py +0 -0
  190. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/render/renderer.py +0 -0
  191. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/render/verify.py +0 -0
  192. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/runtime/__init__.py +0 -0
  193. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/runtime/llm_recorder.py +0 -0
  194. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/runtime/n2m_resolver.py +0 -0
  195. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/runtime/object_manager.py +0 -0
  196. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/runtime/tph.py +0 -0
  197. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/scope.py +0 -0
  198. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/serializer_json.py +0 -0
  199. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/shared/__init__.py +0 -0
  200. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/shared/base_types.py +0 -0
  201. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/shared/separators.py +0 -0
  202. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/shared/structural.py +0 -0
  203. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/source/__init__.py +0 -0
  204. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/source/error_source.py +0 -0
  205. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/source/json_path.py +0 -0
  206. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/source/semantic_diff.py +0 -0
  207. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/source/yaml_positions.py +0 -0
  208. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/source_resolution.py +0 -0
  209. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/spec_metamodel/__init__.py +0 -0
  210. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/spec_metamodel/attr.json +0 -0
  211. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/spec_metamodel/db.json +0 -0
  212. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/spec_metamodel/documentation.json +0 -0
  213. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/spec_metamodel/field.json +0 -0
  214. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/spec_metamodel/identity.json +0 -0
  215. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/spec_metamodel/index.json +0 -0
  216. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/spec_metamodel/layout.json +0 -0
  217. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/spec_metamodel/object.json +0 -0
  218. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/spec_metamodel/origin.json +0 -0
  219. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/spec_metamodel/prompt.json +0 -0
  220. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/spec_metamodel/relationship.json +0 -0
  221. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/spec_metamodel/source.json +0 -0
  222. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/spec_metamodel/template.json +0 -0
  223. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/spec_metamodel/ui-web.json +0 -0
  224. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/spec_metamodel/ui.json +0 -0
  225. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/spec_metamodel/validator.json +0 -0
  226. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/spec_metamodel/view.json +0 -0
  227. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/super_resolve.py +0 -0
  228. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/validation_types.py +0 -0
  229. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/yaml_desugar.py +0 -0
  230. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/__init__.py +0 -0
  231. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/__init__.py +0 -0
  232. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/gen_suite.py +0 -0
  233. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/golden/extends/expected/BaseEntity.py +0 -0
  234. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/golden/extends/expected/Program.py +0 -0
  235. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/golden/extends/meta.json +0 -0
  236. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/golden/nested-array/expected/AuthorBrief.py +0 -0
  237. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/golden/nested-array/expected/PostBrief.py +0 -0
  238. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/golden/nested-array/meta.json +0 -0
  239. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/golden/scalars/expected/Metric.py +0 -0
  240. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/golden/scalars/expected/Report.py +0 -0
  241. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/golden/scalars/meta.json +0 -0
  242. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/golden/vanilla/expected/Subscriber.py +0 -0
  243. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/golden/vanilla/meta.json +0 -0
  244. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_abstract_conformance.py +0 -0
  245. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_api_docs_builder.py +0 -0
  246. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_api_docs_paths.py +0 -0
  247. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_cli.py +0 -0
  248. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_cli_config_gen.py +0 -0
  249. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_cli_config_verify.py +0 -0
  250. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_cli_dependencies.py +0 -0
  251. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_cli_providers.py +0 -0
  252. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_cli_registry.py +0 -0
  253. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_cli_staleness_nudge.py +0 -0
  254. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_cli_verify_strict.py +0 -0
  255. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_cli_verify_subverbs.py +0 -0
  256. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_constants_config.py +0 -0
  257. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_entity_model.py +0 -0
  258. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_enum_conformance.py +0 -0
  259. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_extends_abstract_field_inheritance.py +0 -0
  260. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_extract_tier_collision.py +0 -0
  261. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_extractor_generator.py +0 -0
  262. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_filter_allowlist_generator.py +0 -0
  263. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_format.py +0 -0
  264. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_fr010_nested_object_prompt.py +0 -0
  265. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_fr010_output_codegen.py +0 -0
  266. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_fr019_shared_provided_conformance.py +0 -0
  267. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_gen_state_keying.py +0 -0
  268. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_generator.py +0 -0
  269. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_generator_extension_seams.py +0 -0
  270. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_golden.py +0 -0
  271. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_inherit_without_restate_gate.py +0 -0
  272. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_inheritance_conformance.py +0 -0
  273. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_instance_artifacts.py +0 -0
  274. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_names_extends_chain.py +0 -0
  275. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_names_generator.py +0 -0
  276. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_no_magic_physical_names.py +0 -0
  277. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_origin_nullability.py +0 -0
  278. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_output_parser_generator.py +0 -0
  279. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_overwrite_policy.py +0 -0
  280. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_overwrite_policy_hash.py +0 -0
  281. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_payload_vo_generator.py +0 -0
  282. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_project_config.py +0 -0
  283. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_projection_compile.py +0 -0
  284. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_render_helper_conformance.py +0 -0
  285. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_render_helper_generator.py +0 -0
  286. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_reverse_finders.py +0 -0
  287. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_router_autoset.py +0 -0
  288. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_router_generator.py +0 -0
  289. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_runner.py +0 -0
  290. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_source_resolution.py +0 -0
  291. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_sourceless_projection.py +0 -0
  292. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_template_data.py +0 -0
  293. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_template_generator.py +0 -0
  294. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_template_output_pattern.py +0 -0
  295. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_template_scope_helpers.py +0 -0
  296. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_template_scope_walk.py +0 -0
  297. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_template_spec.py +0 -0
  298. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_tph_codegen.py +0 -0
  299. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_trace_helper_generator.py +0 -0
  300. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_type_map.py +0 -0
  301. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_validation_conformance.py +0 -0
  302. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_write_through.py +0 -0
  303. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/config/__init__.py +0 -0
  304. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/config/test_dependencies.py +0 -0
  305. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/config/test_library_package_guard.py +0 -0
  306. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/config/test_neutral_config.py +0 -0
  307. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/config/test_source_resolver.py +0 -0
  308. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/conformance/__init__.py +0 -0
  309. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/conformance/capabilities.py +0 -0
  310. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/conformance/conformance-expected-failures.json +0 -0
  311. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/conformance/conformance_adapter.py +0 -0
  312. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/conformance/corpus.py +0 -0
  313. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/conformance/expected_failures.py +0 -0
  314. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/conformance/fixture_discovery.py +0 -0
  315. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/conformance/navigator.py +0 -0
  316. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/conformance/test_api_docs_cross_port_conformance.py +0 -0
  317. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/conformance/test_conformance.py +0 -0
  318. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/conformance/test_dependency_conformance.py +0 -0
  319. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/conformance/test_extract_conformance.py +0 -0
  320. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/conformance/test_extract_object_verdict.py +0 -0
  321. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/conformance/test_fr010_loader_attrs.py +0 -0
  322. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/conformance/test_fr011_attrs.py +0 -0
  323. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/conformance/test_generator_registry_conformance.py +0 -0
  324. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/conformance/test_object_model_conformance.py +0 -0
  325. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/conformance/test_provider_composition_conformance.py +0 -0
  326. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/conformance/test_registry_conformance.py +0 -0
  327. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/conformance/test_runner_hardfail.py +0 -0
  328. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/conformance/test_scope_conformance.py +0 -0
  329. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/conformance/test_source_resolution_conformance.py +0 -0
  330. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/conformance/test_spec_metamodel_embed.py +0 -0
  331. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/conformance/test_strict_attr_load.py +0 -0
  332. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/conformance/test_template_codegen_conformance.py +0 -0
  333. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/conformance/test_template_generator_conformance.py +0 -0
  334. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/conformance/test_yaml_conformance.py +0 -0
  335. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/conformance/yaml-conformance-expected-failures.json +0 -0
  336. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/integration/__init__.py +0 -0
  337. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/integration/api_contract_assertions.py +0 -0
  338. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/integration/api_contract_jsonb_server.py +0 -0
  339. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/integration/api_contract_server.py +0 -0
  340. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/integration/generated_jsonb_app.py +0 -0
  341. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/integration/generated_router_app.py +0 -0
  342. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/integration/generated_tph_app.py +0 -0
  343. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/integration/meta_ai_trace.yaml +0 -0
  344. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/integration/normalization.py +0 -0
  345. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/integration/postgres_container.py +0 -0
  346. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/integration/query_runner.py +0 -0
  347. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/integration/scenarios.py +0 -0
  348. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/integration/test_api_contract.py +0 -0
  349. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/integration/test_api_contract_generated.py +0 -0
  350. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/integration/test_api_contract_jsonb.py +0 -0
  351. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/integration/test_api_contract_m2m.py +0 -0
  352. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/integration/test_api_contract_m2m_generated.py +0 -0
  353. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/integration/test_api_contract_tph_generated.py +0 -0
  354. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/integration/test_generated_router_uuid_pk.py +0 -0
  355. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/integration/test_llm_call_trace.py +0 -0
  356. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/integration/test_normalization.py +0 -0
  357. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/integration/test_query_scenarios.py +0 -0
  358. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/integration/test_runtime_return_types.py +0 -0
  359. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/loader/__init__.py +0 -0
  360. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/loader/test_abstract_subtype_authored.py +0 -0
  361. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/loader/test_enum_extends_values_conflict.py +0 -0
  362. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/loader/test_field_mutability_order.py +0 -0
  363. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/loader/test_shipped_library_ai.py +0 -0
  364. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/loader/test_template_payload_target_210.py +0 -0
  365. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/open_closed_proof_test.py +0 -0
  366. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/render/__init__.py +0 -0
  367. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/render/extract/__init__.py +0 -0
  368. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/render/extract/test_coerce.py +0 -0
  369. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/render/extract/test_extract.py +0 -0
  370. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/render/extract/test_extract_map.py +0 -0
  371. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/render/extract/test_json_forgiving_reader.py +0 -0
  372. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/render/extract/test_locate.py +0 -0
  373. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/render/extract/test_model.py +0 -0
  374. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/render/extract/test_normalize.py +0 -0
  375. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/render/extract/test_strip.py +0 -0
  376. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/render/extract/test_xml_forgiving_reader.py +0 -0
  377. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/render/prompt/__init__.py +0 -0
  378. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/render/prompt/test_output_format_renderer.py +0 -0
  379. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/render/test_email_document.py +0 -0
  380. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/render/test_filesystem_provider.py +0 -0
  381. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/render/test_output_format_renderer_nested.py +0 -0
  382. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/render/test_output_prompt_conformance.py +0 -0
  383. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/render/test_render_conformance.py +0 -0
  384. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/render/test_render_max_chars.py +0 -0
  385. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/render/test_verify.py +0 -0
  386. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/render/test_verify_conformance.py +0 -0
  387. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/runtime/test_object_manager_enum_intvaluemap.py +0 -0
  388. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/runtime/test_object_manager_table_name.py +0 -0
  389. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/source/__init__.py +0 -0
  390. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/source/test_fr5c_merge_attribution.py +0 -0
  391. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/source/test_fr5d_reference_resolution.py +0 -0
  392. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/source/test_fr5e_database_source_shape.py +0 -0
  393. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/source/test_json_path.py +0 -0
  394. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/source/test_semantic_diff.py +0 -0
  395. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/source/test_source_on_node.py +0 -0
  396. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/source/test_yaml_positions.py +0 -0
  397. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/test_api_docs_accuracy.py +0 -0
  398. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/__init__.py +0 -0
  399. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_agent_context_staleness.py +0 -0
  400. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_capabilities.py +0 -0
  401. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_column_naming_strategy.py +0 -0
  402. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_common_attrs.py +0 -0
  403. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_core_types.py +0 -0
  404. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_derive_m2m_declaring_entity.py +0 -0
  405. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_effective_children_sibling_shadowing.py +0 -0
  406. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_effective_package.py +0 -0
  407. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_errors.py +0 -0
  408. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_field_enum.py +0 -0
  409. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_field_enum_intvaluemap.py +0 -0
  410. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_field_map_validation.py +0 -0
  411. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_field_uuid_dbcolumntype.py +0 -0
  412. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_file_source_id.py +0 -0
  413. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_fr016_source_name_and_kind_aliases.py +0 -0
  414. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_index_lookup.py +0 -0
  415. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_llm_recorder.py +0 -0
  416. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_loader.py +0 -0
  417. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_loader_bom.py +0 -0
  418. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_loader_class.py +0 -0
  419. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_merge.py +0 -0
  420. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_meta_attr.py +0 -0
  421. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_meta_data.py +0 -0
  422. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_meta_source.py +0 -0
  423. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_module_shortcuts.py +0 -0
  424. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_n2m_resolver.py +0 -0
  425. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_n2m_resolver_inherited.py +0 -0
  426. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_no_dead_config_fields.py +0 -0
  427. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_object_manager_autoset_stamping.py +0 -0
  428. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_object_manager_uuid_coercion.py +0 -0
  429. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_object_manager_write_through.py +0 -0
  430. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_one_primary_source.py +0 -0
  431. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_parser.py +0 -0
  432. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_parser_yaml.py +0 -0
  433. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_provider.py +0 -0
  434. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_provider_extension.py +0 -0
  435. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_registry.py +0 -0
  436. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_registry_completeness.py +0 -0
  437. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_registry_extend.py +0 -0
  438. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_registry_sealed.py +0 -0
  439. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_relationship_m2m_validation.py +0 -0
  440. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_relationship_references.py +0 -0
  441. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_relationship_referential_actions.py +0 -0
  442. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_resolution_key.py +0 -0
  443. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_runtime_resolution_key_binding.py +0 -0
  444. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_serializer.py +0 -0
  445. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_serializer_shared_document.py +0 -0
  446. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_shared_constants.py +0 -0
  447. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_smoke.py +0 -0
  448. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_source_escapes.py +0 -0
  449. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_sources.py +0 -0
  450. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_strict_child_placement.py +0 -0
  451. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_super_resolve.py +0 -0
  452. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_template_toolcall.py +0 -0
  453. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_template_wrong_subtype_attrs.py +0 -0
  454. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_validation_attr_schema.py +0 -0
  455. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_validation_filter_values.py +0 -0
  456. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_validation_origin_195.py +0 -0
  457. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_validation_origin_paths.py +0 -0
  458. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_validation_sort_field.py +0 -0
  459. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_validation_warnings.py +0 -0
  460. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_write_coercion_arrays.py +0 -0
  461. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_yaml_desugar.py +0 -0
  462. {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: metaobjects
3
- Version: 1.0.4
3
+ Version: 1.0.5rc3
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 = "1.0.4"
3
+ version = "1.0.5rc3"
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"
@@ -7,8 +7,10 @@ generators delegate their name computation here, and the
7
7
  calling the SAME functions. So the documented name can never drift from the
8
8
  emitted name — they are computed once, in one place.
9
9
 
10
- Behaviour-preserving: each function reproduces exactly what the corresponding
11
- generator used to compute inline (``_snake_case`` / ``_plural_lowercase`` /
10
+ Behaviour-preserving with ONE deliberate exception: :func:`route_path` no longer
11
+ reproduces the old inline ``name.lower() + "s"``, because that spelling diverged
12
+ from the other ports (see its docstring). Every other function reproduces exactly
13
+ what the corresponding generator used to compute inline (``_snake_case`` /
12
14
  ``<Name>Repository`` / ``<ENTITY>_FILTER_FIELDS`` / ``parse_<snake>`` /
13
15
  ``render_<snake>`` / ``render_<snake>_format`` / ``extract_<snake>`` …). The
14
16
  payload-class / payload-module names already lived in
@@ -23,10 +25,11 @@ from metaobjects.codegen.generators.payload_vo_generator import (
23
25
  response_class_name,
24
26
  response_module_name,
25
27
  )
28
+ from metaobjects.naming import to_snake_case
26
29
 
27
30
  __all__ = [
28
31
  "snake_case",
29
- "plural_lowercase",
32
+ "pluralize",
30
33
  "model_class_name",
31
34
  "router_module_name",
32
35
  "repository_class_name",
@@ -64,9 +67,35 @@ def snake_case(name: str) -> str:
64
67
  return "".join(out)
65
68
 
66
69
 
67
- def plural_lowercase(name: str) -> str:
68
- """``Author`` → ``authors``. Cross-port-aligned trivial pluralization."""
69
- return name.lower() + "s"
70
+ def _route_snake_case(name: str) -> str:
71
+ """``PostCategory`` → ``post_category``; ``HTTPServer`` → ``http_server``.
72
+
73
+ ACRONYM-AWARE, and deliberately NOT :func:`snake_case`. The trivial variant
74
+ above inserts a separator before every capital, which is right for a module
75
+ name but would emit ``h_t_t_p_server`` in a URL. Delegates to
76
+ :func:`metaobjects.naming.to_snake_case` — the port's acronym-aware loop,
77
+ documented there as byte-for-byte the other ports' rule (separate when the
78
+ previous character is lower or a digit, or when it is upper and the NEXT one
79
+ is lower, so a run of capitals stays together until the final one that
80
+ begins a word)."""
81
+ return to_snake_case(name)
82
+
83
+
84
+ def pluralize(name: str) -> str:
85
+ """``post_category`` → ``post_categories``; ``address`` → ``addresses``.
86
+
87
+ The cross-port pluralization contract, byte-identical in every port: a word
88
+ ending ``s``/``x``/``z``/``ch``/``sh`` takes ``es``; a consonant followed by
89
+ ``y`` becomes ``ies``; anything else takes ``s``. The cross-port
90
+ byte-identity covers the already-lowercased word :func:`route_path` feeds
91
+ in — suffix tests here run on a lowercased copy, unlike the JVM port's
92
+ case-sensitive ones, so mixed-case input may diverge."""
93
+ lowered = name.lower()
94
+ if lowered.endswith(("s", "x", "z", "ch", "sh")):
95
+ return name + "es"
96
+ if len(name) >= 2 and lowered[-1] == "y" and lowered[-2] not in "aeiou":
97
+ return name[:-1] + "ies"
98
+ return name + "s"
70
99
 
71
100
 
72
101
  def model_class_name(obj_name: str) -> str:
@@ -100,9 +129,14 @@ def filter_ops_const(obj_name: str) -> str:
100
129
 
101
130
 
102
131
  def route_path(obj_name: str) -> str:
103
- """The default REST base segment — the entity name pluralized + lowercased
104
- (cross-port grammar; e.g. ``Author`` → ``authors``)."""
105
- return plural_lowercase(obj_name)
132
+ """The default REST collection segment — the ENTITY NAME ``snake_case``d and
133
+ then pluralized (``Author`` → ``authors``, ``PostCategory`` →
134
+ ``post_categories``). Never the physical ``@table``.
135
+
136
+ One rule, identical in all five ports. It used to be ``name.lower() + "s"``
137
+ here, which served ``PostCategory`` at ``/postcategorys``; see
138
+ ``fixtures/api-contract-conformance/m2m/`` for the scenario that gates it."""
139
+ return pluralize(_route_snake_case(obj_name))
106
140
 
107
141
 
108
142
  def pk_param(obj_name: str) -> str:
@@ -168,15 +202,14 @@ def extractor_fn(template_name: str) -> str:
168
202
  def _pluralize(name: str) -> str:
169
203
  """Trivial cross-port pluralization (matches the TS ``pluralize`` /
170
204
  ``MetaSource._pluralize``), applied to a PascalCase entity name BEFORE
171
- snake-casing (``GameSession`` → ``GameSessions``)."""
205
+ snake-casing (``GameSession`` → ``GameSessions``).
206
+
207
+ Delegates to :func:`pluralize` so this file states the cross-port inflector
208
+ contract once, not twice; the empty-string guard stays here because it is
209
+ this finder-naming helper's own contract, not ``pluralize``'s."""
172
210
  if not name:
173
211
  return name
174
- lower = name.lower()
175
- if lower.endswith(("s", "x", "z", "ch", "sh")):
176
- return name + "es"
177
- if len(name) >= 2 and lower[-1] == "y" and lower[-2] not in "aeiou":
178
- return name[:-1] + "ies"
179
- return name + "s"
212
+ return pluralize(name)
180
213
 
181
214
 
182
215
  def reverse_finder_fk_segment(fk_field_name: str) -> str:
@@ -25,8 +25,8 @@ from __future__ import annotations
25
25
 
26
26
  from dataclasses import dataclass
27
27
 
28
+ from metaobjects.apidocs.naming import route_path
28
29
  from metaobjects.source_resolution import resolve_table_name
29
- from metaobjects.meta.core.field import field_constants as fc
30
30
  from metaobjects.naming import (
31
31
  DEFAULT_COLUMN_NAMING,
32
32
  apply_column_naming_strategy,
@@ -39,6 +39,8 @@ from metaobjects.meta.core.identity.identity_constants import (
39
39
  IDENTITY_SUBTYPE_PRIMARY,
40
40
  )
41
41
  from metaobjects.meta.core.object.meta_object import MetaObject
42
+ from metaobjects.meta.core.object.object_constants import OBJECT_ATTR_DISCRIMINATOR_VALUE
43
+ from metaobjects.codegen.generators.tph_plan import is_tph_subtype
42
44
  from metaobjects.meta.core.relationship.derive_m2m_fields import (
43
45
  M2MDerivationError,
44
46
  derive_m2m_fields,
@@ -63,6 +65,16 @@ class M2mDescriptor:
63
65
  target_column: str
64
66
  target_pk_column: str
65
67
  symmetric: bool
68
+ #: The target's OWN ``@discriminatorValue`` when ``@objectRef`` resolves to a
69
+ #: concrete TPH subtype (FW-8 follow-up; mirrors the TS reference's
70
+ #: ``targetDiscriminator``) — ``None`` for the overwhelming common case (a
71
+ #: vanilla, non-TPH target). The target's rows physically live in its
72
+ #: discriminator base's shared table, so a route generator threads this value
73
+ #: into the ``find_related_<relation>`` seam as an extra ``target_subtype``
74
+ #: parameter, structurally handing the consumer's join what it needs to
75
+ #: filter to just that subtype rather than every row of the shared table.
76
+ #: Defaulted so existing positional construction (if any) stays valid.
77
+ target_discriminator: str | None = None
66
78
 
67
79
 
68
80
  def _strip_package(name: str) -> str:
@@ -70,12 +82,6 @@ def _strip_package(name: str) -> str:
70
82
  return name[idx + len(PACKAGE_SEP):] if idx >= 0 else name
71
83
 
72
84
 
73
- def plural_lowercase(name: str) -> str:
74
- """``Person`` → ``persons``. Cross-port-aligned trivial pluralization — the
75
- SOURCE URL segment is the entity name pluralized, NOT the physical table."""
76
- return name.lower() + "s"
77
-
78
-
79
85
  def _physical_table(entity: MetaObject, *, relation_context: str) -> str:
80
86
  """The entity's physical SQL table name, via the shared
81
87
  :func:`~metaobjects.source_resolution.resolve_table_name` resolver.
@@ -168,6 +174,9 @@ def resolve_m2m_descriptors(
168
174
  metadata error, surfaced loudly (never silently skipped).
169
175
  """
170
176
  descriptors: list[M2mDescriptor] = []
177
+ # The source is *entity* for every relation, so its route segment is
178
+ # loop-invariant; only the target varies per relation.
179
+ source_plural = route_path(entity.name)
171
180
  for rel in m2m_relationships(entity):
172
181
  target_name = _strip_package(rel.object_ref() or "")
173
182
  junction_name = _strip_package(rel.through() or "")
@@ -186,13 +195,19 @@ def resolve_m2m_descriptors(
186
195
  fields = derive_m2m_fields(rel, entity, meta_index)
187
196
  source_fk = _field_named(junction, fields.source_field)
188
197
  target_fk = _field_named(junction, fields.target_field)
198
+ # FW-8 follow-up: the target's rows live in its discriminator base's shared
199
+ # table when @objectRef resolves to a concrete TPH subtype — @discriminatorValue
200
+ # is the subtype's OWN value (ADR-0039 sanctioned own, same idiom as tph_plan.py).
201
+ target_discriminator = (
202
+ target.attr(OBJECT_ATTR_DISCRIMINATOR_VALUE) if is_tph_subtype(target) else None
203
+ )
189
204
 
190
205
  descriptors.append(
191
206
  M2mDescriptor(
192
207
  relation_name=rel.name,
193
208
  target_entity=target.name,
194
- source_plural=plural_lowercase(entity.name),
195
- target_plural=plural_lowercase(target.name),
209
+ source_plural=source_plural,
210
+ target_plural=route_path(target.name),
196
211
  junction_table=_physical_table(
197
212
  junction,
198
213
  relation_context=(
@@ -213,6 +228,7 @@ def resolve_m2m_descriptors(
213
228
  column_naming,
214
229
  ),
215
230
  symmetric=rel.symmetric(),
231
+ target_discriminator=target_discriminator,
216
232
  )
217
233
  )
218
234
  return descriptors
@@ -34,7 +34,7 @@ from __future__ import annotations
34
34
 
35
35
  from dataclasses import dataclass
36
36
 
37
- from metaobjects.apidocs.naming import plural_lowercase as _plural_lowercase
37
+ from metaobjects.apidocs.naming import route_path as _route_path
38
38
  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
@@ -193,6 +193,48 @@ def _auto_set_stamp_lines(fields: list[MetaField], comment: str) -> list[str]:
193
193
  return lines
194
194
 
195
195
 
196
+ def _m2m_target_discs(mounts: list[M2mDescriptor]) -> dict[str, set[str | None]]:
197
+ """Per relation NAME, the set of target discriminators every mount of that
198
+ name resolves to (``None`` = a non-TPH target) — the ONE decision both the
199
+ ``find_related_<relation>`` seam signature and every call site derive from.
200
+ A name mounted more than once (a TPH hierarchy serves it at the base path and
201
+ under subtype segments, and a subtype may legally SHADOW a base-declared
202
+ relationship with a target of different TPH-ness) must not let one mount's
203
+ descriptor decide the seam for all of them."""
204
+ out: dict[str, set[str | None]] = {}
205
+ for d in mounts:
206
+ out.setdefault(d.relation_name, set()).add(d.target_discriminator)
207
+ return out
208
+
209
+
210
+ def _target_subtype_param(discs: set[str | None]) -> str:
211
+ """FW-8 follow-up: the ``target_subtype`` clause of a
212
+ ``find_related_<relation>`` Protocol signature, decided per relation NAME
213
+ from *discs* (:func:`_m2m_target_discs`): empty when no mount of the name
214
+ targets a TPH subtype (byte-identical output for the overwhelming common
215
+ case); ``, target_subtype: str`` when every mount does; and
216
+ ``, target_subtype: str | None`` when only some do — the parameter then
217
+ threads UNIFORMLY for the name, the non-TPH mounts passing an explicit
218
+ ``None``, so the seam and every call site agree on arity."""
219
+ if not discs or discs == {None}:
220
+ return ""
221
+ if None in discs:
222
+ return ", target_subtype: str | None"
223
+ return ", target_subtype: str"
224
+
225
+
226
+ def _target_subtype_arg(d: M2mDescriptor, discs: set[str | None]) -> str:
227
+ """The call-site counterpart of :func:`_target_subtype_param`, derived from
228
+ the SAME per-name decision: nothing when the name has no parameter, this
229
+ mount's resolved ``@discriminatorValue`` literal when it does, and an
230
+ explicit ``None`` literal for a non-TPH mount of a name that threads it."""
231
+ if not discs or discs == {None}:
232
+ return ""
233
+ if d.target_discriminator is None:
234
+ return ", None"
235
+ return f', "{d.target_discriminator}"'
236
+
237
+
196
238
  def _py_set_literal(names: list[str], *, frozen: bool = False) -> str:
197
239
  """A Python set/frozenset literal from field names, matching the generated
198
240
  allowlist idiom (one quoted name per line). Empty → ``frozenset()`` / ``set()``."""
@@ -305,6 +347,9 @@ class RouterGenerator:
305
347
  create / update / delete), keyed by ``name``.
306
348
  * ``_emit_m2m_route(d, snake, pk_param, pk_type, repo_class)`` — one M:N
307
349
  traversal route.
350
+ * ``_emit_tph_m2m_route(d, ..., subtype_expr)`` — one M:N traversal route
351
+ INSIDE a TPH hierarchy (FW-8), threading the discriminator scope through
352
+ the subtype-keyed ``find_related_*`` seam.
308
353
  * ``render_router(entity, object_index)`` — the whole module (last resort).
309
354
 
310
355
  Skips entities without a ``source.rdb`` child and read-only kinds
@@ -336,9 +381,11 @@ class RouterGenerator:
336
381
  f" def update(self, id: {pk_type}, dto: Any) -> Any | None: ...",
337
382
  f" def delete(self, id: {pk_type}) -> bool: ...",
338
383
  ]
384
+ discs = _m2m_target_discs(m2m)
339
385
  for d in m2m:
340
386
  lines.append(
341
- f" def find_related_{d.relation_name}(self, id: {pk_type}) -> list[Any]: ..."
387
+ f" def find_related_{d.relation_name}(self, id: {pk_type}"
388
+ f"{_target_subtype_param(discs[d.relation_name])}) -> list[Any]: ..."
342
389
  )
343
390
  return lines
344
391
 
@@ -493,15 +540,78 @@ class RouterGenerator:
493
540
  ) -> list[str]:
494
541
  """One M:N traversal route ``GET /{id}/<relationName>`` — a thin pass-through
495
542
  to the repository's ``find_related_*`` finder. Override to add filtering /
496
- pagination on the traversal."""
543
+ pagination on the traversal. FW-8 follow-up: when the target resolves to a
544
+ TPH subtype, the call also carries the resolved ``target_subtype`` literal
545
+ (see :func:`_target_subtype_arg`) — empty for a vanilla target. A vanilla
546
+ render mounts a name exactly once, so the name's decision set is the
547
+ mount's own singleton."""
497
548
  return [
498
549
  f'@router.get("/{{{pk_param}}}/{d.relation_name}")',
499
550
  f"def list_{snake}_{d.relation_name}(",
500
551
  f" {pk_param}: {pk_type},",
501
552
  f" repo: Annotated[{repo_class}, Depends(get_repository)],",
502
553
  ") -> list[Any]:",
503
- f" return repo.find_related_{d.relation_name}({pk_param})",
554
+ f" return repo.find_related_{d.relation_name}"
555
+ f"({pk_param}{_target_subtype_arg(d, {d.target_discriminator})})",
556
+ ]
557
+
558
+ def _emit_tph_m2m_route(
559
+ self,
560
+ d: M2mDescriptor,
561
+ *,
562
+ handler_name: str,
563
+ route: str,
564
+ pk_param: str,
565
+ pk_type: str,
566
+ repo_class: str,
567
+ subtype_expr: str,
568
+ gated: bool,
569
+ target_discs: set[str | None],
570
+ ) -> list[str]:
571
+ """One M:N traversal route INSIDE a TPH hierarchy (FW-8) — ``GET`` *route*
572
+ delegating to the subtype-keyed ``find_related_<relation>(subtype, id)`` seam,
573
+ the same discriminator-first shape every other TPH Protocol method already
574
+ uses (``find_by_id(subtype, id)``, ...).
575
+
576
+ *subtype_expr* is the Python literal threaded as the discriminator scope:
577
+ ``"None"`` for a mount at the BASE path (every row of the shared table is a
578
+ legitimate source — rule a), or a quoted ``@discriminatorValue`` for a mount
579
+ under a subtype's segment (rule b).
580
+
581
+ *target_discs* is the relation NAME's decision set across EVERY mount of
582
+ that name in the hierarchy (:func:`_m2m_target_discs`) — the same decision
583
+ the Protocol seam signature derives from, so this call site and the seam
584
+ cannot disagree on the ``target_subtype`` parameter's arity.
585
+
586
+ *gated* — True for a subtype mount — composes the seam's OWN
587
+ ``find_by_id(subtype, id)`` (the exact lookup the per-subtype GET route
588
+ already calls, and the ``tph-cross-subtype-404`` conformance scenario
589
+ already gates) as a subtype-ownership check BEFORE the join: a miss
590
+ returns ``[]`` (HTTP 200), never 404 and never the ``find_related_*`` call.
591
+ This ENFORCES rule (c) in generated code rather than delegating it to the
592
+ consumer — a ``find_related_*`` implementation that forgets to filter by
593
+ subtype can no longer leak a sibling's rows, because it is never reached
594
+ for a mismatched id. No new query strategy: it reuses a method the
595
+ consumer must already implement correctly. False (the BASE mount) skips
596
+ the gate — a redundant lookup there would be pure overhead, since every
597
+ row of the shared table is already a legitimate source (rule a)."""
598
+ lines = [
599
+ f'@router.get("{route}")',
600
+ f"def {handler_name}(",
601
+ f" {pk_param}: {pk_type},",
602
+ f" repo: Annotated[{repo_class}, Depends(get_repository)],",
603
+ ") -> list[Any]:",
504
604
  ]
605
+ if gated:
606
+ lines += [
607
+ f" if repo.find_by_id({subtype_expr}, {pk_param}) is None:",
608
+ " return []",
609
+ ]
610
+ lines.append(
611
+ f" return repo.find_related_{d.relation_name}"
612
+ f"({subtype_expr}, {pk_param}{_target_subtype_arg(d, target_discs)})"
613
+ )
614
+ return lines
505
615
 
506
616
  def _emit_tph_list_body(
507
617
  self, subtype_expr: str, fields_const: str, ops_const: str, repo_var: str = "repo"
@@ -528,14 +638,27 @@ class RouterGenerator:
528
638
  " return rows",
529
639
  ]
530
640
 
531
- def _render_tph_router(self, entity: MetaObject, plan: TphPlan) -> str:
641
+ def _render_tph_router(
642
+ self,
643
+ entity: MetaObject,
644
+ plan: TphPlan,
645
+ object_index: dict[str, MetaObject],
646
+ column_naming: str = DEFAULT_COLUMN_NAMING,
647
+ ) -> str:
532
648
  """FR-017 TPH: emit the discriminator base's router — a polymorphic collection
533
649
  at the base path + a full per-subtype CRUD set at /<base>/<segment>. The repo
534
650
  seam is subtype-keyed (the ``@discriminatorValue``, or ``None`` for the base);
535
- the consumer's repo applies the single-table discriminator scope."""
651
+ the consumer's repo applies the single-table discriminator scope.
652
+
653
+ FW-8: also emits M:N traversal routes — one for every relationship the BASE
654
+ resolves (rule a), and one for every relationship each CONCRETE SUBTYPE
655
+ resolves, INHERITED ones included (rule b/d). ``resolve_m2m_descriptors`` is
656
+ called directly against the base and against each subtype's entity (never via
657
+ ``render_router``, which returns ``None`` for a TPH subtype before reaching
658
+ M:N resolution at all — the two independent gaps this fix closes)."""
536
659
  short_name = entity.name
537
660
  snake = _snake_case(short_name)
538
- plural = _plural_lowercase(short_name)
661
+ plural = _route_path(short_name)
539
662
  pk_param = f"{snake}_id"
540
663
  # PK type from the BASE's declared primary key (subtypes share the
541
664
  # base's single table — mirrors the C# TPH pkType threading).
@@ -612,6 +735,39 @@ class RouterGenerator:
612
735
  " dto.pop(_frozen, None)",
613
736
  ]
614
737
 
738
+ # FW-8 — FR-018 x FR-017: M:N traversal inside a TPH hierarchy. Resolved
739
+ # directly against the base and against each subtype's OWN entity (never via
740
+ # `render_router`, which bails out for a TPH subtype before any M:N question
741
+ # at all). `resolve_m2m_descriptors` reads the RESOLVING `children()`
742
+ # (m2m_relationships), so a subtype's list already includes relationships
743
+ # inherited from the base or an abstract intermediate level — that overlap
744
+ # with the base's own list is rule (b), not a bug.
745
+ base_m2m = resolve_m2m_descriptors(entity, object_index, column_naming)
746
+ sub_m2m: dict[str, list[M2mDescriptor]] = {
747
+ st.entity.name: resolve_m2m_descriptors(st.entity, object_index, column_naming)
748
+ for st in plan.subtypes
749
+ }
750
+ # One `find_related_<relation>` Protocol seam per DISTINCT relation name
751
+ # anywhere in the hierarchy — mirrors the sort/required/frozen unions above.
752
+ # `subtype` threads through exactly like every other TPH seam method: `None`
753
+ # from the base route, the @discriminatorValue from a subtype route.
754
+ m2m_union: list[M2mDescriptor] = list(base_m2m)
755
+ m2m_seen = {d.relation_name for d in m2m_union}
756
+ for st in plan.subtypes:
757
+ for d in sub_m2m[st.entity.name]:
758
+ if d.relation_name not in m2m_seen:
759
+ m2m_seen.add(d.relation_name)
760
+ m2m_union.append(d)
761
+ # `target_subtype` presence is decided per relation NAME across EVERY mount
762
+ # of that name — base path and subtype segments together — never by the
763
+ # first descriptor the union kept: a subtype may SHADOW a base-declared
764
+ # relationship with a target of different TPH-ness, and the seam signature
765
+ # and every call site below derive from this one map.
766
+ m2m_target_discs = _m2m_target_discs(
767
+ list(base_m2m)
768
+ + [d for st in plan.subtypes for d in sub_m2m[st.entity.name]]
769
+ )
770
+
615
771
  h = generated_header(short_name, _effective_fqn(entity)).rstrip()
616
772
  parts: list[str] = []
617
773
  parts.append(
@@ -705,6 +861,15 @@ class RouterGenerator:
705
861
  parts.append(" def create(self, subtype: str, dto: Any) -> Any: ...")
706
862
  parts.append(f" def update(self, subtype: str, id: {pk_type}, dto: Any) -> Any | None: ...")
707
863
  parts.append(f" def delete(self, subtype: str, id: {pk_type}) -> bool: ...")
864
+ # FW-8: one find_related_<relation> seam per relation name resolved ANYWHERE
865
+ # in the hierarchy (base ∪ every subtype) — `subtype` scopes the traversal the
866
+ # same way every method above does; the consumer verifies a subtype-scoped id
867
+ # really is that subtype's, answering [] rather than a sibling's rows.
868
+ for d in m2m_union:
869
+ parts.append(
870
+ f" def find_related_{d.relation_name}(self, subtype: str | None, id: {pk_type}"
871
+ f"{_target_subtype_param(m2m_target_discs[d.relation_name])}) -> list[Any]: ..."
872
+ )
708
873
  parts.append("")
709
874
  parts.append("")
710
875
  parts.append(f"def get_repository() -> {repo_class}:")
@@ -799,6 +964,27 @@ class RouterGenerator:
799
964
  parts.append("")
800
965
  parts.append("")
801
966
 
967
+ # FW-8 rule (b)/(d): every relationship THIS subtype resolves — inherited
968
+ # from the base or an abstract intermediate level included — mounted under
969
+ # its own segment. Stays inside the subtype loop (registered before the
970
+ # base's `/{id}` routes below) for the same reason the CRUD block above
971
+ # does: a literal segment must be tried before a dynamic one, or FastAPI's
972
+ # path-param coercion 422s before a later, more specific route is tried.
973
+ for d in sub_m2m[sub]:
974
+ parts.extend(self._emit_tph_m2m_route(
975
+ d,
976
+ handler_name=f"list_{plural}_{sfx}_{d.relation_name}",
977
+ route=f"/{seg}/{{{pk_param}}}/{d.relation_name}",
978
+ pk_param=pk_param,
979
+ pk_type=pk_type,
980
+ repo_class=repo_class,
981
+ subtype_expr=f'"{val}"',
982
+ gated=True,
983
+ target_discs=m2m_target_discs[d.relation_name],
984
+ ))
985
+ parts.append("")
986
+ parts.append("")
987
+
802
988
  # --- Polymorphic base routes LAST (so /{id} doesn't shadow /<segment>). ---
803
989
  parts.extend(list_sig(f"list_{plural}", ""))
804
990
  parts.extend(self._emit_tph_list_body("None", fields_const, ops_const))
@@ -815,6 +1001,24 @@ class RouterGenerator:
815
1001
  parts.append(" return row")
816
1002
  parts.append("")
817
1003
 
1004
+ # FW-8 rule (a): every relationship the BASE resolves — every row of the
1005
+ # shared table is a legitimate source, so this is the same shape a vanilla
1006
+ # entity's mount gets (subtype=None, no discriminator gate).
1007
+ for d in base_m2m:
1008
+ parts.append("")
1009
+ parts.extend(self._emit_tph_m2m_route(
1010
+ d,
1011
+ handler_name=f"list_{snake}_{d.relation_name}",
1012
+ route=f"/{{{pk_param}}}/{d.relation_name}",
1013
+ pk_param=pk_param,
1014
+ pk_type=pk_type,
1015
+ repo_class=repo_class,
1016
+ subtype_expr="None",
1017
+ gated=False,
1018
+ target_discs=m2m_target_discs[d.relation_name],
1019
+ ))
1020
+ parts.append("")
1021
+
818
1022
  return "\n".join(parts)
819
1023
 
820
1024
  def render_router(
@@ -868,7 +1072,7 @@ class RouterGenerator:
868
1072
  if object_index is not None:
869
1073
  plan = tph_plan_for(entity, object_index)
870
1074
  if plan is not None:
871
- return self._render_tph_router(entity, plan)
1075
+ return self._render_tph_router(entity, plan, object_index, column_naming)
872
1076
 
873
1077
  m2m: list[M2mDescriptor] = (
874
1078
  resolve_m2m_descriptors(entity, object_index, column_naming)
@@ -878,7 +1082,7 @@ class RouterGenerator:
878
1082
 
879
1083
  short_name = entity.name
880
1084
  snake = _snake_case(short_name)
881
- plural = _plural_lowercase(short_name)
1085
+ plural = _route_path(short_name)
882
1086
  pk_param = f"{snake}_id"
883
1087
  pk = _pk_py_type(entity)
884
1088
  pk_type = pk.expr
@@ -11,7 +11,7 @@ EMBEDDED_LIBRARY: dict[str, str] = {
11
11
  'ai/model': '# library/ai/model.yaml — the CORE layer: the LLM-call trace envelope.\n#\n# Adopters opt in via `libraries: ["ai"]`, then `extends: "metaobjects::ai::LlmCallBase"`.\n#\n# This layer declares NO `source.rdb`, so opting into `"ai"` alone adds zero tables and\n# zero generated code — the design is present and resolvable, and nothing else happens\n# until the adopter adds `"ai/db"`. See library/iam/model.yaml for the full rationale.\n#\n# This file was split out of the former `library/ai/llm-call.yaml`, which shipped the\n# abstract base and a concrete `LlmCall` carrying `source.rdb` together. That was\n# recorded as an accepted wart on the grounds that splitting would change what existing\n# `ai` adopters get; a sweep of the estate found there are none, so it was closed rather\n# than documented (FR-043 Amendment 1).\nmetadata:\n package: metaobjects::ai\n children:\n - object.entity:\n name: LlmCallBase\n abstract: true\n children:\n - field.uuid: { name: traceId }\n - field.uuid: { name: spanId }\n - field.uuid: { name: parentSpanId }\n - field.string: { name: sessionId }\n - field.string: { name: callType }\n - field.string: { name: system }\n - field.string: { name: requestModel }\n - field.string: { name: responseModel }\n - field.int: { name: inputTokens }\n - field.int: { name: outputTokens }\n - field.currency: { name: costMinor, currency: USD }\n - field.int: { name: latencyMs }\n - field.string: { name: finishReason }\n - field.string: { name: status }\n - field.string: { name: errorDetail }\n - field.timestamp: { name: startedAt }\n - field.string: { name: llmRequest, dbColumnType: jsonb } # generic jsonb (no objectRef)\n - field.string: { name: llmResponse, dbColumnType: jsonb }\n - object.entity:\n name: LlmCall\n extends: metaobjects::ai::LlmCallBase\n description: The concrete trace row. Its `source.rdb` lives in db.yaml, so opting into the core layer alone declares the shape without proposing a table.\n children:\n - identity.primary: { name: id, fields: ["spanId"] }\n',
12
12
  'ai/requirements': "# library/ai/requirements.yaml — what the LLM-call trace envelope PROMISES.\n#\n# A RETROFIT, not new design: llm-call.yaml landed 2026-06-03 and `requirement.functional`\n# first appears 2026-08-11, so the library could not have carried requirements when it was\n# written. That is why this file is worth reading as a worked example — it shows what\n# declaring the design of something that already exists actually turns up.\n#\n# The entry that earns its keep is `typedIo`, honestly `partial` + `accepted`: the library\n# declares the ENVELOPE and the adopter declares the typed VO columns. Recording that seam\n# in the ledger is where an agent meets it, before adding a fourth trace column.\n#\n# HIERARCHY IS NESTING, and the L4/L5 split is grain. An L4 names the OBJECT it is about;\n# the fields that carry it hang off it as an L5 child. Writing the fields at L4 is\n# ERR_REQUIREMENT_L4_NOT_OBJECT, and writing the concerns as SIBLINGS of the L2 leaves the\n# L2 claiming nothing — both of which this file did until the standalone verify gate\n# existed (`cli/test/shipped-library-verify.test.ts`).\nmetadata:\n package: metaobjects::ai\n children:\n - requirement.functional:\n name: llmTracing\n level: 2\n status: live\n statement: Every call to a language model leaves a row that says what was asked, what came back, what it cost and how long it took.\n counterexample: A spend figure nobody can attribute to a call.\n description: The segment this library covers. Its three children below are the concerns it decomposes into.\n children:\n - requirement.functional:\n name: envelope\n level: 4\n status: live\n statement: A trace row identifies its call and its place in a trace — trace, span, parent span, session, call type, system.\n counterexample: A log line that cannot be joined to the request that produced it.\n implementedBy: [LlmCallBase]\n children:\n - requirement.functional:\n name: traceAddressing\n level: 5\n status: live\n statement: The four addressing columns are declared on the base — trace, span, parent span and session.\n counterexample: A row whose place in a trace is inferred from insertion order.\n description: >-\n The member grain exists here so the claim RESOLVES against the fields\n themselves: renaming or dropping one of them dangles this reference and\n fails the build, which naming the object alone would not.\n implementedBy: [LlmCallBase.traceId, LlmCallBase.spanId, LlmCallBase.parentSpanId, LlmCallBase.sessionId]\n\n - requirement.functional:\n name: accounting\n level: 4\n status: live\n statement: A trace row carries the tokens in, the tokens out, and the cost in integer minor units.\n counterexample: A cost stored as a float.\n description: >-\n `field.currency` — integer minor units on the wire, always. Float arithmetic for\n money is forbidden by the cross-port wire contract, and a spend total is exactly\n the sum that exposes it.\n implementedBy: [LlmCallBase]\n children:\n - requirement.functional:\n name: tokenAndCostColumns\n level: 5\n status: live\n statement: Tokens in, tokens out and cost are three declared columns, the cost a field.currency.\n counterexample: A cost column declared as a double.\n implementedBy: [LlmCallBase.inputTokens, LlmCallBase.outputTokens, LlmCallBase.costMinor]\n\n - requirement.functional:\n name: typedIo\n level: 4\n status: partial\n disposition: accepted\n statement: The request and response bodies are stored as structured jsonb, not as opaque text.\n counterexample: A prompt stored as a string nobody can query a field out of.\n notes: >-\n The library declares the two columns as generic jsonb with no `@objectRef`,\n because it cannot know the adopter's request/response shape. Typing them is the\n ADOPTER's move: declare an `object.value` and overlay the field with\n `@objectRef` + `@storage: jsonb`. This is the seam ADR-0024 drew, recorded here\n rather than in prose so it is in the ledger an agent reads before adding a\n fourth trace column of its own.\n implementedBy: [LlmCallBase]\n children:\n - requirement.functional:\n name: jsonbBodies\n level: 5\n status: live\n statement: The request and response bodies are declared as jsonb columns on the base.\n counterexample: A prompt stored in a text column.\n description: >-\n `live` where its parent is `partial`, and the split is the point: the\n COLUMNS are shipped and this claim is fully realised; what is outstanding\n is the TYPING of them, which is the parent's gap and the adopter's move.\n implementedBy: [LlmCallBase.llmRequest, LlmCallBase.llmResponse]\n\n - requirement.architectural:\n name: traceRowsCarryTiming\n status: live\n statement: Every trace row records when the call started and how long it took.\n counterexample: A latency figure derived from log timestamps after the fact.\n description: >-\n Architectural, so it propagates down `extends` to every adopter entity deriving\n from LlmCallBase — which is the point: an adopter's own trace table is claimed\n by this requirement for free, and dropping the columns breaks the build.\n implementedBy: [LlmCallBase]\n\n - requirement.architectural:\n name: traceRowsCarryOutcome\n status: live\n statement: Every trace row records how the call ended — a status, a finish reason, and the error detail when there was one.\n counterexample: A failed call indistinguishable from one that never happened.\n implementedBy: [LlmCallBase]\n",
13
13
  'iam/db': '# library/iam/db.yaml — the DB PERSISTENCE layer for metaobjects::iam.\n#\n# Opted into as `"iam/db"`, which IMPLIES `"iam"`: this file is nothing but\n# `overlay: true` redeclarations, and an overlay whose target was never declared is\n# ERR_OVERLAY_NO_TARGET.\n#\n# It carries exactly two kinds of child — `source.rdb` and `index.lookup` — and nothing\n# else. The field set, the identities and the relationships all live in model.yaml,\n# because they are the DESIGN; what lives here is where the rows go and which lookups are\n# worth an index. Add a field here and the core layer stops being the whole model, which\n# is the thing the split exists to guarantee.\n#\n# Physical names are `iam_`-prefixed. Two reasons, both real: `user` and `group` are\n# reserved words in Postgres, and an adopter very likely has tables of their own by those\n# names. A library that collides on a table name is a library nobody can adopt.\nmetadata:\n package: metaobjects::iam\n children:\n - object.entity:\n name: User\n overlay: true\n children:\n - source.rdb: { table: iam_user, role: primary }\n\n - object.entity:\n name: GroupType\n overlay: true\n children:\n - source.rdb: { table: iam_group_type, role: primary }\n\n - object.entity:\n name: Group\n overlay: true\n children:\n - source.rdb: { table: iam_group, role: primary }\n # Nesting is walked parent-ward constantly; the FK alone gives no index.\n - index.lookup: { name: ixParent, fields: [parentId] }\n\n - object.entity:\n name: Role\n overlay: true\n children:\n - source.rdb: { table: iam_role, role: primary }\n\n - object.entity:\n name: Permission\n overlay: true\n children:\n - source.rdb: { table: iam_permission, role: primary }\n\n - object.entity:\n name: GroupMember\n overlay: true\n children:\n - source.rdb: { table: iam_group_member, role: primary }\n # The composite PK covers (userId, groupId), so "who is in this group?" —\n # the other direction — has no index without this one. Same reasoning for\n # every ixSecond below.\n - index.lookup: { name: ixGroup, fields: [groupId] }\n\n - object.entity:\n name: RolePermission\n overlay: true\n children:\n - source.rdb: { table: iam_role_permission, role: primary }\n - index.lookup: { name: ixPermission, fields: [permissionId] }\n\n - object.entity:\n name: UserRole\n overlay: true\n children:\n - source.rdb: { table: iam_user_role, role: primary }\n - index.lookup: { name: ixRole, fields: [roleId] }\n\n - object.entity:\n name: GroupMemberRole\n overlay: true\n children:\n - source.rdb: { table: iam_group_member_role, role: primary }\n # "who holds this role in this group?" — the scoped-grant read.\n - index.lookup: { name: ixGroupRole, fields: [groupId, roleId] }\n',
14
- 'iam/model': '# library/iam/model.yaml — the CORE layer: identity and access management.\n#\n# Adopters opt in via `libraries: ["iam"]` in .metaobjects/config.json.\n#\n# This layer declares NO `source.rdb`, and that is the whole point of the split. A\n# sourceless object is inert by a contract that already ships: migrate skips an object\n# with no writable source, and codegen emits no route, queries, hooks, grid or form for\n# one (both citing #248 — persistability derives from source presence, never from the\n# object subtype). It still gets a type-only interface, so `extends` and reference work.\n#\n# So `libraries: ["iam"]` adds ZERO tables and ZERO generated code. What an adopter gains\n# is the design being present and resolvable: an agent working in the repo knows the\n# capability exists and can draw on it, and nothing else happens until the adopter adds\n# `"iam/db"`.\n#\n# Authoring discipline (FR-043 §3.1), so the departures are visible:\n# - `field.uuid` + `generation: uuid` on principals; composite ASSIGNED keys on\n# junctions. Never `increment` — a library cannot know the adopter\'s id strategy.\n# - Physical names carry the `iam_` prefix (in db.yaml): `user` and `group` are\n# reserved words in Postgres, and an adopter has tables of their own.\n# - No adopter-facing profile data. That arrives by `overlay: true`.\n# - No credentials. See requirements.yaml → `noCredentialsOnUser`.\nmetadata:\n package: metaobjects::iam\n children:\n - object.entity:\n name: IamBase\n abstract: true\n description: Shared shape of every iam principal and definition — a stable uuid plus change timestamps. Junctions do not extend it; they are addressed by their participants.\n children:\n - field.uuid: { name: id, required: true }\n - field.timestamp: { name: createdAt, autoSet: onCreate }\n - field.timestamp: { name: updatedAt, autoSet: onUpdate }\n\n - object.entity:\n name: User\n extends: IamBase\n description: A person or service account that can be granted access. Carries no authentication secret of any kind — see the noCredentialsOnUser requirement.\n children:\n - field.string: { name: username, required: true, maxLength: 64, filterable: true }\n - field.string: { name: email, required: true, maxLength: 254, stringFormat: email, filterable: true }\n - field.string: { name: displayName, maxLength: 120 }\n # NOT `filterable: true`, deliberately. The loader warns when a filterable\n # field is in no identity — filtering on it sequential-scans — and a library\n # must not ship a warning to every adopter. `username` and `email` carry it\n # because they have identity.secondary; `status` does not. An adopter who\n # wants to filter on status overlays `filterable` AND an index together,\n # which is exactly what the layer split is for.\n - field.enum: { name: status, required: true, values: [invited, active, suspended, closed], default: active }\n - field.timestamp: { name: emailVerifiedAt }\n - field.timestamp: { name: lastSeenAt }\n - identity.primary: { name: pk, fields: [id], generation: uuid }\n - identity.secondary: { name: uqUsername, fields: [username] }\n - identity.secondary: { name: uqEmail, fields: [email] }\n - relationship.association: { name: groups, objectRef: Group, cardinality: many, through: GroupMember }\n - relationship.association: { name: roles, objectRef: Role, cardinality: many, through: UserRole }\n\n - object.entity:\n name: GroupType\n extends: IamBase\n description: What KIND of group this is — a team, a tenant, a project. An entity rather than an enum, because "which roles may be held in this kind of group" is data an adopter extends, and an enum\'s values cannot be extended by overlay.\n children:\n - field.string: { name: key, required: true, maxLength: 64 }\n - field.string: { name: name, required: true, maxLength: 120 }\n - field.string: { name: description, maxLength: 500 }\n - identity.primary: { name: pk, fields: [id], generation: uuid }\n - identity.secondary: { name: uqKey, fields: [key] }\n\n - object.entity:\n name: Group\n extends: IamBase\n description: A nestable collection of users, of a declared GroupType. Nesting is by parentId; acyclicity is an invariant the schema cannot express — see the acyclicGroupNesting requirement.\n children:\n - field.uuid: { name: groupTypeId, required: true }\n - field.uuid: { name: parentId }\n - field.string: { name: key, required: true, maxLength: 64 }\n # Not filterable for the same reason as User.status above.\n - field.string: { name: name, required: true, maxLength: 120 }\n - field.string: { name: description, maxLength: 500 }\n - identity.primary: { name: pk, fields: [id], generation: uuid }\n - identity.secondary: { name: uqKey, fields: [key] }\n - identity.reference: { name: fkGroupType, fields: [groupTypeId], references: GroupType, onDelete: restrict }\n - identity.reference: { name: fkParent, fields: [parentId], references: Group, onDelete: restrict }\n\n - object.entity:\n name: Role\n extends: IamBase\n description: A reusable bundle of permissions. Code never compares a role NAME to a literal — it asks whether a user holds a permission, and the mapping is data.\n children:\n - field.string: { name: key, required: true, maxLength: 64 }\n - field.string: { name: name, required: true, maxLength: 120 }\n - field.string: { name: description, maxLength: 500 }\n - field.uuid: { name: groupTypeId, description: "When set, this role may be held only within groups of this type; absent means grantable anywhere." }\n - identity.primary: { name: pk, fields: [id], generation: uuid }\n - identity.secondary: { name: uqKey, fields: [key] }\n - identity.reference: { name: fkGroupType, fields: [groupTypeId], references: GroupType, onDelete: restrict }\n - relationship.association: { name: permissions, objectRef: Permission, cardinality: many, through: RolePermission }\n\n - object.entity:\n name: Permission\n extends: IamBase\n description: "The assignable unit — a stable <resource>:<action> key the application checks against. An entity, not an enum, on ADR-0037\'s own reasoning: it has its own identity, its own lifecycle, and a junction with real foreign keys."\n children:\n - field.string: { name: key, required: true, maxLength: 128, description: "Stable <resource>:<action> key the application checks against." }\n - field.string: { name: description, maxLength: 500 }\n - identity.primary: { name: pk, fields: [id], generation: uuid }\n - identity.secondary: { name: uqKey, fields: [key] }\n\n # ---- grant surface: every grant is a row, addressed by its participants ----\n #\n # Junctions do NOT extend IamBase: they have no identity of their own, and adding a\n # surrogate uuid to a row whose identity IS its participants invites a duplicate.\n\n - object.entity:\n name: GroupMember\n description: A user\'s membership of a group.\n children:\n - field.uuid: { name: userId, required: true }\n - field.uuid: { name: groupId, required: true }\n - field.timestamp: { name: joinedAt, autoSet: onCreate }\n - identity.primary: { name: pk, fields: [userId, groupId], generation: assigned }\n - identity.reference: { name: fkUser, fields: [userId], references: User, onDelete: cascade }\n - identity.reference: { name: fkGroup, fields: [groupId], references: Group, onDelete: cascade }\n\n - object.entity:\n name: RolePermission\n description: A permission granted by a role.\n children:\n - field.uuid: { name: roleId, required: true }\n - field.uuid: { name: permissionId, required: true }\n - identity.primary: { name: pk, fields: [roleId, permissionId], generation: assigned }\n - identity.reference: { name: fkRole, fields: [roleId], references: Role, onDelete: cascade }\n - identity.reference: { name: fkPermission, fields: [permissionId], references: Permission, onDelete: restrict }\n\n - object.entity:\n name: UserRole\n description: A system-wide grant of a role to a user.\n children:\n - field.uuid: { name: userId, required: true }\n - field.uuid: { name: roleId, required: true }\n - field.timestamp: { name: grantedAt, autoSet: onCreate }\n - identity.primary: { name: pk, fields: [userId, roleId], generation: assigned }\n - identity.reference: { name: fkUser, fields: [userId], references: User, onDelete: cascade }\n - identity.reference: { name: fkRole, fields: [roleId], references: Role, onDelete: restrict }\n\n - object.entity:\n name: GroupMemberRole\n description: A grant of a role to a user WITHIN one group. Three foreign keys, so it is not an M:N @through junction (which must declare exactly two identity.reference children); it is read by explicit finders.\n children:\n - field.uuid: { name: userId, required: true }\n - field.uuid: { name: groupId, required: true }\n - field.uuid: { name: roleId, required: true }\n - field.timestamp: { name: grantedAt, autoSet: onCreate }\n - identity.primary: { name: pk, fields: [userId, groupId, roleId], generation: assigned }\n - identity.reference: { name: fkUser, fields: [userId], references: User, onDelete: cascade }\n - identity.reference: { name: fkGroup, fields: [groupId], references: Group, onDelete: cascade }\n - identity.reference: { name: fkRole, fields: [roleId], references: Role, onDelete: restrict }\n',
14
+ 'iam/model': '# library/iam/model.yaml — the CORE layer: identity and access management.\n#\n# Adopters opt in via `libraries: ["iam"]` in .metaobjects/config.json.\n#\n# This layer declares NO `source.rdb`, and that is the whole point of the split. A\n# sourceless object is inert by a contract that already ships: migrate skips an object\n# with no writable source, and codegen emits no route, queries, hooks, grid or form for\n# one (both citing #248 — persistability derives from source presence, never from the\n# object subtype). It still gets a type-only interface, so `extends` and reference work.\n#\n# So `libraries: ["iam"]` adds ZERO tables and ZERO generated code. What an adopter gains\n# is the design being present and resolvable: an agent working in the repo knows the\n# capability exists and can draw on it, and nothing else happens until the adopter adds\n# `"iam/db"`.\n#\n# Authoring discipline (FR-043 §3.1), so the departures are visible:\n# - `field.uuid` + `generation: uuid` on principals; composite ASSIGNED keys on\n# junctions. Never `increment` — a library cannot know the adopter\'s id strategy.\n# - Physical names carry the `iam_` prefix (in db.yaml): `user` and `group` are\n# reserved words in Postgres, and an adopter has tables of their own.\n# - No adopter-facing profile data. That arrives by `overlay: true`.\n# - No credentials. See requirements.yaml → `noCredentialsOnUser`.\nmetadata:\n package: metaobjects::iam\n children:\n - object.entity:\n name: IamBase\n abstract: true\n description: Shared shape of every iam principal and definition — a stable uuid plus change timestamps. Junctions do not extend it; they are addressed by their participants.\n children:\n - field.uuid: { name: id, required: true }\n - field.timestamp: { name: createdAt, autoSet: onCreate }\n - field.timestamp: { name: updatedAt, autoSet: onUpdate }\n\n - object.entity:\n name: User\n extends: IamBase\n description: A person or service account that can be granted access. Carries no authentication secret of any kind — see the noCredentialsOnUser requirement.\n children:\n - field.string: { name: username, required: true, maxLength: 64, filterable: true }\n - field.string: { name: email, required: true, maxLength: 254, stringFormat: email, filterable: true }\n - field.string: { name: displayName, maxLength: 120 }\n # NOT `filterable: true`, deliberately. The loader warns when a filterable\n # field is in no identity — filtering on it sequential-scans — and a library\n # must not ship a warning to every adopter. `username` and `email` carry it\n # because they have identity.secondary; `status` does not. An adopter who\n # wants to filter on status overlays `filterable` AND an index together,\n # which is exactly what the layer split is for.\n - field.enum: { name: status, required: true, values: [invited, active, suspended, closed], default: active }\n - field.timestamp: { name: emailVerifiedAt }\n - field.timestamp: { name: lastSeenAt }\n - identity.primary: { name: pk, fields: [id], generation: uuid }\n - identity.secondary: { name: uqUsername, fields: [username] }\n - identity.secondary: { name: uqEmail, fields: [email] }\n - relationship.association: { name: groups, objectRef: Group, cardinality: many, through: GroupMember }\n - relationship.association: { name: roles, objectRef: Role, cardinality: many, through: UserRole }\n\n - object.entity:\n name: GroupType\n extends: IamBase\n description: What KIND of group this is — a team, a tenant, a project. An entity rather than an enum, because "which roles may be held in this kind of group" is data an adopter extends, and an enum\'s values cannot be extended by overlay.\n children:\n - field.string: { name: key, required: true, maxLength: 64 }\n - field.string: { name: name, required: true, maxLength: 120 }\n - field.string: { name: description, maxLength: 500 }\n - identity.primary: { name: pk, fields: [id], generation: uuid }\n - identity.secondary: { name: uqGroupTypeKey, fields: [key] }\n\n - object.entity:\n name: Group\n extends: IamBase\n description: A nestable collection of users, of a declared GroupType. Nesting is by parentId; acyclicity is an invariant the schema cannot express — see the acyclicGroupNesting requirement.\n children:\n - field.uuid: { name: groupTypeId, required: true }\n - field.uuid: { name: parentId }\n - field.string: { name: key, required: true, maxLength: 64 }\n # Not filterable for the same reason as User.status above.\n - field.string: { name: name, required: true, maxLength: 120 }\n - field.string: { name: description, maxLength: 500 }\n - identity.primary: { name: pk, fields: [id], generation: uuid }\n - identity.secondary: { name: uqGroupKey, fields: [key] }\n - identity.reference: { name: fkGroupType, fields: [groupTypeId], references: GroupType, onDelete: restrict }\n - identity.reference: { name: fkParent, fields: [parentId], references: Group, onDelete: restrict }\n\n - object.entity:\n name: Role\n extends: IamBase\n description: A reusable bundle of permissions. Code never compares a role NAME to a literal — it asks whether a user holds a permission, and the mapping is data.\n children:\n - field.string: { name: key, required: true, maxLength: 64 }\n - field.string: { name: name, required: true, maxLength: 120 }\n - field.string: { name: description, maxLength: 500 }\n - field.uuid: { name: groupTypeId, description: "When set, this role may be held only within groups of this type; absent means grantable anywhere." }\n - identity.primary: { name: pk, fields: [id], generation: uuid }\n - identity.secondary: { name: uqRoleKey, fields: [key] }\n - identity.reference: { name: fkGroupType, fields: [groupTypeId], references: GroupType, onDelete: restrict }\n - relationship.association: { name: permissions, objectRef: Permission, cardinality: many, through: RolePermission }\n\n - object.entity:\n name: Permission\n extends: IamBase\n description: "The assignable unit — a stable <resource>:<action> key the application checks against. An entity, not an enum, on ADR-0037\'s own reasoning: it has its own identity, its own lifecycle, and a junction with real foreign keys."\n children:\n - field.string: { name: key, required: true, maxLength: 128, description: "Stable <resource>:<action> key the application checks against." }\n - field.string: { name: description, maxLength: 500 }\n - identity.primary: { name: pk, fields: [id], generation: uuid }\n - identity.secondary: { name: uqPermissionKey, fields: [key] }\n\n # ---- grant surface: every grant is a row, addressed by its participants ----\n #\n # Junctions do NOT extend IamBase: they have no identity of their own, and adding a\n # surrogate uuid to a row whose identity IS its participants invites a duplicate.\n\n - object.entity:\n name: GroupMember\n description: A user\'s membership of a group.\n children:\n - field.uuid: { name: userId, required: true }\n - field.uuid: { name: groupId, required: true }\n - field.timestamp: { name: joinedAt, autoSet: onCreate }\n - identity.primary: { name: pk, fields: [userId, groupId], generation: assigned }\n - identity.reference: { name: fkUser, fields: [userId], references: User, onDelete: cascade }\n - identity.reference: { name: fkGroup, fields: [groupId], references: Group, onDelete: cascade }\n\n - object.entity:\n name: RolePermission\n description: A permission granted by a role.\n children:\n - field.uuid: { name: roleId, required: true }\n - field.uuid: { name: permissionId, required: true }\n - identity.primary: { name: pk, fields: [roleId, permissionId], generation: assigned }\n - identity.reference: { name: fkRole, fields: [roleId], references: Role, onDelete: cascade }\n - identity.reference: { name: fkPermission, fields: [permissionId], references: Permission, onDelete: restrict }\n\n - object.entity:\n name: UserRole\n description: A system-wide grant of a role to a user.\n children:\n - field.uuid: { name: userId, required: true }\n - field.uuid: { name: roleId, required: true }\n - field.timestamp: { name: grantedAt, autoSet: onCreate }\n - identity.primary: { name: pk, fields: [userId, roleId], generation: assigned }\n - identity.reference: { name: fkUser, fields: [userId], references: User, onDelete: cascade }\n - identity.reference: { name: fkRole, fields: [roleId], references: Role, onDelete: restrict }\n\n - object.entity:\n name: GroupMemberRole\n description: A grant of a role to a user WITHIN one group. Three foreign keys, so it is not an M:N @through junction (which must declare exactly two identity.reference children); it is read by explicit finders.\n children:\n - field.uuid: { name: userId, required: true }\n - field.uuid: { name: groupId, required: true }\n - field.uuid: { name: roleId, required: true }\n - field.timestamp: { name: grantedAt, autoSet: onCreate }\n - identity.primary: { name: pk, fields: [userId, groupId, roleId], generation: assigned }\n - identity.reference: { name: fkUser, fields: [userId], references: User, onDelete: cascade }\n - identity.reference: { name: fkGroup, fields: [groupId], references: Group, onDelete: cascade }\n - identity.reference: { name: fkRole, fields: [roleId], references: Role, onDelete: restrict }\n',
15
15
  'iam/requirements': '# library/iam/requirements.yaml — what this library\'s design PROMISES.\n#\n# This is what makes iam a library rather than a schema snippet. Without requirements an\n# adopter gets nine tables; with them they get nine tables plus a build that is held to\n# "no authorization decision is hard-wired to a name", which no snippet can do.\n#\n# Two reading rules, both load-bearing:\n#\n# `live` here means "the model AS SHIPPED realises this" — never "your application\n# does". A ledger binds to model nodes; runtime guarantees are the runtime\'s tests, and\n# this library does not invent a way to point a requirement at code (@verifiedBy was\n# retired for exactly that). Behaviour the model cannot carry ships as `partial` +\n# `disposition: accepted` with a notes sentence naming what the adopter must do.\n#\n# The functional tree roots at L2, not L1. L1 is the adopter\'s SOLUTION, and a library\n# is by definition a segment of someone else\'s. Architectural claims ship flat.\n#\n# HIERARCHY IS NESTING, and the L4/L5 split is grain. The concerns are CHILDREN of the L2\n# rather than its siblings, and an L4 names the OBJECT it is about while the field that\n# carries it hangs off it as an L5 child. Written flat, the L2 claims nothing in its whole\n# subtree; written at L4, a field reference is ERR_REQUIREMENT_L4_NOT_OBJECT. Both shipped\n# here until the standalone verify gate existed (`cli/test/shipped-library-verify.test.ts`).\nmetadata:\n package: metaobjects::iam\n children:\n # ---- functional: the L2 segment and the concerns nested under it --------\n - requirement.functional:\n name: accessControl\n level: 2\n status: live\n statement: Who may do what is answered from stored grants, never from a name compared to a literal in code.\n counterexample: A branch that reads `if (user.role === "admin")`.\n description: The segment this library covers. The concerns beneath it are what it decomposes into.\n children:\n - requirement.functional:\n name: identity\n level: 4\n status: live\n statement: A person or service account is represented once, addressed by a uuid, and reachable by username or email.\n counterexample: Two rows for the same person because the email changed.\n implementedBy: [User]\n\n - requirement.functional:\n name: grouping\n level: 4\n status: live\n statement: Users are collected into typed, nestable groups, and the kind of group is data rather than a hard-coded set.\n counterexample: A `teamOrTenant` boolean.\n implementedBy: [Group, GroupType, GroupMember]\n\n - requirement.functional:\n name: acyclicGroupNesting\n level: 4\n status: partial\n disposition: accepted\n statement: A group is never its own ancestor.\n counterexample: Two groups each naming the other as parent.\n notes: >-\n The schema cannot express this — a self-referencing FK admits a cycle, and the\n only relational forms that would catch it (a recursive CHECK, a closure table\n maintained by trigger) are DB-specific and would not survive three dialects.\n The adopter enforces it where the write happens. Recorded rather than omitted\n so an agent reading the ledger before adding a parent-setting endpoint sees the\n obligation.\n implementedBy: [Group]\n\n - requirement.functional:\n name: grants\n level: 4\n status: live\n statement: A role is granted to a user either system-wide or scoped to one group, and both are ordinary rows.\n counterexample: A nullable `groupId` on one grant table, where NULL means "everywhere".\n description: >-\n Two junctions, not one with a nullable scope. A NULL in a unique key is DISTINCT\n from every other NULL in SQL, so a nullable-scope design lets the same global\n grant be inserted twice; the fix needs a partial index whose expression carries\n a physical column name. Two composite-keyed tables need no escape hatch and\n survive three dialects and five ports unchanged.\n implementedBy: [UserRole, GroupMemberRole]\n\n - requirement.functional:\n name: roleScopedToGroupType\n level: 4\n status: partial\n disposition: accepted\n statement: A role bound to a group type is granted only within groups of that type.\n counterexample: A "tenant admin" role granted inside a project group.\n notes: >-\n Expressing this relationally needs the grant row to carry the group\'s type and\n a composite FK back to (group, type) — three foreign keys deep, unverified\n across five ports\' DDL and ORM paths. The adopter checks it at the point of\n grant. The declared half is the L5 child below; the enforcement is not.\n implementedBy: [Role, GroupMemberRole]\n children:\n - requirement.functional:\n name: roleDeclaresItsGroupType\n level: 5\n status: live\n statement: A role declares the group type it is bound to, as a nullable reference.\n counterexample: A role whose intended scope is recoverable only from its name.\n description: >-\n `live` where its parent is `partial`, and the split is grain as much as\n verdict: the DECLARATION is shipped and resolves against the field itself,\n so dropping the column fails the build — while the ENFORCEMENT, which no\n schema here can carry, stays the parent\'s accepted gap.\n implementedBy: [Role.groupTypeId]\n\n - requirement.functional:\n name: decision\n level: 4\n status: live\n statement: An authorization decision is the question "does this user hold this permission key", answered from rows.\n counterexample: A hard-coded list of usernames that bypass a check.\n implementedBy: [Permission, RolePermission]\n\n # ---- architectural: prohibitions in force --------------------------------\n\n - requirement.architectural:\n name: grantsAreRows\n status: live\n statement: A grant exists only as a stored row; nothing is granted by naming, position or convention.\n counterexample: A superuser recognised by username.\n implementedBy: [UserRole, GroupMemberRole, RolePermission, GroupMember]\n\n - requirement.architectural:\n name: noCredentialsOnUser\n status: live\n statement: A user row carries no authentication secret — no password, no hash, no knowledge-based question or answer.\n counterexample: A password or secret-answer column on the user table.\n description: >-\n Authentication is a separate capability with an entity per factor; this library\n is identity and authorization only.\n notes: >-\n This is the one thing every reader of a user table proposes adding, and a real\n legacy model of this shape stored a length-bounded plaintext password and a\n knowledge-based secret pair on the user row. Stating it as a prohibition IN\n FORCE — claimable, and rendered on agent/requirements.md — is what stops an\n agent extending "the user model" from re-deriving it on sight. It is\n `architectural`, not `retired`: retired is chartered for a capability built\n here and removed, and this library never built one.\n implementedBy: [User]\n\n - requirement.architectural:\n name: principalDeletionRevokesGrants\n status: live\n statement: Deleting a user or group removes its grants; deleting a role or permission still in use is refused.\n counterexample: A grant row pointing at a user who no longer exists.\n description: The referential rule in one sentence — cascade from a principal, restrict from a definition.\n implementedBy: [GroupMember, UserRole, GroupMemberRole, RolePermission]\n\n - requirement.architectural:\n name: stableIdentifiers\n status: live\n statement: Every principal and definition is addressed by a uuid that never changes; every grant by its participants.\n counterexample: A group referenced by its display name.\n implementedBy: [IamBase]\n',
16
16
  }
17
17
 
@@ -9,8 +9,10 @@ the loader like everything else — never hand-parsed from a side file.
9
9
  # ---------------------------------------------------------------------------
10
10
  # Subtypes — the axis is the CHECK POLARITY, a genuine behaviour difference and
11
11
  # therefore a subtype under ADR-0037 §2:
12
- # functional -> EXISTENCE: fails when nothing implements it
13
- # architectural -> UNIVERSALITY: fails when something violates it
12
+ # functional -> EXISTENCE: `meta verify` warns when nothing implements it,
13
+ # and fails when a node it names is gone
14
+ # architectural -> UNIVERSALITY: `meta verify` fails a live policy applied to nothing
15
+ # (it does not check that each claimed node complies)
14
16
  # ---------------------------------------------------------------------------
15
17
  REQUIREMENT_SUBTYPE_FUNCTIONAL = "functional"
16
18
  REQUIREMENT_SUBTYPE_ARCHITECTURAL = "architectural"