MAS-WP-0008: v0.2 wording pass, statute-aware conformance corpus in the bundle

conformance/statute-aware.json (vendored as mas_conformance/statute_aware.json)
covers the 6.3 stand-in table, resource_id_case equivalence and the 12.1
destination results and refusals; mas test runs it. Misread refusals now
name the field that would change.

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

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 5787@bnt-lap001
Assistant-Session: 5db26b82-138b-40cc-9856-4d5eb0f752fd
This commit is contained in:
tegwick 2026-10-19 09:31:30 +02:00
parent 499ddce015
commit c4719a238d
11 changed files with 988 additions and 41 deletions

View file

@ -24,6 +24,11 @@ New features, opt-in per statute with `"mas_version": "0.2"`
unquoted form. Receivers treat them identically; generators emit the
unquoted form; canonical parsers still reject the quoted form (§4).
Fixture p1-040 note updated.
- **Conformance (MAS-WP-0008).** `conformance/statute-aware.json` and
the `MAS-V0.2-STATUTE-AWARE` surface cover the stand-in table, the
`resource_id_case` equivalence and destination-transform refusals.
The text inherited from v0.1.1 no longer says "v0.1" for rules that are
unchanged.
## v0.1.1 (errata)

View file

@ -1,4 +1,4 @@
# MAS v0.1 Conformance
# MAS v0.2 Conformance (draft)
## 1. Test surfaces
@ -6,8 +6,9 @@ An implementation can claim any of the following independently:
- `MAS-P1-PARSER`
- `MAS-P2-PARSER`
- `MAS-V0.1-GENERATOR`
- `MAS-V0.1-STATUTE`
- `MAS-V0.2-GENERATOR`
- `MAS-V0.2-STATUTE`
- `MAS-V0.2-STATUTE-AWARE`
## 2. Parser requirements
@ -30,7 +31,7 @@ A generator:
- MUST emit `+` before `!`;
- MUST NOT emit more than one `+` or one `!`;
- MUST NOT emit `$` or `^`;
- MUST NOT emit both `+` and `!` in v0.1;
- MUST NOT emit both `+` and `!`;
- MUST keep the encoded SMTP local-part at or below 64 octets.
Fixtures with `generator_allowed: false` may still be accepted by a P2 parser,
@ -39,7 +40,8 @@ but a conforming generator MUST NOT create them.
## 4. Statute requirements
A statute implementation MUST validate discovery documents using
`schema/mas-statute-v0.1.schema.json`.
`schema/mas-statute-v0.2.schema.json` when `mas_version` is `0.2`. A `0.1`
statute is validated with the v0.1.1 schema and gets no v0.2 features.
It MUST NOT apply:
@ -51,7 +53,23 @@ It MUST NOT apply:
unless the exact-host statute successfully validates and declares them.
## 5. Discovery failure
## 5. Statute-aware requirements (v0.2)
An implementation claiming `MAS-V0.2-STATUTE-AWARE` MUST produce the
expected result for every case in `conformance/statute-aware.json`. Each
case names one of the file's inline statutes, whose `namespace` is the host.
- `interpretation`: the received local-part is read as `canonical`, using
the listed `stand_ins` (§6.3). The parser alone never interprets
stand-ins.
- `equivalence`: `a` and `b` have the same equivalence key exactly when
`equivalent` is true (§8, `resource_id_case`).
- `destination`: applying the transforms in `needs` to `local_part` yields
`expect`. When `expect` is `null`, the generator MUST refuse (§12.1).
`reason` is informative, and refusal codes are not normative.
`allow_lossy` marks an explicit request for a drop transform.
## 6. Discovery failure
Failure to retrieve or validate a statute:
@ -59,7 +77,7 @@ Failure to retrieve or validate a statute:
- does invalidate any claim that MAS namespace semantics were discovered;
- falls back to ordinary email handling plus exact local configuration.
## 6. Inheritance
## 7. Inheritance
Implementations are not required to implement `inherits`.
@ -71,7 +89,7 @@ Implementations that do implement it MUST:
- never inherit explicit `addresses` registry entries;
- let the child override inherited resource types and interaction modes.
## 7. Test command
## 8. Test command
The bundled reference parser can be run with:

View file

