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:
parent
7d978f1f26
commit
eb10f54fd2
6 changed files with 165 additions and 7 deletions
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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("/"):
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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")}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue