Skip to content

Commit 845b8cf

Browse files
dmealingclaude
andcommitted
feat(#195): Python per-capability origin validation + native typing (cross-port parity)
Mirrors the committed TS reference (63943d0 validation, aff49ce typing) into the Python port: the four #195 capabilities are now validated + typed identically. - validation_passes.py: ports the closed expression grammar (validate_expr_node + infer_expr_type, mirroring meta-attr-expression.ts) + the shared _validate_order_by_keys helper, and the per-@agg branches — any/all (boolean non-array; @filter required; @Of forbidden; @via required + to-many), collect (isArray; @Of required + element-type preservation; @distinct/@orderby collect-only + mutually exclusive; @orderby resolves on the @Of entity), the inverse rule (non-collect ⇒ isArray:false), origin.computed (structural grammar → ERR_UNKNOWN_EXPR_NODE; inference vs base effective fields; type mismatch → ERR_COMPUTED_TYPE_MISMATCH), origin.first (@Of required + type-preserving; not @required; @via inference; @orderby resolution). - errors.py: ERR_UNKNOWN_EXPR_NODE + ERR_COMPUTED_TYPE_MISMATCH. - entity_model.py: origin_guaranteed_non_null — any/all/collect projection DTO fields non-null; first Optional; computed conservative-nullable. - origin_constants.py: AGG_ANY/AGG_ALL/AGG_COLLECT. - test_validation_origin_195.py: 20 tests mirroring the TS suite. Verified: conformance 336 + the 20 validation tests = 356 passed; error CODES identical to TS (message text keeps Python phrasing — conformance compares codes). Tier-3: the expression grammar lives in validation_passes.py (beside ops_for_subtype) to avoid an import cycle. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NGQ7oSuNcjhsMHWwZzhBwr
1 parent aff49ce commit 845b8cf

5 files changed

Lines changed: 775 additions & 13 deletions

File tree

server/python/src/metaobjects/codegen/generators/entity_model.py

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,14 @@
1414
IDENTITY_ATTR_GENERATION,
1515
)
1616
from metaobjects.meta.core.validator import validator_constants as vc
17-
from metaobjects.shared.base_types import TYPE_VALIDATOR
17+
from metaobjects.meta.persistence.origin.origin_constants import (
18+
AGG_ALL,
19+
AGG_ANY,
20+
AGG_COLLECT,
21+
ORIGIN_ATTR_AGG,
22+
ORIGIN_SUBTYPE_AGGREGATE,
23+
)
24+
from metaobjects.shared.base_types import TYPE_ORIGIN, TYPE_VALIDATOR
1825
from metaobjects.shared.separators import PACKAGE_SEP
1926
from metaobjects.codegen.config import GenConfig
2027
from metaobjects.codegen.constants import generated_header
@@ -221,6 +228,22 @@ def _type_expr_for_field(
221228
return type_expr, enum_type_name
222229

223230

231+
def origin_guaranteed_non_null(field: MetaField) -> bool:
232+
"""#195 — a field derived by an origin.aggregate ``@agg:any|all|collect`` is
233+
COALESCE-guaranteed non-null in the synthesized view (any→false, all→true,
234+
collect→[]), so its read type is non-null even when the field is not ``@required``.
235+
origin.first is deliberately NOT here — an empty related set selects no row (→ null);
236+
origin.computed nullability is expression-dependent, so it stays the conservative
237+
nullable default. Mirrors the TS originGuaranteedNonNull."""
238+
# ADR-0039 sanctioned own — origin.* never inherits (ADR-0029), so own is correct.
239+
for child in field.own_children():
240+
if child.type == TYPE_ORIGIN and child.sub_type == ORIGIN_SUBTYPE_AGGREGATE:
241+
agg = child.attr(ORIGIN_ATTR_AGG) # ADR-0039 own — origin attr
242+
if agg in (AGG_ANY, AGG_ALL, AGG_COLLECT):
243+
return True
244+
return False
245+
246+
224247
def _field_line(field: MetaField, imports: set[str], config: GenConfig) -> tuple[str, bool]:
225248
"""Return (source line, uses_field). Collects required imports into *imports*."""
226249
type_expr, enum_type_name = _type_expr_for_field(field, imports, config)
@@ -243,15 +266,20 @@ def _field_line(field: MetaField, imports: set[str], config: GenConfig) -> tuple
243266
else:
244267
default_expr = None
245268

246-
# A field is Optional only when it is neither required nor carries a @default.
247-
annotation = type_expr if (required or has_default) else f"{type_expr} | None"
269+
# #195 — a field derived by an origin.aggregate any|all|collect always COALESCEs
270+
# to a value (any→false, all→true, collect→[]), so its read type is mandatory
271+
# non-null exactly like a @required field (no `| None`, no default). origin.first /
272+
# origin.computed stay the conservative nullable default.
273+
mandatory = required or origin_guaranteed_non_null(field)
274+
# A field is Optional only when it is neither mandatory nor carries a @default.
275+
annotation = type_expr if (mandatory or has_default) else f"{type_expr} | None"
248276
if has_default and uses_field:
249277
assignment = f" = Field(default={default_expr}, {', '.join(parts)})"
250278
elif has_default:
251279
assignment = f" = {default_expr}"
252-
elif required and uses_field:
280+
elif mandatory and uses_field:
253281
assignment = f" = Field({', '.join(parts)})"
254-
elif required:
282+
elif mandatory:
255283
assignment = ""
256284
elif uses_field:
257285
assignment = f" = Field(default=None, {', '.join(parts)})"

server/python/src/metaobjects/errors.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,13 @@ class ErrorCode(str, Enum):
6060
# is to-one at every hop (you meant passthrough).
6161
ERR_AMBIGUOUS_PATH = "ERR_AMBIGUOUS_PATH"
6262
ERR_ORIGIN_CARDINALITY = "ERR_ORIGIN_CARDINALITY"
63+
# #195 — origin.computed @expr validation. ERR_UNKNOWN_EXPR_NODE: the expression
64+
# tree contains a node whose kind/op/fn is not in the closed grammar (fail-closed
65+
# per ADR-0023). ERR_COMPUTED_TYPE_MISMATCH: the @expr tree's inferred root type
66+
# does not equal the carrying field's declared field.<subType> (a computed column's
67+
# type is DERIVED from its expression, never asserted — no @convert escape).
68+
ERR_UNKNOWN_EXPR_NODE = "ERR_UNKNOWN_EXPR_NODE"
69+
ERR_COMPUTED_TYPE_MISMATCH = "ERR_COMPUTED_TYPE_MISMATCH"
6370
# FR-024 B6 — extends/origin agreement + derived-field providability. Vocabulary-
6471
# only here until FR-024 Phase E (the Python loader does not run these checks yet):
6572
# ERR_EXTENDS_ORIGIN_MISMATCH — a field's entity-nested extends (shape lineage)

0 commit comments

Comments
 (0)