@ -11,17 +11,18 @@ This bundle is the **draft** of MAS v0.2. It extends v0.1.1 (see `CHANGELOG.md`)
- `examples/mas-statute.example.json` — example statute
- `CONFORMANCE.md` — parser/generator/statute conformance rules
- `conformance/addresses.json` — positive and negative address fixtures
- `conformance/statute-aware.json` — v0.2 stand-in, equivalence and destination-transform cases
- `reference/mas_parser.py` — small non-normative reference parser
## Profiles
- **P1 / Naming + Resource:** naming hints, `=` resource binding, `+` facet
- **P2 / Interaction:** optional `!` interaction preference
- **P3 / Economic:** reserved only; `$` and `^` have no v0.1 semantics
- **P3 / Economic:** reserved only; `$` and `^` have no v0.2 semantics
The normative machine grammar is deliberately ASCII-only. SMTPUTF8/Unicode
addresses may exist, but Unicode MAS semantics are not part of the canonical
v0.1 grammar.
grammar.
## Discovery status

View file

@ -20,7 +20,7 @@ contractual commitments.
## 2. Scope
MAS v0.1 standardizes:
MAS standardizes:
1. non-structural naming hints for people, actors, and functions;
2. structured resource targets using `=`;
@ -28,15 +28,16 @@ MAS v0.1 standardizes:
4. an optional interaction profile using `!`;
5. exact-host statute discovery;
6. conservative equivalence behavior;
7. parser and generator conformance.
7. parser and generator conformance;
8. statute-enabled stand-ins and destination transforms (v0.2).
`$` and `^` are reserved. They have no MAS v0.1 meaning.
`$` and `^` are reserved. They have no MAS v0.2 meaning.
## 3. Profiles
### 3.1 Profile P1 — Naming + Resource
P1 is the base v0.1 profile.
P1 is the base profile.
- `.` MAY be used as a person-style naming separator.
- `-` MAY be used as an actor-style naming separator.
@ -58,22 +59,22 @@ constitute acceptance. Acceptance is external to the address.
### 3.3 Profile P3 — Economic
P3 is not defined in v0.1.
P3 is not defined in v0.2.
`$` and `^` are reserved so that later profiles may define consideration and
conditions without creating v0.1 parsing ambiguity.
conditions without creating parsing ambiguity.
## 4. Canonical ASCII local-part
The normative v0.1 machine grammar is the ABNF in
The normative machine grammar is the ABNF in
`grammar/mas-v0.2.abnf`.
Canonical MAS v0.1 local-parts are unquoted ASCII dot-atom-like strings.
Quoted local-parts and SMTPUTF8 local-parts are outside the canonical v0.1
Canonical MAS local-parts are unquoted ASCII dot-atom-like strings.
Quoted local-parts and SMTPUTF8 local-parts are outside the canonical
machine grammar.
A MAS-aware implementation MAY transport such non-canonical email addresses
as ordinary email, but MUST NOT claim that they are canonical MAS v0.1
as ordinary email, but MUST NOT claim that they are canonical MAS
addresses.
A quoted local-part whose content, after removing the quotes and with no
@ -144,7 +145,7 @@ A parser implementing P2 MUST be able to parse the canonical combined form:
TARGET+FACET!INTERACTION
A v0.1 generator MUST NOT emit both `+` and `!` in the same generated
A generator MUST NOT emit both `+` and `!` in the same generated
local-part. Implementations requiring both semantics SHOULD move one of them
to external metadata.
@ -153,15 +154,16 @@ while retaining one canonical parse.
### 4.5 Reserved characters
Within canonical MAS v0.1 structured values:
Within canonical MAS structured values:
+ ! $ ^
are reserved.
MAS v0.1 defines no escaping mechanism.
MAS defines no escaping mechanism. The stand-ins (§6.3) are alternate
forms enabled by a statute, not escapes.
A percent-looking sequence such as `%2B` is ordinary data in v0.1 and MUST NOT
A percent-looking sequence such as `%2B` is ordinary data and MUST NOT
be percent-decoded by the MAS parser.
## 5. Naming semantics
@ -339,15 +341,15 @@ DNS stack; MAS does not invent a second IDN mapping algorithm.
## 9. Unicode
SMTPUTF8 allows non-ASCII local-parts, but MAS v0.1 does not define a
SMTPUTF8 allows non-ASCII local-parts, but MAS v0.2 does not define a
canonical Unicode local-part grammar.
A statute may declare `unicode_local_part: true` for experimental/private
extensions. Such addresses MUST NOT be advertised as canonical MAS v0.1
extensions. Such addresses MUST NOT be advertised as canonical MAS
machine syntax.
A future MAS version may define NFC normalization and Unicode comparison
rules. v0.1 deliberately avoids doing so.
rules. v0.2 deliberately avoids doing so.
## 10. Statute discovery
@ -383,7 +385,9 @@ A statute-aware client:
1. constructs the HTTPS URI for the exact host;
2. performs an HTTPS GET;
3. requires a successful 2xx response;
4. requires a document conforming to the v0.1 statute schema;
4. requires a document conforming to the statute schema for its
`mas_version` (`schema/mas-statute-v0.2.schema.json` for `0.2`; a `0.1`
statute is validated and interpreted under v0.1.1);
5. verifies that `namespace` equals the queried host after normal domain-name
canonicalization;
6. otherwise treats discovery as failed.
@ -398,13 +402,13 @@ Clients SHOULD NOT follow cross-origin redirects for statute discovery.
Same-origin redirects MAY be followed.
A cross-origin policy transfer requires an explicit future delegation
mechanism and is not defined in v0.1.
mechanism and is not defined in v0.2.
### 10.4 Inheritance
No parent-domain inheritance is implicit.
A child statute MAY contain an `inherits` URI. v0.1 inheritance is
A child statute MAY contain an `inherits` URI. Inheritance is
**defaults-only**:
- the parent document is fetched and validated independently;
@ -510,7 +514,7 @@ An SMTP local-part is limited to 64 octets.
Generators MUST enforce the underlying transport limit after encoding and
after destination transforms. `--` adds one octet and `---` adds two.
Because the budget is small, v0.1 permits at most one facet and one
Because the budget is small, MAS permits at most one facet and one
interaction, and generators MUST NOT emit both simultaneously.
## 14. Security
@ -522,7 +526,7 @@ interaction, and generators MUST NOT emit both simultaneously.
expired resources, verification traffic, and abuse.
5. Receivers MUST avoid backscatter behavior.
6. Syntax-derived classifications MUST NOT substitute for statute metadata.
7. `$` and `^` MUST NOT be rendered as payment/contract semantics in v0.1.
7. `$` and `^` MUST NOT be rendered as payment/contract semantics in v0.2.
8. Authentication remains external to MAS.
9. A statute endpoint is security-relevant configuration and SHOULD be
protected with the same care as other origin-wide metadata.
@ -558,7 +562,7 @@ A generator:
- emits no reserved operator within a value;
- emits at most one `+`;
- emits at most one `!`;
- MUST NOT emit both `+` and `!` in v0.1;
- MUST NOT emit both `+` and `!`;
- enforces the SMTP local-part octet limit.
### 15.4 Statute-aware implementation

