MAS-WP-0005-T08: v0.2 stand-ins and resource_id_case in policy; lint; gateway reports canonical form

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

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 252470@bnt-lap001
Assistant-Session: ad628c8f-4be2-4979-94c9-76c7ed6f8241
This commit is contained in:
tegwick 2026-10-18 22:24:28 +02:00
parent 7d978f1f26
commit eb10f54fd2
6 changed files with 165 additions and 7 deletions

View file

@ -10,7 +10,7 @@ from __future__ import annotations
from dataclasses import dataclass, field
from typing import Mapping, Optional
from .model import ParsedAddress, ParsedLocalPart, ResourceTarget
from .model import Facet, Interaction, OpaqueTarget, ParsedAddress, ParsedLocalPart, ResourceTarget, _render
from .statute import Statute
# Kind sources, in the order they are consulted.
@ -41,6 +41,7 @@ class AddressSemantics:
equivalence_key: str = ""
failures: tuple[SemanticFailure, ...] = ()
unknowns: tuple[str, ...] = ()
stand_ins: tuple[str, ...] = () # v0.2: "resource" and/or "interaction"
@property
def ok(self) -> bool:
@ -60,6 +61,8 @@ class AddressSemantics:
"interaction": self.interaction,
"required_profile": self.parsed.required_profile.value,
"equivalence_key": self.equivalence_key,
"canonical_local_part": str(self.parsed.raw),
"stand_ins": list(self.stand_ins),
"failures": [{"code": f.code, "message": f.message} for f in self.failures],
"unknowns": list(self.unknowns),
}
@ -82,10 +85,56 @@ def equivalence_key(parsed: ParsedLocalPart, statute: Optional[Statute]) -> str:
if statute is None:
return base
if isinstance(parsed.target, ResourceTarget):
return _fold(base, statute.local_part_case, False)
key = _fold(base, statute.local_part_case, False)
if statute.resource_id_case == "insensitive":
key = key.lower() # resource types are lower-case already (v0.1.1)
return key
return _fold(base, statute.local_part_case, statute.dot_folding)
def _split_interaction(value: str, statute: Statute) -> tuple[str, Optional[str]]:
"""`x---MODE` -> (`x`, MODE) when MODE is enabled; split at the last `---`."""
i = value.rfind("---")
if i > 0:
mode = value[i + 3:]
if mode and not mode.startswith("-") and statute.interaction_mode_enabled(mode):
return value[:i], mode
return value, None
def apply_stand_ins(parsed: ParsedLocalPart, statute: Optional[Statute]) -> tuple[ParsedLocalPart, tuple[str, ...]]:
"""Read v0.2 stand-ins as canonical syntax (spec v0.2 section 6.3).
Only a statute that enables a stand-in gives it meaning. Interaction
(`---MODE`, enabled mode) is matched first, then resource (`TYPE--ID`,
declared type, first `--`). Returns the canonical parse and the stand-ins used.
"""
if statute is None or not (statute.alt_resource or statute.alt_interaction):
return parsed, ()
target, facet, interaction = parsed.target, parsed.facet, parsed.interaction
used: list[str] = []
if statute.alt_interaction and statute.p2 and interaction is None:
if facet is not None:
rest, mode = _split_interaction(facet.value, statute)
if mode and rest:
facet, interaction = Facet(rest), Interaction(mode)
elif isinstance(target, OpaqueTarget):
rest, mode = _split_interaction(target.value, statute)
if mode and rest:
target, interaction = OpaqueTarget(rest), Interaction(mode)
if interaction is not None:
used.append("interaction")
if statute.alt_resource and isinstance(target, OpaqueTarget) and "--" in target.value:
rtype, rid = target.value.split("--", 1)
if rtype in statute.resource_types and rid and not rid.startswith("-"):
target = ResourceTarget(rtype, rid)
used.append("resource")
if not used:
return parsed, ()
return ParsedLocalPart(raw=_render(target, facet, interaction), target=target,
facet=facet, interaction=interaction), tuple(used)
def _lookup(registry: Mapping[str, Mapping], key: str, case: str, dots: bool):
if key in registry:
return key, registry[key]
@ -103,6 +152,7 @@ def resolve(parsed, statute: Optional[Statute] = None,
if isinstance(parsed, ParsedAddress):
domain, parsed = parsed.domain, parsed.local
address = f"{parsed.raw}@{domain}" if domain else parsed.raw
parsed, stand_ins = apply_stand_ins(parsed, statute)
local = local_registry or {}
failures: list[SemanticFailure] = []
unknowns: list[str] = []
@ -164,5 +214,5 @@ def resolve(parsed, statute: Optional[Statute] = None,
kind=kind, kind_source=source, registry_key=reg_key, intake_for=intake_for,
resource_type=rtype, resource_id=rid, facet=facet, interaction=mode,
equivalence_key=equivalence_key(parsed, statute),
failures=tuple(failures), unknowns=tuple(unknowns),
failures=tuple(failures), unknowns=tuple(unknowns), stand_ins=stand_ins,
)

View file

@ -195,6 +195,26 @@ def semantic_findings(doc: Mapping, host: Optional[str] = None,
f"collides with {seen[folded]!r} under the declared equivalence policy")
seen[folded] = key
# v0.2 stand-ins (spec 6.3): an enabled stand-in must not change the
# meaning of names the statute already registers.
alt = doc.get("alternate_syntax", {})
modes = doc.get("interaction_modes", {})
if alt.get("interaction"):
if not doc["profiles"]["p2"]:
add(WARNING, "stand-in-without-p2", "/alternate_syntax/interaction",
"the '---' stand-in has no effect while P2 is disabled")
for mode in modes:
if mode.startswith("-"):
add(ERROR, "stand-in-mode-name", f"/interaction_modes/{mode}",
"mode names must not start with '-' when the '---' stand-in is enabled")
for key in doc.get("addresses", {}):
path = f"/addresses/{key}"
if alt.get("resource") and "--" in key and key.split("--", 1)[0] in doc.get("resource_types", {}):
add(ERROR, "stand-in-collision", path, f"{key!r} would read as a resource under the '--' stand-in")
i = key.rfind("---")
if alt.get("interaction") and i > 0 and modes.get(key[i + 3:], {}).get("enabled"):
add(ERROR, "stand-in-collision", path, f"{key!r} would read as an interaction under the '---' stand-in")
inherits = doc.get("inherits")
own_url = statute_url or f"https://{canonical_host(ns)}/.well-known/mas"
if inherits and inherits.rstrip("/") == own_url.rstrip("/"):

View file

@ -92,9 +92,9 @@ def build_event(recipient: str, message: dict, resolver: Resolver) -> dict:
"valid": True,
"domain": addr.domain,
"local_part": addr.local.raw,
"base": addr.local.base,
"target": str(addr.local.target),
"target_syntax": addr.local.target.kind,
"base": sem.parsed.base,
"target": str(sem.parsed.target),
"target_syntax": sem.parsed.target.kind + ("-alternate" if "resource" in sem.stand_ins else ""),
"discovery": {"ok": True, "url": disc.url} if disc.ok else
{"ok": False, "failure": disc.kind.value, "detail": disc.detail},
**{k: v for k, v in sem.to_dict().items() if k != "address"},

View file

@ -103,3 +103,57 @@ def test_intake_for_is_reported():
assert sem.kind == "function" and sem.intake_for == "ticket" and sem.interaction == "callback"
assert resolve(parse_address("support@great-service.example"), GS).intake_for is None
assert resolve(parse_address("ticket@great-service.example"), None).intake_for is None
# -- v0.2 stand-ins (spec v0.2 section 6.3) ----------------------------------------
def _v02_statute(resource=True, interaction=True, id_case="insensitive"):
doc = statute_document("valid-great-service.json")
doc["mas_version"] = "0.2"
doc["alternate_syntax"] = {"resource": resource, "interaction": interaction}
doc["equivalence"]["resource_id_case"] = id_case
return build_statute(doc, "great-service.example")
V2 = _v02_statute()
@pytest.mark.parametrize("raw, canonical, stand_ins", [
("ticket---callback", "ticket!callback", ("interaction",)),
("ticket--gs1---callback", "ticket=gs1!callback", ("interaction", "resource")),
("ticket--a---b", "ticket=a---b", ("resource",)), # b is not an enabled mode
("ticket--gs1+comments", "ticket=gs1+comments", ("resource",)),
("ticket--gs-85qj8wc09p+comments", "ticket=gs-85qj8wc09p+comments", ("resource",)),
("support---callback", "support!callback", ("interaction",)),
("invoice--x1", "invoice--x1", ()), # undeclared type
("ticket=gs1!callback", "ticket=gs1!callback", ()), # already canonical
])
def test_stand_in_table(raw, canonical, stand_ins):
sem = resolve(parse_address(f"{raw}@great-service.example"), V2)
assert sem.to_dict()["canonical_local_part"] == canonical and sem.stand_ins == stand_ins
def test_stand_in_resource_correlates_like_canonical():
alt = resolve(parse_address("ticket--GS-85QJ8WC09P+comments@great-service.example"), V2)
canon = resolve(parse_address("ticket=gs-85qj8wc09p@great-service.example"), V2)
assert alt.kind == "resource" and alt.resource_id == "GS-85QJ8WC09P" and alt.facet == "comments"
assert alt.equivalence_key == canon.equivalence_key == "ticket=gs-85qj8wc09p"
def test_stand_in_intake_reaches_registry():
sem = resolve(parse_address("ticket---callback@great-service.example"), V2)
assert sem.kind == "function" and sem.intake_for == "ticket" and sem.interaction == "callback"
def test_stand_ins_need_the_statute_switch():
off = _v02_statute(resource=False, interaction=False, id_case="exact")
for raw in ("ticket--gs1", "ticket---callback"):
sem = resolve(parse_address(f"{raw}@great-service.example"), off)
assert sem.stand_ins == () and sem.kind == "opaque"
assert resolve(parse_address("ticket--gs1@great-service.example"), GS).stand_ins == () # v0.1
assert resolve(parse_address("ticket--gs1@great-service.example"), None).stand_ins == ()
def test_resource_id_case_exact_keeps_case():
exact = _v02_statute(id_case="exact")
assert resolve(parse_address("ticket=GS-1@great-service.example"), exact).equivalence_key == "ticket=GS-1"

View file

@ -110,3 +110,14 @@ def test_v01_statute_rejects_v02_fields():
doc = statute_document("valid-great-service.json")
doc["alternate_syntax"] = {"resource": True}
assert any(f.severity == ERROR and f.layer == "schema" for f in lint(doc, "great-service.example"))
def test_stand_in_lint():
doc = _v02(alternate_syntax={"resource": True, "interaction": True})
doc["addresses"]["ticket--legacy"] = {"kind": "function"}
doc["addresses"]["sales---callback"] = {"kind": "function"}
codes = [f.code for f in lint(doc, "great-service.example") if f.severity == ERROR]
assert codes.count("stand-in-collision") == 2
off = _v02()
off["addresses"]["ticket--legacy"] = {"kind": "function"}
assert "stand-in-collision" not in {f.code for f in lint(off, "great-service.example")}

View file

@ -256,7 +256,7 @@ against its own schema. Discovery accepts both.
```task
id: MAS-WP-0005-T08
status: todo
status: done
priority: medium
state_hub_task_id: "a345e073-04ca-54e1-bac7-837fe7955e4e"
```
@ -266,6 +266,29 @@ The policy interprets `---MODE` (interaction first, enabled mode only) and
`resource_id_case: insensitive` in the equivalence key. Statute lint covers
the stand-in constraints. Statute-aware fixtures.
- 2026-10-18 done:
- `mas_core.policy.apply_stand_ins` runs first in `resolve()`. It
matches the interaction (`---MODE`, enabled mode, last `---`; in the
facet or the target) and then the resource (`TYPE--ID`, declared type,
first `--`). Everything later (classification, registry,
`intake_for`, equivalence) sees the canonical parse, and the parser
stays statute-blind.
- `AddressSemantics` gains `stand_ins` and `canonical_local_part`; the
equivalence key lower-cases resource ids under
`resource_id_case: insensitive`.
- The gateway event reports the canonical target and base, plus
`target_syntax: resource-alternate` when `--` was used, while
`local_part` keeps what arrived.
- Lint: `stand-in-collision` (registry keys that would change meaning),
`stand-in-mode-name`, and `stand-in-without-p2` (warning).
- Tests: the spec §6.3 example table, correlation of
`ticket--GS-…+comments` with `ticket=gs-…`, intake via `---`, the
switch requirement (v0.1, off, no statute), exact id case, and lint.
168 tests pass.
- A v0.2 statute for mas.coulomb.social lints clean, and the gateway
turns `ticket--gs-85qj8wc09p+comments@` into resource
`ticket=gs-85qj8wc09p` with facet `comments`.
## v0.2 sending side: destination transforms in the generator
```task