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.
- metaobjects/__init__.py +75 -0
- metaobjects/agent_context/__init__.py +55 -0
- metaobjects/agent_context/_content/README.md +14 -0
- metaobjects/agent_context/_content/servers/csharp.meta.json +5 -0
- metaobjects/agent_context/_content/servers/java.meta.json +5 -0
- metaobjects/agent_context/_content/servers/kotlin.meta.json +5 -0
- metaobjects/agent_context/_content/servers/python.meta.json +5 -0
- metaobjects/agent_context/_content/servers/typescript.meta.json +5 -0
- metaobjects/agent_context/_content/skills/metaobjects-authoring/SKILL.md +301 -0
- metaobjects/agent_context/_content/skills/metaobjects-codegen/SKILL.md +99 -0
- metaobjects/agent_context/_content/skills/metaobjects-codegen/references/csharp.md +87 -0
- metaobjects/agent_context/_content/skills/metaobjects-codegen/references/java.md +94 -0
- metaobjects/agent_context/_content/skills/metaobjects-codegen/references/kotlin.md +110 -0
- metaobjects/agent_context/_content/skills/metaobjects-codegen/references/typescript.md +135 -0
- metaobjects/agent_context/_content/skills/metaobjects-prompts/SKILL.md +148 -0
- metaobjects/agent_context/_content/skills/metaobjects-prompts/references/csharp.md +110 -0
- metaobjects/agent_context/_content/skills/metaobjects-prompts/references/java.md +108 -0
- metaobjects/agent_context/_content/skills/metaobjects-prompts/references/kotlin.md +130 -0
- metaobjects/agent_context/_content/skills/metaobjects-prompts/references/python.md +116 -0
- metaobjects/agent_context/_content/skills/metaobjects-prompts/references/typescript.md +150 -0
- metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/SKILL.md +130 -0
- metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/java.md +96 -0
- metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/kotlin.md +99 -0
- metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/react.md +86 -0
- metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/tanstack.md +119 -0
- metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/typescript.md +92 -0
- metaobjects/agent_context/_content/skills/metaobjects-verify/SKILL.md +107 -0
- metaobjects/agent_context/_content/skills/metaobjects-verify/references/migration.md +72 -0
- metaobjects/agent_context/_content/templates/always-on.md.mustache +27 -0
- metaobjects/agent_context/assemble.py +133 -0
- metaobjects/agent_context/content_root.py +54 -0
- metaobjects/agent_context/scaffold.py +191 -0
- metaobjects/agent_context/types.py +44 -0
- metaobjects/attr_class_map.py +23 -0
- metaobjects/cli.py +696 -0
- metaobjects/codegen/__init__.py +0 -0
- metaobjects/codegen/config.py +11 -0
- metaobjects/codegen/constants.py +13 -0
- metaobjects/codegen/extract_delegate_emitter.py +384 -0
- metaobjects/codegen/extract_schema_emitter.py +139 -0
- metaobjects/codegen/format.py +31 -0
- metaobjects/codegen/fr010_field_mapping.py +220 -0
- metaobjects/codegen/generator.py +62 -0
- metaobjects/codegen/generator_registry.py +163 -0
- metaobjects/codegen/generators/__init__.py +0 -0
- metaobjects/codegen/generators/entity_model.py +263 -0
- metaobjects/codegen/generators/extractor_generator.py +317 -0
- metaobjects/codegen/generators/filter_allowlist_generator.py +309 -0
- metaobjects/codegen/generators/m2m_codegen.py +192 -0
- metaobjects/codegen/generators/output_parser_generator.py +272 -0
- metaobjects/codegen/generators/output_prompt_generator.py +192 -0
- metaobjects/codegen/generators/payload_vo_generator.py +672 -0
- metaobjects/codegen/generators/render_helper_generator.py +451 -0
- metaobjects/codegen/generators/router_generator.py +635 -0
- metaobjects/codegen/generators/template_generator.py +70 -0
- metaobjects/codegen/generators/tph_plan.py +120 -0
- metaobjects/codegen/generators/trace_helper_generator.py +336 -0
- metaobjects/codegen/instance_artifacts.py +15 -0
- metaobjects/codegen/output_format_spec_emitter.py +79 -0
- metaobjects/codegen/overwrite_policy.py +27 -0
- metaobjects/codegen/runner.py +110 -0
- metaobjects/codegen/runtime/__init__.py +6 -0
- metaobjects/codegen/runtime/filter_parser.py +193 -0
- metaobjects/codegen/type_map.py +84 -0
- metaobjects/core_types.py +809 -0
- metaobjects/datatype.py +19 -0
- metaobjects/documentation/__init__.py +28 -0
- metaobjects/documentation/doc_constants.py +20 -0
- metaobjects/documentation/doc_provider.py +20 -0
- metaobjects/documentation/doc_schema.py +24 -0
- metaobjects/errors.py +124 -0
- metaobjects/loader/__init__.py +0 -0
- metaobjects/loader/merge.py +287 -0
- metaobjects/loader/meta_data_loader.py +245 -0
- metaobjects/loader/sources/__init__.py +24 -0
- metaobjects/loader/sources/directory_source.py +50 -0
- metaobjects/loader/sources/file_source.py +41 -0
- metaobjects/loader/sources/meta_data_source.py +67 -0
- metaobjects/loader/sources/uri_source.py +56 -0
- metaobjects/loader/validate_discriminator.py +181 -0
- metaobjects/loader/validate_field_readonly.py +146 -0
- metaobjects/loader/validate_source_parameter_ref.py +159 -0
- metaobjects/loader/validate_source_physical_names.py +140 -0
- metaobjects/loader/validation_passes.py +1513 -0
- metaobjects/meta/__init__.py +1 -0
- metaobjects/meta/core/__init__.py +0 -0
- metaobjects/meta/core/attr/__init__.py +0 -0
- metaobjects/meta/core/attr/attr_constants.py +31 -0
- metaobjects/meta/core/attr/meta_attr.py +136 -0
- metaobjects/meta/core/field/__init__.py +0 -0
- metaobjects/meta/core/field/field_constants.py +105 -0
- metaobjects/meta/core/field/meta_field.py +76 -0
- metaobjects/meta/core/identity/__init__.py +0 -0
- metaobjects/meta/core/identity/identity_constants.py +19 -0
- metaobjects/meta/core/identity/meta_identity.py +8 -0
- metaobjects/meta/core/object/__init__.py +0 -0
- metaobjects/meta/core/object/meta_object.py +65 -0
- metaobjects/meta/core/object/meta_object_aware.py +43 -0
- metaobjects/meta/core/object/object_class_registry.py +56 -0
- metaobjects/meta/core/object/object_constants.py +13 -0
- metaobjects/meta/core/object/object_extract.py +400 -0
- metaobjects/meta/core/object/value_object.py +70 -0
- metaobjects/meta/core/relationship/__init__.py +0 -0
- metaobjects/meta/core/relationship/derive_m2m_fields.py +180 -0
- metaobjects/meta/core/relationship/meta_relationship.py +54 -0
- metaobjects/meta/core/relationship/relationship_constants.py +51 -0
- metaobjects/meta/core/validator/__init__.py +0 -0
- metaobjects/meta/core/validator/validator_constants.py +18 -0
- metaobjects/meta/meta_data.py +206 -0
- metaobjects/meta/meta_root.py +8 -0
- metaobjects/meta/persistence/__init__.py +0 -0
- metaobjects/meta/persistence/db/__init__.py +1 -0
- metaobjects/meta/persistence/db/db_constants.py +41 -0
- metaobjects/meta/persistence/db/db_provider.py +60 -0
- metaobjects/meta/persistence/origin/__init__.py +0 -0
- metaobjects/meta/persistence/origin/meta_origin.py +8 -0
- metaobjects/meta/persistence/origin/origin_constants.py +20 -0
- metaobjects/meta/persistence/source/__init__.py +0 -0
- metaobjects/meta/persistence/source/meta_source.py +137 -0
- metaobjects/meta/persistence/source/source_constants.py +115 -0
- metaobjects/meta/presentation/__init__.py +0 -0
- metaobjects/meta/presentation/layout/__init__.py +0 -0
- metaobjects/meta/presentation/layout/layout_constants.py +13 -0
- metaobjects/meta/presentation/layout/meta_layout.py +8 -0
- metaobjects/meta/presentation/view/__init__.py +0 -0
- metaobjects/meta/presentation/view/meta_view.py +8 -0
- metaobjects/meta/presentation/view/view_constants.py +22 -0
- metaobjects/meta/template/__init__.py +0 -0
- metaobjects/meta/template/meta_template.py +46 -0
- metaobjects/meta/template/template_constants.py +112 -0
- metaobjects/meta/template/template_provider.py +43 -0
- metaobjects/parser.py +380 -0
- metaobjects/parser_yaml.py +82 -0
- metaobjects/provider.py +111 -0
- metaobjects/py.typed +0 -0
- metaobjects/registry.py +210 -0
- metaobjects/registry_manifest.py +223 -0
- metaobjects/render/__init__.py +74 -0
- metaobjects/render/email_document.py +14 -0
- metaobjects/render/escapers.py +109 -0
- metaobjects/render/extract/__init__.py +59 -0
- metaobjects/render/extract/coerce.py +279 -0
- metaobjects/render/extract/extract.py +211 -0
- metaobjects/render/extract/extract_map.py +61 -0
- metaobjects/render/extract/json_forgiving_reader.py +203 -0
- metaobjects/render/extract/locate.py +65 -0
- metaobjects/render/extract/normalize.py +96 -0
- metaobjects/render/extract/strip.py +20 -0
- metaobjects/render/extract/types.py +332 -0
- metaobjects/render/extract/xml_forgiving_reader.py +162 -0
- metaobjects/render/filesystem_provider.py +51 -0
- metaobjects/render/prompt/__init__.py +32 -0
- metaobjects/render/prompt/output_format_renderer.py +340 -0
- metaobjects/render/prompt/output_format_spec.py +28 -0
- metaobjects/render/prompt/prompt_field.py +29 -0
- metaobjects/render/prompt/prompt_overrides.py +29 -0
- metaobjects/render/prompt/prompt_style.py +38 -0
- metaobjects/render/renderer.py +358 -0
- metaobjects/render/verify.py +266 -0
- metaobjects/runtime/__init__.py +39 -0
- metaobjects/runtime/llm_recorder.py +210 -0
- metaobjects/runtime/n2m_resolver.py +155 -0
- metaobjects/runtime/object_manager.py +715 -0
- metaobjects/runtime/tph.py +50 -0
- metaobjects/serializer_json.py +172 -0
- metaobjects/shared/__init__.py +0 -0
- metaobjects/shared/base_types.py +16 -0
- metaobjects/shared/separators.py +4 -0
- metaobjects/shared/structural.py +9 -0
- metaobjects/source/__init__.py +79 -0
- metaobjects/source/error_source.py +266 -0
- metaobjects/source/json_path.py +106 -0
- metaobjects/source/semantic_diff.py +98 -0
- metaobjects/source/yaml_positions.py +174 -0
- metaobjects/super_resolve.py +128 -0
- metaobjects/yaml_desugar.py +481 -0
- metaobjects-0.9.0.dist-info/METADATA +97 -0
- metaobjects-0.9.0.dist-info/RECORD +181 -0
- metaobjects-0.9.0.dist-info/WHEEL +4 -0
- metaobjects-0.9.0.dist-info/entry_points.txt +2 -0
- 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)
|