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,50 @@
1
+ """FR-017 — table-per-hierarchy (TPH) discriminator resolution for the runtime.
2
+
3
+ A TPH SUBTYPE is an entity that declares ``@discriminatorValue`` and ``extends``
4
+ (transitively) a base that declares ``@discriminator``. The ObjectManager uses
5
+ this to:
6
+ - inject the discriminator value on create (the entity names the subtype; the
7
+ caller never sets it);
8
+ - scope every read/update/delete to the subtype (a row of a different subtype
9
+ is invisible), mirroring the generated per-subtype route's cross-subtype 404;
10
+ - treat the discriminator as immutable (stripped from update patches).
11
+
12
+ The discriminator FIELD name lives on the base (``@discriminator``); the VALUE
13
+ lives on the subtype (``@discriminatorValue``). For deep hierarchies the base may
14
+ be any ancestor, so we walk the resolved super chain to find it.
15
+
16
+ Mirrors the TS reference ``runtime-ts/src/tph.ts``.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ from typing import NamedTuple
21
+
22
+ from metaobjects.meta.core.object.object_constants import (
23
+ OBJECT_ATTR_DISCRIMINATOR,
24
+ OBJECT_ATTR_DISCRIMINATOR_VALUE,
25
+ )
26
+ from metaobjects.meta.meta_data import MetaData
27
+
28
+
29
+ class TphSubtype(NamedTuple):
30
+ #: The discriminator field NAME (declared on the base via ``@discriminator``).
31
+ field: str
32
+ #: This subtype's discriminator VALUE (declared via ``@discriminatorValue``).
33
+ value: str
34
+
35
+
36
+ def tph_subtype_of(entity: MetaData) -> TphSubtype | None:
37
+ """If *entity* is a TPH subtype, return its discriminator field + value; else
38
+ ``None``. A subtype declares ``@discriminatorValue`` (own attr) and inherits
39
+ ``@discriminator`` from an ancestor in its resolved super chain.
40
+ """
41
+ value = entity.attr(OBJECT_ATTR_DISCRIMINATOR_VALUE) # own attr
42
+ if value is None:
43
+ return None
44
+ ancestor = entity.super_data
45
+ while ancestor is not None:
46
+ field = ancestor.attr(OBJECT_ATTR_DISCRIMINATOR) # own attr on the base
47
+ if field is not None:
48
+ return TphSubtype(field=str(field), value=str(value))
49
+ ancestor = ancestor.super_data
50
+ return None
@@ -0,0 +1,172 @@
1
+ """Canonical (fused-key) serializer. Deterministic per spec/conformance-tests.md."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+
6
+ from .meta.meta_data import MetaData
7
+ from .meta.persistence.source.source_constants import (
8
+ DEFAULT_SOURCE_KIND,
9
+ PHYSICAL_NAME_ATTR_BY_KIND,
10
+ SOURCE_ATTR_KIND,
11
+ SOURCE_ATTR_TABLE,
12
+ SOURCE_SUBTYPE_RDB,
13
+ )
14
+ from .shared.base_types import TYPE_SOURCE
15
+ from .shared.separators import ATTR_PREFIX, FUSED_KEY_SEP
16
+ from .shared.structural import (
17
+ KEY_ABSTRACT,
18
+ KEY_CHILDREN,
19
+ KEY_EXTENDS,
20
+ KEY_IS_ARRAY,
21
+ KEY_NAME,
22
+ KEY_PACKAGE,
23
+ )
24
+
25
+ _SOURCE_RDB_FUSED_KEY = f"{TYPE_SOURCE}{FUSED_KEY_SEP}{SOURCE_SUBTYPE_RDB}"
26
+
27
+
28
+ def canonical_serialize(node: MetaData) -> str:
29
+ return _serialize(node, effective=False)
30
+
31
+
32
+ def canonical_serialize_effective(node: MetaData) -> str:
33
+ """Like :func:`canonical_serialize`, but emits the EFFECTIVE tree —
34
+ ``children()`` + ``attrs()`` at every node (own + inherited via the super
35
+ chain), so the super-chain merge is materialized in the output.
36
+
37
+ Used by the conformance harness's ``expected-effective.json`` fixtures.
38
+ Mirrors the TS reference ``canonicalSerializeEffective``: ``extends`` is
39
+ still emitted on every node (the ref stays in the body), but inherited
40
+ members are inlined rather than referenced.
41
+ """
42
+ return _serialize(node, effective=True)
43
+
44
+
45
+ def _serialize(node: MetaData, effective: bool) -> str:
46
+ parsed = _to_canonical(node, effective)
47
+ # FR-016 / ADR-0018 — rewrite legacy @table → kind-matching alias on
48
+ # source.rdb wrappers; run before serialization so the rewritten key sorts
49
+ # naturally with the rest of the body (alphabetical at our depth).
50
+ _rewrite_source_rdb_physical_names(parsed)
51
+ text = json.dumps(parsed, indent=2, ensure_ascii=False)
52
+ return text + "\n"
53
+
54
+
55
+ def _to_canonical(node: MetaData, effective: bool = False) -> dict[str, object]:
56
+ return {f"{node.type}{FUSED_KEY_SEP}{node.sub_type}": _body(node, effective)}
57
+
58
+
59
+ def _body(node: MetaData, effective: bool = False) -> dict[str, object]:
60
+ body: dict[str, object] = {}
61
+ if node.name:
62
+ body[KEY_NAME] = node.name
63
+ if node.package:
64
+ body[KEY_PACKAGE] = node.package
65
+ if node.super_ref:
66
+ body[KEY_EXTENDS] = node.super_ref
67
+ if node.is_abstract:
68
+ body[KEY_ABSTRACT] = True
69
+ if node.is_array:
70
+ body[KEY_IS_ARRAY] = True
71
+
72
+ # In effective mode use attrs()/children() (own + inherited via the super
73
+ # chain); in own mode use own_meta_attrs()/own_children() (declared here).
74
+ if effective:
75
+ for name in sorted(node.attrs()):
76
+ body[f"{ATTR_PREFIX}{name}"] = _normalize(node.attrs()[name])
77
+ else:
78
+ for attr in sorted(node.own_meta_attrs(), key=lambda a: a.name):
79
+ body[f"{ATTR_PREFIX}{attr.name}"] = _normalize(getattr(attr, "value", None))
80
+
81
+ children = node.children() if effective else node.own_children()
82
+ if children:
83
+ body[KEY_CHILDREN] = [_to_canonical(c, effective) for c in children]
84
+ return body
85
+
86
+
87
+ def _rewrite_source_rdb_physical_names(value: object) -> None:
88
+ """FR-016 / ADR-0018 — rewrite legacy ``@table`` on source.rdb wrappers
89
+ whose ``@kind`` is non-table to the kind-matching alias
90
+ (``@view`` / ``@materializedView`` / ``@proc`` / ``@function``).
91
+
92
+ Mutates the parsed JSON in place; idempotent and a no-op for canonical
93
+ inputs (mirrors the TS reference ``rewriteSourceRdbPhysicalNames``).
94
+ """
95
+ if isinstance(value, list):
96
+ for item in value:
97
+ _rewrite_source_rdb_physical_names(item)
98
+ return
99
+ if not isinstance(value, dict):
100
+ return
101
+
102
+ rdb_body = value.get(_SOURCE_RDB_FUSED_KEY)
103
+ if isinstance(rdb_body, dict):
104
+ kind_raw = rdb_body.get(f"{ATTR_PREFIX}{SOURCE_ATTR_KIND}")
105
+ kind = kind_raw if isinstance(kind_raw, str) and kind_raw else DEFAULT_SOURCE_KIND
106
+ canonical_alias = PHYSICAL_NAME_ATTR_BY_KIND.get(kind)
107
+ if canonical_alias is not None and canonical_alias != SOURCE_ATTR_TABLE:
108
+ legacy_key = f"{ATTR_PREFIX}{SOURCE_ATTR_TABLE}"
109
+ canonical_key = f"{ATTR_PREFIX}{canonical_alias}"
110
+ legacy_value = rdb_body.get(legacy_key)
111
+ if legacy_value is not None and canonical_key not in rdb_body:
112
+ # Re-insert in alphabetical position by rebuilding the dict.
113
+ new_body: dict[str, object] = {}
114
+ # The rewrite changes the key — preserve original insertion
115
+ # order semantics by walking, swapping in the canonical key in
116
+ # @table's slot, then re-sorting only the @-prefixed keys.
117
+ for k, v in rdb_body.items():
118
+ if k == legacy_key:
119
+ continue
120
+ new_body[k] = v
121
+ new_body[canonical_key] = legacy_value
122
+ # Restore structural-then-attr ordering with attrs alphabetically.
123
+ struct_keys = [
124
+ KEY_NAME, KEY_PACKAGE, KEY_EXTENDS,
125
+ KEY_ABSTRACT, KEY_IS_ARRAY,
126
+ ]
127
+ ordered: dict[str, object] = {}
128
+ for k in struct_keys:
129
+ if k in new_body:
130
+ ordered[k] = new_body[k]
131
+ attr_keys = sorted(
132
+ k for k in new_body
133
+ if k.startswith(ATTR_PREFIX)
134
+ )
135
+ for k in attr_keys:
136
+ ordered[k] = new_body[k]
137
+ if KEY_CHILDREN in new_body:
138
+ ordered[KEY_CHILDREN] = new_body[KEY_CHILDREN]
139
+ rdb_body.clear()
140
+ rdb_body.update(ordered)
141
+
142
+ # Recurse through every value (in particular ``children``).
143
+ for v in value.values():
144
+ _rewrite_source_rdb_physical_names(v)
145
+
146
+
147
+ _SCALAR_TYPES = (str, int, float, bool, type(None))
148
+
149
+
150
+ def _normalize(value: object) -> object:
151
+ """Mirror JSON.stringify: a whole-number float serializes as an int.
152
+
153
+ Object-valued attrs need different key order in canonical form, and the
154
+ serializer is schema-free — so distinguish by value shape, which exactly
155
+ tracks the object-attr subtypes:
156
+ * `properties` (e.g. @enumDoc / @enumAlias) is a flat scalar->scalar map ->
157
+ keys sort ordinally (cross-port canonical form; matches the Java
158
+ reference, whose Properties is unordered and serializes sorted).
159
+ * `filter` maps a field to an operator object ({eq:...}/{in:[...]}) — at
160
+ least one value is itself a dict/list -> declaration order is preserved
161
+ (significant). The FilterAttr desugar runs at set_value time, so filter
162
+ values are always op-object dicts before serialization reaches here.
163
+ """
164
+ if isinstance(value, float) and value.is_integer():
165
+ return int(value)
166
+ if isinstance(value, list):
167
+ return [_normalize(v) for v in value]
168
+ if isinstance(value, dict):
169
+ all_scalar = all(isinstance(v, _SCALAR_TYPES) for v in value.values())
170
+ items = sorted(value.items()) if all_scalar else value.items()
171
+ return {k: _normalize(v) for k, v in items}
172
+ return value
File without changes
@@ -0,0 +1,16 @@
1
+ """The base metamodel type names + the shared base/root subtype names."""
2
+ TYPE_METADATA = "metadata"
3
+ TYPE_OBJECT = "object"
4
+ TYPE_FIELD = "field"
5
+ TYPE_ATTR = "attr"
6
+ TYPE_VALIDATOR = "validator"
7
+ TYPE_IDENTITY = "identity"
8
+ TYPE_RELATIONSHIP = "relationship"
9
+ TYPE_VIEW = "view"
10
+ TYPE_LAYOUT = "layout"
11
+ TYPE_SOURCE = "source"
12
+ TYPE_ORIGIN = "origin"
13
+ TYPE_TEMPLATE = "template"
14
+
15
+ SUBTYPE_BASE = "base"
16
+ SUBTYPE_ROOT = "root"
@@ -0,0 +1,4 @@
1
+ """Structural separators shared across the metamodel (Tier-1 vocabulary)."""
2
+ ATTR_PREFIX = "@"
3
+ PACKAGE_SEP = "::"
4
+ FUSED_KEY_SEP = "."
@@ -0,0 +1,9 @@
1
+ """Reserved structural body keys (NOT @-attrs). Documented order in conformance-tests.md."""
2
+ KEY_NAME = "name"
3
+ KEY_PACKAGE = "package"
4
+ KEY_EXTENDS = "extends"
5
+ KEY_ABSTRACT = "abstract"
6
+ KEY_OVERLAY = "overlay"
7
+ KEY_IS_ARRAY = "isArray"
8
+ KEY_CHILDREN = "children"
9
+ KEY_VALUE = "value"
@@ -0,0 +1,79 @@
1
+ """FR5a / ADR-0009 — Loader error envelope + source-on-node.
2
+
3
+ Cross-port-aligned types: every metaobjects port emits the same envelope shape
4
+ so a tool consuming errors from multiple language ports can compare them
5
+ byte-identically.
6
+
7
+ Public surface re-exported here:
8
+ * ErrorSource hierarchy (abstract base + 6 variants).
9
+ * JsonPathBuilder + JsonPath helpers (canonical JSONPath construction).
10
+ * LoaderError / LoaderWarning / NodeContext / Contributor envelope types.
11
+ * semantic_diff(a, b) for FR5c overlay-merge consumption.
12
+
13
+ See `docs/superpowers/specs/2026-05-25-fr5a-json-shape-loader-errors.md` and
14
+ `spec/decisions/ADR-0009-loader-error-envelope-and-source-on-node.md`.
15
+ """
16
+ from __future__ import annotations
17
+
18
+ from .error_source import (
19
+ CodeSource,
20
+ Contributor,
21
+ DatabaseSource,
22
+ DbLocation,
23
+ ErrorSource,
24
+ JsonSource,
25
+ LoaderError,
26
+ LoaderWarning,
27
+ MergedSource,
28
+ NodeContext,
29
+ ResolvedSource,
30
+ WARN_DUPLICATE_DECLARATION,
31
+ WARN_LEGACY,
32
+ WARN_LEGACY_PHYSICAL_NAME_ALIAS,
33
+ YamlPosition,
34
+ YamlSource,
35
+ resolved_source,
36
+ )
37
+ from .json_path import JsonPath, JsonPathBuilder
38
+ from .semantic_diff import semantic_diff
39
+ from .yaml_positions import (
40
+ YamlPositionMap,
41
+ get_position_map,
42
+ get_yaml_position,
43
+ parse_yaml_with_positions,
44
+ set_position_map,
45
+ )
46
+
47
+ __all__ = [
48
+ # ErrorSource hierarchy
49
+ "ErrorSource",
50
+ "JsonSource",
51
+ "YamlSource",
52
+ "MergedSource",
53
+ "ResolvedSource",
54
+ "resolved_source",
55
+ "DatabaseSource",
56
+ "CodeSource",
57
+ # Supporting types
58
+ "Contributor",
59
+ "DbLocation",
60
+ "NodeContext",
61
+ "YamlPosition",
62
+ "LoaderError",
63
+ "LoaderWarning",
64
+ # FR5c — warning code constants
65
+ "WARN_DUPLICATE_DECLARATION",
66
+ "WARN_LEGACY",
67
+ "WARN_LEGACY_PHYSICAL_NAME_ALIAS",
68
+ # JSONPath
69
+ "JsonPathBuilder",
70
+ "JsonPath",
71
+ # Semantic diff
72
+ "semantic_diff",
73
+ # FR5b — YAML positions
74
+ "YamlPositionMap",
75
+ "get_position_map",
76
+ "get_yaml_position",
77
+ "set_position_map",
78
+ "parse_yaml_with_positions",
79
+ ]
@@ -0,0 +1,266 @@
1
+ """FR5a / ADR-0009 — ErrorSource discriminated union + envelope types.
2
+
3
+ Closed hierarchy over the provenance variants a metadata node or error can
4
+ carry. The cross-port realization in Python uses ``@dataclass(frozen=True)`` so
5
+ each variant is value-equal, hashable, and immutable.
6
+
7
+ Pattern-match on the concrete subtype (``isinstance(src, JsonSource)``) to
8
+ access variant-specific fields, or read ``src.format`` for the discriminant
9
+ tag.
10
+
11
+ Mirrors:
12
+ * TS — `server/typescript/packages/metadata/src/source.ts`
13
+ * C# — `server/csharp/MetaObjects/Source/ErrorSource.cs`
14
+ """
15
+ from __future__ import annotations
16
+
17
+ from dataclasses import dataclass, field
18
+ from typing import ClassVar, Optional
19
+
20
+
21
+ # ---------------------------------------------------------------------------
22
+ # Discriminated union: ErrorSource hierarchy
23
+ # ---------------------------------------------------------------------------
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class ErrorSource:
28
+ """Provenance envelope for a metadata node or loader error.
29
+
30
+ Abstract base — never instantiated directly. Every concrete variant
31
+ declares a class-level ``format`` constant that names its discriminant tag
32
+ in the cross-port wire shape.
33
+ """
34
+
35
+ # Subclasses override this class-level constant. Declared here as a
36
+ # placeholder so static type-checkers see a string attribute on the base.
37
+ format: ClassVar[str] = ""
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class YamlPosition:
42
+ """Optional YAML line/col source position (FR5b). 1-based."""
43
+
44
+ line: int
45
+ col: int
46
+
47
+
48
+ @dataclass(frozen=True)
49
+ class JsonSource(ErrorSource):
50
+ """Authoring-time: single JSON file (FR5a).
51
+
52
+ Args:
53
+ files: Length-1 tuple of project-root-relative file paths. The FR5a
54
+ invariant guarantees exactly one file — multi-file provenance lives
55
+ on :class:`MergedSource` (FR5c). Enforced at construction.
56
+ json_path: Canonical JSONPath string for the node within ``files[0]``.
57
+ yaml_position: FR5b — optional YAML line/col position. Populated by
58
+ the YAML loader when the JSON document was authored as YAML; for
59
+ cross-port-safety, the envelope's ``format`` discriminator stays
60
+ ``"json"`` (so the yaml-conformance fixtures stay byte-stable
61
+ until all four ports ship FR5b and the discriminator flips to
62
+ ``"yaml"``). See TS reference (server/typescript/packages/metadata/
63
+ src/source.ts) for the rationale.
64
+ """
65
+
66
+ files: tuple[str, ...]
67
+ json_path: str
68
+ yaml_position: Optional[YamlPosition] = None
69
+ format: ClassVar[str] = "json"
70
+
71
+ def __post_init__(self) -> None:
72
+ # FR5a invariant: exactly one file. Multi-file provenance is
73
+ # MergedSource (FR5c) territory; enforce here so the type can be
74
+ # trusted by cross-port comparison.
75
+ if len(self.files) != 1:
76
+ raise ValueError(
77
+ f"JsonSource requires exactly one file path; got {len(self.files)}. "
78
+ "Use MergedSource for multi-file provenance."
79
+ )
80
+
81
+
82
+ @dataclass(frozen=True)
83
+ class YamlSource(ErrorSource):
84
+ """Authoring-time: single YAML file with optional source-map positions (FR5b)."""
85
+
86
+ files: tuple[str, ...]
87
+ json_path: str
88
+ yaml_position: Optional[YamlPosition] = None
89
+ format: ClassVar[str] = "yaml"
90
+
91
+
92
+ @dataclass(frozen=True)
93
+ class Contributor:
94
+ """One contributor in a :class:`MergedSource`.
95
+
96
+ Args:
97
+ file: Path to the contributing file (project-root relative; forward slashes).
98
+ role: One of ``"overlay-base"``, ``"overlay-extension"``,
99
+ ``"extends-base"``, ``"extends-extension"``.
100
+ """
101
+
102
+ file: str
103
+ role: str
104
+
105
+
106
+ @dataclass(frozen=True)
107
+ class MergedSource(ErrorSource):
108
+ """Post-load: overlay merge that produced semantic change (FR5c)."""
109
+
110
+ files: tuple[str, ...]
111
+ json_path: str
112
+ contributors: tuple[Contributor, ...]
113
+ format: ClassVar[str] = "merged"
114
+
115
+
116
+ @dataclass(frozen=True)
117
+ class ResolvedSource(ErrorSource):
118
+ """Post-load: extends / @via / @objectRef / @payloadRef resolution failure (FR5d)."""
119
+
120
+ files: tuple[str, ...]
121
+ json_path: Optional[str] = None
122
+ referrer: Optional[str] = None
123
+ target: Optional[str] = None
124
+ format: ClassVar[str] = "resolved"
125
+
126
+
127
+ def resolved_source(
128
+ referrer_source: ErrorSource,
129
+ referrer: str,
130
+ target: str,
131
+ ) -> ResolvedSource:
132
+ """FR5d — build a ``ResolvedSource`` envelope from a referrer node's source
133
+ envelope (typically a parse-time JsonSource / YamlSource) plus the referrer
134
+ FQN and the unresolved target string.
135
+
136
+ Mirrors TS ``resolvedSource()`` (server/typescript/packages/metadata/src/
137
+ source.ts). The resolved envelope carries:
138
+
139
+ * ``files``: the referrer's source files (best-effort — for json/yaml/
140
+ merged variants this is the referrer's files tuple; for code/database
141
+ variants, an empty tuple).
142
+ * ``json_path``: the referrer's json_path when known (the location of the
143
+ broken reference on disk).
144
+ * ``referrer``: the FQN of the metadata node that declared the broken
145
+ reference (e.g. ``"myapp::content::Video"``).
146
+ * ``target``: the unresolved reference string itself (e.g. ``"BaseEntity"``,
147
+ ``"Program.weeks.invalid"``).
148
+ """
149
+ if isinstance(referrer_source, (JsonSource, YamlSource, MergedSource, ResolvedSource)):
150
+ files = tuple(referrer_source.files)
151
+ json_path = referrer_source.json_path
152
+ else:
153
+ # CodeSource / DatabaseSource / abstract base — no files/json_path to plumb.
154
+ files = ()
155
+ json_path = None
156
+ return ResolvedSource(
157
+ files=files,
158
+ json_path=json_path,
159
+ referrer=referrer,
160
+ target=target,
161
+ )
162
+
163
+
164
+ @dataclass(frozen=True)
165
+ class DbLocation:
166
+ """Database location for a node sourced from a row."""
167
+
168
+ table: str
169
+ id: str
170
+
171
+
172
+ @dataclass(frozen=True)
173
+ class DatabaseSource(ErrorSource):
174
+ """Future: database-sourced metadata (FR5e, gated on FR-003)."""
175
+
176
+ db_location: DbLocation
177
+ json_path: Optional[str] = None
178
+ format: ClassVar[str] = "database"
179
+
180
+
181
+ @dataclass(frozen=True)
182
+ class CodeSource(ErrorSource):
183
+ """Programmatic / test construction.
184
+
185
+ The default for any node not built by a loader phase. Mirrors TS
186
+ ``codeSource(caller?)``.
187
+
188
+ Args:
189
+ caller: Optional human label (e.g. ``"QueriesTest.makePost"``).
190
+ """
191
+
192
+ caller: Optional[str] = None
193
+ format: ClassVar[str] = "code"
194
+
195
+ # Canonical singleton for the no-caller case (mirrors C# `CodeSource.Default`).
196
+ # The actual assignment lives below — Python doesn't allow self-reference in
197
+ # the dataclass body.
198
+ DEFAULT: ClassVar[CodeSource]
199
+
200
+
201
+ CodeSource.DEFAULT = CodeSource(caller=None)
202
+
203
+
204
+ # ---------------------------------------------------------------------------
205
+ # Envelope types: error / warning structures
206
+ # ---------------------------------------------------------------------------
207
+
208
+
209
+ @dataclass(frozen=True)
210
+ class NodeContext:
211
+ """Optional structural context attached to a loader error.
212
+
213
+ RECOMMENDED per ADR-0009; conformance does not enforce population.
214
+ """
215
+
216
+ type: Optional[str] = None
217
+ sub_type: Optional[str] = None
218
+ name: Optional[str] = None
219
+ fqn: Optional[str] = None
220
+
221
+
222
+ @dataclass(frozen=True)
223
+ class LoaderError:
224
+ """Envelope shape every loader error conforms to. ADR-0009 §Decision.
225
+
226
+ ``code``, ``message`` and ``source`` are REQUIRED (conformance-enforced).
227
+ The remaining fields are RECOMMENDED; FR5a does not populate them, FR5b–FR5e
228
+ may.
229
+ """
230
+
231
+ code: str
232
+ message: str
233
+ source: ErrorSource
234
+ suggestions: tuple[str, ...] = field(default_factory=tuple)
235
+ fixture: Optional[str] = None
236
+ node: Optional[NodeContext] = None
237
+
238
+
239
+ @dataclass(frozen=True)
240
+ class LoaderWarning:
241
+ """Warning envelope — same shape as :class:`LoaderError` but a ``WARN_*`` code.
242
+
243
+ Today's initial warning codes:
244
+ * ``WARN_DUPLICATE_DECLARATION`` — emitted by overlay-merge when a
245
+ duplicate-with-no-change is detected (FR5c).
246
+ * ``WARN_LEGACY`` — legacy ``warnings: list[str]`` messages wrapped at
247
+ the loader boundary so the envelope channel is uniform.
248
+ """
249
+
250
+ code: str
251
+ message: str
252
+ source: ErrorSource
253
+ suggestions: tuple[str, ...] = field(default_factory=tuple)
254
+ fixture: Optional[str] = None
255
+ node: Optional[NodeContext] = None
256
+
257
+
258
+ # FR5c — string constants for warning codes. Mirrors the TS WARNING_CODES
259
+ # export. Keeping the values aligned across ports is part of the conformance
260
+ # contract.
261
+ WARN_DUPLICATE_DECLARATION: str = "WARN_DUPLICATE_DECLARATION"
262
+ WARN_LEGACY: str = "WARN_LEGACY"
263
+ #: FR-016 / ADR-0018 — emitted when a source.rdb uses the pre-1.0 ``@table``
264
+ #: spelling with a non-table ``@kind`` (e.g. ``@kind: "view"`` + ``@table``).
265
+ #: Loader accepts; the canonical serializer rewrites to the kind-matching alias.
266
+ WARN_LEGACY_PHYSICAL_NAME_ALIAS: str = "WARN_LEGACY_PHYSICAL_NAME_ALIAS"