metaobjects 0.9.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. metaobjects/__init__.py +75 -0
  2. metaobjects/agent_context/__init__.py +55 -0
  3. metaobjects/agent_context/_content/README.md +14 -0
  4. metaobjects/agent_context/_content/servers/csharp.meta.json +5 -0
  5. metaobjects/agent_context/_content/servers/java.meta.json +5 -0
  6. metaobjects/agent_context/_content/servers/kotlin.meta.json +5 -0
  7. metaobjects/agent_context/_content/servers/python.meta.json +5 -0
  8. metaobjects/agent_context/_content/servers/typescript.meta.json +5 -0
  9. metaobjects/agent_context/_content/skills/metaobjects-authoring/SKILL.md +301 -0
  10. metaobjects/agent_context/_content/skills/metaobjects-codegen/SKILL.md +99 -0
  11. metaobjects/agent_context/_content/skills/metaobjects-codegen/references/csharp.md +87 -0
  12. metaobjects/agent_context/_content/skills/metaobjects-codegen/references/java.md +94 -0
  13. metaobjects/agent_context/_content/skills/metaobjects-codegen/references/kotlin.md +110 -0
  14. metaobjects/agent_context/_content/skills/metaobjects-codegen/references/typescript.md +135 -0
  15. metaobjects/agent_context/_content/skills/metaobjects-prompts/SKILL.md +148 -0
  16. metaobjects/agent_context/_content/skills/metaobjects-prompts/references/csharp.md +110 -0
  17. metaobjects/agent_context/_content/skills/metaobjects-prompts/references/java.md +108 -0
  18. metaobjects/agent_context/_content/skills/metaobjects-prompts/references/kotlin.md +130 -0
  19. metaobjects/agent_context/_content/skills/metaobjects-prompts/references/python.md +116 -0
  20. metaobjects/agent_context/_content/skills/metaobjects-prompts/references/typescript.md +150 -0
  21. metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/SKILL.md +130 -0
  22. metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/java.md +96 -0
  23. metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/kotlin.md +99 -0
  24. metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/react.md +86 -0
  25. metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/tanstack.md +119 -0
  26. metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/typescript.md +92 -0
  27. metaobjects/agent_context/_content/skills/metaobjects-verify/SKILL.md +107 -0
  28. metaobjects/agent_context/_content/skills/metaobjects-verify/references/migration.md +72 -0
  29. metaobjects/agent_context/_content/templates/always-on.md.mustache +27 -0
  30. metaobjects/agent_context/assemble.py +133 -0
  31. metaobjects/agent_context/content_root.py +54 -0
  32. metaobjects/agent_context/scaffold.py +191 -0
  33. metaobjects/agent_context/types.py +44 -0
  34. metaobjects/attr_class_map.py +23 -0
  35. metaobjects/cli.py +696 -0
  36. metaobjects/codegen/__init__.py +0 -0
  37. metaobjects/codegen/config.py +11 -0
  38. metaobjects/codegen/constants.py +13 -0
  39. metaobjects/codegen/extract_delegate_emitter.py +384 -0
  40. metaobjects/codegen/extract_schema_emitter.py +139 -0
  41. metaobjects/codegen/format.py +31 -0
  42. metaobjects/codegen/fr010_field_mapping.py +220 -0
  43. metaobjects/codegen/generator.py +62 -0
  44. metaobjects/codegen/generator_registry.py +163 -0
  45. metaobjects/codegen/generators/__init__.py +0 -0
  46. metaobjects/codegen/generators/entity_model.py +263 -0
  47. metaobjects/codegen/generators/extractor_generator.py +317 -0
  48. metaobjects/codegen/generators/filter_allowlist_generator.py +309 -0
  49. metaobjects/codegen/generators/m2m_codegen.py +192 -0
  50. metaobjects/codegen/generators/output_parser_generator.py +272 -0
  51. metaobjects/codegen/generators/output_prompt_generator.py +192 -0
  52. metaobjects/codegen/generators/payload_vo_generator.py +672 -0
  53. metaobjects/codegen/generators/render_helper_generator.py +451 -0
  54. metaobjects/codegen/generators/router_generator.py +635 -0
  55. metaobjects/codegen/generators/template_generator.py +70 -0
  56. metaobjects/codegen/generators/tph_plan.py +120 -0
  57. metaobjects/codegen/generators/trace_helper_generator.py +336 -0
  58. metaobjects/codegen/instance_artifacts.py +15 -0
  59. metaobjects/codegen/output_format_spec_emitter.py +79 -0
  60. metaobjects/codegen/overwrite_policy.py +27 -0
  61. metaobjects/codegen/runner.py +110 -0
  62. metaobjects/codegen/runtime/__init__.py +6 -0
  63. metaobjects/codegen/runtime/filter_parser.py +193 -0
  64. metaobjects/codegen/type_map.py +84 -0
  65. metaobjects/core_types.py +809 -0
  66. metaobjects/datatype.py +19 -0
  67. metaobjects/documentation/__init__.py +28 -0
  68. metaobjects/documentation/doc_constants.py +20 -0
  69. metaobjects/documentation/doc_provider.py +20 -0
  70. metaobjects/documentation/doc_schema.py +24 -0
  71. metaobjects/errors.py +124 -0
  72. metaobjects/loader/__init__.py +0 -0
  73. metaobjects/loader/merge.py +287 -0
  74. metaobjects/loader/meta_data_loader.py +245 -0
  75. metaobjects/loader/sources/__init__.py +24 -0
  76. metaobjects/loader/sources/directory_source.py +50 -0
  77. metaobjects/loader/sources/file_source.py +41 -0
  78. metaobjects/loader/sources/meta_data_source.py +67 -0
  79. metaobjects/loader/sources/uri_source.py +56 -0
  80. metaobjects/loader/validate_discriminator.py +181 -0
  81. metaobjects/loader/validate_field_readonly.py +146 -0
  82. metaobjects/loader/validate_source_parameter_ref.py +159 -0
  83. metaobjects/loader/validate_source_physical_names.py +140 -0
  84. metaobjects/loader/validation_passes.py +1513 -0
  85. metaobjects/meta/__init__.py +1 -0
  86. metaobjects/meta/core/__init__.py +0 -0
  87. metaobjects/meta/core/attr/__init__.py +0 -0
  88. metaobjects/meta/core/attr/attr_constants.py +31 -0
  89. metaobjects/meta/core/attr/meta_attr.py +136 -0
  90. metaobjects/meta/core/field/__init__.py +0 -0
  91. metaobjects/meta/core/field/field_constants.py +105 -0
  92. metaobjects/meta/core/field/meta_field.py +76 -0
  93. metaobjects/meta/core/identity/__init__.py +0 -0
  94. metaobjects/meta/core/identity/identity_constants.py +19 -0
  95. metaobjects/meta/core/identity/meta_identity.py +8 -0
  96. metaobjects/meta/core/object/__init__.py +0 -0
  97. metaobjects/meta/core/object/meta_object.py +65 -0
  98. metaobjects/meta/core/object/meta_object_aware.py +43 -0
  99. metaobjects/meta/core/object/object_class_registry.py +56 -0
  100. metaobjects/meta/core/object/object_constants.py +13 -0
  101. metaobjects/meta/core/object/object_extract.py +400 -0
  102. metaobjects/meta/core/object/value_object.py +70 -0
  103. metaobjects/meta/core/relationship/__init__.py +0 -0
  104. metaobjects/meta/core/relationship/derive_m2m_fields.py +180 -0
  105. metaobjects/meta/core/relationship/meta_relationship.py +54 -0
  106. metaobjects/meta/core/relationship/relationship_constants.py +51 -0
  107. metaobjects/meta/core/validator/__init__.py +0 -0
  108. metaobjects/meta/core/validator/validator_constants.py +18 -0
  109. metaobjects/meta/meta_data.py +206 -0
  110. metaobjects/meta/meta_root.py +8 -0
  111. metaobjects/meta/persistence/__init__.py +0 -0
  112. metaobjects/meta/persistence/db/__init__.py +1 -0
  113. metaobjects/meta/persistence/db/db_constants.py +41 -0
  114. metaobjects/meta/persistence/db/db_provider.py +60 -0
  115. metaobjects/meta/persistence/origin/__init__.py +0 -0
  116. metaobjects/meta/persistence/origin/meta_origin.py +8 -0
  117. metaobjects/meta/persistence/origin/origin_constants.py +20 -0
  118. metaobjects/meta/persistence/source/__init__.py +0 -0
  119. metaobjects/meta/persistence/source/meta_source.py +137 -0
  120. metaobjects/meta/persistence/source/source_constants.py +115 -0
  121. metaobjects/meta/presentation/__init__.py +0 -0
  122. metaobjects/meta/presentation/layout/__init__.py +0 -0
  123. metaobjects/meta/presentation/layout/layout_constants.py +13 -0
  124. metaobjects/meta/presentation/layout/meta_layout.py +8 -0
  125. metaobjects/meta/presentation/view/__init__.py +0 -0
  126. metaobjects/meta/presentation/view/meta_view.py +8 -0
  127. metaobjects/meta/presentation/view/view_constants.py +22 -0
  128. metaobjects/meta/template/__init__.py +0 -0
  129. metaobjects/meta/template/meta_template.py +46 -0
  130. metaobjects/meta/template/template_constants.py +112 -0
  131. metaobjects/meta/template/template_provider.py +43 -0
  132. metaobjects/parser.py +380 -0
  133. metaobjects/parser_yaml.py +82 -0
  134. metaobjects/provider.py +111 -0
  135. metaobjects/py.typed +0 -0
  136. metaobjects/registry.py +210 -0
  137. metaobjects/registry_manifest.py +223 -0
  138. metaobjects/render/__init__.py +74 -0
  139. metaobjects/render/email_document.py +14 -0
  140. metaobjects/render/escapers.py +109 -0
  141. metaobjects/render/extract/__init__.py +59 -0
  142. metaobjects/render/extract/coerce.py +279 -0
  143. metaobjects/render/extract/extract.py +211 -0
  144. metaobjects/render/extract/extract_map.py +61 -0
  145. metaobjects/render/extract/json_forgiving_reader.py +203 -0
  146. metaobjects/render/extract/locate.py +65 -0
  147. metaobjects/render/extract/normalize.py +96 -0
  148. metaobjects/render/extract/strip.py +20 -0
  149. metaobjects/render/extract/types.py +332 -0
  150. metaobjects/render/extract/xml_forgiving_reader.py +162 -0
  151. metaobjects/render/filesystem_provider.py +51 -0
  152. metaobjects/render/prompt/__init__.py +32 -0
  153. metaobjects/render/prompt/output_format_renderer.py +340 -0
  154. metaobjects/render/prompt/output_format_spec.py +28 -0
  155. metaobjects/render/prompt/prompt_field.py +29 -0
  156. metaobjects/render/prompt/prompt_overrides.py +29 -0
  157. metaobjects/render/prompt/prompt_style.py +38 -0
  158. metaobjects/render/renderer.py +358 -0
  159. metaobjects/render/verify.py +266 -0
  160. metaobjects/runtime/__init__.py +39 -0
  161. metaobjects/runtime/llm_recorder.py +210 -0
  162. metaobjects/runtime/n2m_resolver.py +155 -0
  163. metaobjects/runtime/object_manager.py +715 -0
  164. metaobjects/runtime/tph.py +50 -0
  165. metaobjects/serializer_json.py +172 -0
  166. metaobjects/shared/__init__.py +0 -0
  167. metaobjects/shared/base_types.py +16 -0
  168. metaobjects/shared/separators.py +4 -0
  169. metaobjects/shared/structural.py +9 -0
  170. metaobjects/source/__init__.py +79 -0
  171. metaobjects/source/error_source.py +266 -0
  172. metaobjects/source/json_path.py +106 -0
  173. metaobjects/source/semantic_diff.py +98 -0
  174. metaobjects/source/yaml_positions.py +174 -0
  175. metaobjects/super_resolve.py +128 -0
  176. metaobjects/yaml_desugar.py +481 -0
  177. metaobjects-0.9.0.dist-info/METADATA +97 -0
  178. metaobjects-0.9.0.dist-info/RECORD +181 -0
  179. metaobjects-0.9.0.dist-info/WHEEL +4 -0
  180. metaobjects-0.9.0.dist-info/entry_points.txt +2 -0
  181. metaobjects-0.9.0.dist-info/licenses/LICENSE +189 -0
