Skip to content

Commit 434f738

Browse files
dmealingclaude
andcommitted
Merge worktree-fr5a-python-2026-05-26 — FR5a (loader error envelope + source-on-node) for Python
Per ADR-0009 + FR5a spec (docs/superpowers/specs/2026-05-25-fr5a-json-shape-loader-errors.md). Python is the fourth and final port to land FR5a's per-port runtime + API work. The conformance fixture migration to the { errors: [...envelopes], warnings: [...] } shape is now unblocked and becomes the next cross-port step. Branch commits: c163bae feat(python): FR5a foundation — ErrorSource hierarchy, JsonPath, SemanticDiff fd03b22 feat(python): FR5a — thread source through parser + validation + MetaData.source 78ff0b8 fix(python): pre-merge review — complete FR5a envelope coverage 2a48841 chore(python): pre-merge simplifier pass — FR5a polish What FR5a delivers in Python: - New metaobjects.source package: @DataClass(frozen=True) discriminated union (ErrorSource abstract base + JsonSource / YamlSource / MergedSource / ResolvedSource / DatabaseSource / CodeSource) + Contributor / NodeContext / YamlPosition / DbLocation / LoaderError / LoaderWarning. Each variant carries a `format` ClassVar discriminant matching TS/C#/Java strings. JsonSource enforces the FR5a length-1 invariant in __post_init__. - metaobjects.source.json_path — canonical JSONPath builder matching TS + C# + Java byte-for-byte (identifier-safe → dot; otherwise single-quoted bracket with `'` escape; `[N]` indices; root `$`). - metaobjects.source.semantic_diff — both JSON-value and (MetaData, MetaData) entry points; lazy MetaData import resolves the source ↔ meta_data module cycle. - MetaData._source attribute (default CodeSource.DEFAULT) with the existing _require_mutable() freeze guard reused for set_source. - MetaError carries an optional envelope: ErrorSource | None alongside legacy source / path (conformance adapter only inspects code). - parser.py threads a JsonPathBuilder through parse_document → _build → _parse_attr_child. Every constructed node + every parser-built MetaAttribute child (inline @-attrs AND typed attr.* children) gets set_source(JsonSource(...)). Every error site passes envelope. - validation_passes.py — all 29 MetaError sites pass envelope (28 initially, plus ERR_PROVIDER_ATTR_CONFLICT fixed in the review pass). Pre-merge gate: code-reviewer flagged the parser-attr source-tagging gap, a `<inline>` source-id carve-out inconsistent with mid-tree envelope construction, and one missing-envelope site. All three fixed in 78ff0b8. Simplifier unquoted two now-redundant string annotations (with __future__ annotations at the top of both files). Verification: 496 Python tests green (was 466 baseline, +30 new FR5a tests for JsonPath edges, SemanticDiff smoke, source-on-node, parser attr source-tagging). Conformance fixture corpus stays in the OLD { "code": "ERR_*" } shape — Python's conformance_adapter continues to extract code from envelopes for the cross-port assertion. What stays unchanged: - Conformance fixtures still in the OLD shape (cross-port artifact); fixture rewrite is now the next coordinated step across all 4 ports. - No new ERR_* codes; existing codes get richer envelopes. With this merge, FR5a per-port runtime + API work is COMPLETE across TS / C# / Java / Python. ADR-0009 envelope shape is consistent on the wire end-to-end. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2 parents 5ada0a3 + 2a48841 commit 434f738

13 files changed

Lines changed: 1149 additions & 24 deletions

File tree

server/python/src/metaobjects/errors.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
from __future__ import annotations
33

44
from enum import Enum
5+
from typing import TYPE_CHECKING, Optional
6+
7+
if TYPE_CHECKING: # avoid runtime import cycles
8+
from .source import ErrorSource
59

610

711
class ErrorCode(str, Enum):
@@ -47,19 +51,28 @@ class ErrorCode(str, Enum):
4751

4852

4953
class MetaError:
50-
"""A loader error. `code` is the conformance-compared value; `message` is human text."""
54+
"""A loader error. ``code`` is the conformance-compared value; ``message`` is human text.
55+
56+
FR5a / ADR-0009: ``envelope`` is the structured provenance envelope every
57+
cross-language port emits — populated by the parser (JSON tree-walk) and by
58+
validation passes that have access to a node's ``source``. Legacy ``source``
59+
(the file path) / ``path`` remain for backward-compat (the conformance
60+
adapter only inspects ``code``); new sites should pass ``envelope``.
61+
"""
5162

5263
def __init__(
5364
self,
5465
message: str,
5566
code: ErrorCode = ErrorCode.ERR_UNKNOWN,
5667
source: str | None = None,
5768
path: str | None = None,
69+
envelope: Optional[ErrorSource] = None,
5870
) -> None:
5971
self.message = message
6072
self.code = code
6173
self.source = source
6274
self.path = path
75+
self.envelope = envelope
6376

6477
def __repr__(self) -> str:
6578
return f"MetaError({self.code.name}: {self.message!r})"

server/python/src/metaobjects/loader/meta_data_loader.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from ..provider import Provider, compose_registry
2828
from ..registry import TypeRegistry
2929
from ..shared.base_types import SUBTYPE_ROOT, TYPE_METADATA
30+
from ..source import CodeSource, ErrorSource, JsonSource
3031
from ..super_resolve import resolve_supers
3132
from .merge import merge_roots
3233
from .sources import (
@@ -139,23 +140,41 @@ def _parse_source(
139140
malformed; the caller appends an error and skips this source from
140141
the merge set.
141142
"""
143+
# FR5a / ADR-0009 — top-level envelope for the source. Until the parser
144+
# walk descends, the offending location is the root (`$`). Matches C#
145+
# Parser.cs:140 — only fall back to CodeSource when there is no source
146+
# id at all; "<inline>" is a valid source identifier and must yield a
147+
# JsonSource so envelope formats remain consistent across error sites.
148+
envelope: ErrorSource = (
149+
JsonSource(files=(src.id,), json_path="$")
150+
if src.id
151+
else CodeSource.DEFAULT
152+
)
153+
142154
try:
143155
text = src.read()
144156
except OSError as exc:
145-
errors.append(MetaError(str(exc), ErrorCode.ERR_UNKNOWN, src.id))
157+
errors.append(MetaError(
158+
str(exc), ErrorCode.ERR_UNKNOWN, src.id, envelope=envelope,
159+
))
146160
return None
147161

148162
match src.format:
149163
case MetaDataFormat.YAML:
150164
try:
151165
return parse_yaml(text, self._registry, source=src.id)
152166
except ParseError as exc:
153-
errors.append(MetaError(str(exc), exc.code, src.id))
167+
errors.append(MetaError(
168+
str(exc), exc.code, src.id, envelope=envelope,
169+
))
154170
return None
155171
case MetaDataFormat.JSON:
156172
try:
157173
doc = json.loads(text)
158174
except json.JSONDecodeError as exc:
159-
errors.append(MetaError(str(exc), ErrorCode.ERR_MALFORMED_JSON, src.id))
175+
errors.append(MetaError(
176+
str(exc), ErrorCode.ERR_MALFORMED_JSON, src.id,
177+
envelope=envelope,
178+
))
160179
return None
161180
return parse_document(doc, self._registry, source=src.id)

0 commit comments

Comments
 (0)