View file

@ -0,0 +1,426 @@
{
"description": "MAS v0.2 statute-aware cases: stand-in interpretation (6.3), resource_id_case equivalence (8), destination-transform results and refusals (12.1). Each case names a statute in 'statutes'; the host is the statute's namespace.",
"statutes": {
"stand-ins": {
"mas_version": "0.2",
"namespace": "great-service.example",
"profiles": {
"p1": true,
"p2": true
},
"resource_types": {
"ticket": {
"enabled": true,
"description": "Customer-service ticket",
"identifier_policy": "non-guessable-preferred"
}
},
"interaction_modes": {
"callback": {
"enabled": true,
"description": "Continue by a voice callback"
}
},
"equivalence": {
"local_part_case": "exact",
"dot_folding": false,
"resource_id_case": "insensitive"
},
"addresses": {
"support": {
"kind": "function"
},
"ticket": {
"kind": "function",
"intake_for": "ticket"
}
},
"alternate_syntax": {
"resource": true,
"interaction": true
}
},
"stand-ins-exact-ids": {
"mas_version": "0.2",
"namespace": "great-service.example",
"profiles": {
"p1": true,
"p2": true
},
"resource_types": {
"ticket": {
"enabled": true,
"description": "Customer-service ticket",
"identifier_policy": "non-guessable-preferred"
}
},
"interaction_modes": {
"callback": {
"enabled": true,
"description": "Continue by a voice callback"
}
},
"equivalence": {
"local_part_case": "exact",
"dot_folding": false,
"resource_id_case": "exact"
},
"addresses": {
"support": {
"kind": "function"
},
"ticket": {
"kind": "function",
"intake_for": "ticket"
}
},
"alternate_syntax": {
"resource": true,
"interaction": true
}
},
"no-stand-ins": {
"mas_version": "0.2",
"namespace": "great-service.example",
"profiles": {
"p1": true,
"p2": true
},
"resource_types": {
"ticket": {
"enabled": true,
"description": "Customer-service ticket",
"identifier_policy": "non-guessable-preferred"
}
},
"interaction_modes": {
"callback": {
"enabled": true,
"description": "Continue by a voice callback"
}
},
"equivalence": {
"local_part_case": "exact",
"dot_folding": false,
"resource_id_case": "exact"
},
"addresses": {
"support": {
"kind": "function"
},
"ticket": {
"kind": "function",
"intake_for": "ticket"
}
},
"alternate_syntax": {
"resource": false,
"interaction": false
}
},
"v0.1": {
"mas_version": "0.1",
"namespace": "great-service.example",
"profiles": {
"p1": true,
"p2": true
},
"resource_types": {
"ticket": {
"enabled": true,
"description": "Customer-service ticket",
"identifier_policy": "non-guessable-preferred"
}
},
"interaction_modes": {
"callback": {
"enabled": true,
"description": "Continue by a voice callback"
}
},
"equivalence": {
"local_part_case": "exact",
"dot_folding": false
},
"addresses": {
"support": {
"kind": "function"
},
"ticket": {
"kind": "function",
"intake_for": "ticket"
}
}
}
},
"interpretation": [
{
"id": "si-001",
"statute": "stand-ins",
"local_part": "ticket---callback",
"canonical": "ticket!callback",
"stand_ins": [
"interaction"
]
},
{
"id": "si-002",
"statute": "stand-ins",
"local_part": "ticket--gs1---callback",
"canonical": "ticket=gs1!callback",
"stand_ins": [
"interaction",
"resource"
]
},
{
"id": "si-003",
"statute": "stand-ins",
"local_part": "ticket--a---b",
"canonical": "ticket=a---b",
"stand_ins": [
"resource"
],
"note": "b is not an enabled mode"
},
{
"id": "si-004",
"statute": "stand-ins",
"local_part": "ticket--gs1+comments",
"canonical": "ticket=gs1+comments",
"stand_ins": [
"resource"
]
},
{
"id": "si-005",
"statute": "stand-ins",
"local_part": "support---callback",
"canonical": "support!callback",
"stand_ins": [
"interaction"
]
},
{
"id": "si-006",
"statute": "stand-ins",
"local_part": "invoice--x1",
"canonical": "invoice--x1",
"stand_ins": [],
"note": "undeclared resource type"
},
{
"id": "si-007",
"statute": "stand-ins",
"local_part": "ticket=gs1!callback",
"canonical": "ticket=gs1!callback",
"stand_ins": [],
"note": "already canonical"
},
{
"id": "si-008",
"statute": "no-stand-ins",
"local_part": "ticket--gs1",
"canonical": "ticket--gs1",
"stand_ins": [],
"note": "switch off"
},
{
"id": "si-009",
"statute": "no-stand-ins",
"local_part": "ticket---callback",
"canonical": "ticket---callback",
"stand_ins": []
},
{
"id": "si-010",
"statute": "v0.1",
"local_part": "ticket--gs1",
"canonical": "ticket--gs1",
"stand_ins": [],
"note": "v0.1 statute has no stand-ins"
}
],
"equivalence": [
{
"id": "eq-001",
"statute": "stand-ins",
"a": "ticket=GS-1",
"b": "ticket=gs-1",
"equivalent": true,
"note": "resource_id_case insensitive"
},
{
"id": "eq-002",
"statute": "stand-ins",
"a": "ticket--GS-85QJ8WC09P",
"b": "ticket=gs-85qj8wc09p",
"equivalent": true,
"note": "stand-in + insensitive id"
},
{
"id": "eq-003",
"statute": "stand-ins-exact-ids",
"a": "ticket=GS-1",
"b": "ticket=gs-1",
"equivalent": false,
"note": "resource_id_case exact"
},
{
"id": "eq-004",
"statute": "stand-ins",
"a": "Support",
"b": "support",
"equivalent": false,
"note": "only resource ids fold; local_part_case exact"
},
{
"id": "eq-005",
"statute": "stand-ins",
"a": "ticket=gs-1+comments",
"b": "ticket=gs-1",
"equivalent": true,
"note": "facet does not change the target"
}
],
"destination": [
{
"id": "dt-001",
"statute": "stand-ins",
"local_part": "ticket=GS-1+comments",
"needs": [],
"expect": "ticket=GS-1+comments"
},
{
"id": "dt-002",
"statute": "stand-ins",
"local_part": "ticket=GS-85QJ8WC09P+comments",
"needs": [
"resource-alternate",
"lowercase"
],
"expect": "ticket--gs-85qj8wc09p+comments",
"note": "IONOS forwarding form"
},
{
"id": "dt-003",
"statute": "stand-ins",
"local_part": "ticket=GS-85QJ8WC09P+comments",
"needs": [
"lowercase"
],
"expect": "ticket=gs-85qj8wc09p+comments",
"note": "Google Groups"
},
{
"id": "dt-004",
"statute": "stand-ins",
"local_part": "ticket!callback",
"needs": [
"interaction-alternate"
],
"expect": "ticket---callback"
},
{
"id": "dt-005",
"statute": "stand-ins",
"local_part": "support",
"needs": [
"resource-alternate",
"lowercase"
],
"expect": "support",
"note": "transforms apply only where they change something"
},
{
"id": "dt-006",
"statute": "stand-ins",
"local_part": "ticket!callback",
"needs": [
"drop-interaction"
],
"allow_lossy": true,
"expect": "ticket"
},
{
"id": "dt-101",
"statute": "no-stand-ins",
"local_part": "ticket=GS-1",
"needs": [
"resource-alternate"
],
"expect": null,
"reason": "statute does not enable the stand-in"
},
{
"id": "dt-102",
"statute": "v0.1",
"local_part": "ticket=GS-1",
"needs": [
"resource-alternate"
],
"expect": null,
"reason": "v0.1 statute"
},
{
"id": "dt-103",
"statute": "stand-ins-exact-ids",
"local_part": "ticket=GS-1",
"needs": [
"lowercase"
],
"expect": null,
"reason": "lower-casing changes an exact id"
},
{
"id": "dt-104",
"statute": "stand-ins",
"local_part": "ticket=RE=2026-4711",
"needs": [
"resource-alternate"
],
"expect": null,
"reason": "id contains '='; TYPE--ID would split at the first --"
},
{
"id": "dt-105",
"statute": "stand-ins",
"local_part": "ticket=gs-1+Comments",
"needs": [
"lowercase"
],
"expect": null,
"reason": "facet case would change"
},
{
"id": "dt-106",
"statute": "stand-ins",
"local_part": "ticket=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"needs": [
"resource-alternate"
],
"expect": null,
"reason": "65 octets after the transform"
},
{
"id": "dt-107",
"statute": "stand-ins",
"local_part": "ticket=gs-1+comments",
"needs": [
"drop-facet"
],
"expect": null,
"reason": "lossy transform without explicit request"
},
{
"id": "dt-108",
"statute": "stand-ins",
"local_part": "ticket=gs-1!chat",
"needs": [
"interaction-alternate"
],
"expect": null,
"reason": "mode not enabled by the statute"
}
]
}