@@ -0,0 +1,19 @@
1
+ """Coarse value-type classification shared across nodes."""
2
+ from __future__ import annotations
3
+
4
+ from enum import Enum
5
+
6
+
7
+ class DataType(str, Enum):
8
+ STRING = "string"
9
+ INT = "int"
10
+ LONG = "long"
11
+ DOUBLE = "double"
12
+ # DECIMAL is the exact, Decimal-preserving numeric type (NUMERIC columns).
13
+ # Distinct from DOUBLE so field.decimal surfaces a native ``Decimal`` (lossless),
14
+ # never a lossy float — see ADR-0019 + the SP-D runtime return-type contract.
15
+ DECIMAL = "decimal"
16
+ BOOLEAN = "boolean"
17
+ DATE = "date"
18
+ OBJECT = "object"
19
+ STRING_ARRAY = "stringArray"
@@ -0,0 +1,28 @@
1
+ """Documentation provider — 7 universal common attrs (cross-language parity)."""
2
+ from __future__ import annotations
3
+
4
+ from .doc_constants import (
5
+ DOC_ATTR_ALIASES,
6
+ DOC_ATTR_DEPRECATED,
7
+ DOC_ATTR_DESCRIPTION,
8
+ DOC_ATTR_NAMES,
9
+ DOC_ATTR_NOTES,
10
+ DOC_ATTR_REPLACED_BY,
11
+ DOC_ATTR_SEE_ALSO,
12
+ DOC_ATTR_TITLE,
13
+ )
14
+ from .doc_provider import doc_provider
15
+ from .doc_schema import common_doc_attrs
16
+
17
+ __all__ = [
18
+ "DOC_ATTR_DESCRIPTION",
19
+ "DOC_ATTR_TITLE",
20
+ "DOC_ATTR_NOTES",
21
+ "DOC_ATTR_DEPRECATED",
22
+ "DOC_ATTR_REPLACED_BY",
23
+ "DOC_ATTR_SEE_ALSO",
24
+ "DOC_ATTR_ALIASES",
25
+ "DOC_ATTR_NAMES",
26
+ "common_doc_attrs",
27
+ "doc_provider",
28
+ ]
@@ -0,0 +1,20 @@
1
+ """Cross-language doc-attr constants. Bare strings (identical to TS/C#/Java)."""
2
+ from __future__ import annotations
3
+
4
+ DOC_ATTR_DESCRIPTION = "description"
5
+ DOC_ATTR_TITLE = "title"
6
+ DOC_ATTR_NOTES = "notes"
7
+ DOC_ATTR_DEPRECATED = "deprecated"
8
+ DOC_ATTR_REPLACED_BY = "replacedBy"
9
+ DOC_ATTR_SEE_ALSO = "seeAlso"
10
+ DOC_ATTR_ALIASES = "aliases"
11
+
12
+ DOC_ATTR_NAMES: tuple[str, ...] = (
13
+ DOC_ATTR_DESCRIPTION,
14
+ DOC_ATTR_TITLE,
15
+ DOC_ATTR_NOTES,
16
+ DOC_ATTR_DEPRECATED,
17
+ DOC_ATTR_REPLACED_BY,
18
+ DOC_ATTR_SEE_ALSO,
19
+ DOC_ATTR_ALIASES,
20
+ )
@@ -0,0 +1,20 @@
1
+ """The DocumentationProvider — registers the 7 common doc attrs on every metatype."""
2
+ from __future__ import annotations
3
+
4
+ from ..provider import Provider
5
+ from ..registry import TypeRegistry
6
+ from .doc_schema import common_doc_attrs
7
+
8
+
9
+ class _DocProvider(Provider):
10
+ """Subclass of Provider that also registers common doc attrs into the registry."""
11
+
12
+ def register_types(self, registry: TypeRegistry) -> None:
13
+ super().register_types(registry)
14
+ registry.register_common_attrs(common_doc_attrs)
15
+
16
+
17
+ doc_provider = _DocProvider(
18
+ "metaobjects-documentation",
19
+ dependencies=("metaobjects-core-types",),
20
+ )
@@ -0,0 +1,24 @@
1
+ """The 7 universal doc common attrs as AttrSchema records."""
2
+ from __future__ import annotations
3
+
4
+ from ..meta.core.attr.attr_constants import ATTR_SUBTYPE_STRING
5
+ from ..registry import AttrSchema
6
+ from .doc_constants import (
7
+ DOC_ATTR_ALIASES,
8
+ DOC_ATTR_DEPRECATED,
9
+ DOC_ATTR_DESCRIPTION,
10
+ DOC_ATTR_NOTES,
11
+ DOC_ATTR_REPLACED_BY,
12
+ DOC_ATTR_SEE_ALSO,
13
+ DOC_ATTR_TITLE,
14
+ )
15
+
16
+ common_doc_attrs: list[AttrSchema] = [
17
+ AttrSchema(name=DOC_ATTR_DESCRIPTION, value_type=ATTR_SUBTYPE_STRING, required=False),
18
+ AttrSchema(name=DOC_ATTR_TITLE, value_type=ATTR_SUBTYPE_STRING, required=False),
19
+ AttrSchema(name=DOC_ATTR_NOTES, value_type=ATTR_SUBTYPE_STRING, required=False),
20
+ AttrSchema(name=DOC_ATTR_DEPRECATED, value_type=ATTR_SUBTYPE_STRING, required=False),
21
+ AttrSchema(name=DOC_ATTR_REPLACED_BY, value_type=ATTR_SUBTYPE_STRING, required=False),
22
+ AttrSchema(name=DOC_ATTR_SEE_ALSO, value_type=ATTR_SUBTYPE_STRING, required=False, is_array=True),
23
+ AttrSchema(name=DOC_ATTR_ALIASES, value_type=ATTR_SUBTYPE_STRING, required=False, is_array=True),
24
+ ]
metaobjects/errors.py ADDED
@@ -0,0 +1,124 @@
1
+ """Stable error/warning vocabulary. Codes (not messages) are the conformance contract."""
2
+ from __future__ import annotations
3
+
4
+ from enum import Enum
5
+ from typing import TYPE_CHECKING, Optional
6
+
7
+ if TYPE_CHECKING: # avoid runtime import cycles
8
+ from .source import ErrorSource
9
+
10
+
11
+ class ErrorCode(str, Enum):
12
+ ERR_MALFORMED_JSON = "ERR_MALFORMED_JSON"
13
+ ERR_TOP_LEVEL_NOT_OBJECT = "ERR_TOP_LEVEL_NOT_OBJECT"
14
+ ERR_UNKNOWN_TYPE = "ERR_UNKNOWN_TYPE"
15
+ ERR_UNKNOWN_SUBTYPE = "ERR_UNKNOWN_SUBTYPE"
16
+ ERR_MISSING_SUBTYPE = "ERR_MISSING_SUBTYPE"
17
+ ERR_DUPLICATE_NAME = "ERR_DUPLICATE_NAME"
18
+ ERR_UNRESOLVED_SUPER = "ERR_UNRESOLVED_SUPER"
19
+ ERR_INVALID_SUBTYPE_CHILD = "ERR_INVALID_SUBTYPE_CHILD"
20
+ ERR_UNKNOWN_ATTR = "ERR_UNKNOWN_ATTR"
21
+ ERR_MISSING_REQUIRED_ATTR = "ERR_MISSING_REQUIRED_ATTR"
22
+ ERR_BAD_ATTR_VALUE = "ERR_BAD_ATTR_VALUE"
23
+ ERR_BAD_DEFAULT_SORT_FIELD = "ERR_BAD_DEFAULT_SORT_FIELD"
24
+ ERR_PROVIDER_DEPENDENCY_CYCLE = "ERR_PROVIDER_DEPENDENCY_CYCLE"
25
+ ERR_PROVIDER_DUPLICATE_ID = "ERR_PROVIDER_DUPLICATE_ID"
26
+ ERR_PROVIDER_MISSING_DEPENDENCY = "ERR_PROVIDER_MISSING_DEPENDENCY"
27
+ ERR_PROVIDER_ATTR_CONFLICT = "ERR_PROVIDER_ATTR_CONFLICT"
28
+ ERR_SUBTYPE_RULE_VIOLATION = "ERR_SUBTYPE_RULE_VIOLATION"
29
+ ERR_OVERLAY_NO_TARGET = "ERR_OVERLAY_NO_TARGET"
30
+ # FR5c — two contributing files set the same @attr to different non-empty
31
+ # values on the same node. Carries a `MergedSource` envelope with both
32
+ # contributors listed (ADR-0009 §Overlay-merge).
33
+ ERR_MERGE_CONFLICT = "ERR_MERGE_CONFLICT"
34
+ ERR_MALFORMED_YAML = "ERR_MALFORMED_YAML"
35
+ # YAML 1.2 silently coerced an unquoted scalar to a type incompatible with the
36
+ # declared attr valueType (ADR-0006 D2). Authors should quote the value.
37
+ ERR_YAML_COERCION = "ERR_YAML_COERCION"
38
+ ERR_INVALID_ORIGIN = "ERR_INVALID_ORIGIN"
39
+ # FR-017 — M:N relationship slim-vocabulary validation (junction-missing-two-
40
+ # references / sourceRefField-not-matching / M:N-attr-on-1:N). The symmetric-
41
+ # on-hetero + symmetric+sourceRefField rules emit ERR_BAD_ATTR_VALUE instead.
42
+ ERR_INVALID_RELATIONSHIP = "ERR_INVALID_RELATIONSHIP"
43
+ ERR_BAD_ATTR_FILTER = "ERR_BAD_ATTR_FILTER"
44
+ # Reserved structural body key authored as an @-attr (source-v2 / ADR-0007).
45
+ ERR_RESERVED_ATTR = "ERR_RESERVED_ATTR"
46
+ # Source-v2 multi-source one-primary rule (ADR-0007).
47
+ ERR_SOURCE_NO_PRIMARY = "ERR_SOURCE_NO_PRIMARY"
48
+ ERR_SOURCE_MULTIPLE_PRIMARY = "ERR_SOURCE_MULTIPLE_PRIMARY"
49
+ # FR-016 / ADR-0018 — per-kind physical-name aliases on source.rdb.
50
+ ERR_PHYSICAL_NAME_KIND_MISMATCH = "ERR_PHYSICAL_NAME_KIND_MISMATCH"
51
+ ERR_PHYSICAL_NAME_MULTIPLE = "ERR_PHYSICAL_NAME_MULTIPLE"
52
+ # FR-013 — field-level @readOnly cross-attribute rules. Cross-language
53
+ # vocabulary; Python loader does not emit these yet (FR-013 Python fan-out
54
+ # is a separate workstream), but the enum tracks the shared corpus codes.
55
+ ERR_READONLY_ASSIGNED_PRIMARY = "ERR_READONLY_ASSIGNED_PRIMARY"
56
+ ERR_READONLY_DOWNGRADE = "ERR_READONLY_DOWNGRADE"
57
+ # FR-015 — source.rdb @parameterRef typed-input validation. Cross-language
58
+ # vocabulary; Python loader does not emit these yet, but the enum tracks
59
+ # the shared corpus codes.
60
+ ERR_PARAMETER_REF_UNRESOLVED = "ERR_PARAMETER_REF_UNRESOLVED"
61
+ ERR_PARAMETER_REF_NOT_VALUE_OBJECT = "ERR_PARAMETER_REF_NOT_VALUE_OBJECT"
62
+ ERR_PARAMETER_REF_ON_NON_CALLABLE_KIND = "ERR_PARAMETER_REF_ON_NON_CALLABLE_KIND"
63
+ ERR_PARAMETER_REF_PASSTHROUGH_TYPE_MISMATCH = "ERR_PARAMETER_REF_PASSTHROUGH_TYPE_MISMATCH"
64
+ # FR-014 — TPH discriminator cross-attribute validation. Cross-language
65
+ # vocabulary; Python loader does not emit these yet.
66
+ ERR_DISCRIMINATOR_FIELD_NOT_FOUND = "ERR_DISCRIMINATOR_FIELD_NOT_FOUND"
67
+ ERR_DISCRIMINATOR_VALUE_DUPLICATE = "ERR_DISCRIMINATOR_VALUE_DUPLICATE"
68
+ ERR_DISCRIMINATOR_VALUE_MISSING = "ERR_DISCRIMINATOR_VALUE_MISSING"
69
+ ERR_DISCRIMINATOR_VALUE_TYPE_MISMATCH = "ERR_DISCRIMINATOR_VALUE_TYPE_MISMATCH"
70
+ # Cross-language vocabulary for features other ports added (FR-003 storage, FR-004 template);
71
+ # the Python loader does not emit these yet, but the enum tracks the shared corpus codes.
72
+ ERR_INVALID_TEMPLATE = "ERR_INVALID_TEMPLATE"
73
+ ERR_STORAGE_FLATTENED_ARRAY = "ERR_STORAGE_FLATTENED_ARRAY"
74
+ ERR_STORAGE_WITHOUT_OBJECT_REF = "ERR_STORAGE_WITHOUT_OBJECT_REF"
75
+ # ADR-0013: a field.object REQUIRES @objectRef (open/untyped JSON uses the
76
+ # physical @dbColumnType: jsonb escape hatch on field.string).
77
+ ERR_OBJECT_FIELD_WITHOUT_OBJECT_REF = "ERR_OBJECT_FIELD_WITHOUT_OBJECT_REF"
78
+ ERR_PARTIAL_UNRESOLVED = "ERR_PARTIAL_UNRESOLVED"
79
+ ERR_REQUIRED_SLOT_UNUSED = "ERR_REQUIRED_SLOT_UNUSED"
80
+ ERR_VAR_NOT_ON_PAYLOAD = "ERR_VAR_NOT_ON_PAYLOAD"
81
+ ERR_OUTPUT_TAG_MISSING = "ERR_OUTPUT_TAG_MISSING"
82
+ # SP-H Unit9 — @filterable: true on a field subtype with no filter-operator
83
+ # band (e.g. field.object). Would silently generate an empty-ops filter.
84
+ ERR_FILTERABLE_UNSUPPORTED_SUBTYPE = "ERR_FILTERABLE_UNSUPPORTED_SUBTYPE"
85
+ # ADR-0023 — a registration was attempted against a registry sealed after its
86
+ # agreed metamodel-provider bootstrap. Codegen cannot invent metamodel attrs.
87
+ ERR_REGISTRY_SEALED = "ERR_REGISTRY_SEALED"
88
+ ERR_UNKNOWN = "ERR_UNKNOWN"
89
+
90
+
91
+ class MetaError:
92
+ """A loader error. ``code`` is the conformance-compared value; ``message`` is human text.
93
+
94
+ FR5a / ADR-0009: ``envelope`` is the structured provenance envelope every
95
+ cross-language port emits — populated by the parser (JSON tree-walk) and by
96
+ validation passes that have access to a node's ``source``. Legacy ``source``
97
+ (the file path) / ``path`` remain for backward-compat (the conformance
98
+ adapter only inspects ``code``); new sites should pass ``envelope``.
99
+ """
100
+
101
+ def __init__(
102
+ self,
103
+ message: str,
104
+ code: ErrorCode = ErrorCode.ERR_UNKNOWN,
105
+ source: str | None = None,
106
+ path: str | None = None,
107
+ envelope: Optional[ErrorSource] = None,
108
+ ) -> None:
109
+ self.message = message
110
+ self.code = code
111
+ self.source = source
112
+ self.path = path
113
+ self.envelope = envelope
114
+
115
+ def __repr__(self) -> str:
116
+ return f"MetaError({self.code.name}: {self.message!r})"
117
+
118
+
119
+ class ParseError(Exception):
120
+ """Raised by the parser in strict mode; carries a code."""
121
+
122
+ def __init__(self, message: str, code: ErrorCode = ErrorCode.ERR_UNKNOWN) -> None:
123
+ super().__init__(message)
124
+ self.code = code
File without changes
@@ -0,0 +1,287 @@
1
+ """Multi-file / overlay merge: fold parsed roots into one (post-parse, pre-super-resolve).
2
+
3
+ FR5c — the merge phase is the cross-port attribution hub. Tracks which
4
+ files contributed to each post-merge node and, on each ``_merge_into``
5
+ call:
6
+
7
+ 1. **ERR_MERGE_CONFLICT** — same ``@attr`` name set with different
8
+ non-empty values on both contributors → hard error with a
9
+ :class:`MergedSource` envelope listing both files and the conflicting
10
+ attr's jsonPath. Last-writer-wins still applies to the merged tree.
11
+ 2. **MergedSource upgrade** — when the new contributor actually changed
12
+ the post-merge canonical (semantic_diff returns True), the target's
13
+ ``source`` envelope is upgraded to :class:`MergedSource` with
14
+ alphabetically-sorted ``files``/``contributors`` (overlay-base for
15
+ the first file, overlay-extension for the rest).
16
+ 3. **WARN_DUPLICATE_DECLARATION** — when the new contributor's content
17
+ produced no semantic change AND the new file isn't already a
18
+ contributor, emit a warning (string channel + envelope channel).
19
+ The node's source is NOT upgraded — the warning surfaces the
20
+ redundancy.
21
+
22
+ Cross-port contract (TS reference ``parser-core.ts``):
23
+ * Alphabetical file order across contributors[] (matches DirectorySource
24
+ sorting).
25
+ * Single error code ``ERR_MERGE_CONFLICT``; per-attr provenance deferred.
26
+ * "Conflict" = both sides set, both non-empty, values differ. Empty
27
+ string and absent are NOT conflicts.
28
+ """
29
+ from __future__ import annotations
30
+
31
+ import json
32
+ from typing import Optional
33
+
34
+ from ..errors import ErrorCode, MetaError
35
+ from ..meta.meta_data import MetaData
36
+ from ..serializer_json import canonical_serialize
37
+ from ..source import (
38
+ Contributor,
39
+ ErrorSource,
40
+ JsonSource,
41
+ LoaderWarning,
42
+ MergedSource,
43
+ WARN_DUPLICATE_DECLARATION,
44
+ YamlSource,
45
+ )
46
+ from ..source.semantic_diff import semantic_diff
47
+
48
+
49
+ def merge_roots(
50
+ roots: list[MetaData],
51
+ errors: list[MetaError],
52
+ warnings: Optional[list[str]] = None,
53
+ envelope_warnings: Optional[list[LoaderWarning]] = None,
54
+ ) -> MetaData:
55
+ """Merge all roots into the first. Returns the merged root (or raises if empty).
56
+
57
+ FR5c — *warnings* and *envelope_warnings*, when provided, receive
58
+ :data:`WARN_DUPLICATE_DECLARATION` messages for duplicate-with-no-change
59
+ contributions. They default to throwaway lists to preserve the prior
60
+ `merge_roots(roots, errors)` two-arg signature for any in-tree caller
61
+ that hasn't been updated yet.
62
+ """
63
+ if not roots:
64
+ raise ValueError("merge_roots requires at least one root")
65
+ if warnings is None:
66
+ warnings = []
67
+ if envelope_warnings is None:
68
+ envelope_warnings = []
69
+ target = roots[0]
70
+ for src in roots[1:]:
71
+ _merge_into(target, src, errors, warnings, envelope_warnings)
72
+ return target
73
+
74
+
75
+ def _source_files(env: ErrorSource) -> tuple[str, ...]:
76
+ """Extract the ``files`` tuple from any envelope that carries one.
77
+
78
+ Returns an empty tuple for code / database envelopes (no file context).
79
+ """
80
+ if isinstance(env, (JsonSource, YamlSource, MergedSource)):
81
+ return tuple(env.files)
82
+ return ()
83
+
84
+
85
+ def _source_json_path(env: ErrorSource) -> Optional[str]:
86
+ """Extract the ``json_path`` from any envelope that carries one."""
87
+ if isinstance(env, (JsonSource, YamlSource, MergedSource)):
88
+ return env.json_path
89
+ return None
90
+
91
+
92
+ def _build_contributors(files: tuple[str, ...]) -> tuple[Contributor, ...]:
93
+ """Build a contributors tuple — first file is overlay-base, rest are
94
+ overlay-extension. *files* must already be deduplicated + sorted.
95
+ """
96
+ return tuple(
97
+ Contributor(
98
+ file=f,
99
+ role="overlay-base" if i == 0 else "overlay-extension",
100
+ )
101
+ for i, f in enumerate(files)
102
+ )
103
+
104
+
105
+ def _is_empty_value(v: object) -> bool:
106
+ """Mirror TS ``isEmptyValue``: ``None``, ``""``, and ``[]`` are empty;
107
+ everything else is set.
108
+ """
109
+ if v is None:
110
+ return True
111
+ if isinstance(v, str) and v == "":
112
+ return True
113
+ if isinstance(v, list) and not v:
114
+ return True
115
+ return False
116
+
117
+
118
+ def _attr_values_equal(a: object, b: object) -> bool:
119
+ """Structural value equality matching TS ``attrValuesEqual``: scalars
120
+ compared by ``==``; lists/dicts compared key-order-independently via a
121
+ canonical-key-sorted JSON dump (the same substrate the canonical
122
+ serializer + semantic_diff use).
123
+ """
124
+ if a == b:
125
+ return True
126
+ if isinstance(a, (list, dict)) and isinstance(b, (list, dict)):
127
+ try:
128
+ return json.dumps(a, sort_keys=True) == json.dumps(b, sort_keys=True)
129
+ except (TypeError, ValueError):
130
+ return False
131
+ return False
132
+
133
+
134
+ def _detect_attr_merge_conflicts(
135
+ target: MetaData,
136
+ src: MetaData,
137
+ errors: list[MetaError],
138
+ ) -> None:
139
+ """FR5c — for every own @-attr on *src*, check whether *target* already
140
+ declares the same attr with a different non-empty value. If so, emit an
141
+ ``ERR_MERGE_CONFLICT`` carrying a :class:`MergedSource` envelope naming
142
+ both contributors.
143
+
144
+ The merge itself proceeds (last-writer-wins) so the loader sees one
145
+ canonical tree; the error surfaces the conflict.
146
+ """
147
+ target_files = _source_files(target.source)
148
+ src_files = _source_files(src.source)
149
+ target_json_path = _source_json_path(target.source)
150
+
151
+ pre_attrs: dict[str, object] = {
152
+ attr.name: getattr(attr, "value", None) for attr in target.own_meta_attrs()
153
+ }
154
+
155
+ for src_attr in src.own_meta_attrs():
156
+ attr_name = src_attr.name
157
+ if attr_name not in pre_attrs:
158
+ continue
159
+ existing_val = pre_attrs[attr_name]
160
+ new_val = getattr(src_attr, "value", None)
161
+
162
+ if _is_empty_value(new_val) or _is_empty_value(existing_val):
163
+ continue
164
+ if _attr_values_equal(existing_val, new_val):
165
+ continue
166
+
167
+ # Conflict — build the MergedSource envelope.
168
+ combined = sorted(set(target_files) | set(src_files))
169
+ conflict_files = tuple(combined)
170
+ attr_path = (
171
+ f"{target_json_path}.@{attr_name}"
172
+ if target_json_path
173
+ else f"@{attr_name}"
174
+ )
175
+ envelope = MergedSource(
176
+ files=conflict_files,
177
+ json_path=attr_path,
178
+ contributors=_build_contributors(conflict_files),
179
+ )
180
+ errors.append(
181
+ MetaError(
182
+ f"attr '@{attr_name}' conflicts: existing value "
183
+ f"{json.dumps(existing_val)} differs from new value "
184
+ f"{json.dumps(new_val)} on {target.fqn()}",
185
+ ErrorCode.ERR_MERGE_CONFLICT,
186
+ envelope=envelope,
187
+ )
188
+ )
189
+
190
+
191
+ def _merge_into(
192
+ target: MetaData,
193
+ src: MetaData,
194
+ errors: list[MetaError],
195
+ warnings: list[str],
196
+ envelope_warnings: list[LoaderWarning],
197
+ ) -> None:
198
+ """Merge *src*'s own attrs/children into *target* in place.
199
+
200
+ FR5c — runs three diagnostics around the merge:
201
+ 1. ``ERR_MERGE_CONFLICT`` on conflicting @-attrs (before the write).
202
+ 2. ``MergedSource`` upgrade when the merge produced semantic change.
203
+ 3. ``WARN_DUPLICATE_DECLARATION`` when no semantic change occurred
204
+ AND the contributor file is new.
205
+
206
+ The root is intentionally excluded from FR5c diagnostics: it is a
207
+ synthetic accumulator (every file declares ``metadata.root``), not an
208
+ author-meaningful node. The merge attribution applies to
209
+ ``object.entity`` / ``field.*`` / etc.
210
+ """
211
+ is_root = target.parent is None # root has no parent
212
+ fr5c_active = not is_root and target.name != ""
213
+
214
+ pre_canonical: Optional[str] = None
215
+ if fr5c_active:
216
+ pre_canonical = canonical_serialize(target)
217
+ _detect_attr_merge_conflicts(target, src, errors)
218
+
219
+ # attrs: source overwrites target (last-writer-wins)
220
+ for attr in src.own_meta_attrs():
221
+ target.set_attr(attr.name, getattr(attr, "value", None), sub_type=attr.sub_type)
222
+ # children: merge by (type, name), else append
223
+ for sc in src.own_children():
224
+ tc = next(
225
+ (c for c in target.own_children() if c.type == sc.type and c.name == sc.name),
226
+ None,
227
+ )
228
+ if tc is not None:
229
+ _merge_into(tc, sc, errors, warnings, envelope_warnings)
230
+ else:
231
+ if getattr(sc, "is_overlay", False):
232
+ errors.append(
233
+ MetaError(
234
+ f"overlay node '{sc.fqn()}' has no merge target",
235
+ ErrorCode.ERR_OVERLAY_NO_TARGET,
236
+ path=sc.fqn(),
237
+ )
238
+ )
239
+ sc.parent = target
240
+ target.add_child(sc)
241
+
242
+ if not fr5c_active or pre_canonical is None:
243
+ return
244
+
245
+ # Post-merge: compare shapes and upgrade source / emit warning.
246
+ post_canonical = canonical_serialize(target)
247
+ pre_parsed = json.loads(pre_canonical)
248
+ post_parsed = json.loads(post_canonical)
249
+ changed = semantic_diff(pre_parsed, post_parsed)
250
+
251
+ existing_env = target.source
252
+ existing_files = list(_source_files(existing_env))
253
+ src_files = list(_source_files(src.source))
254
+ json_path = _source_json_path(existing_env)
255
+
256
+ new_contributor_file = src_files[0] if src_files else "<unknown>"
257
+
258
+ if changed:
259
+ all_files = sorted(set(existing_files + src_files))
260
+ merged_files = tuple(all_files)
261
+ merged_env = MergedSource(
262
+ files=merged_files,
263
+ json_path=json_path if json_path is not None else "$",
264
+ contributors=_build_contributors(merged_files),
265
+ )
266
+ target.set_source(merged_env)
267
+ elif existing_files and new_contributor_file not in existing_files:
268
+ # Identical re-declaration from a different file → warn.
269
+ all_files = sorted(set(existing_files + [new_contributor_file]))
270
+ warn_files = tuple(all_files)
271
+ warn_env = MergedSource(
272
+ files=warn_files,
273
+ json_path=json_path if json_path is not None else "$",
274
+ contributors=_build_contributors(warn_files),
275
+ )
276
+ message = f"duplicate declaration of {target.fqn()} with no semantic change"
277
+ # Legacy string channel — what the conformance runner checks against
278
+ # expected-warnings.json (a list[str]).
279
+ warnings.append(message)
280
+ # Envelope channel — typed code + source for downstream tooling.
281
+ envelope_warnings.append(
282
+ LoaderWarning(
283
+ code=WARN_DUPLICATE_DECLARATION,
284
+ message=message,
285
+ source=warn_env,
286
+ )
287
+ )