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,481 @@
1
+ """YAML authoring -> canonical desugar (ADR-0006).
2
+
3
+ Mirrors the TS reference at server/typescript/packages/metadata/src/core/yaml-desugar.ts.
4
+
5
+ desugar() turns the sugared authoring object (from yaml.safe_load) into the
6
+ canonical-shaped object that parse_document (parser.py) consumes. It applies
7
+ the five format-spec sugar rules:
8
+
9
+ 1. Fused key, subType omittable - a bare `type` key resolves to the type's
10
+ registry default subType.
11
+ 2. Scalar-or-map body - a scalar body becomes { name: <scalar> }.
12
+ 3. Omit empties - absent keys stay absent; the desugar invents nothing.
13
+ 4. `[]` arrays - a trailing `[]` on the key strips to isArray: true.
14
+ 5. Sigil-free attributes (ADR-0006 D1) - every body key not in
15
+ RESERVED_KEYS is treated as an inline attribute and re-prefixed with `@`
16
+ when lowering to canonical JSON. Keys already prefixed with `@` are
17
+ left as-is (backward-compat). Reserved structural keywords (name,
18
+ package, extends, abstract, overlay, isArray, children, value) stay
19
+ bare. Note: an already-`@`-prefixed reserved word remains an error
20
+ downstream - parser.py's ERR_RESERVED_ATTR check fires.
21
+
22
+ In addition, the desugar runs the ADR-0006 D2 type-coercion guard: for every
23
+ inline attr whose owning (type, subType) has a declared schema, if the raw
24
+ Python value's type does not match the declared valueType AND the value is
25
+ one of YAML 1.2's silently coerced shapes (bool/int/float/None), the desugar
26
+ collects an ERR_YAML_COERCION error telling the author to quote the value.
27
+
28
+ Pure and total: it never throws. Malformed fragments are collected as
29
+ CollectedError entries (a string message + optional stable error code) and a
30
+ safe placeholder is substituted so parse_document does not double-report.
31
+ """
32
+ from __future__ import annotations
33
+
34
+ from dataclasses import dataclass, field
35
+ from typing import Any
36
+
37
+ from .errors import ErrorCode
38
+ from .meta.core.attr.attr_constants import (
39
+ ATTR_SUBTYPE_BOOLEAN,
40
+ ATTR_SUBTYPE_CLASS,
41
+ ATTR_SUBTYPE_DOUBLE,
42
+ ATTR_SUBTYPE_INT,
43
+ ATTR_SUBTYPE_LONG,
44
+ ATTR_SUBTYPE_STRING,
45
+ ATTR_SUBTYPE_STRINGARRAY,
46
+ )
47
+ from .registry import AttrSchema, TypeRegistry
48
+ from .shared.separators import ATTR_PREFIX, FUSED_KEY_SEP
49
+ from .shared.structural import (
50
+ KEY_ABSTRACT,
51
+ KEY_CHILDREN,
52
+ KEY_EXTENDS,
53
+ KEY_IS_ARRAY,
54
+ KEY_NAME,
55
+ KEY_OVERLAY,
56
+ KEY_PACKAGE,
57
+ KEY_VALUE,
58
+ )
59
+ from .source import YamlPosition
60
+ from .source.yaml_positions import (
61
+ YamlPositionMap,
62
+ get_position_map,
63
+ set_position_map,
64
+ )
65
+
66
+ ARRAY_SUFFIX = "[]"
67
+
68
+ RESERVED_KEYS: frozenset[str] = frozenset({
69
+ KEY_NAME,
70
+ KEY_PACKAGE,
71
+ KEY_EXTENDS,
72
+ KEY_ABSTRACT,
73
+ KEY_OVERLAY,
74
+ KEY_IS_ARRAY,
75
+ KEY_CHILDREN,
76
+ KEY_VALUE,
77
+ })
78
+
79
+
80
+ @dataclass
81
+ class CollectedError:
82
+ """A collected desugar problem - message plus optional stable error code.
83
+
84
+ Absent codes map to ERR_MALFORMED_YAML in parser_yaml.py (matches TS).
85
+ """
86
+ message: str
87
+ code: ErrorCode | None = None
88
+
89
+
90
+ @dataclass
91
+ class DesugarResult:
92
+ """Outcome of desugaring a parsed-YAML document."""
93
+ canonical: dict[str, Any] = field(default_factory=dict)
94
+ errors: list[CollectedError] = field(default_factory=list)
95
+
96
+
97
+ def desugar(input_obj: object, registry: TypeRegistry) -> DesugarResult:
98
+ """Desugar a parsed-YAML authoring document into a canonical-shaped object."""
99
+ errors: list[CollectedError] = []
100
+ node = _desugar_node(input_obj, registry, errors, "<root>")
101
+ return DesugarResult(canonical=node if node is not None else {}, errors=errors)
102
+
103
+
104
+ def _desugar_node(
105
+ input_obj: object,
106
+ registry: TypeRegistry,
107
+ errors: list[CollectedError],
108
+ path: str,
109
+ ) -> dict[str, Any] | None:
110
+ """Desugar one node - a single-key mapping { "type.subType": body }.
111
+
112
+ Returns the canonical node object, or None if `input_obj` is not a usable
113
+ node (the caller substitutes a placeholder).
114
+ """
115
+ if not isinstance(input_obj, dict):
116
+ errors.append(CollectedError(
117
+ message=f"Node at {path} must be a mapping with one type key",
118
+ ))
119
+ return None
120
+
121
+ keys = list(input_obj.keys())
122
+ if len(keys) != 1:
123
+ found = "none" if not keys else ", ".join(str(k) for k in keys)
124
+ errors.append(CollectedError(
125
+ message=(
126
+ f"Node at {path} must have exactly one type key "
127
+ f"(found: {found})"
128
+ ),
129
+ ))
130
+ return None
131
+
132
+ raw_key = keys[0]
133
+ if not isinstance(raw_key, str):
134
+ errors.append(CollectedError(
135
+ message=f"Node key at {path} must be a string, got {type(raw_key).__name__}",
136
+ ))
137
+ return None
138
+ raw_body = input_obj[raw_key]
139
+
140
+ # FR5b - capture the wrapper-level position-by-key map BEFORE re-keying.
141
+ # The author's raw key (with `[]` suffix and possibly omitted subType) is
142
+ # the lookup key; the desugar's canonical key is what we emit.
143
+ wrapper_positions = get_position_map(input_obj)
144
+
145
+ # Rule 4: a trailing "[]" on the key -> isArray.
146
+ key = raw_key
147
+ is_array = False
148
+ if key.endswith(ARRAY_SUFFIX):
149
+ key = key[: -len(ARRAY_SUFFIX)]
150
+ is_array = True
151
+
152
+ # Rule 1: a bare `type` key -> the type's registry default subType.
153
+ canonical_key = _resolve_key(key, registry, errors, path)
154
+
155
+ # FR5b - position of the wrapper key (the `field.string:` line). Used to
156
+ # back-fill `yaml_position` on synthesized bodies (Rule 2 scalar lift) and
157
+ # on the canonical wrapper after Rule 1 / Rule 4 rewrites.
158
+ wrapper_key_pos = (
159
+ wrapper_positions.get(raw_key) if wrapper_positions is not None else None
160
+ )
161
+
162
+ # Rule 2: a scalar body -> { name: <scalar> }.
163
+ body = _desugar_body(
164
+ raw_body, registry, canonical_key, errors, path, wrapper_key_pos,
165
+ )
166
+
167
+ # Rule 4 (cont.): stamp isArray onto the canonical body.
168
+ if is_array:
169
+ body[KEY_IS_ARRAY] = True
170
+
171
+ # Recurse into children.
172
+ raw_children = body.get(KEY_CHILDREN)
173
+ if isinstance(raw_children, list):
174
+ children: list[Any] = []
175
+ for i, raw_child in enumerate(raw_children):
176
+ child_path = f"{path}.{KEY_CHILDREN}[{i}]"
177
+ child = _desugar_node(raw_child, registry, errors, child_path)
178
+ # On a bad child keep an empty-object placeholder so sibling
179
+ # indices stay stable; the error is already collected.
180
+ children.append(child if child is not None else {})
181
+ body[KEY_CHILDREN] = children
182
+ # A non-list `children` value is left untouched - parse_document reports it.
183
+
184
+ # FR5b - emit a wrapper-level position-by-key map for the canonical wrapper
185
+ # so parse_document's per-child iteration can read the position via the
186
+ # same lookup it uses for JSON input. The single key transformation is
187
+ # raw_key -> canonical_key (Rule 1 fuses the subType, Rule 4 strips `[]`).
188
+ out_wrapper = YamlPositionMap({canonical_key: body})
189
+ if wrapper_key_pos is not None:
190
+ set_position_map(out_wrapper, {canonical_key: wrapper_key_pos})
191
+ return out_wrapper
192
+
193
+
194
+ def _resolve_key(
195
+ key: str,
196
+ registry: TypeRegistry,
197
+ errors: list[CollectedError],
198
+ path: str,
199
+ ) -> str:
200
+ """Rule 1 - resolve a possibly-bare key to a fused `type.subType` token."""
201
+ if FUSED_KEY_SEP in key:
202
+ return key # already fused
203
+ sub_type = registry.default_sub_type_of(key)
204
+ if sub_type is None:
205
+ errors.append(CollectedError(
206
+ message=(
207
+ f"Cannot resolve subType for bare type key '{key}' at {path} - "
208
+ f"type '{key}' has no default subType; write the full 'type.subType'"
209
+ ),
210
+ ))
211
+ return key # pass through; parse_document reports the unknown type
212
+ return f"{key}{FUSED_KEY_SEP}{sub_type}"
213
+
214
+
215
+ def _desugar_body(
216
+ raw_body: object,
217
+ registry: TypeRegistry,
218
+ canonical_key: str,
219
+ errors: list[CollectedError],
220
+ path: str,
221
+ wrapper_key_pos: YamlPosition | None = None,
222
+ ) -> dict[str, Any]:
223
+ """Rule 2 + 5 - normalize a node body into a canonical mapping.
224
+
225
+ Reserved structural keys stay bare; every other key is treated as an inline
226
+ attribute and `@`-prefixed (Rule 5 / ADR-0006 D1). Keys already starting
227
+ with `@` are kept as-authored so the awkward "@column: foo" form remains
228
+ accepted.
229
+
230
+ Also runs the D2 type-coercion guard: for each inline attr that the owning
231
+ (type, subType) declares with a typed `value_type`, if the raw Python
232
+ value's type was silently coerced by YAML 1.2 to something incompatible
233
+ (e.g. a `bool` for a `string`-declared attr), an ERR_YAML_COERCION is
234
+ collected.
235
+
236
+ FR5b — *wrapper_key_pos* is the YAML position of the wrapper key (the
237
+ ``field.string:`` line) used to back-fill ``yaml_position`` on synthesized
238
+ bodies (Rule 2 scalar lift). Reserved structural keys keep their bare
239
+ form and carry their own positions from the source body's position map;
240
+ sigil-free attrs (Rule 5) re-key the position map across the ``@``-prefix
241
+ rewrite.
242
+ """
243
+ if isinstance(raw_body, str) or isinstance(raw_body, bool) or (
244
+ isinstance(raw_body, (int, float)) and not isinstance(raw_body, bool)
245
+ ):
246
+ # FR5b - the synthesized `{ name: rawBody }` has no YAML-side
247
+ # counterpart; we attribute the `name` slot to the wrapper-key's
248
+ # position (the only YAML position that meaningfully belongs to
249
+ # this synthesis).
250
+ out_scalar = YamlPositionMap({KEY_NAME: raw_body})
251
+ if wrapper_key_pos is not None:
252
+ set_position_map(out_scalar, {KEY_NAME: wrapper_key_pos})
253
+ return out_scalar
254
+ if raw_body is None:
255
+ # An empty body (`field.string:` with nothing after) -> an empty node.
256
+ # No body keys to position; the wrapper carries the node's position.
257
+ return YamlPositionMap()
258
+ if isinstance(raw_body, list):
259
+ errors.append(CollectedError(
260
+ message=f"Node body at {path} must be a scalar or mapping, not a list",
261
+ ))
262
+ return YamlPositionMap()
263
+ if not isinstance(raw_body, dict):
264
+ # Catch-all for non-dict, non-scalar shapes (e.g. tuples).
265
+ errors.append(CollectedError(
266
+ message=f"Node body at {path} must be a scalar or mapping",
267
+ ))
268
+ return YamlPositionMap()
269
+
270
+ # A mapping - shallow-copy so isArray / children replacement do not mutate
271
+ # the caller's parsed-YAML object, AND apply Rule 5 (sigil-free attrs) +
272
+ # Rule D2 (type-coercion guard).
273
+ out = YamlPositionMap()
274
+ schema_index = _attr_schema_index(registry, canonical_key)
275
+ # FR5b - translate the body's position-by-key map across the sigil-free
276
+ # rewrite. A bare `filterable` key in the source maps to `@filterable`
277
+ # in the canonical body; the YAML position belongs to BOTH names (the
278
+ # YAML author only wrote one). We re-key the position map to match
279
+ # the canonical body's keys so parse_document's per-attr inspection
280
+ # can find the position via the canonical key.
281
+ src_positions = get_position_map(raw_body)
282
+ out_positions: dict[str, YamlPosition] = {}
283
+ for key, value in raw_body.items():
284
+ if not isinstance(key, str):
285
+ errors.append(CollectedError(
286
+ message=(
287
+ f"Body key at {path} must be a string, got "
288
+ f"{type(key).__name__}"
289
+ ),
290
+ ))
291
+ continue
292
+
293
+ if key in RESERVED_KEYS or key.startswith(ATTR_PREFIX):
294
+ out[key] = value
295
+ out_key = key
296
+ # D2 also applies to author-written @-keys (the awkward form).
297
+ if key.startswith(ATTR_PREFIX):
298
+ attr_name = key[len(ATTR_PREFIX):]
299
+ if attr_name and attr_name not in RESERVED_KEYS:
300
+ _check_coercion(attr_name, value, schema_index, errors, path)
301
+ else:
302
+ out_key = f"{ATTR_PREFIX}{key}"
303
+ out[out_key] = value
304
+ _check_coercion(key, value, schema_index, errors, path)
305
+
306
+ if src_positions is not None:
307
+ pos = src_positions.get(key)
308
+ if pos is not None:
309
+ out_positions[out_key] = pos
310
+
311
+ if out_positions:
312
+ set_position_map(out, out_positions)
313
+ return out
314
+
315
+
316
+ # ---------------------------------------------------------------------------
317
+ # D2 - YAML type-coercion guard
318
+ # ---------------------------------------------------------------------------
319
+
320
+
321
+ def _attr_schema_index(
322
+ registry: TypeRegistry,
323
+ canonical_key: str,
324
+ ) -> dict[str, AttrSchema] | None:
325
+ """Build a name -> AttrSchema map for the given canonical key (type.subType).
326
+
327
+ Returns None when the key has no declared attrs (open schema).
328
+ """
329
+ # canonical_key is "type.subType" - split on the FIRST dot only so a subType
330
+ # that happens to contain a dot still works.
331
+ dot = canonical_key.find(FUSED_KEY_SEP)
332
+ if dot < 0:
333
+ return None
334
+ type_ = canonical_key[:dot]
335
+ sub_type = canonical_key[dot + 1:]
336
+ schemas = registry.attrs_of(type_, sub_type)
337
+ if not schemas:
338
+ return None
339
+ return {spec.name: spec for spec in schemas}
340
+
341
+
342
+ def _check_coercion(
343
+ attr_name: str,
344
+ raw: object,
345
+ schema_index: dict[str, AttrSchema] | None,
346
+ errors: list[CollectedError],
347
+ path: str,
348
+ ) -> None:
349
+ """Check a single attr value against its declared schema's `value_type`.
350
+
351
+ Emits ERR_YAML_COERCION when YAML 1.2's core schema silently changed the
352
+ Python type (bool/int/float/None where a string/stringArray was declared,
353
+ or vice versa for booleans/numbers).
354
+ """
355
+ if schema_index is None:
356
+ return
357
+ spec = schema_index.get(attr_name)
358
+ if spec is None or spec.value_type is None:
359
+ return
360
+
361
+ # Array-valued attrs (the `string` + is_array model that replaced the
362
+ # `stringarray` subtype): validate as a string array regardless of the scalar
363
+ # value_type token. Also tolerate a legacy stringarray token.
364
+ if spec.is_array or spec.value_type == ATTR_SUBTYPE_STRINGARRAY:
365
+ if isinstance(raw, str):
366
+ return
367
+ if not isinstance(raw, list):
368
+ _emit_coercion(
369
+ attr_name, raw, "string-array (or single string)", errors, path,
370
+ )
371
+ return
372
+ for i, elem in enumerate(raw):
373
+ if not isinstance(elem, str):
374
+ _emit_coercion(
375
+ f"{attr_name}[{i}]", elem, "string (in string-array)", errors, path,
376
+ )
377
+ return
378
+
379
+ if spec.value_type in (ATTR_SUBTYPE_STRING, ATTR_SUBTYPE_CLASS):
380
+ if not isinstance(raw, str):
381
+ _emit_coercion(attr_name, raw, "string", errors, path)
382
+ return
383
+ if spec.value_type == ATTR_SUBTYPE_BOOLEAN:
384
+ if not isinstance(raw, bool):
385
+ _emit_coercion(attr_name, raw, "boolean", errors, path)
386
+ return
387
+ if spec.value_type in (ATTR_SUBTYPE_INT, ATTR_SUBTYPE_LONG, ATTR_SUBTYPE_DOUBLE):
388
+ # In Python, bool is a subclass of int; YAML 1.2 boolean literals
389
+ # (true/false/TRUE/FALSE) should not satisfy a numeric attr.
390
+ if isinstance(raw, bool) or not isinstance(raw, (int, float)):
391
+ _emit_coercion(attr_name, raw, "number", errors, path)
392
+ return
393
+ if spec.value_type == ATTR_SUBTYPE_STRINGARRAY:
394
+ # A bare string at the value position is the legitimate one-element
395
+ # authoring shorthand for a string-array attr (StringArrayAttr.coerce
396
+ # wraps it into a one-element array). It is NOT a coercion. A
397
+ # non-string non-array scalar (boolean/number/null), however, is.
398
+ if isinstance(raw, str):
399
+ return
400
+ if not isinstance(raw, list):
401
+ _emit_coercion(
402
+ attr_name, raw, "string-array (or single string)", errors, path,
403
+ )
404
+ return
405
+ # For a list, check every element. A non-string element is a YAML
406
+ # coercion (e.g. unquoted `true` in a string-array list).
407
+ for i, elem in enumerate(raw):
408
+ if not isinstance(elem, str):
409
+ _emit_coercion(
410
+ f"{attr_name}[{i}]",
411
+ elem,
412
+ "string (in string-array)",
413
+ errors,
414
+ path,
415
+ )
416
+ return
417
+ # Object-shaped attrs (properties, filter) - accept any object/array,
418
+ # no YAML coercion path applies.
419
+ return
420
+
421
+
422
+ def _emit_coercion(
423
+ attr_name: str,
424
+ raw: object,
425
+ expected: str,
426
+ errors: list[CollectedError],
427
+ path: str,
428
+ ) -> None:
429
+ """Build the "quote this value" error.
430
+
431
+ The shape is intentionally explicit so AI authors can act on it: it
432
+ identifies the attr, the (coerced) value, its Python type, the declared
433
+ expected type, and a one-line fix hint.
434
+ """
435
+ actual_type = _coerced_type_name(raw)
436
+ literal = _literal_repr(raw)
437
+ errors.append(CollectedError(
438
+ message=(
439
+ f"Attribute '@{attr_name}' at {path}: expected {expected} but got "
440
+ f"{actual_type} ({literal}). YAML 1.2 silently coerced an unquoted "
441
+ f"value - quote it in YAML: "
442
+ f"'@{attr_name}: \"{literal}\"' not '@{attr_name}: {literal}'."
443
+ ),
444
+ code=ErrorCode.ERR_YAML_COERCION,
445
+ ))
446
+
447
+
448
+ def _coerced_type_name(raw: object) -> str:
449
+ if raw is None:
450
+ return "null"
451
+ if isinstance(raw, bool):
452
+ # bool must be checked before int (Python: bool is subclass of int).
453
+ return "boolean"
454
+ if isinstance(raw, int):
455
+ return "number"
456
+ if isinstance(raw, float):
457
+ return "number"
458
+ if isinstance(raw, str):
459
+ return "string"
460
+ if isinstance(raw, list):
461
+ return "array"
462
+ if isinstance(raw, dict):
463
+ return "object"
464
+ return type(raw).__name__
465
+
466
+
467
+ def _literal_repr(raw: object) -> str:
468
+ if raw is None:
469
+ return "null"
470
+ if isinstance(raw, bool):
471
+ return "true" if raw else "false"
472
+ if isinstance(raw, (int, float)):
473
+ return str(raw)
474
+ if isinstance(raw, str):
475
+ return raw
476
+ # Best-effort JSON-style for non-scalar (for the error message only).
477
+ import json
478
+ try:
479
+ return json.dumps(raw)
480
+ except (TypeError, ValueError):
481
+ return repr(raw)
@@ -0,0 +1,97 @@
1
+ Metadata-Version: 2.4
2
+ Name: metaobjects
3
+ Version: 0.9.0
4
+ Summary: Cross-language metadata standard: declare typed entities once, generate idiomatic drift-checked code across languages — Python port.
5
+ Project-URL: Homepage, https://metaobjects.dev
6
+ Project-URL: Repository, https://github.com/metaobjectsdev/metaobjects
7
+ Project-URL: Documentation, https://github.com/metaobjectsdev/metaobjects/tree/main/docs
8
+ Project-URL: Issues, https://github.com/metaobjectsdev/metaobjects/issues
9
+ Author-email: Doug Mealing <doug@metaobjects.com>
10
+ License-Expression: Apache-2.0
11
+ License-File: LICENSE
12
+ Keywords: code-generation,codegen,cross-language,drift-detection,fastapi,metadata,orm,pydantic,schema,sqlalchemy
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Software Development :: Code Generators
21
+ Classifier: Topic :: Software Development :: Libraries
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.11
24
+ Requires-Dist: pyyaml>=6.0
25
+ Provides-Extra: dev
26
+ Requires-Dist: mypy>=1.10; extra == 'dev'
27
+ Requires-Dist: pydantic>=2; extra == 'dev'
28
+ Requires-Dist: pytest>=8; extra == 'dev'
29
+ Requires-Dist: ruff>=0.6; extra == 'dev'
30
+ Provides-Extra: integration
31
+ Requires-Dist: fastapi>=0.110; extra == 'integration'
32
+ Requires-Dist: httpx>=0.27; extra == 'integration'
33
+ Requires-Dist: pg8000>=1.31; extra == 'integration'
34
+ Description-Content-Type: text/markdown
35
+
36
+ # MetaObjects (Python)
37
+
38
+ The Python port of the [MetaObjects](https://metaobjects.dev) cross-language metadata
39
+ standard: declare your typed entity model once, then generate idiomatic, drift-checked
40
+ code across TypeScript, Java, C#, Python, and Kotlin. The metamodel is the durable spine;
41
+ generated code is the disposable artifact.
42
+
43
+ Behavior is verified byte-for-byte against the same shared conformance corpora as every
44
+ other language port.
45
+
46
+ ## Install
47
+
48
+ ```bash
49
+ pip install metaobjects
50
+ ```
51
+
52
+ Requires Python 3.11+. The only runtime dependency is PyYAML.
53
+
54
+ ## Quick start
55
+
56
+ Load a directory of metadata (`*.json` canonical or sigil-free `*.yaml`):
57
+
58
+ ```python
59
+ from metaobjects import load_directory
60
+
61
+ result = load_directory("metaobjects/") # your *.json / *.yaml metadata files
62
+
63
+ if result.errors:
64
+ for err in result.errors:
65
+ print(err) # structured MetaError with a stable ErrorCode
66
+ else:
67
+ root = result.root # the merged metadata tree (a MetaData node)
68
+ print(root)
69
+ ```
70
+
71
+ `load_directory`, `load_uris`, and `load_string` are module-level shortcuts over
72
+ `MetaDataLoader`; all return a `LoadResult` with the same field shape as the other ports.
73
+
74
+ ## What's in the package
75
+
76
+ The primary public API is the **loader** (`load_directory` / `load_uris` /
77
+ `load_string`, `MetaDataLoader`, `LoadResult`, `ErrorCode`, `MetaError`). The
78
+ distribution also ships the Python implementations of the other pillars used by the CLI
79
+ and tooling: `codegen` (Pydantic + FastAPI emit), `render` (Mustache + payload-VO +
80
+ verify), `runtime` (SQLAlchemy-Core object manager), and `migrate`.
81
+
82
+ ## Authoring formats
83
+
84
+ - **Canonical JSON** (`*.json`) — the cross-language interchange shape.
85
+ - **Sigil-free YAML** (`*.yaml` / `*.yml`) — the AI-first authoring front-end
86
+ ([ADR-0006](https://github.com/metaobjectsdev/metaobjects/blob/main/spec/decisions/ADR-0006-ai-first-yaml-authoring.md)).
87
+ Desugared to canonical JSON at load time. A directory may mix both freely.
88
+
89
+ ## Links
90
+
91
+ - Standard, docs, and the other four ports: <https://metaobjects.dev>
92
+ - Source & issues: <https://github.com/metaobjectsdev/metaobjects>
93
+ - Full docs: <https://github.com/metaobjectsdev/metaobjects/tree/main/docs>
94
+
95
+ ## License
96
+
97
+ Apache-2.0. See [LICENSE](https://github.com/metaobjectsdev/metaobjects/blob/main/LICENSE).