View file

@ -220,8 +220,9 @@ def cmd_lint(args):
def cmd_test(args):
from mas_conformance import statute_cases, statute_document
from mas_conformance.runner import run_parser_corpus
from mas_conformance.runner import run_parser_corpus, run_statute_aware_corpus
rep = run_parser_corpus()
aware = run_statute_aware_corpus()
st_fail = []
for c in statute_cases():
fs = lint(statute_document(c["file"]), c["host"])
@ -238,7 +239,10 @@ def cmd_test(args):
print(f"statute corpus: {n_st - len(st_fail)}/{n_st} passed")
for fid, file, why in st_fail:
print(f" FAIL {fid} {file}: {why}")
return 0 if rep.ok and not st_fail else 1
print(f"v0.2 statute-aware corpus: {aware.passed}/{aware.passed + len(aware.failures)} passed")
for fid, subject, why in aware.failures:
print(f" FAIL {fid} {subject!r}: {why}")
return 0 if rep.ok and not st_fail and aware.ok else 1
def build_parser():

View file

@ -3,7 +3,9 @@
`addresses.json` is vendored unchanged from the spec bundle
(`mas-v0.1-spec/conformance/addresses.json`) and is normative.
`properties.json` adds implementation-independent property and boundary
cases. `statutes/` holds statute validation cases. Every language
cases. `statutes/` holds statute validation cases. `statute_aware.json` is
vendored unchanged from the v0.2 draft bundle
(`mas-v0.2-spec/conformance/statute-aware.json`). Every language
implementation should consume these files unchanged.
"""
@ -32,3 +34,8 @@ def statute_cases() -> list[dict]:
def statute_document(file: str) -> dict:
return _load(f"statutes/{file}")
def statute_aware_fixtures() -> dict:
"""v0.2: {statutes, interpretation, equivalence, destination}."""
return _load("statute_aware.json")

View file

@ -6,7 +6,7 @@ from dataclasses import dataclass, field
from mas_core.parser import MasSyntaxError, parse_for_profile
from . import address_fixtures, property_fixtures
from . import address_fixtures, property_fixtures, statute_aware_fixtures
@dataclass
@ -48,3 +48,45 @@ def run_parser_corpus() -> Report:
else:
report.passed += 1
return report
def run_statute_aware_corpus() -> Report:
"""v0.2 stand-ins, resource_id_case equivalence and destination transforms."""
from mas_core.generator import CannotExpress, for_destination
from mas_core.parser import parse_address
from mas_core.policy import resolve
from mas_core.statute import build_statute
fx = statute_aware_fixtures()
statutes = {k: build_statute(doc, doc["namespace"]) for k, doc in fx["statutes"].items()}
def sem(st, lp):
return resolve(parse_address(f"{lp}@{st.namespace}"), st)
report = Report()
def record(f, subject, problem):
if problem:
report.failures.append((f["id"], subject, problem))
else:
report.passed += 1
for f in fx["interpretation"]:
d = sem(statutes[f["statute"]], f["local_part"]).to_dict()
got = (d["canonical_local_part"], d["stand_ins"])
record(f, f["local_part"], None if got == (f["canonical"], f["stand_ins"])
else f"got {got}, expected {(f['canonical'], f['stand_ins'])}")
for f in fx["equivalence"]:
st = statutes[f["statute"]]
same = sem(st, f["a"]).equivalence_key == sem(st, f["b"]).equivalence_key
record(f, f"{f['a']} ~ {f['b']}", None if same == f["equivalent"]
else f"equivalent={same}, expected {f['equivalent']}")
for f in fx["destination"]:
try:
got = for_destination(f["local_part"], statutes[f["statute"]], f["needs"],
allow_lossy=f.get("allow_lossy", False)).local_part
except CannotExpress as e:
got, why = None, e.code
record(f, f["local_part"], None if got == f["expect"]
else f"got {got!r}" + (f" ({why})" if got is None else "") + f", expected {f['expect']!r}")
return report

View file

@ -0,0 +1,426 @@
{
"description": "MAS v0.2 statute-aware cases: stand-in interpretation (6.3), resource_id_case equivalence (8), destination-transform results and refusals (12.1). Each case names a statute in 'statutes'; the host is the statute's namespace.",
"statutes": {
"stand-ins": {
"mas_version": "0.2",
"namespace": "great-service.example",
"profiles": {
"p1": true,
"p2": true
},
"resource_types": {
"ticket": {
"enabled": true,
"description": "Customer-service ticket",
"identifier_policy": "non-guessable-preferred"
}
},
"interaction_modes": {
"callback": {
"enabled": true,
"description": "Continue by a voice callback"
}
},
"equivalence": {
"local_part_case": "exact",
"dot_folding": false,
"resource_id_case": "insensitive"
},
"addresses": {
"support": {
"kind": "function"
},
"ticket": {
"kind": "function",
"intake_for": "ticket"
}
},
"alternate_syntax": {
"resource": true,
"interaction": true
}
},
"stand-ins-exact-ids": {
"mas_version": "0.2",
"namespace": "great-service.example",
"profiles": {
"p1": true,
"p2": true
},
"resource_types": {
"ticket": {
"enabled": true,
"description": "Customer-service ticket",
"identifier_policy": "non-guessable-preferred"
}
},
"interaction_modes": {
"callback": {
"enabled": true,
"description": "Continue by a voice callback"
}
},
"equivalence": {
"local_part_case": "exact",
"dot_folding": false,
"resource_id_case": "exact"
},
"addresses": {
"support": {
"kind": "function"
},
"ticket": {
"kind": "function",
"intake_for": "ticket"
}
},
"alternate_syntax": {
"resource": true,
"interaction": true
}
},
"no-stand-ins": {
"mas_version": "0.2",
"namespace": "great-service.example",
"profiles": {
"p1": true,
"p2": true
},
"resource_types": {
"ticket": {
"enabled": true,
"description": "Customer-service ticket",
"identifier_policy": "non-guessable-preferred"
}
},
"interaction_modes": {
"callback": {
"enabled": true,
"description": "Continue by a voice callback"
}
},
"equivalence": {
"local_part_case": "exact",
"dot_folding": false,
"resource_id_case": "exact"
},
"addresses": {
"support": {
"kind": "function"
},
"ticket": {
"kind": "function",
"intake_for": "ticket"
}
},
"alternate_syntax": {
"resource": false,
"interaction": false
}
},
"v0.1": {
"mas_version": "0.1",
"namespace": "great-service.example",
"profiles": {
"p1": true,
"p2": true
},
"resource_types": {
"ticket": {
"enabled": true,
"description": "Customer-service ticket",
"identifier_policy": "non-guessable-preferred"
}
},
"interaction_modes": {
"callback": {
"enabled": true,
"description": "Continue by a voice callback"
}
},
"equivalence": {
"local_part_case": "exact",
"dot_folding": false
},
"addresses": {
"support": {
"kind": "function"
},
"ticket": {
"kind": "function",
"intake_for": "ticket"
}
}
}
},
"interpretation": [
{
"id": "si-001",
"statute": "stand-ins",
"local_part": "ticket---callback",
"canonical": "ticket!callback",
"stand_ins": [
"interaction"
]
},
{
"id": "si-002",
"statute": "stand-ins",
"local_part": "ticket--gs1---callback",
"canonical": "ticket=gs1!callback",
"stand_ins": [
"interaction",
"resource"
]
},
{
"id": "si-003",
"statute": "stand-ins",
"local_part": "ticket--a---b",
"canonical": "ticket=a---b",
"stand_ins": [
"resource"
],
"note": "b is not an enabled mode"
},
{
"id": "si-004",
"statute": "stand-ins",
"local_part": "ticket--gs1+comments",
"canonical": "ticket=gs1+comments",
"stand_ins": [
"resource"
]
},
{
"id": "si-005",
"statute": "stand-ins",
"local_part": "support---callback",
"canonical": "support!callback",
"stand_ins": [
"interaction"
]
},
{
"id": "si-006",
"statute": "stand-ins",
"local_part": "invoice--x1",
"canonical": "invoice--x1",
"stand_ins": [],
"note": "undeclared resource type"
},
{
"id": "si-007",
"statute": "stand-ins",
"local_part": "ticket=gs1!callback",
"canonical": "ticket=gs1!callback",
"stand_ins": [],
"note": "already canonical"
},
{
"id": "si-008",
"statute": "no-stand-ins",
"local_part": "ticket--gs1",
"canonical": "ticket--gs1",
"stand_ins": [],
"note": "switch off"
},
{
"id": "si-009",
"statute": "no-stand-ins",
"local_part": "ticket---callback",
"canonical": "ticket---callback",
"stand_ins": []
},
{
"id": "si-010",
"statute": "v0.1",
"local_part": "ticket--gs1",
"canonical": "ticket--gs1",
"stand_ins": [],
"note": "v0.1 statute has no stand-ins"
}
],
"equivalence": [
{
"id": "eq-001",
"statute": "stand-ins",
"a": "ticket=GS-1",
"b": "ticket=gs-1",
"equivalent": true,
"note": "resource_id_case insensitive"
},
{
"id": "eq-002",
"statute": "stand-ins",
"a": "ticket--GS-85QJ8WC09P",
"b": "ticket=gs-85qj8wc09p",
"equivalent": true,
"note": "stand-in + insensitive id"
},
{
"id": "eq-003",
"statute": "stand-ins-exact-ids",
"a": "ticket=GS-1",
"b": "ticket=gs-1",
"equivalent": false,
"note": "resource_id_case exact"
},
{
"id": "eq-004",
"statute": "stand-ins",
"a": "Support",
"b": "support",
"equivalent": false,
"note": "only resource ids fold; local_part_case exact"
},
{
"id": "eq-005",
"statute": "stand-ins",
"a": "ticket=gs-1+comments",
"b": "ticket=gs-1",
"equivalent": true,
"note": "facet does not change the target"
}
],
"destination": [
{
"id": "dt-001",
"statute": "stand-ins",
"local_part": "ticket=GS-1+comments",
"needs": [],
"expect": "ticket=GS-1+comments"
},
{
"id": "dt-002",
"statute": "stand-ins",
"local_part": "ticket=GS-85QJ8WC09P+comments",
"needs": [
"resource-alternate",
"lowercase"
],
"expect": "ticket--gs-85qj8wc09p+comments",
"note": "IONOS forwarding form"
},
{
"id": "dt-003",
"statute": "stand-ins",
"local_part": "ticket=GS-85QJ8WC09P+comments",
"needs": [
"lowercase"
],
"expect": "ticket=gs-85qj8wc09p+comments",
"note": "Google Groups"
},
{
"id": "dt-004",
"statute": "stand-ins",
"local_part": "ticket!callback",
"needs": [
"interaction-alternate"
],
"expect": "ticket---callback"
},
{
"id": "dt-005",
"statute": "stand-ins",
"local_part": "support",
"needs": [
"resource-alternate",
"lowercase"
],
"expect": "support",
"note": "transforms apply only where they change something"
},
{
"id": "dt-006",
"statute": "stand-ins",
"local_part": "ticket!callback",
"needs": [
"drop-interaction"
],
"allow_lossy": true,
"expect": "ticket"
},
{
"id": "dt-101",
"statute": "no-stand-ins",
"local_part": "ticket=GS-1",
"needs": [
"resource-alternate"
],
"expect": null,
"reason": "statute does not enable the stand-in"
},
{
"id": "dt-102",
"statute": "v0.1",
"local_part": "ticket=GS-1",
"needs": [
"resource-alternate"
],
"expect": null,
"reason": "v0.1 statute"
},
{
"id": "dt-103",
"statute": "stand-ins-exact-ids",
"local_part": "ticket=GS-1",
"needs": [
"lowercase"
],
"expect": null,
"reason": "lower-casing changes an exact id"
},
{
"id": "dt-104",
"statute": "stand-ins",
"local_part": "ticket=RE=2026-4711",
"needs": [
"resource-alternate"
],
"expect": null,
"reason": "id contains '='; TYPE--ID would split at the first --"
},
{
"id": "dt-105",
"statute": "stand-ins",
"local_part": "ticket=gs-1+Comments",
"needs": [
"lowercase"
],
"expect": null,
"reason": "facet case would change"
},
{
"id": "dt-106",
"statute": "stand-ins",
"local_part": "ticket=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"needs": [
"resource-alternate"
],
"expect": null,
"reason": "65 octets after the transform"
},
{
"id": "dt-107",
"statute": "stand-ins",
"local_part": "ticket=gs-1+comments",
"needs": [
"drop-facet"
],
"expect": null,
"reason": "lossy transform without explicit request"
},
{
"id": "dt-108",
"statute": "stand-ins",
"local_part": "ticket=gs-1!chat",
"needs": [
"interaction-alternate"
],
"expect": null,
"reason": "mode not enabled by the statute"
}
]
}

View file

@ -128,8 +128,9 @@ def for_destination(local_part: str, statute: "Statute", needs: Iterable[str] =
expected = resolve(ParsedLocalPart(raw=_render(target, facet, interaction), target=target,
facet=facet, interaction=interaction), statute)
got = resolve(out, statute)
if (got.equivalence_key, got.facet, got.interaction) != \
(expected.equivalence_key, expected.facet, expected.interaction):
raise CannotExpress("would-be-misread",
f"{text!r} would be read as {got.to_dict()['canonical_local_part']!r}")
fields = ("equivalence_key", "facet", "interaction")
diff = [f"{k} {getattr(got, k)!r} instead of {getattr(expected, k)!r}"
for k in fields if getattr(got, k) != getattr(expected, k)]
if diff:
raise CannotExpress("would-be-misread", f"{text!r} would be read with " + ", ".join(diff))
return DestinationAddress(text, tuple(applied), tuple(warnings))

View file

@ -138,3 +138,16 @@ def test_vendored_v02_schema_matches_draft_bundle():
a = (ROOT / "mas-v0.2-spec/schema/mas-statute-v0.2.schema.json").read_bytes()
b = (ROOT / "mas_core/schema/mas-statute-v0.2.schema.json").read_bytes()
assert a == b
def test_vendored_v02_statute_aware_corpus_matches_draft_bundle():
a = (ROOT / "mas-v0.2-spec/conformance/statute-aware.json").read_bytes()
b = (ROOT / "mas_conformance/statute_aware.json").read_bytes()
assert a == b
def test_v02_statute_aware_corpus():
from mas_conformance.runner import run_statute_aware_corpus
rep = run_statute_aware_corpus()
assert rep.ok, rep.failures
assert rep.passed == 29