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.
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/PKG-INFO +1 -1
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/pyproject.toml +1 -1
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/apidocs/naming.py +49 -16
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/generators/m2m_codegen.py +25 -9
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/generators/router_generator.py +213 -9
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/library/embedded_library.py +1 -1
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/requirement/requirement_constants.py +4 -2
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/spec_metamodel/requirement.json +2 -2
- metaobjects-1.0.5rc3/tests/codegen/test_codegen_compile_conformance.py +156 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_m2m_codegen.py +20 -16
- metaobjects-1.0.5rc3/tests/codegen/test_route_path_naming.py +67 -0
- metaobjects-1.0.5rc3/tests/codegen/test_router_generator_m2m_target_tph.py +156 -0
- metaobjects-1.0.5rc3/tests/codegen/test_router_generator_tph_m2m.py +378 -0
- metaobjects-1.0.5rc3/tests/codegen/test_router_generator_tph_m2m_shadowed.py +205 -0
- metaobjects-1.0.5rc3/tests/integration/api_contract_m2m_server.py +424 -0
- metaobjects-1.0.5rc3/tests/integration/generated_m2m_app.py +562 -0
- metaobjects-1.0.5rc3/tests/integration/test_m2m_target_tph_generated.py +291 -0
- metaobjects-1.0.5rc3/tests/integration/test_tph_m2m_generated.py +420 -0
- metaobjects-1.0.5rc3/tests/integration/test_tph_m2m_shadowed_generated.py +358 -0
- metaobjects-1.0.4/tests/integration/api_contract_m2m_server.py +0 -220
- metaobjects-1.0.4/tests/integration/generated_m2m_app.py +0 -220
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/.gitignore +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/LICENSE +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/README.md +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/hatch_build.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/scripts/generate_embedded_library.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/__init__.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/agent_context/__init__.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/agent_context/scaffold.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/apidocs/__init__.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/apidocs/api_model.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/apidocs/builder.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/apidocs/paths.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/apidocs/renderer.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/attr_class_map.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/cli.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/KNOWN_GAPS.md +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/__init__.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/collision_names.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/config.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/constants.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/extract_delegate_emitter.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/extract_schema_emitter.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/format.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/fr010_field_mapping.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/generator.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/generator_registry.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/generators/__init__.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/generators/entity_model.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/generators/extractor_generator.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/generators/filter_allowlist_generator.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/generators/find_inbound.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/generators/fr019_shared_enum.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/generators/names_generator.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/generators/output_parser_generator.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/generators/output_prompt_generator.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/generators/payload_vo_generator.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/generators/render_helper_generator.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/generators/template_generator.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/generators/tph_plan.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/generators/trace_helper_generator.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/instance_artifacts.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/metaobjects-config.schema.json +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/output_format_spec_emitter.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/overwrite_policy.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/project_config.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/runner.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/runtime/__init__.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/runtime/filter_parser.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/template_codegen/__init__.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/template_codegen/output_pattern.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/template_codegen/template_data.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/template_codegen/template_spec.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/type_map.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/config/__init__.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/config/dependencies.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/config/neutral_config.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/config/source_resolver.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/core_types.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/datatype.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/documentation/__init__.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/documentation/doc_constants.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/documentation/doc_provider.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/documentation/doc_schema.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/errors.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/library/__init__.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/library/library_sources.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/loader/__init__.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/loader/merge.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/loader/meta_data_loader.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/loader/registered_validation.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/loader/sources/__init__.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/loader/sources/directory_source.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/loader/sources/file_source.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/loader/sources/meta_data_source.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/loader/sources/uri_source.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/loader/validate_discriminator.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/loader/validate_enum_normalize_ambiguity.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/loader/validate_field_mutability.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/loader/validate_source_escapes.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/loader/validate_source_parameter_ref.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/loader/validate_source_physical_names.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/loader/validation_passes.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/__init__.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/__init__.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/attr/__init__.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/attr/attr_constants.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/attr/meta_attr.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/field/__init__.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/field/field_constants.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/field/meta_field.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/identity/__init__.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/identity/identity_constants.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/identity/meta_identity.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/index/__init__.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/index/index_constants.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/index/meta_index.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/object/__init__.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/object/meta_object.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/object/meta_object_aware.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/object/object_class_registry.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/object/object_constants.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/object/object_extract.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/object/value_object.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/relationship/__init__.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/relationship/derive_m2m_fields.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/relationship/meta_relationship.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/relationship/relationship_constants.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/relationship/relationship_references.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/requirement/__init__.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/requirement/meta_requirement.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/validator/__init__.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/core/validator/validator_constants.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/meta_data.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/meta_root.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/persistence/__init__.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/persistence/db/__init__.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/persistence/db/db_constants.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/persistence/db/db_provider.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/persistence/origin/__init__.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/persistence/origin/meta_origin.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/persistence/origin/origin_constants.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/persistence/source/__init__.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/persistence/source/meta_source.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/persistence/source/source_constants.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/presentation/__init__.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/presentation/layout/__init__.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/presentation/layout/layout_constants.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/presentation/layout/meta_layout.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/presentation/ui/__init__.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/presentation/ui/ui_provider.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/presentation/view/__init__.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/presentation/view/meta_view.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/presentation/view/view_constants.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/provider_extends.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/template/__init__.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/template/meta_template.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/template/prompt_provider.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/meta/template/template_constants.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/naming.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/naming_refs.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/parser.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/parser_yaml.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/provider.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/py.typed +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/registry.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/registry_manifest.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/render/__init__.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/render/email_document.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/render/escapers.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/render/extract/KNOWN_GAPS.md +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/render/extract/__init__.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/render/extract/coerce.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/render/extract/extract.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/render/extract/extract_map.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/render/extract/json_forgiving_reader.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/render/extract/locate.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/render/extract/normalize.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/render/extract/strip.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/render/extract/types.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/render/extract/xml_forgiving_reader.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/render/filesystem_provider.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/render/payload_accessors.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/render/prompt/__init__.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/render/prompt/output_format_renderer.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/render/prompt/output_format_spec.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/render/prompt/prompt_field.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/render/prompt/prompt_overrides.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/render/prompt/prompt_style.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/render/renderer.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/render/verify.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/runtime/__init__.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/runtime/llm_recorder.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/runtime/n2m_resolver.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/runtime/object_manager.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/runtime/tph.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/scope.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/serializer_json.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/shared/__init__.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/shared/base_types.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/shared/separators.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/shared/structural.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/source/__init__.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/source/error_source.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/source/json_path.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/source/semantic_diff.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/source/yaml_positions.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/source_resolution.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/spec_metamodel/__init__.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/spec_metamodel/attr.json +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/spec_metamodel/db.json +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/spec_metamodel/documentation.json +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/spec_metamodel/field.json +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/spec_metamodel/identity.json +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/spec_metamodel/index.json +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/spec_metamodel/layout.json +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/spec_metamodel/object.json +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/spec_metamodel/origin.json +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/spec_metamodel/prompt.json +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/spec_metamodel/relationship.json +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/spec_metamodel/source.json +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/spec_metamodel/template.json +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/spec_metamodel/ui-web.json +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/spec_metamodel/ui.json +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/spec_metamodel/validator.json +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/spec_metamodel/view.json +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/super_resolve.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/validation_types.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/yaml_desugar.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/__init__.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/__init__.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/gen_suite.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/golden/extends/expected/BaseEntity.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/golden/extends/expected/Program.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/golden/extends/meta.json +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/golden/nested-array/expected/AuthorBrief.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/golden/nested-array/expected/PostBrief.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/golden/nested-array/meta.json +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/golden/scalars/expected/Metric.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/golden/scalars/expected/Report.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/golden/scalars/meta.json +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/golden/vanilla/expected/Subscriber.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/golden/vanilla/meta.json +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_abstract_conformance.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_api_docs_builder.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_api_docs_paths.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_cli.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_cli_config_gen.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_cli_config_verify.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_cli_dependencies.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_cli_providers.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_cli_registry.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_cli_staleness_nudge.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_cli_verify_strict.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_cli_verify_subverbs.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_constants_config.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_entity_model.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_enum_conformance.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_extends_abstract_field_inheritance.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_extract_tier_collision.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_extractor_generator.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_filter_allowlist_generator.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_format.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_fr010_nested_object_prompt.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_fr010_output_codegen.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_fr019_shared_provided_conformance.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_gen_state_keying.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_generator.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_generator_extension_seams.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_golden.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_inherit_without_restate_gate.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_inheritance_conformance.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_instance_artifacts.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_names_extends_chain.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_names_generator.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_no_magic_physical_names.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_origin_nullability.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_output_parser_generator.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_overwrite_policy.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_overwrite_policy_hash.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_payload_vo_generator.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_project_config.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_projection_compile.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_render_helper_conformance.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_render_helper_generator.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_reverse_finders.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_router_autoset.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_router_generator.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_runner.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_source_resolution.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_sourceless_projection.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_template_data.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_template_generator.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_template_output_pattern.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_template_scope_helpers.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_template_scope_walk.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_template_spec.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_tph_codegen.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_trace_helper_generator.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_type_map.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_validation_conformance.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/codegen/test_write_through.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/config/__init__.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/config/test_dependencies.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/config/test_library_package_guard.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/config/test_neutral_config.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/config/test_source_resolver.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/conformance/__init__.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/conformance/capabilities.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/conformance/conformance-expected-failures.json +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/conformance/conformance_adapter.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/conformance/corpus.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/conformance/expected_failures.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/conformance/fixture_discovery.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/conformance/navigator.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/conformance/test_api_docs_cross_port_conformance.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/conformance/test_conformance.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/conformance/test_dependency_conformance.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/conformance/test_extract_conformance.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/conformance/test_extract_object_verdict.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/conformance/test_fr010_loader_attrs.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/conformance/test_fr011_attrs.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/conformance/test_generator_registry_conformance.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/conformance/test_object_model_conformance.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/conformance/test_provider_composition_conformance.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/conformance/test_registry_conformance.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/conformance/test_runner_hardfail.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/conformance/test_scope_conformance.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/conformance/test_source_resolution_conformance.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/conformance/test_spec_metamodel_embed.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/conformance/test_strict_attr_load.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/conformance/test_template_codegen_conformance.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/conformance/test_template_generator_conformance.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/conformance/test_yaml_conformance.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/conformance/yaml-conformance-expected-failures.json +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/integration/__init__.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/integration/api_contract_assertions.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/integration/api_contract_jsonb_server.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/integration/api_contract_server.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/integration/generated_jsonb_app.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/integration/generated_router_app.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/integration/generated_tph_app.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/integration/meta_ai_trace.yaml +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/integration/normalization.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/integration/postgres_container.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/integration/query_runner.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/integration/scenarios.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/integration/test_api_contract.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/integration/test_api_contract_generated.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/integration/test_api_contract_jsonb.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/integration/test_api_contract_m2m.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/integration/test_api_contract_m2m_generated.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/integration/test_api_contract_tph_generated.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/integration/test_generated_router_uuid_pk.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/integration/test_llm_call_trace.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/integration/test_normalization.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/integration/test_query_scenarios.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/integration/test_runtime_return_types.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/loader/__init__.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/loader/test_abstract_subtype_authored.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/loader/test_enum_extends_values_conflict.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/loader/test_field_mutability_order.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/loader/test_shipped_library_ai.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/loader/test_template_payload_target_210.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/open_closed_proof_test.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/render/__init__.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/render/extract/__init__.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/render/extract/test_coerce.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/render/extract/test_extract.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/render/extract/test_extract_map.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/render/extract/test_json_forgiving_reader.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/render/extract/test_locate.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/render/extract/test_model.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/render/extract/test_normalize.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/render/extract/test_strip.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/render/extract/test_xml_forgiving_reader.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/render/prompt/__init__.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/render/prompt/test_output_format_renderer.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/render/test_email_document.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/render/test_filesystem_provider.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/render/test_output_format_renderer_nested.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/render/test_output_prompt_conformance.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/render/test_render_conformance.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/render/test_render_max_chars.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/render/test_verify.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/render/test_verify_conformance.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/runtime/test_object_manager_enum_intvaluemap.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/runtime/test_object_manager_table_name.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/source/__init__.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/source/test_fr5c_merge_attribution.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/source/test_fr5d_reference_resolution.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/source/test_fr5e_database_source_shape.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/source/test_json_path.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/source/test_semantic_diff.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/source/test_source_on_node.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/source/test_yaml_positions.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/test_api_docs_accuracy.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/__init__.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_agent_context_staleness.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_capabilities.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_column_naming_strategy.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_common_attrs.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_core_types.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_derive_m2m_declaring_entity.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_effective_children_sibling_shadowing.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_effective_package.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_errors.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_field_enum.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_field_enum_intvaluemap.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_field_map_validation.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_field_uuid_dbcolumntype.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_file_source_id.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_fr016_source_name_and_kind_aliases.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_index_lookup.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_llm_recorder.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_loader.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_loader_bom.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_loader_class.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_merge.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_meta_attr.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_meta_data.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_meta_source.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_module_shortcuts.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_n2m_resolver.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_n2m_resolver_inherited.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_no_dead_config_fields.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_object_manager_autoset_stamping.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_object_manager_uuid_coercion.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_object_manager_write_through.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_one_primary_source.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_parser.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_parser_yaml.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_provider.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_provider_extension.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_registry.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_registry_completeness.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_registry_extend.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_registry_sealed.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_relationship_m2m_validation.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_relationship_references.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_relationship_referential_actions.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_resolution_key.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_runtime_resolution_key_binding.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_serializer.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_serializer_shared_document.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_shared_constants.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_smoke.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_source_escapes.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_sources.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_strict_child_placement.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_super_resolve.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_template_toolcall.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_template_wrong_subtype_attrs.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_validation_attr_schema.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_validation_filter_values.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_validation_origin_195.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_validation_origin_paths.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_validation_sort_field.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_validation_warnings.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_write_coercion_arrays.py +0 -0
- {metaobjects-1.0.4 → metaobjects-1.0.5rc3}/tests/unit/test_yaml_desugar.py +0 -0
- {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.
|
|
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.
|
|
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
|
|
11
|
-
|
|
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
|
-
"
|
|
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
|
|
68
|
-
"""``
|
|
69
|
-
|
|
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
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
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:
|
{metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/generators/m2m_codegen.py
RENAMED
|
@@ -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=
|
|
195
|
-
target_plural=
|
|
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
|
{metaobjects-1.0.4 → metaobjects-1.0.5rc3}/src/metaobjects/codegen/generators/router_generator.py
RENAMED
|
@@ -34,7 +34,7 @@ from __future__ import annotations
|
|
|
34
34
|
|
|
35
35
|
from dataclasses import dataclass
|
|
36
36
|
|
|
37
|
-
from metaobjects.apidocs.naming import
|
|
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}
|
|
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}
|
|
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(
|
|
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 =
|
|
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 =
|
|
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:
|
|
13
|
-
#
|
|
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"
|