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,245 @@
|
|
|
1
|
+
"""MetaDataLoader: source-polymorphic loader.
|
|
2
|
+
|
|
3
|
+
The cross-language Tier-1 loader: takes a list of MetaDataSource impls,
|
|
4
|
+
runs parse -> merge -> super-resolve -> validate -> freeze, and returns a
|
|
5
|
+
LoadResult { root, errors, warnings }.
|
|
6
|
+
|
|
7
|
+
Source format (JSON vs YAML) is declared by the source, not sniffed by the
|
|
8
|
+
loader. This replaces both the per-file extension switch from the old
|
|
9
|
+
free function and the TS FileMetaDataLoader.parseSource override.
|
|
10
|
+
|
|
11
|
+
The three class-method factories (from_directory / from_uris / from_string)
|
|
12
|
+
cover the 99% case; module-level shortcuts in `metaobjects.__init__` wrap
|
|
13
|
+
them with Pythonic ergonomics.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from ..core_types import core_providers
|
|
22
|
+
from ..errors import ErrorCode, MetaError, ParseError
|
|
23
|
+
from ..meta.meta_data import MetaData
|
|
24
|
+
from ..meta.meta_root import MetaRoot
|
|
25
|
+
from ..parser import ParseResult, parse_document
|
|
26
|
+
from ..parser_yaml import parse_yaml
|
|
27
|
+
from ..provider import Provider, compose_registry
|
|
28
|
+
from ..registry import TypeRegistry
|
|
29
|
+
from ..shared.base_types import SUBTYPE_ROOT, TYPE_METADATA
|
|
30
|
+
from ..source import CodeSource, ErrorSource, JsonSource, LoaderWarning
|
|
31
|
+
from ..super_resolve import resolve_supers
|
|
32
|
+
from .merge import merge_roots
|
|
33
|
+
from .sources import (
|
|
34
|
+
DirectorySource,
|
|
35
|
+
InMemoryStringSource,
|
|
36
|
+
MetaDataFormat,
|
|
37
|
+
MetaDataSource,
|
|
38
|
+
UriSource,
|
|
39
|
+
)
|
|
40
|
+
from .validation_passes import run_validations
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class LoadResult:
|
|
45
|
+
"""The Tier-1 load result. Same field shape across all four ports."""
|
|
46
|
+
|
|
47
|
+
root: MetaData
|
|
48
|
+
errors: list[MetaError] = field(default_factory=list)
|
|
49
|
+
warnings: list[str] = field(default_factory=list)
|
|
50
|
+
# FR5c — envelope-shaped warnings (e.g. ``WARN_DUPLICATE_DECLARATION``)
|
|
51
|
+
# produced during parse / merge. Distinct from the legacy
|
|
52
|
+
# ``warnings: list[str]`` channel: those flow through the parser /
|
|
53
|
+
# validator surface as plain strings and get wrapped at the loader
|
|
54
|
+
# boundary (or surfaced verbatim by the conformance runner for the
|
|
55
|
+
# expected-warnings.json check); envelope warnings already carry their
|
|
56
|
+
# own ``code`` + ``source``. Mirrors TS ``LoadResult.warnings`` (whose
|
|
57
|
+
# singular channel ships ``LoaderWarning`` envelopes after the FR5c
|
|
58
|
+
# WARN_LEGACY-wrapping pass).
|
|
59
|
+
envelope_warnings: list[LoaderWarning] = field(default_factory=list)
|
|
60
|
+
|
|
61
|
+
def get_envelope_warnings(self) -> list[LoaderWarning]:
|
|
62
|
+
"""FR5c — accessor mirroring TS ``loader.envelopeWarnings`` /
|
|
63
|
+
Java ``loader.getEnvelopeWarnings()``.
|
|
64
|
+
|
|
65
|
+
Returns the in-order list of envelope-shaped warnings (typed with a
|
|
66
|
+
``code`` + ``source`` envelope) produced during parse / merge. The
|
|
67
|
+
legacy string channel (``self.warnings``) is unchanged.
|
|
68
|
+
"""
|
|
69
|
+
return list(self.envelope_warnings)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class MetaDataLoader:
|
|
73
|
+
"""Source-polymorphic metadata loader.
|
|
74
|
+
|
|
75
|
+
Construct once with a provider list (defaults to ``core_providers`` —
|
|
76
|
+
core types + DB-domain + documentation + template/output domain);
|
|
77
|
+
call ``.load(sources)`` for arbitrary source combinations, or use the
|
|
78
|
+
``from_*`` class-method factories for the common cases.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
def __init__(
|
|
82
|
+
self,
|
|
83
|
+
providers: list[Provider] | None = None,
|
|
84
|
+
strict: bool = False,
|
|
85
|
+
) -> None:
|
|
86
|
+
self._registry: TypeRegistry = compose_registry(
|
|
87
|
+
providers if providers is not None else list(core_providers)
|
|
88
|
+
)
|
|
89
|
+
# ADR-0023 — strict load closes the open-attr policy: an authored own
|
|
90
|
+
# @-attr matching no per-type schema and no commonAttr → ERR_UNKNOWN_ATTR
|
|
91
|
+
# (alongside Python's always-on unknown TYPE/SUBTYPE rejection). Defaults
|
|
92
|
+
# False so a downstream app keeps the legacy open-attr behavior; the
|
|
93
|
+
# library's own conformance corpora load strict.
|
|
94
|
+
self._strict: bool = strict
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def registry(self) -> TypeRegistry:
|
|
98
|
+
return self._registry
|
|
99
|
+
|
|
100
|
+
# --- core API ------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
def load(self, sources: list[MetaDataSource]) -> LoadResult:
|
|
103
|
+
"""Parse -> merge -> super-resolve -> validate -> freeze."""
|
|
104
|
+
# Canonical empty root (matches parse_document's seed).
|
|
105
|
+
result = LoadResult(root=MetaRoot(TYPE_METADATA, SUBTYPE_ROOT, ""))
|
|
106
|
+
roots: list[MetaData] = []
|
|
107
|
+
|
|
108
|
+
for src in sources:
|
|
109
|
+
parsed = self._parse_source(src, result.errors)
|
|
110
|
+
if parsed is None:
|
|
111
|
+
continue
|
|
112
|
+
result.errors.extend(parsed.errors)
|
|
113
|
+
result.warnings.extend(parsed.warnings)
|
|
114
|
+
# FR5c — collect envelope-shaped warnings from each parse pass.
|
|
115
|
+
# Today these only arrive from merge sites (see merge_roots);
|
|
116
|
+
# the channel is exposed here for future parser-emitted ones.
|
|
117
|
+
result.envelope_warnings.extend(parsed.envelope_warnings)
|
|
118
|
+
if not parsed.errors:
|
|
119
|
+
roots.append(parsed.root)
|
|
120
|
+
|
|
121
|
+
if roots:
|
|
122
|
+
# FR5c — merge_roots is the site that emits ERR_MERGE_CONFLICT
|
|
123
|
+
# into errors and WARN_DUPLICATE_DECLARATION into both channels
|
|
124
|
+
# (legacy string + envelope) so the conformance runner's
|
|
125
|
+
# expected-warnings.json check sees the string forms while
|
|
126
|
+
# downstream tooling can consume the envelope shape.
|
|
127
|
+
result.root = merge_roots(
|
|
128
|
+
roots,
|
|
129
|
+
result.errors,
|
|
130
|
+
result.warnings,
|
|
131
|
+
result.envelope_warnings,
|
|
132
|
+
)
|
|
133
|
+
resolve_supers(result.root, result.errors)
|
|
134
|
+
|
|
135
|
+
run_validations(
|
|
136
|
+
result.root,
|
|
137
|
+
self._registry,
|
|
138
|
+
result.errors,
|
|
139
|
+
result.warnings,
|
|
140
|
+
envelope_warnings=result.envelope_warnings,
|
|
141
|
+
strict=self._strict,
|
|
142
|
+
)
|
|
143
|
+
result.root.freeze()
|
|
144
|
+
return result
|
|
145
|
+
|
|
146
|
+
# --- static factories (the 99% case, cross-language consistent) ----
|
|
147
|
+
|
|
148
|
+
@classmethod
|
|
149
|
+
def from_directory(
|
|
150
|
+
cls,
|
|
151
|
+
directory: Path | str,
|
|
152
|
+
providers: list[Provider] | None = None,
|
|
153
|
+
exclude: list[str] | None = None,
|
|
154
|
+
recurse: bool = True,
|
|
155
|
+
strict: bool = False,
|
|
156
|
+
) -> LoadResult:
|
|
157
|
+
"""Load every JSON/YAML file under ``directory`` (recursive by default).
|
|
158
|
+
|
|
159
|
+
``strict`` (ADR-0023) — when True, an undeclared own ``@-attr`` →
|
|
160
|
+
``ERR_UNKNOWN_ATTR``. Defaults False (downstream-friendly open policy).
|
|
161
|
+
"""
|
|
162
|
+
loader = cls(providers=providers, strict=strict)
|
|
163
|
+
sources = list(
|
|
164
|
+
DirectorySource(directory, exclude=exclude, recurse=recurse).expand()
|
|
165
|
+
)
|
|
166
|
+
return loader.load(sources)
|
|
167
|
+
|
|
168
|
+
@classmethod
|
|
169
|
+
def from_uris(
|
|
170
|
+
cls,
|
|
171
|
+
uris: list[str],
|
|
172
|
+
providers: list[Provider] | None = None,
|
|
173
|
+
strict: bool = False,
|
|
174
|
+
) -> LoadResult:
|
|
175
|
+
"""Load metadata from a list of URIs (file:// or http(s)://).
|
|
176
|
+
|
|
177
|
+
``strict`` (ADR-0023) — see :meth:`from_directory`.
|
|
178
|
+
"""
|
|
179
|
+
loader = cls(providers=providers, strict=strict)
|
|
180
|
+
return loader.load([UriSource(u) for u in uris])
|
|
181
|
+
|
|
182
|
+
@classmethod
|
|
183
|
+
def from_string(
|
|
184
|
+
cls,
|
|
185
|
+
content: str,
|
|
186
|
+
format: MetaDataFormat = MetaDataFormat.JSON,
|
|
187
|
+
providers: list[Provider] | None = None,
|
|
188
|
+
strict: bool = False,
|
|
189
|
+
) -> LoadResult:
|
|
190
|
+
"""Load metadata from an in-memory string (defaults to JSON).
|
|
191
|
+
|
|
192
|
+
``strict`` (ADR-0023) — see :meth:`from_directory`.
|
|
193
|
+
"""
|
|
194
|
+
loader = cls(providers=providers, strict=strict)
|
|
195
|
+
return loader.load([InMemoryStringSource(content, format=format)])
|
|
196
|
+
|
|
197
|
+
# --- internals -----------------------------------------------------
|
|
198
|
+
|
|
199
|
+
def _parse_source(
|
|
200
|
+
self, src: MetaDataSource, errors: list[MetaError]
|
|
201
|
+
) -> ParseResult | None:
|
|
202
|
+
"""Read a source's content and dispatch to the JSON or YAML parser.
|
|
203
|
+
|
|
204
|
+
Returns ``None`` if the source could not be read or its syntax was
|
|
205
|
+
malformed; the caller appends an error and skips this source from
|
|
206
|
+
the merge set.
|
|
207
|
+
"""
|
|
208
|
+
# FR5a / ADR-0009 — top-level envelope for the source. Until the parser
|
|
209
|
+
# walk descends, the offending location is the root (`$`). Matches C#
|
|
210
|
+
# Parser.cs:140 — only fall back to CodeSource when there is no source
|
|
211
|
+
# id at all; "<inline>" is a valid source identifier and must yield a
|
|
212
|
+
# JsonSource so envelope formats remain consistent across error sites.
|
|
213
|
+
envelope: ErrorSource = (
|
|
214
|
+
JsonSource(files=(src.id,), json_path="$")
|
|
215
|
+
if src.id
|
|
216
|
+
else CodeSource.DEFAULT
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
try:
|
|
220
|
+
text = src.read()
|
|
221
|
+
except OSError as exc:
|
|
222
|
+
errors.append(MetaError(
|
|
223
|
+
str(exc), ErrorCode.ERR_UNKNOWN, src.id, envelope=envelope,
|
|
224
|
+
))
|
|
225
|
+
return None
|
|
226
|
+
|
|
227
|
+
match src.format:
|
|
228
|
+
case MetaDataFormat.YAML:
|
|
229
|
+
try:
|
|
230
|
+
return parse_yaml(text, self._registry, source=src.id)
|
|
231
|
+
except ParseError as exc:
|
|
232
|
+
errors.append(MetaError(
|
|
233
|
+
str(exc), exc.code, src.id, envelope=envelope,
|
|
234
|
+
))
|
|
235
|
+
return None
|
|
236
|
+
case MetaDataFormat.JSON:
|
|
237
|
+
try:
|
|
238
|
+
doc = json.loads(text)
|
|
239
|
+
except json.JSONDecodeError as exc:
|
|
240
|
+
errors.append(MetaError(
|
|
241
|
+
str(exc), ErrorCode.ERR_MALFORMED_JSON, src.id,
|
|
242
|
+
envelope=envelope,
|
|
243
|
+
))
|
|
244
|
+
return None
|
|
245
|
+
return parse_document(doc, self._registry, source=src.id)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Polymorphic MetaDataSource implementations.
|
|
2
|
+
|
|
3
|
+
The cross-language Tier-1 contract: every source declares an identity (for
|
|
4
|
+
error locations), a format (json | yaml), and a read() returning the decoded
|
|
5
|
+
text content. File / directory / URI / in-memory sources are all just
|
|
6
|
+
implementations of that contract — the loader is source-agnostic.
|
|
7
|
+
|
|
8
|
+
See `docs/superpowers/specs/2026-05-25-cross-language-loader-architecture-unification.md`.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from .directory_source import DirectorySource
|
|
13
|
+
from .file_source import FileSource
|
|
14
|
+
from .meta_data_source import InMemoryStringSource, MetaDataFormat, MetaDataSource
|
|
15
|
+
from .uri_source import UriSource
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"MetaDataFormat",
|
|
19
|
+
"MetaDataSource",
|
|
20
|
+
"InMemoryStringSource",
|
|
21
|
+
"FileSource",
|
|
22
|
+
"DirectorySource",
|
|
23
|
+
"UriSource",
|
|
24
|
+
]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Directory expander -> sorted list of FileSource.
|
|
2
|
+
|
|
3
|
+
Walks the directory (recursively by default), filters to supported authoring
|
|
4
|
+
extensions (``.json`` / ``.yaml`` / ``.yml``), honors a name-based exclude
|
|
5
|
+
list, and yields FileSource instances in deterministic ordinal-filename
|
|
6
|
+
order. Deterministic order is required because overlay merging is
|
|
7
|
+
order-sensitive (last-writer-wins on attr conflicts).
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from collections.abc import Iterable, Iterator
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from .file_source import FileSource
|
|
15
|
+
|
|
16
|
+
_SUPPORTED_SUFFIXES = (".json", ".yaml", ".yml")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DirectorySource:
|
|
20
|
+
"""Expands a directory into a sorted, filtered list of FileSource objects."""
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
directory: Path | str,
|
|
25
|
+
exclude: Iterable[str] | None = None,
|
|
26
|
+
recurse: bool = True,
|
|
27
|
+
) -> None:
|
|
28
|
+
self._directory = Path(directory)
|
|
29
|
+
self._exclude = set(exclude or ())
|
|
30
|
+
self._recurse = recurse
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def directory(self) -> Path:
|
|
34
|
+
return self._directory
|
|
35
|
+
|
|
36
|
+
def expand(self) -> Iterator[FileSource]:
|
|
37
|
+
candidates = (
|
|
38
|
+
self._directory.rglob("*") if self._recurse else self._directory.iterdir()
|
|
39
|
+
)
|
|
40
|
+
files = sorted(
|
|
41
|
+
(
|
|
42
|
+
p
|
|
43
|
+
for p in candidates
|
|
44
|
+
if p.is_file()
|
|
45
|
+
and p.suffix.lower() in _SUPPORTED_SUFFIXES
|
|
46
|
+
and p.name not in self._exclude
|
|
47
|
+
),
|
|
48
|
+
key=lambda p: p.name,
|
|
49
|
+
)
|
|
50
|
+
yield from (FileSource(p) for p in files)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Single-file MetaDataSource.
|
|
2
|
+
|
|
3
|
+
Format defaults to extension-derived (``.yaml`` / ``.yml`` -> YAML; otherwise
|
|
4
|
+
JSON). Reads with ``utf-8-sig`` so a leading UTF-8 BOM is silently stripped —
|
|
5
|
+
matches the prior `load_directory` behavior.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from .meta_data_source import MetaDataFormat, MetaDataSource
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _infer_format(path: Path) -> MetaDataFormat:
|
|
15
|
+
suffix = path.suffix.lower()
|
|
16
|
+
if suffix in (".yaml", ".yml"):
|
|
17
|
+
return MetaDataFormat.YAML
|
|
18
|
+
return MetaDataFormat.JSON
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class FileSource(MetaDataSource):
|
|
22
|
+
"""A single on-disk file, decoded eagerly via ``utf-8-sig``."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, path: Path | str, format: MetaDataFormat | None = None) -> None:
|
|
25
|
+
self._path = Path(path)
|
|
26
|
+
self._format = format if format is not None else _infer_format(self._path)
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def path(self) -> Path:
|
|
30
|
+
return self._path
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def id(self) -> str:
|
|
34
|
+
return self._path.name
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def format(self) -> MetaDataFormat:
|
|
38
|
+
return self._format
|
|
39
|
+
|
|
40
|
+
def read(self) -> str:
|
|
41
|
+
return self._path.read_text(encoding="utf-8-sig")
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""MetaDataSource abstract base + InMemoryStringSource impl.
|
|
2
|
+
|
|
3
|
+
Tier-1 contract: identity (for MetaError.location), declared format, and a
|
|
4
|
+
read() that returns decoded UTF-8 content.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from enum import Enum
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MetaDataFormat(str, Enum):
|
|
13
|
+
"""Authoring format vocabulary. Cross-language Tier 1: lowercase strings."""
|
|
14
|
+
|
|
15
|
+
JSON = "json"
|
|
16
|
+
YAML = "yaml"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class MetaDataSource(ABC):
|
|
20
|
+
"""A source of metadata content.
|
|
21
|
+
|
|
22
|
+
Implementations declare identity + format up front; bytes may be read
|
|
23
|
+
eagerly or lazily (the loader treats `read()` as decoded text).
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
@abstractmethod
|
|
28
|
+
def id(self) -> str:
|
|
29
|
+
"""Stable identity used in MetaError.location (e.g. file name, URI, ``<inline>``)."""
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
@abstractmethod
|
|
33
|
+
def format(self) -> MetaDataFormat:
|
|
34
|
+
"""Declared authoring format. Source-declared, never sniffed by the loader."""
|
|
35
|
+
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def read(self) -> str:
|
|
38
|
+
"""Return the decoded text content."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class InMemoryStringSource(MetaDataSource):
|
|
42
|
+
"""In-memory string source; no I/O.
|
|
43
|
+
|
|
44
|
+
Default identity is ``<inline>``; callers may pass a more descriptive id
|
|
45
|
+
(e.g. ``<test:foo>``) — it surfaces in MetaError.location.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
content: str,
|
|
51
|
+
id: str = "<inline>",
|
|
52
|
+
format: MetaDataFormat = MetaDataFormat.JSON,
|
|
53
|
+
) -> None:
|
|
54
|
+
self._content = content
|
|
55
|
+
self._id = id
|
|
56
|
+
self._format = format
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def id(self) -> str:
|
|
60
|
+
return self._id
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def format(self) -> MetaDataFormat:
|
|
64
|
+
return self._format
|
|
65
|
+
|
|
66
|
+
def read(self) -> str:
|
|
67
|
+
return self._content
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""URI-backed MetaDataSource.
|
|
2
|
+
|
|
3
|
+
Supports ``file://``, ``http://``, and ``https://`` schemes. Format defaults to
|
|
4
|
+
extension-derived from the URI path; callers may override.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from urllib.parse import urlparse
|
|
10
|
+
from urllib.request import urlopen
|
|
11
|
+
|
|
12
|
+
from .meta_data_source import MetaDataFormat, MetaDataSource
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _infer_format(uri: str) -> MetaDataFormat:
|
|
16
|
+
path = urlparse(uri).path
|
|
17
|
+
suffix = Path(path).suffix.lower()
|
|
18
|
+
if suffix in (".yaml", ".yml"):
|
|
19
|
+
return MetaDataFormat.YAML
|
|
20
|
+
return MetaDataFormat.JSON
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class UriSource(MetaDataSource):
|
|
24
|
+
"""A URI-backed source. Lazily fetched on ``read()``."""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
uri: str,
|
|
29
|
+
format: MetaDataFormat | None = None,
|
|
30
|
+
timeout: float = 30.0,
|
|
31
|
+
) -> None:
|
|
32
|
+
self._uri = uri
|
|
33
|
+
self._format = format if format is not None else _infer_format(uri)
|
|
34
|
+
self._timeout = timeout
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def id(self) -> str:
|
|
38
|
+
return self._uri
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def format(self) -> MetaDataFormat:
|
|
42
|
+
return self._format
|
|
43
|
+
|
|
44
|
+
def read(self) -> str:
|
|
45
|
+
parsed = urlparse(self._uri)
|
|
46
|
+
if parsed.scheme == "file":
|
|
47
|
+
# urlparse splits the leading slashes off the path on file:// URIs.
|
|
48
|
+
return Path(parsed.path).read_text(encoding="utf-8-sig")
|
|
49
|
+
if parsed.scheme in ("http", "https"):
|
|
50
|
+
# Schemes are explicitly allowlisted (file/http/https) above; arbitrary
|
|
51
|
+
# URI handlers (ftp, etc.) reject with ValueError before urlopen is called.
|
|
52
|
+
with urlopen(self._uri, timeout=self._timeout) as resp: # noqa: S310
|
|
53
|
+
return resp.read().decode("utf-8")
|
|
54
|
+
raise ValueError(
|
|
55
|
+
f"UriSource: unsupported scheme '{parsed.scheme}' on {self._uri}"
|
|
56
|
+
)
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""FR-014 — TPH discriminator cross-attribute rules.
|
|
2
|
+
|
|
3
|
+
Codes (all errors):
|
|
4
|
+
* ``ERR_DISCRIMINATOR_FIELD_NOT_FOUND`` — ``@discriminator`` names a field
|
|
5
|
+
that does not exist on the entity (own or via extends chain).
|
|
6
|
+
* ``ERR_DISCRIMINATOR_VALUE_DUPLICATE`` — two subtypes of the same
|
|
7
|
+
``@discriminator``-bearing root claim the same ``@discriminatorValue``.
|
|
8
|
+
* ``ERR_DISCRIMINATOR_VALUE_MISSING`` — a concrete (non-abstract) entity
|
|
9
|
+
extends a chain whose root carries ``@discriminator`` but lacks
|
|
10
|
+
``@discriminatorValue``.
|
|
11
|
+
* ``ERR_DISCRIMINATOR_VALUE_TYPE_MISMATCH`` — ``@discriminatorValue`` cannot be
|
|
12
|
+
coerced to the discriminator field's subtype (enum: not in ``@values``;
|
|
13
|
+
integer-family: not numeric; string: always OK).
|
|
14
|
+
|
|
15
|
+
Mirrors the TS reference
|
|
16
|
+
``packages/metadata/src/core/object/validate-discriminator.ts``.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import re
|
|
21
|
+
|
|
22
|
+
from ..errors import ErrorCode, MetaError
|
|
23
|
+
from ..meta.meta_data import MetaData
|
|
24
|
+
from ..meta.core.field.field_constants import (
|
|
25
|
+
FIELD_ATTR_VALUES,
|
|
26
|
+
FIELD_SUBTYPE_ENUM,
|
|
27
|
+
FIELD_SUBTYPE_INT,
|
|
28
|
+
FIELD_SUBTYPE_LONG,
|
|
29
|
+
FIELD_SUBTYPE_STRING,
|
|
30
|
+
)
|
|
31
|
+
from ..meta.core.object.object_constants import (
|
|
32
|
+
OBJECT_ATTR_DISCRIMINATOR,
|
|
33
|
+
OBJECT_ATTR_DISCRIMINATOR_VALUE,
|
|
34
|
+
OBJECT_SUBTYPE_ENTITY,
|
|
35
|
+
)
|
|
36
|
+
from ..shared.base_types import TYPE_FIELD, TYPE_OBJECT
|
|
37
|
+
|
|
38
|
+
_NUMERIC_DISCRIMINATOR_SUBTYPES = frozenset(
|
|
39
|
+
{FIELD_SUBTYPE_INT, FIELD_SUBTYPE_LONG}
|
|
40
|
+
)
|
|
41
|
+
_INT_RE = re.compile(r"^-?\d+$")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _find_field_on_entity(entity: MetaData, name: str) -> MetaData | None:
|
|
45
|
+
"""A field with ``name`` on ``entity`` — own first, then via extends chain."""
|
|
46
|
+
for child in entity.own_children():
|
|
47
|
+
if child.type == TYPE_FIELD and child.name == name:
|
|
48
|
+
return child
|
|
49
|
+
cursor = entity.super_data
|
|
50
|
+
while cursor is not None:
|
|
51
|
+
for child in cursor.own_children():
|
|
52
|
+
if child.type == TYPE_FIELD and child.name == name:
|
|
53
|
+
return child
|
|
54
|
+
cursor = cursor.super_data
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _find_discriminator_root(entity: MetaData) -> tuple[MetaData | None, str | None]:
|
|
59
|
+
"""First ancestor (or self) carrying ``@discriminator``: (root, fieldName)."""
|
|
60
|
+
cursor: MetaData | None = entity
|
|
61
|
+
while cursor is not None:
|
|
62
|
+
v = cursor.attr(OBJECT_ATTR_DISCRIMINATOR)
|
|
63
|
+
if isinstance(v, str) and v != "":
|
|
64
|
+
return cursor, v
|
|
65
|
+
cursor = cursor.super_data
|
|
66
|
+
return None, None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def validate_discriminator(root: MetaData, errors: list[MetaError]) -> None:
|
|
70
|
+
entities = [
|
|
71
|
+
c
|
|
72
|
+
for c in root.own_children()
|
|
73
|
+
if c.type == TYPE_OBJECT and c.sub_type == OBJECT_SUBTYPE_ENTITY
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
# Pass 1: @discriminator name resolution (own + inherited fields).
|
|
77
|
+
for obj in entities:
|
|
78
|
+
disc = obj.attr(OBJECT_ATTR_DISCRIMINATOR)
|
|
79
|
+
if not isinstance(disc, str) or disc == "":
|
|
80
|
+
continue
|
|
81
|
+
if _find_field_on_entity(obj, disc) is None:
|
|
82
|
+
errors.append(
|
|
83
|
+
MetaError(
|
|
84
|
+
f'object.entity "{obj.name}" @discriminator: "{disc}" does not '
|
|
85
|
+
"name a field on this entity (checked own children and the "
|
|
86
|
+
"extends chain)",
|
|
87
|
+
ErrorCode.ERR_DISCRIMINATOR_FIELD_NOT_FOUND,
|
|
88
|
+
envelope=obj.source,
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Pass 2: @discriminatorValue type-check + collect bindings per root.
|
|
93
|
+
bindings_by_root: list[tuple[MetaData, list[tuple[MetaData, str]]]] = []
|
|
94
|
+
_root_index: dict[int, list[tuple[MetaData, str]]] = {}
|
|
95
|
+
|
|
96
|
+
for obj in entities:
|
|
97
|
+
value = obj.attr(OBJECT_ATTR_DISCRIMINATOR_VALUE)
|
|
98
|
+
if not isinstance(value, str) or value == "":
|
|
99
|
+
continue
|
|
100
|
+
|
|
101
|
+
disc_root, field_name = _find_discriminator_root(obj)
|
|
102
|
+
if disc_root is None or field_name is None:
|
|
103
|
+
continue
|
|
104
|
+
field = _find_field_on_entity(disc_root, field_name)
|
|
105
|
+
if field is None:
|
|
106
|
+
continue # root's own ERR_DISCRIMINATOR_FIELD_NOT_FOUND already fires
|
|
107
|
+
|
|
108
|
+
if field.sub_type == FIELD_SUBTYPE_ENUM:
|
|
109
|
+
enum_values = field.attr(FIELD_ATTR_VALUES)
|
|
110
|
+
members = [str(v) for v in enum_values] if isinstance(enum_values, (list, tuple)) else []
|
|
111
|
+
if value not in members:
|
|
112
|
+
errors.append(
|
|
113
|
+
MetaError(
|
|
114
|
+
f'object.entity "{obj.name}" @discriminatorValue: "{value}" '
|
|
115
|
+
f'is not a member of the discriminator enum field '
|
|
116
|
+
f'"{field_name}" @values [{", ".join(members)}]',
|
|
117
|
+
ErrorCode.ERR_DISCRIMINATOR_VALUE_TYPE_MISMATCH,
|
|
118
|
+
envelope=obj.source,
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
elif field.sub_type in _NUMERIC_DISCRIMINATOR_SUBTYPES:
|
|
122
|
+
if _INT_RE.match(value) is None:
|
|
123
|
+
errors.append(
|
|
124
|
+
MetaError(
|
|
125
|
+
f'object.entity "{obj.name}" @discriminatorValue: "{value}" '
|
|
126
|
+
f'does not coerce to numeric discriminator field '
|
|
127
|
+
f'"{field_name}" (field.{field.sub_type})',
|
|
128
|
+
ErrorCode.ERR_DISCRIMINATOR_VALUE_TYPE_MISMATCH,
|
|
129
|
+
envelope=obj.source,
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
elif field.sub_type != FIELD_SUBTYPE_STRING:
|
|
133
|
+
# Non-{enum, integer-family, string} discriminators accepted silently.
|
|
134
|
+
pass
|
|
135
|
+
|
|
136
|
+
existing = _root_index.get(id(disc_root))
|
|
137
|
+
if existing is None:
|
|
138
|
+
existing = []
|
|
139
|
+
_root_index[id(disc_root)] = existing
|
|
140
|
+
bindings_by_root.append((disc_root, existing))
|
|
141
|
+
existing.append((obj, value))
|
|
142
|
+
|
|
143
|
+
# Pass 3: ERR_DISCRIMINATOR_VALUE_DUPLICATE within each root's subtypes.
|
|
144
|
+
for _disc_root, bindings in bindings_by_root:
|
|
145
|
+
seen: dict[str, MetaData] = {}
|
|
146
|
+
for subtype, value in bindings:
|
|
147
|
+
prev = seen.get(value)
|
|
148
|
+
if prev is not None:
|
|
149
|
+
errors.append(
|
|
150
|
+
MetaError(
|
|
151
|
+
f'object.entity "{subtype.name}" @discriminatorValue: '
|
|
152
|
+
f'"{value}" duplicates the value already claimed by '
|
|
153
|
+
f'"{prev.name}"',
|
|
154
|
+
ErrorCode.ERR_DISCRIMINATOR_VALUE_DUPLICATE,
|
|
155
|
+
envelope=subtype.source,
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
else:
|
|
159
|
+
seen[value] = subtype
|
|
160
|
+
|
|
161
|
+
# Pass 4: ERR_DISCRIMINATOR_VALUE_MISSING — every concrete entity that extends
|
|
162
|
+
# a @discriminator-bearing root must declare a value.
|
|
163
|
+
for obj in entities:
|
|
164
|
+
if obj.is_abstract is True:
|
|
165
|
+
continue
|
|
166
|
+
if isinstance(obj.attr(OBJECT_ATTR_DISCRIMINATOR_VALUE), str):
|
|
167
|
+
continue
|
|
168
|
+
if isinstance(obj.attr(OBJECT_ATTR_DISCRIMINATOR), str):
|
|
169
|
+
continue # a root, not a subtype
|
|
170
|
+
disc_root, _ = _find_discriminator_root(obj)
|
|
171
|
+
if disc_root is None or disc_root is obj:
|
|
172
|
+
continue
|
|
173
|
+
errors.append(
|
|
174
|
+
MetaError(
|
|
175
|
+
f'object.entity "{obj.name}" extends the @discriminator-bearing '
|
|
176
|
+
f'root "{disc_root.name}" but is missing @discriminatorValue '
|
|
177
|
+
"(required on every concrete subtype)",
|
|
178
|
+
ErrorCode.ERR_DISCRIMINATOR_VALUE_MISSING,
|
|
179
|
+
envelope=obj.source,
|
|
180
|
+
)
|
|
181
|
+
)
|