mail-address-semantics/mas_core/model.py
tegwick 59815701c1 Implement MAS v0.1 reference experiment (MAS-WP-0002 M0-M8, M9 first pass)
- mas_core: P2-maximal parser with required_profile, generator, statute
  schema + semantic lint + defaults-only inheritance, exact-host discovery
  with typed failures and same-origin redirects, pure policy resolver
- mas_conformance: vendored spec fixtures (drift-tested) + property and
  statute cases
- mas CLI: parse / explain / discover / lint-statute / test
- mas-gateway: generic mail -> MAS event -> HTTP (MTA pipe + webhook)
- examples: static great-service.example statute, reference ticket service
- docs/experiment-review.md: first-pass findings; live MTA test still open

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 623586@bnt-lap001
Assistant-Session: 10b30928-5b3b-4c11-9934-5ecca07367d8
2026-10-08 00:24:43 +02:00

131 lines
3.4 KiB
Python

"""Immutable syntax model for MAS v0.1 addresses.
These objects carry syntactic facts only. Nothing here encodes a semantic
kind (person, actor, function): that requires statute or registry metadata.
"""
from __future__ import annotations
from dataclasses import dataclass
from enum import Enum
from typing import Optional, Union
class RequiredProfile(str, Enum):
"""The minimum MAS profile whose grammar the syntax requires."""
P1 = "P1"
P2 = "P2"
@dataclass(frozen=True)
class OpaqueTarget:
value: str
kind = "opaque"
def __str__(self) -> str:
return self.value
@dataclass(frozen=True)
class ResourceTarget:
resource_type: str
resource_id: str
kind = "resource"
def __str__(self) -> str:
return f"{self.resource_type}={self.resource_id}"
Target = Union[OpaqueTarget, ResourceTarget]
@dataclass(frozen=True)
class Facet:
value: str
def __str__(self) -> str:
return self.value
@dataclass(frozen=True)
class Interaction:
mode: str
def __str__(self) -> str:
return self.mode
@dataclass(frozen=True)
class ParsedLocalPart:
"""A canonical MAS v0.1 local-part, parsed with the maximal (P2) grammar."""
raw: str
target: Target
facet: Optional[Facet] = None
interaction: Optional[Interaction] = None
@property
def required_profile(self) -> RequiredProfile:
return RequiredProfile.P2 if self.interaction is not None else RequiredProfile.P1
@property
def base(self) -> str:
"""The base target: what remains if a mail system strips the facet."""
return str(self.target)
@property
def generator_allowed(self) -> bool:
"""v0.1 generators MUST NOT emit both a facet and an interaction."""
return not (self.facet is not None and self.interaction is not None)
def without_facet(self) -> "ParsedLocalPart":
return ParsedLocalPart(raw=_render(self.target, None, self.interaction),
target=self.target, interaction=self.interaction)
def to_fixture_dict(self) -> dict:
"""Shape used by the conformance corpus (`parsed` objects)."""
out = {
"target": str(self.target),
"target_kind": self.target.kind,
"facet": self.facet.value if self.facet else None,
"interaction": self.interaction.mode if self.interaction else None,
}
if isinstance(self.target, ResourceTarget):
out["resource_type"] = self.target.resource_type
out["resource_id"] = self.target.resource_id
return out
def to_dict(self) -> dict:
out = self.to_fixture_dict()
out["required_profile"] = self.required_profile.value
out["base"] = self.base
out["generator_allowed"] = self.generator_allowed
return out
@dataclass(frozen=True)
class ParsedAddress:
local: ParsedLocalPart
domain: str
@property
def required_profile(self) -> RequiredProfile:
return self.local.required_profile
def __str__(self) -> str:
return f"{self.local.raw}@{self.domain}"
def to_dict(self) -> dict:
return {"address": str(self), "domain": self.domain, **self.local.to_dict()}
def _render(target: Target, facet: Optional[Facet], interaction: Optional[Interaction]) -> str:
s = str(target)
if facet is not None:
s += "+" + facet.value
if interaction is not None:
s += "!" + interaction.mode
return s