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,106 @@
1
+ """FR5a / ADR-0009 — Canonical JSONPath builder.
2
+
3
+ Construction rules (cross-port-aligned; every port emits this canonical form
4
+ byte-identically):
5
+ * Root is ``$``.
6
+ * Object keys matching ``^[A-Za-z_][A-Za-z0-9_]*$`` use dot notation: ``.foo``.
7
+ * All other keys use single-quoted bracket form: ``['my-key']``,
8
+ ``['@attr']``. Embedded single quotes are escaped with ``\\'``.
9
+ * Array indices use bracket form: ``[N]`` (zero-based).
10
+ * No trailing dots, no whitespace.
11
+
12
+ Mirrors:
13
+ * TS — `server/typescript/packages/metadata/src/json-path.ts`
14
+ * C# — `server/csharp/MetaObjects/Source/JsonPath.cs`
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import re
19
+ from typing import Optional, Union
20
+
21
+ # Identifier regex shared with the static :class:`JsonPath` helpers. Compiled
22
+ # once at module load.
23
+ _IDENT_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
24
+
25
+
26
+ def _render_key_segment(key: str) -> str:
27
+ """Render a single object-key segment: dot notation if identifier-safe, else bracket form."""
28
+ if _IDENT_RE.match(key):
29
+ return f".{key}"
30
+ escaped = key.replace("'", "\\'")
31
+ return f"['{escaped}']"
32
+
33
+
34
+ def _render_index_segment(idx: int) -> str:
35
+ """Render a single array-index segment: ``[N]``."""
36
+ return f"[{idx}]"
37
+
38
+
39
+ class JsonPathBuilder:
40
+ """Builds the canonical JSONPath string for a node as the parser walks the
41
+ JSON tree. Push a key or index when descending; pop when returning.
42
+
43
+ Mirrors ``JsonPathBuilder`` in
44
+ ``typescript/packages/metadata/src/json-path.ts`` and the C# implementation.
45
+ """
46
+
47
+ __slots__ = ("_segments",)
48
+
49
+ def __init__(self) -> None:
50
+ # Each segment is (kind, payload): kind is "key" or "index".
51
+ self._segments: list[tuple[str, Union[str, int]]] = []
52
+
53
+ def push_key(self, key: str) -> None:
54
+ """Push an object key segment (e.g. ``.foo`` or ``['my-key']``)."""
55
+ self._segments.append(("key", key))
56
+
57
+ def push_index(self, idx: int) -> None:
58
+ """Push an array-index segment (e.g. ``[2]``)."""
59
+ self._segments.append(("index", idx))
60
+
61
+ def pop(self) -> None:
62
+ """Pop the most recently pushed segment."""
63
+ if self._segments:
64
+ self._segments.pop()
65
+
66
+ @property
67
+ def depth(self) -> int:
68
+ """Number of segments currently on the stack (root is segment 0; not counted)."""
69
+ return len(self._segments)
70
+
71
+ def to_string(self) -> str:
72
+ """Render the current stack as a canonical JSONPath string."""
73
+ parts: list[str] = ["$"]
74
+ for kind, payload in self._segments:
75
+ if kind == "index":
76
+ # mypy-friendly: cast not needed; assignment ensures int.
77
+ parts.append(_render_index_segment(int(payload)))
78
+ else:
79
+ parts.append(_render_key_segment(str(payload)))
80
+ return "".join(parts)
81
+
82
+ def __str__(self) -> str: # pragma: no cover — convenience
83
+ return self.to_string()
84
+
85
+
86
+ class JsonPath:
87
+ """Static helpers for one-shot JSONPath rendering when a builder is overkill.
88
+
89
+ Equivalent to the C# ``JsonPath`` static helpers; namespaced as a class so
90
+ the call sites read identically in Python (``JsonPath.segment_for_key(...)``).
91
+ """
92
+
93
+ @staticmethod
94
+ def segment_for_key(key: str) -> str:
95
+ """Render a single object-key segment as it would appear in canonical
96
+ form, without the leading ``$`` — i.e. ``.foo`` or ``['my-key']``.
97
+ """
98
+ return _render_key_segment(key)
99
+
100
+ @staticmethod
101
+ def segment_for_index(idx: int) -> str:
102
+ """Render a single array-index segment: ``[N]``."""
103
+ return _render_index_segment(idx)
104
+
105
+
106
+ __all__ = ["JsonPath", "JsonPathBuilder"]
@@ -0,0 +1,98 @@
1
+ """FR5a / ADR-0009 — Cross-port-aligned semantic-equality compare for metadata
2
+ trees. Returns ``True`` if the two inputs differ in any semantically-meaningful
3
+ way (excluding ``source``, which is loader output).
4
+
5
+ Algorithm (ADR-0009 §semantic_diff):
6
+
7
+ 1. Sort attrs lexicographically; compare attr-by-attr; values by canonical
8
+ structural equality (key-order independent, whitespace-insensitive).
9
+ 2. Children are compared as ordered sequences.
10
+ 3. Reserved structural keys (``name``, ``package``, ``extends``,
11
+ ``abstract``, ``overlay``, ``isArray``, ``value``) participate like attrs.
12
+ 4. ``source`` excluded from the diff.
13
+
14
+ FR5a does not exercise this — FR5c will consume the boolean to drive the
15
+ duplicate-with-no-change ``WARN_DUPLICATE_DECLARATION`` path. We ship the
16
+ skeleton + tests now so the port is ready when FR5c lands.
17
+
18
+ Mirrors:
19
+ * TS — `server/typescript/packages/metadata/src/semantic-diff.ts`
20
+ * C# — `server/csharp/MetaObjects/Source/SemanticDiff.cs`
21
+ """
22
+ from __future__ import annotations
23
+
24
+ import json
25
+ from typing import TYPE_CHECKING, Any, Union
26
+
27
+ if TYPE_CHECKING: # avoid an import cycle (MetaData imports `..source`).
28
+ from ..meta.meta_data import MetaData
29
+
30
+ # Keys excluded from the structural diff: ``source`` is loader output, not
31
+ # metadata. Documented for parity with the C# constant set.
32
+ _EXCLUDED_KEYS: frozenset[str] = frozenset({"source"})
33
+
34
+ JsonValue = Union[None, bool, int, float, str, list[Any], dict[str, Any]]
35
+
36
+
37
+ def semantic_diff(a: "Union[MetaData, JsonValue]", b: "Union[MetaData, JsonValue]") -> bool:
38
+ """Return ``True`` when *a* and *b* differ in any semantically-meaningful
39
+ way; otherwise ``False``.
40
+
41
+ Inputs may be:
42
+ * Two :class:`MetaData` instances — both are serialized to canonical
43
+ JSON and the resulting trees are compared. Useful for the
44
+ overlay-merge consumer in FR5c.
45
+ * Two raw JSON values (dict / list / scalar) — compared directly.
46
+ """
47
+ # Lazy import to break the meta_data ↔ source cycle: MetaData imports
48
+ # `..source` at module load to default `_source` to CodeSource.DEFAULT.
49
+ from ..meta.meta_data import MetaData as _MetaData
50
+ from ..serializer_json import canonical_serialize
51
+
52
+ if isinstance(a, _MetaData) and isinstance(b, _MetaData):
53
+ serialized_a = canonical_serialize(a)
54
+ serialized_b = canonical_serialize(b)
55
+ # Fast path: byte-identical canonical output → no diff.
56
+ if serialized_a == serialized_b:
57
+ return False
58
+ return not _equal(json.loads(serialized_a), json.loads(serialized_b))
59
+ # Raw-value path.
60
+ return not _equal(a, b)
61
+
62
+
63
+ def _equal(a: Any, b: Any) -> bool: # noqa: ANN401 — recursive JSON value compare
64
+ """Structural equality with the ADR-0009 rules: key-order independent for
65
+ dicts; ordered for lists; ``source`` excluded; values compared via Python
66
+ equality (which collapses ``True == 1`` etc. — matches the TS/C# behaviour
67
+ where canonical-JSON output is the comparison substrate).
68
+ """
69
+ if a is None and b is None:
70
+ return True
71
+ if a is None or b is None:
72
+ return False
73
+
74
+ if isinstance(a, dict) and isinstance(b, dict):
75
+ a_keys = sorted(k for k in a if k not in _EXCLUDED_KEYS)
76
+ b_keys = sorted(k for k in b if k not in _EXCLUDED_KEYS)
77
+ if a_keys != b_keys:
78
+ return False
79
+ for k in a_keys:
80
+ if not _equal(a[k], b[k]):
81
+ return False
82
+ return True
83
+
84
+ if isinstance(a, list) and isinstance(b, list):
85
+ if len(a) != len(b):
86
+ return False
87
+ for ai, bi in zip(a, b):
88
+ if not _equal(ai, bi):
89
+ return False
90
+ return True
91
+
92
+ # Scalars / mismatched container types fall through to value equality.
93
+ # bool / int collapse the way Python does (True == 1) — matches what the TS
94
+ # / C# canonical-JSON byte-compare produces on whole-number floats etc.
95
+ return bool(a == b)
96
+
97
+
98
+ __all__ = ["semantic_diff"]
@@ -0,0 +1,174 @@
1
+ """FR5b — YAML authoring source-position carrier + walker (per ADR-0009).
2
+
3
+ Mirrors the TS reference split across:
4
+ * server/typescript/packages/metadata/src/core/yaml-positions.ts
5
+ (pure types + Symbol carrier + accessors)
6
+ * server/typescript/packages/metadata/src/core/yaml-positions-walker.ts
7
+ (YAML AST → JS walker that attaches position-by-key maps)
8
+
9
+ The TS reference splits these for browser-bundle safety (the walker imports
10
+ ``yaml``, which is Node-only). Python has no such boundary — both layers live
11
+ here in one module.
12
+
13
+ Source-map carrier (the FR5b spec's "open question" §2): TS uses a Symbol-keyed,
14
+ non-enumerable property on each mapping object. Python's equivalent is a
15
+ ``dict`` subclass (:class:`YamlPositionMap`) with a private ``_yaml_positions``
16
+ slot. Rationale:
17
+
18
+ * ``isinstance(d, dict)`` is still ``True`` — code that walks the parsed
19
+ structure as plain dicts is unaffected.
20
+ * ``json.dumps`` and ``for k in d`` ignore non-key attributes (the
21
+ slot is invisible to standard iteration/serialization, matching the
22
+ "non-enumerable" property of the TS Symbol-keyed prop).
23
+ * No parallel ``id(obj) → positions`` sidecar to keep in sync — the
24
+ position rides with the dict it describes.
25
+
26
+ On desugar-synthesized nodes (Rule 2's scalar-body lift): the synthesized body
27
+ ``{ name: rawScalar }`` inherits the wrapper key's position from the parent's
28
+ position map. On any other synthesis (Rule 4's isArray stamping, Rule 5's
29
+ ``@``-prefix rewrite), the position survives because the desugar carries the
30
+ position map across the rewrite.
31
+ """
32
+ from __future__ import annotations
33
+
34
+ from typing import Any, Optional
35
+
36
+ import yaml # type: ignore[import-untyped] # PyYAML ships no type stubs
37
+
38
+ from .error_source import YamlPosition
39
+
40
+
41
+ class YamlPositionMap(dict):
42
+ """A ``dict`` subclass that carries a sidecar position-by-key map.
43
+
44
+ The carrier of FR5b YAML positions. Instances of this class behave as
45
+ plain ``dict`` for every consumer that does not know to look (iteration,
46
+ ``in``, ``json.dumps``, ``isinstance`` checks), but expose a private
47
+ ``_yaml_positions`` slot that the loader reads to surface
48
+ :class:`YamlPosition` data on ``node.source``.
49
+
50
+ Key (string) → :class:`YamlPosition` (line, col, 1-based).
51
+ """
52
+
53
+ __slots__ = ("_yaml_positions",)
54
+
55
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
56
+ super().__init__(*args, **kwargs)
57
+ self._yaml_positions: dict[str, YamlPosition] = {}
58
+
59
+
60
+ def get_position_map(obj: Any) -> Optional[dict[str, YamlPosition]]:
61
+ """Read the position-by-key map from a value, if present.
62
+
63
+ Returns ``None`` for primitives, lists, ``None``, and untagged dicts.
64
+ Only :class:`YamlPositionMap` instances carry a map.
65
+ """
66
+ if isinstance(obj, YamlPositionMap):
67
+ return obj._yaml_positions
68
+ return None
69
+
70
+
71
+ def get_yaml_position(obj: Any, key: str) -> Optional[YamlPosition]:
72
+ """Read the position for a specific key on a mapping object.
73
+
74
+ Returns ``None`` when the mapping is not a :class:`YamlPositionMap` or
75
+ the key has no recorded position.
76
+ """
77
+ pmap = get_position_map(obj)
78
+ if pmap is None:
79
+ return None
80
+ return pmap.get(key)
81
+
82
+
83
+ def set_position_map(
84
+ obj: YamlPositionMap,
85
+ positions: dict[str, YamlPosition],
86
+ ) -> None:
87
+ """Attach (or replace) the position-by-key map on a mapping object.
88
+
89
+ Mirrors TS ``setPositionMap`` — the map is stored on the dict's private
90
+ slot, invisible to ``for k in d`` / ``json.dumps``.
91
+ """
92
+ obj._yaml_positions = positions
93
+
94
+
95
+ # ---------------------------------------------------------------------------
96
+ # Walker — YAML AST → Python dict tree with positions attached
97
+ # ---------------------------------------------------------------------------
98
+
99
+
100
+ def parse_yaml_with_positions(text: str) -> Any:
101
+ """Parse YAML text and return a Python structure with positions attached.
102
+
103
+ Mirrors the contract of :func:`yaml.safe_load` for the shapes the
104
+ metaobjects authoring grammar uses (mappings, sequences, scalars). Aliases
105
+ are resolved via the underlying SafeLoader pipeline — i.e. they resolve
106
+ as the library normally would.
107
+
108
+ Every mapping in the resulting structure is a :class:`YamlPositionMap`
109
+ with a populated ``_yaml_positions`` sidecar; the (1-based) line/col is
110
+ the position of the KEY token in the YAML source.
111
+
112
+ Raises:
113
+ :class:`yaml.YAMLError`: on YAML syntax errors (same as
114
+ ``yaml.safe_load``).
115
+ """
116
+ loader = yaml.SafeLoader(text)
117
+ try:
118
+ node = loader.get_single_node()
119
+ if node is None:
120
+ return None
121
+ return _yaml_node_to_py(node, loader)
122
+ finally:
123
+ loader.dispose()
124
+
125
+
126
+ def _yaml_node_to_py(node: Any, loader: Any) -> Any:
127
+ """Walk a yaml AST node into a Python structure.
128
+
129
+ For each :class:`yaml.MappingNode`, attach a position-by-key map (via
130
+ :class:`YamlPositionMap`) onto the resulting dict — the position of each
131
+ key is the (line, col) of the KEY token in the YAML source.
132
+
133
+ Scalar nodes are constructed via the SafeLoader's constructor pipeline,
134
+ so YAML 1.1's typed scalars (null, bool, int, float) coerce exactly the
135
+ way :func:`yaml.safe_load` produces them. The desugar's D2 coercion
136
+ guard fires on those coerced values, matching TS / Java / C# behavior.
137
+ """
138
+ if isinstance(node, yaml.ScalarNode):
139
+ return loader.construct_object(node, deep=True)
140
+ if isinstance(node, yaml.SequenceNode):
141
+ return [_yaml_node_to_py(item, loader) for item in node.value]
142
+ if isinstance(node, yaml.MappingNode):
143
+ out = YamlPositionMap()
144
+ positions: dict[str, YamlPosition] = {}
145
+ for key_node, value_node in node.value:
146
+ # Only string-keyed entries are valid in metaobjects authoring;
147
+ # ignore exotic keys (numeric / complex) — they would already
148
+ # break the desugar.
149
+ if not isinstance(key_node, yaml.ScalarNode):
150
+ continue
151
+ key_text = str(key_node.value)
152
+ value = _yaml_node_to_py(value_node, loader)
153
+ out[key_text] = value
154
+ # start_mark is 0-indexed; FR5b positions are 1-indexed.
155
+ mark = key_node.start_mark
156
+ if mark is not None:
157
+ positions[key_text] = YamlPosition(
158
+ line=mark.line + 1, col=mark.column + 1
159
+ )
160
+ if positions:
161
+ set_position_map(out, positions)
162
+ return out
163
+ # Tags / unsupported — fall back to None. The metaobjects authoring
164
+ # grammar does not use them.
165
+ return None
166
+
167
+
168
+ __all__ = [
169
+ "YamlPositionMap",
170
+ "get_position_map",
171
+ "get_yaml_position",
172
+ "set_position_map",
173
+ "parse_yaml_with_positions",
174
+ ]
@@ -0,0 +1,128 @@
1
+ """Deferred super/extends resolution over the merged tree (2nd pass, pre-freeze).
2
+
3
+ Mirrors TS ``resolveDeferredSupers`` in super-resolve.ts:
4
+ - Walk the tree over own_children(), tracking an inherited context package.
5
+ - Build the FQN index keyed by node.fqn() (own).
6
+ - For each node with an unresolved super_ref, resolve using
7
+ ``effective_pkg = node.package or inherited_context_pkg``.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ from .errors import ErrorCode, MetaError
12
+ from .meta.meta_data import MetaData
13
+ from .shared.separators import PACKAGE_SEP
14
+ from .source import resolved_source
15
+
16
+
17
+ def resolve_supers(root: MetaData, errors: list[MetaError]) -> None:
18
+ """Walk every node in the merged tree; for each unresolved super_ref, resolve it.
19
+
20
+ Resolved → sets node.super_data.
21
+ Unresolved → appends ERR_UNRESOLVED_SUPER to errors.
22
+ Already-resolved nodes (super_data is not None) are skipped (idempotent).
23
+ """
24
+ index = _build_index(root)
25
+ _walk(root, "", index, errors)
26
+
27
+
28
+ def _walk(
29
+ node: MetaData,
30
+ ctx_pkg: str,
31
+ index: dict[str, MetaData],
32
+ errors: list[MetaError],
33
+ ) -> None:
34
+ """Visit *node* then recurse over own_children(), carrying an inherited context package."""
35
+ if node.super_ref and node.super_data is None:
36
+ # Referrer context package: own ``package`` if declared, else the
37
+ # file-default package captured at parse time, else the inherited
38
+ # walk context. Using file_default_package (not just the threaded
39
+ # ctx_pkg) is what lets a bare / same-package / cross-package
40
+ # ``extends`` resolve over the MERGED tree, where object nodes carry
41
+ # no own package and the parent chain no longer reaches the per-file
42
+ # root. Mirrors TS ``resolveDeferredSupers``
43
+ # (``node.package ?? node.fileDefaultPackage``).
44
+ effective_pkg = node.package or node.file_default_package or ctx_pkg or None
45
+ target = _resolve(node.super_ref, effective_pkg, index)
46
+ if target is None:
47
+ # FR5d / ADR-0009: emit a ResolvedSource envelope carrying the
48
+ # referrer's files / json_path plus the referrer FQN + unresolved
49
+ # target. Mirrors TS resolveDeferredSupers in meta-data-loader.ts.
50
+ errors.append(MetaError(
51
+ f"the SuperClass '{node.super_ref}' does not exist "
52
+ f"(referenced by {node.fqn()})",
53
+ ErrorCode.ERR_UNRESOLVED_SUPER,
54
+ path=node.fqn(),
55
+ envelope=resolved_source(node.source, node.fqn(), node.super_ref),
56
+ ))
57
+ else:
58
+ node.super_data = target
59
+
60
+ # The context package for children is this node's own package, if set, else inherit.
61
+ next_ctx = node.package or ctx_pkg
62
+ for child in node.own_children():
63
+ _walk(child, next_ctx, index, errors)
64
+
65
+
66
+ def _build_index(root: MetaData) -> dict[str, MetaData]:
67
+ """Build a lookup index over the whole merged tree (own_children walk).
68
+
69
+ Each named node is registered under its own ``fqn()`` AND — when it has no
70
+ own ``package`` but a file-default package was captured at parse time —
71
+ under the package-folded key ``<file_default_package>::<name>``. The second
72
+ key is what makes a cross-package fully-qualified ``extends`` resolve over
73
+ the merged tree (object ``fqn()`` stays bare because the parser does not
74
+ fold the file-default package onto the object's own package). Mirrors the
75
+ TS ``findInTree`` matcher (own ``fqn()`` OR ``resolutionKey()``).
76
+ """
77
+ idx: dict[str, MetaData] = {}
78
+ _index_walk(root, idx)
79
+ return idx
80
+
81
+
82
+ def _index_walk(node: MetaData, idx: dict[str, MetaData]) -> None:
83
+ if node.name:
84
+ idx.setdefault(node.fqn(), node)
85
+ if not node.package and node.file_default_package:
86
+ folded = f"{node.file_default_package}{PACKAGE_SEP}{node.name}"
87
+ idx.setdefault(folded, node)
88
+ for child in node.own_children():
89
+ _index_walk(child, idx)
90
+
91
+
92
+ def _resolve(
93
+ ref: str, context_pkg: str | None, index: dict[str, MetaData]
94
+ ) -> MetaData | None:
95
+ """Resolve a super_ref string against the FQN index.
96
+
97
+ Resolution forms:
98
+ - absolute ``::pkg::Name`` → strip leading ``::``, look up ``pkg::Name``.
99
+ - relative ``..::rest`` → count leading ``..::`` levels; if levels exceed
100
+ context depth or remainder is empty → ``None``
101
+ (→ ERR_UNRESOLVED_SUPER); else look up
102
+ ``reducedCtx::rest`` (mirrors TS exactly).
103
+ - bare/qualified ``Name`` → try ``context::ref`` first, then bare ``ref``.
104
+ """
105
+ abs_prefix = PACKAGE_SEP # "::"
106
+ rel_prefix = ".." + PACKAGE_SEP # "..::
107
+
108
+ if ref.startswith(abs_prefix): # absolute ::pkg::Name
109
+ return index.get(ref[len(abs_prefix):])
110
+
111
+ if ref.startswith(rel_prefix): # relative ..::rest
112
+ parts = ref.split(PACKAGE_SEP)
113
+ levels = 0
114
+ while levels < len(parts) and parts[levels] == "..":
115
+ levels += 1
116
+ pkg_parts = context_pkg.split(PACKAGE_SEP) if context_pkg else []
117
+ remainder = parts[levels:]
118
+ if len(pkg_parts) < levels or len(remainder) == 0:
119
+ return None
120
+ all_parts = pkg_parts[: len(pkg_parts) - levels] + remainder
121
+ return index.get(PACKAGE_SEP.join(all_parts))
122
+
123
+ # bare or pkg-qualified (no leading :: / ..)
124
+ if context_pkg:
125
+ hit = index.get(f"{context_pkg}{PACKAGE_SEP}{ref}")
126
+ if hit is not None:
127
+ return hit
128
+ return index.get(ref)