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
metaobjects/parser.py ADDED
@@ -0,0 +1,380 @@
1
+ """JSON document -> node tree. Owns inline-vs-child attr syntax (ADR-0002)."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass, field
5
+ from typing import cast
6
+
7
+ from .errors import ErrorCode, MetaError
8
+ from .meta.core.attr.attr_constants import ATTR_SUBTYPE_STRINGARRAY
9
+ from .meta.meta_data import MetaData
10
+ from .meta.meta_root import MetaRoot
11
+ from .registry import TypeRegistry
12
+ from .shared.base_types import SUBTYPE_ROOT, TYPE_ATTR, TYPE_FIELD, TYPE_METADATA, TYPE_OBJECT
13
+ from .shared.separators import ATTR_PREFIX, FUSED_KEY_SEP
14
+ from .shared.structural import (
15
+ KEY_ABSTRACT,
16
+ KEY_CHILDREN,
17
+ KEY_EXTENDS,
18
+ KEY_IS_ARRAY,
19
+ KEY_NAME,
20
+ KEY_OVERLAY,
21
+ KEY_PACKAGE,
22
+ KEY_VALUE,
23
+ )
24
+ from .source import (
25
+ CodeSource,
26
+ ErrorSource,
27
+ JsonPathBuilder,
28
+ JsonSource,
29
+ LoaderWarning,
30
+ YamlPosition,
31
+ YamlSource,
32
+ )
33
+ from .source.yaml_positions import get_yaml_position
34
+
35
+ # Reserved structural body keys — authoring any of these with the @-prefix is a
36
+ # hard ERR_RESERVED_ATTR (ADR-0007). Detected inline as each @-key is processed.
37
+ _RESERVED_STRUCTURAL_KEYS: frozenset[str] = frozenset({
38
+ KEY_NAME,
39
+ KEY_PACKAGE,
40
+ KEY_EXTENDS,
41
+ KEY_ABSTRACT,
42
+ KEY_OVERLAY,
43
+ KEY_IS_ARRAY,
44
+ KEY_CHILDREN,
45
+ KEY_VALUE,
46
+ })
47
+
48
+
49
+ @dataclass
50
+ class ParseResult:
51
+ root: MetaData
52
+ errors: list[MetaError] = field(default_factory=list)
53
+ warnings: list[str] = field(default_factory=list)
54
+ # FR5c — envelope-shaped warnings (e.g. WARN_DUPLICATE_DECLARATION)
55
+ # produced during parse. Distinct from the legacy ``warnings: list[str]``
56
+ # channel: those get wrapped in a ``WARN_LEGACY`` envelope at the loader
57
+ # boundary, while envelope warnings already carry their own ``code`` +
58
+ # ``source`` and surface unchanged. Empty by default.
59
+ envelope_warnings: list[LoaderWarning] = field(default_factory=list)
60
+
61
+
62
+ # FR5b — module-level source-format discriminant. Set at the top of
63
+ # :func:`parse_document` and read by :func:`_current_envelope`. Safe because
64
+ # parse_document is fully synchronous — no reentrancy within a single parse
65
+ # call. Mirrors TS's ``_currentFormat`` module-level state in parser-core.ts.
66
+ _current_source_format: str = "json"
67
+
68
+
69
+ def _current_envelope(
70
+ source: str | None,
71
+ builder: JsonPathBuilder,
72
+ yaml_position: YamlPosition | None = None,
73
+ ) -> ErrorSource:
74
+ """Build a source envelope for the current parser location.
75
+
76
+ Returns :class:`CodeSource` when *source* is missing (parser invoked
77
+ without a source id — emitting a JsonSource with an empty file list
78
+ would violate the FR5a length-1 invariant). Mirrors the C# fallback in
79
+ ``Parser.ParseState.CurrentSource``.
80
+
81
+ FR5b finalized 2026-05-27 — when the module-level
82
+ :data:`_current_source_format` is ``"yaml"`` (set by :func:`parse_document`'s
83
+ ``source_format`` kwarg, supplied by :func:`parse_yaml`), emits a
84
+ :class:`YamlSource` (format ``"yaml"``) carrying the optional
85
+ *yaml_position*. Otherwise emits a :class:`JsonSource`.
86
+ """
87
+ if source is None or source == "":
88
+ return CodeSource.DEFAULT
89
+ if _current_source_format == "yaml":
90
+ return YamlSource(
91
+ files=(source,),
92
+ json_path=builder.to_string(),
93
+ yaml_position=yaml_position,
94
+ )
95
+ return JsonSource(
96
+ files=(source,),
97
+ json_path=builder.to_string(),
98
+ yaml_position=yaml_position,
99
+ )
100
+
101
+
102
+ def parse_document(
103
+ doc: object,
104
+ registry: TypeRegistry,
105
+ source: str,
106
+ *,
107
+ source_format: str = "json",
108
+ ) -> ParseResult:
109
+ # FR5b — set the module-level source-format discriminant for the duration
110
+ # of this parse call. parse_yaml passes source_format="yaml" so every
111
+ # envelope emitted during this run is a YamlSource (format "yaml").
112
+ global _current_source_format
113
+ prior_format = _current_source_format
114
+ _current_source_format = source_format
115
+ try:
116
+ return _parse_document_inner(doc, registry, source)
117
+ finally:
118
+ _current_source_format = prior_format
119
+
120
+
121
+ def _parse_document_inner(doc: object, registry: TypeRegistry, source: str) -> ParseResult:
122
+ builder = JsonPathBuilder()
123
+ result = ParseResult(root=MetaRoot(TYPE_METADATA, SUBTYPE_ROOT, ""))
124
+ if not isinstance(doc, dict):
125
+ result.errors.append(MetaError(
126
+ "top-level is not an object",
127
+ ErrorCode.ERR_TOP_LEVEL_NOT_OBJECT,
128
+ source,
129
+ envelope=_current_envelope(source, builder),
130
+ ))
131
+ return result
132
+ if len(doc) != 1:
133
+ result.errors.append(MetaError(
134
+ "expected one wrapper key",
135
+ ErrorCode.ERR_TOP_LEVEL_NOT_OBJECT,
136
+ source,
137
+ envelope=_current_envelope(source, builder),
138
+ ))
139
+ return result
140
+
141
+ (wrapper, body), = cast(list[tuple[str, object]], list(doc.items()))
142
+ builder.push_key(wrapper)
143
+ # FR5b — look up the root wrapper's YAML position (when the input was
144
+ # YAML-loaded). For JSON input, this returns None and the envelope
145
+ # remains a plain JsonSource with no yaml_position.
146
+ root_yaml_position = get_yaml_position(doc, wrapper)
147
+ node = _build(
148
+ wrapper, body, registry, source, result, builder,
149
+ yaml_position=root_yaml_position,
150
+ )
151
+ builder.pop()
152
+ if isinstance(node, MetaData):
153
+ result.root = node
154
+ return result
155
+
156
+
157
+ def _build(
158
+ wrapper: str,
159
+ body: object,
160
+ registry: TypeRegistry,
161
+ source: str,
162
+ result: ParseResult,
163
+ builder: JsonPathBuilder,
164
+ ctx_pkg: str = "",
165
+ parent_type: str = "",
166
+ yaml_position: YamlPosition | None = None,
167
+ ) -> MetaData | None:
168
+ """Build a node from a fused-key wrapper and its body dict.
169
+
170
+ *ctx_pkg* is the effective package inherited from the nearest ancestor that
171
+ declared one. *parent_type* is the type of the immediate parent node (used
172
+ for the field-package-inheritance rule: fields NOT inside objects inherit the
173
+ context package, mirroring the TS/Java loader behaviour for abstract fields
174
+ declared at the root level).
175
+
176
+ *yaml_position* is the FR5b YAML line/col of the wrapper key (if the input
177
+ was YAML-loaded); ``None`` for JSON input. Stamped onto the constructed
178
+ node's source envelope.
179
+ """
180
+ type_, _, sub_type = wrapper.partition(FUSED_KEY_SEP)
181
+ if not sub_type:
182
+ result.errors.append(MetaError(
183
+ f"node '{wrapper}' omits subType",
184
+ ErrorCode.ERR_MISSING_SUBTYPE,
185
+ source,
186
+ envelope=_current_envelope(source, builder, yaml_position),
187
+ ))
188
+ return None
189
+ if not registry.has_type(type_):
190
+ result.errors.append(MetaError(
191
+ f"unknown type '{type_}'",
192
+ ErrorCode.ERR_UNKNOWN_TYPE,
193
+ source,
194
+ envelope=_current_envelope(source, builder, yaml_position),
195
+ ))
196
+ return None
197
+ definition = registry.find(type_, sub_type)
198
+ if definition is None:
199
+ result.errors.append(MetaError(
200
+ f"unknown subType '{type_}.{sub_type}'",
201
+ ErrorCode.ERR_UNKNOWN_SUBTYPE,
202
+ source,
203
+ envelope=_current_envelope(source, builder, yaml_position),
204
+ ))
205
+ return None
206
+
207
+ body_dict: dict[str, object] = body if isinstance(body, dict) else {}
208
+ name = str(body_dict.get(KEY_NAME, "") or "")
209
+ node = definition.factory(type_, sub_type, name)
210
+ assert isinstance(node, MetaData)
211
+ # FR5a / ADR-0009 — every parser-constructed node carries its origin.
212
+ # FR5b — when YAML-sourced, the envelope also carries yaml_position.
213
+ node.set_source(_current_envelope(source, builder, yaml_position))
214
+
215
+ pkg = body_dict.get(KEY_PACKAGE)
216
+ # Capture the file-default package at PARSE time so cross-package
217
+ # fully-qualified ``extends`` resolves over the MERGED tree (where per-file
218
+ # root packages are no longer reachable via the parent chain). The node's
219
+ # own ``package`` if declared, else the inherited context package (the
220
+ # file's root package). Mirrors TS ``MetaData.fileDefaultPackage``.
221
+ node.file_default_package = (str(pkg) if pkg else None) or (ctx_pkg or None)
222
+
223
+ if pkg:
224
+ node.package = str(pkg)
225
+ elif type_ == TYPE_FIELD and parent_type != TYPE_OBJECT and ctx_pkg:
226
+ # Fields NOT inside objects inherit the context package.
227
+ # This covers abstract fields declared at root level (e.g. abstract field.enum).
228
+ # Mirrors TS parser-core.ts and Java BaseMetaDataParser.shouldInheritPackageFromParent.
229
+ node.package = ctx_pkg
230
+ else:
231
+ node.package = None
232
+
233
+ if body_dict.get(KEY_EXTENDS):
234
+ node.super_ref = str(body_dict[KEY_EXTENDS])
235
+ node.is_abstract = bool(body_dict.get(KEY_ABSTRACT, False))
236
+ node.is_overlay = bool(body_dict.get(KEY_OVERLAY, False))
237
+ node.is_array = bool(body_dict.get(KEY_IS_ARRAY, False))
238
+
239
+ for key, value in body_dict.items():
240
+ if key.startswith(ATTR_PREFIX):
241
+ attr_name = key[len(ATTR_PREFIX):]
242
+ # ADR-0007: @-prefixing a reserved structural body key is invalid.
243
+ # Detected inline as each @-attr key is processed (matches TS parser-core).
244
+ # FR5a: emit the envelope at the PARENT body level (do NOT push the
245
+ # offending @-key onto the path) — matches TS parser-core which calls
246
+ # errSource() without descending into the @-key. Pushing would emit
247
+ # a deeper jsonPath than the reference port.
248
+ if attr_name in _RESERVED_STRUCTURAL_KEYS:
249
+ result.errors.append(
250
+ MetaError(
251
+ f"node '{wrapper}' uses reserved structural key '{attr_name}' "
252
+ f"with @-prefix; bare '{attr_name}' is the canonical form",
253
+ ErrorCode.ERR_RESERVED_ATTR,
254
+ source,
255
+ envelope=_current_envelope(source, builder, yaml_position),
256
+ )
257
+ )
258
+ continue
259
+ schema = registry.attr_schema(type_, sub_type, attr_name)
260
+ # Array-valued attrs (the `string` + is_array model that replaced the
261
+ # `stringarray` subtype) coerce through the array string-attr class
262
+ # (bare-string → one-element list), keyed off the retired-as-a-subtype-
263
+ # but-kept-as-a-coercion `stringarray` class-map entry.
264
+ attr_sub_type = (
265
+ ATTR_SUBTYPE_STRINGARRAY
266
+ if schema is not None and schema.is_array
267
+ else (schema.value_type if schema else None)
268
+ )
269
+ node.set_attr(attr_name, value, sub_type=attr_sub_type)
270
+ # FR5a / ADR-0009 — stamp the just-constructed MetaAttribute node with
271
+ # its origin envelope. Mirrors C# Parser.cs:1039 (attrModel.SetSource).
272
+ # The attr's JsonPath points at the @-key on the parent body.
273
+ attr_node = node.own_meta_attr(attr_name)
274
+ if attr_node is not None:
275
+ # FR5b — the inline attr's YAML position is the body's
276
+ # position-by-key map entry for this canonical key (the
277
+ # desugar re-keys sigil-free attrs to @-prefixed form, so
278
+ # the lookup key matches `key` directly).
279
+ attr_yaml_pos = get_yaml_position(body_dict, key)
280
+ builder.push_key(key)
281
+ attr_node.set_source(
282
+ _current_envelope(source, builder, attr_yaml_pos),
283
+ )
284
+ builder.pop()
285
+
286
+ # The context package for children: use this node's own package if set, else inherit.
287
+ child_ctx_pkg = node.package or ctx_pkg
288
+
289
+ # Descend into children: push `children` key, then `[i]` index for each entry,
290
+ # then the child wrapper key, so JsonPath segments stack correctly.
291
+ children_entries = _iter_children(body_dict)
292
+ if children_entries:
293
+ builder.push_key(KEY_CHILDREN)
294
+ for idx, (entry_dict, cw, cbody) in enumerate(children_entries):
295
+ builder.push_index(idx)
296
+ builder.push_key(cw)
297
+ child_type, _, child_sub = cw.partition(FUSED_KEY_SEP)
298
+ # FR5b — the child wrapper's YAML position lives on the entry
299
+ # dict (the one-key wrapper holding `{cw: cbody}`). For JSON
300
+ # input, get_yaml_position returns None.
301
+ child_yaml_pos = get_yaml_position(entry_dict, cw)
302
+ if child_type == TYPE_ATTR:
303
+ _parse_attr_child(
304
+ node, child_sub, cbody, registry, source, result, builder,
305
+ yaml_position=child_yaml_pos,
306
+ )
307
+ else:
308
+ child = _build(
309
+ cw, cbody, registry, source, result, builder,
310
+ ctx_pkg=child_ctx_pkg, parent_type=type_,
311
+ yaml_position=child_yaml_pos,
312
+ )
313
+ if child is not None:
314
+ node.add_child(child)
315
+ builder.pop()
316
+ builder.pop()
317
+ builder.pop()
318
+
319
+ return node
320
+
321
+
322
+ def _parse_attr_child(
323
+ parent: MetaData,
324
+ sub_type: str,
325
+ body: object,
326
+ registry: TypeRegistry,
327
+ source: str,
328
+ result: ParseResult,
329
+ builder: JsonPathBuilder,
330
+ yaml_position: YamlPosition | None = None,
331
+ ) -> None:
332
+ """Handle a typed attr child: { "attr.<sub>": { "name": ..., "value": ... } }.
333
+
334
+ Attaches via set_attr (not add_child) — attrs are not structural children.
335
+ Uses the child's own sub_type to pick the correct attr class (coerce + desugar).
336
+
337
+ FR5b — *yaml_position* is the YAML line/col of the attr child's wrapper key
338
+ (when YAML-loaded); stamped on the resulting attribute node's source.
339
+ """
340
+ body_dict: dict[str, object] = body if isinstance(body, dict) else {}
341
+ attr_name = body_dict.get(KEY_NAME)
342
+ if not isinstance(attr_name, str) or not attr_name:
343
+ result.errors.append(
344
+ MetaError(
345
+ f"attr child requires a non-empty 'name'",
346
+ ErrorCode.ERR_MISSING_REQUIRED_ATTR,
347
+ source,
348
+ envelope=_current_envelope(source, builder, yaml_position),
349
+ )
350
+ )
351
+ return
352
+ raw_value = body_dict.get(KEY_VALUE)
353
+ # Resolve the attr sub_type; fall back to base if unregistered.
354
+ resolved_sub = sub_type if registry.find(TYPE_ATTR, sub_type) is not None else None
355
+ parent.set_attr(attr_name, raw_value, sub_type=resolved_sub)
356
+ # FR5a / ADR-0009 — stamp the just-constructed MetaAttribute node with its
357
+ # origin envelope. Mirrors C# Parser.cs:1039 (attrModel.SetSource). The
358
+ # builder already points at the `attr.<sub>` wrapper (caller pushed it).
359
+ attr_node = parent.own_meta_attr(attr_name)
360
+ if attr_node is not None:
361
+ attr_node.set_source(_current_envelope(source, builder, yaml_position))
362
+
363
+
364
+ def _iter_children(
365
+ body: dict[str, object],
366
+ ) -> list[tuple[dict[str, object], str, object]]:
367
+ """Iterate over a body's `children` entries.
368
+
369
+ Returns a list of ``(entry_dict, wrapper_key, body_value)`` triples. The
370
+ ``entry_dict`` is the original one-key wrapper from the children array,
371
+ retained so callers can read its FR5b YAML position-by-key map.
372
+ """
373
+ raw = body.get(KEY_CHILDREN, [])
374
+ out: list[tuple[dict[str, object], str, object]] = []
375
+ if isinstance(raw, list):
376
+ for entry in raw:
377
+ if isinstance(entry, dict) and len(entry) == 1:
378
+ (cw, cbody), = cast(list[tuple[str, object]], list(entry.items()))
379
+ out.append((entry, cw, cbody))
380
+ return out
@@ -0,0 +1,82 @@
1
+ """YAML authoring front-end: text -> desugar -> canonical -> shared tree-builder.
2
+
3
+ Mirrors the TS reference at server/typescript/packages/metadata/src/core/parser-yaml.ts.
4
+
5
+ parse_yaml is the YAML peer of parse_document (parser.py). It is the only
6
+ front-end that should ever see sugared YAML; downstream code (validators,
7
+ serializer, conformance) sees the same canonical-JSON-shaped tree regardless
8
+ of authoring format. ADR-0006 D4: canonical JSON is the cross-language
9
+ interchange; YAML is a sigil-free authoring front-end.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import yaml # type: ignore[import-untyped] # PyYAML ships no type stubs
14
+
15
+ from .errors import ErrorCode, MetaError, ParseError
16
+ from .parser import ParseResult, parse_document
17
+ from .registry import TypeRegistry
18
+ from .source.yaml_positions import parse_yaml_with_positions
19
+ from .yaml_desugar import desugar
20
+
21
+
22
+ def parse_yaml(text: str, registry: TypeRegistry, source: str) -> ParseResult:
23
+ """Parse a YAML authoring document into a canonical-shaped tree.
24
+
25
+ Steps mirror TS parser-yaml.ts:
26
+ 1. Strip UTF-8 BOM if present (consistent with parse_document).
27
+ 2. parse_yaml_with_positions -> a Python object whose mappings carry
28
+ line/col positions for each key (FR5b). Functionally equivalent to
29
+ ``yaml.safe_load`` for downstream consumers; YamlPositionMap is a
30
+ dict subclass.
31
+ 3. desugar(raw, registry) -> canonical-JSON-shaped dict + CollectedErrors
32
+ (positions are preserved across Rules 1, 2, 4, 5).
33
+ 4. parse_document(canonical, registry, source) -> ParseResult; the
34
+ parser detects position-tagged dicts and populates
35
+ ``source.yaml_position`` on every node it builds.
36
+ 5. Merge desugar errors (each carrying its own stable code, e.g.
37
+ ERR_YAML_COERCION) ahead of parse_document's errors.
38
+
39
+ PyYAML failures (YAMLError) are wrapped as ParseError(ERR_MALFORMED_YAML).
40
+ """
41
+ # Strip UTF-8 BOM if present (consistent with the JSON file loader's
42
+ # `encoding="utf-8-sig"` behavior).
43
+ if text.startswith(""):
44
+ text = text[1:]
45
+
46
+ try:
47
+ parsed = parse_yaml_with_positions(text)
48
+ except yaml.YAMLError as exc:
49
+ raise ParseError(f"Invalid YAML: {exc}", ErrorCode.ERR_MALFORMED_YAML) from exc
50
+
51
+ result = desugar(parsed, registry)
52
+
53
+ # If the desugar produced nothing usable, surface that up-front via a
54
+ # MetaError so the loader pipeline still terminates gracefully (parallels
55
+ # parse_document's top-level ERR_TOP_LEVEL_NOT_OBJECT path).
56
+ if not result.canonical:
57
+ first = result.errors[0] if result.errors else None
58
+ message = first.message if first is not None else "empty YAML document"
59
+ code = (
60
+ first.code
61
+ if first is not None and first.code is not None
62
+ else ErrorCode.ERR_MALFORMED_YAML
63
+ )
64
+ parse_result = parse_document({}, registry, source, source_format="yaml")
65
+ parse_result.errors.insert(0, MetaError(message, code, source))
66
+ return parse_result
67
+
68
+ # FR5b — pass source_format="yaml" so parse_document emits YamlSource
69
+ # envelopes (format "yaml") on every node and error from this YAML input.
70
+ parsed_result = parse_document(result.canonical, registry, source, source_format="yaml")
71
+
72
+ # Merge collected desugar errors ahead of parse_document's own errors.
73
+ desugar_metaerrors = [
74
+ MetaError(
75
+ err.message,
76
+ err.code if err.code is not None else ErrorCode.ERR_MALFORMED_YAML,
77
+ source,
78
+ )
79
+ for err in result.errors
80
+ ]
81
+ parsed_result.errors = desugar_metaerrors + parsed_result.errors
82
+ return parsed_result
@@ -0,0 +1,111 @@
1
+ """Composable type providers (ADR-0004). Subtypes self-register via @provider.register."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Callable, TypeVar
5
+
6
+ from .errors import ErrorCode, ParseError
7
+ from .registry import AttrSchema, ChildRule, TypeDefinition, TypeRegistry
8
+
9
+ T = TypeVar("T", bound=type)
10
+
11
+
12
+ class Provider:
13
+ def __init__(self, provider_id: str, dependencies: tuple[str, ...] = ()) -> None:
14
+ self.id = provider_id
15
+ self.dependencies = dependencies
16
+ self._defs: list[TypeDefinition] = []
17
+ # Post-register hook. Receives the composed registry so a DOMAIN provider
18
+ # can call ``registry.extend(...)`` to enrich types another (already-ordered)
19
+ # provider registered — mirroring TS's ``registerTypes(registry)`` and the
20
+ # ``dbProvider`` / ``templateProvider`` ``registry.extend`` loops. Set via
21
+ # ``on_register`` or by overriding ``register_types``.
22
+ self._on_register: Callable[[TypeRegistry], None] | None = None
23
+
24
+ def add(self, definition: TypeDefinition) -> None:
25
+ self._defs.append(definition)
26
+
27
+ def register(self, cls: T) -> T:
28
+ """Class decorator: build a TypeDefinition from class attributes and add it."""
29
+ type_ = getattr(cls, "TYPE")
30
+ sub_type = getattr(cls, "SUBTYPE")
31
+ attrs: list[AttrSchema] = list(getattr(cls, "ATTRS", []))
32
+ child_rules: list[ChildRule] = list(getattr(cls, "CHILD_RULES", []))
33
+
34
+ def factory(t: str, s: str, n: str, _cls: T = cls) -> object:
35
+ return _cls(t, s, n)
36
+
37
+ self.add(
38
+ TypeDefinition(
39
+ type=type_,
40
+ sub_type=sub_type,
41
+ factory=factory,
42
+ attrs=attrs,
43
+ child_rules=child_rules,
44
+ )
45
+ )
46
+ return cls
47
+
48
+ def on_register(self, hook: Callable[[TypeRegistry], None]) -> None:
49
+ """Register a post-register hook that receives the composed registry.
50
+
51
+ Runs AFTER this provider's own ``add``-ed definitions are registered (so
52
+ a provider can both register and extend). The hook is the ergonomic way
53
+ for a DOMAIN provider to call ``registry.extend(...)`` without subclassing
54
+ ``Provider``. Mirrors the TS provider's ``registerTypes(registry)`` body.
55
+ """
56
+ self._on_register = hook
57
+
58
+ def register_types(self, registry: TypeRegistry) -> None:
59
+ for definition in self._defs:
60
+ registry.register(definition)
61
+ if self._on_register is not None:
62
+ self._on_register(registry)
63
+
64
+
65
+ def compose_registry(providers: list[Provider]) -> TypeRegistry:
66
+ """Topologically sort providers by dependency, then register each into a fresh registry."""
67
+ ordered = _topo_sort(providers)
68
+ registry = TypeRegistry()
69
+ for provider in ordered:
70
+ provider.register_types(registry)
71
+ return registry
72
+
73
+
74
+ def _topo_sort(providers: list[Provider]) -> list[Provider]:
75
+ by_id: dict[str, Provider] = {}
76
+ for provider in providers:
77
+ if provider.id in by_id:
78
+ raise ParseError(
79
+ f'Duplicate provider id "{provider.id}"', ErrorCode.ERR_PROVIDER_DUPLICATE_ID
80
+ )
81
+ by_id[provider.id] = provider
82
+
83
+ for provider in providers:
84
+ for dep in provider.dependencies:
85
+ if dep not in by_id:
86
+ raise ParseError(
87
+ f'Provider "{provider.id}" depends on missing "{dep}"',
88
+ ErrorCode.ERR_PROVIDER_MISSING_DEPENDENCY,
89
+ )
90
+
91
+ ordered: list[Provider] = []
92
+ visited: dict[str, int] = {} # 0 = visiting, 1 = done
93
+
94
+ def visit(p: Provider) -> None:
95
+ state = visited.get(p.id)
96
+ if state == 1:
97
+ return
98
+ if state == 0:
99
+ raise ParseError(
100
+ f'Provider dependency cycle at "{p.id}"',
101
+ ErrorCode.ERR_PROVIDER_DEPENDENCY_CYCLE,
102
+ )
103
+ visited[p.id] = 0
104
+ for dep in p.dependencies:
105
+ visit(by_id[dep])
106
+ visited[p.id] = 1
107
+ ordered.append(p)
108
+
109
+ for provider in providers:
110
+ visit(provider)
111
+ return ordered
metaobjects/py.typed ADDED
File without changes