From 5b57a9941eec637011c15136f227dcdefb41d6d5 Mon Sep 17 00:00:00 2001 From: tegwick Date: Sun, 18 Oct 2026 18:08:36 +0200 Subject: [PATCH] MAS-WP-0005-T02: SCP-2 lower-case resource types (spec v0.1.1) Grammar, statute schema key pattern, parser (is_resource_type, also used by statute lint), reference parser, spec 6.1 and a new CHANGELOG. Fixtures p1-042..045; prop-case-001 Ticket=PR42 is now invalid. Co-Authored-By: Claude Opus 5.5 Assistant: claude-code Assistant-Model: opus Assistant-Process: 252470@bnt-lap001 Assistant-Session: ad628c8f-4be2-4979-94c9-76c7ed6f8241 --- docs/spec-change-proposals.md | 7 +++- mas-v0.1-spec/CHANGELOG.md | 17 +++++++++ mas-v0.1-spec/README.md | 3 +- mas-v0.1-spec/SPECIFICATION.md | 13 ++++++- mas-v0.1-spec/conformance/addresses.json | 38 +++++++++++++++++++ mas-v0.1-spec/grammar/mas-v0.1.abnf | 7 +++- mas-v0.1-spec/reference/mas_parser.py | 5 ++- .../schema/mas-statute-v0.1.schema.json | 2 +- mas_conformance/addresses.json | 38 +++++++++++++++++++ mas_conformance/properties.json | 13 ++----- mas_core/parser.py | 11 +++++- mas_core/schema/mas-statute-v0.1.schema.json | 2 +- mas_core/statute.py | 4 +- workplans/MAS-WP-0005-spec-errata-v0-1-1.md | 20 +++++++++- 14 files changed, 155 insertions(+), 25 deletions(-) create mode 100644 mas-v0.1-spec/CHANGELOG.md diff --git a/docs/spec-change-proposals.md b/docs/spec-change-proposals.md index 50a1363..2e65c23 100644 --- a/docs/spec-change-proposals.md +++ b/docs/spec-change-proposals.md @@ -49,8 +49,11 @@ lower-case in the grammar. **Options:** - **(a) recommended:** restrict `resource-type` in the grammar to lower-case - ALPHA, DIGIT and `-`. `Ticket=PR42` then parses as an opaque target, which - is the safe outcome under §6.1 ("no semantics solely because of `=`"). + ALPHA, DIGIT and `-`. (Correction during implementation: `Ticket=PR42` + does **not** become an opaque target, because opaque targets cannot + contain `=`. It becomes a non-canonical address, rejected with + `invalid-resource-type` and carried as ordinary email. That is stricter + and still safe under §6.1.) - (b) keep the grammar and state in §8 that the statute's equivalence policy also applies to the type name. diff --git a/mas-v0.1-spec/CHANGELOG.md b/mas-v0.1-spec/CHANGELOG.md new file mode 100644 index 0000000..08cf044 --- /dev/null +++ b/mas-v0.1-spec/CHANGELOG.md @@ -0,0 +1,17 @@ +# MAS specification changelog + +## v0.1.1 (errata, in progress) + +Errata from the reference experiment and the live provider test +(mail-address-semantics MAS-WP-0002/0003). Proposals and evidence: +`docs/spec-change-proposals.md` in the implementation repository. + +- **SCP-2, resource types are lower-case.** `resource-type` is now + lower-case words of `a-z`/`0-9` joined by single `-` or `_` (§6.1, + grammar, statute schema `resource_types` key pattern). `Ticket=PR42`, + `my--type=X` and `type-=X` are no longer canonical. New fixtures: p1-042 + to p1-045. + +## v0.1 + +Initial draft. diff --git a/mas-v0.1-spec/README.md b/mas-v0.1-spec/README.md index 38d2c63..c802f56 100644 --- a/mas-v0.1-spec/README.md +++ b/mas-v0.1-spec/README.md @@ -4,7 +4,8 @@ This bundle turns the MAS v0.1 concept into an implementable draft. ## Contents -- `SPECIFICATION.md` — normative protocol/convention text +- `SPECIFICATION.md` — normative protocol/convention text (revision v0.1.1) +- `CHANGELOG.md` — errata applied since v0.1 - `grammar/mas-v0.1.abnf` — canonical ASCII local-part grammar - `schema/mas-statute-v0.1.schema.json` — statute discovery document schema - `examples/mas-statute.example.json` — example statute diff --git a/mas-v0.1-spec/SPECIFICATION.md b/mas-v0.1-spec/SPECIFICATION.md index 1e2f3ec..fe871dd 100644 --- a/mas-v0.1-spec/SPECIFICATION.md +++ b/mas-v0.1-spec/SPECIFICATION.md @@ -2,7 +2,9 @@ ## 1. Status and normative language -This document defines MAS v0.1. +This document defines MAS v0.1, revision v0.1.1. The errata it applies are +listed in `CHANGELOG.md`; they clarify or tighten v0.1 without changing what +a valid canonical v0.1.1 address means. The key words **MUST**, **MUST NOT**, **REQUIRED**, **SHOULD**, **SHOULD NOT**, **RECOMMENDED**, **MAY**, and **OPTIONAL** are to be interpreted as described @@ -186,6 +188,15 @@ resource. ### 6.1 Resource types +A resource type is lower-case: one or more words of `a-z` and `0-9`, joined +by single `-` or `_` characters (grammar rule `resource-type`). A +local-part whose part before the first `=` is not a valid resource type, +such as `Ticket=PR42`, is not a canonical MAS address. + +Resource types are protocol tokens, not user data. Forwarders and list +servers commonly lower-case local-parts, so mixed-case types would not +survive transit. + A statute SHOULD enumerate resource types it recognizes. Unknown resource types MUST NOT acquire application semantics solely because diff --git a/mas-v0.1-spec/conformance/addresses.json b/mas-v0.1-spec/conformance/addresses.json index 00afc69..5347be7 100644 --- a/mas-v0.1-spec/conformance/addresses.json +++ b/mas-v0.1-spec/conformance/addresses.json @@ -311,5 +311,43 @@ "profile": "p1", "valid": false, "error": "non-ASCII outside canonical MAS v0.1" + }, + { + "id": "p1-042", + "local_part": "Ticket=PR42", + "profile": "p1", + "valid": false, + "error": "invalid resource type", + "note": "v0.1.1 (SCP-2): resource types are lower-case" + }, + { + "id": "p1-043", + "local_part": "my--type=PR42", + "profile": "p1", + "valid": false, + "error": "invalid resource type", + "note": "v0.1.1 (SCP-2): no doubled separator in resource types" + }, + { + "id": "p1-044", + "local_part": "type-=PR42", + "profile": "p1", + "valid": false, + "error": "invalid resource type", + "note": "v0.1.1 (SCP-2): no trailing separator" + }, + { + "id": "p1-045", + "local_part": "support-case_v2=PR42", + "profile": "p1", + "valid": true, + "parsed": { + "target": "support-case_v2=PR42", + "target_kind": "resource", + "resource_type": "support-case_v2", + "resource_id": "PR42", + "facet": null, + "interaction": null + } } ] diff --git a/mas-v0.1-spec/grammar/mas-v0.1.abnf b/mas-v0.1-spec/grammar/mas-v0.1.abnf index b0feedc..18212f1 100644 --- a/mas-v0.1-spec/grammar/mas-v0.1.abnf +++ b/mas-v0.1-spec/grammar/mas-v0.1.abnf @@ -18,7 +18,10 @@ target = resource-target / opaque-target resource-target = resource-type "=" resource-id opaque-target = dot-string -resource-type = resource-type-char *resource-type-char +; v0.1.1 (SCP-2): lower-case words joined by single "-" or "_". +; Never upper case, never "--", "__" or a leading/trailing separator. +resource-type = type-word *(("-" / "_") type-word) +type-word = 1*(LCALPHA / DIGIT) ; Resource identifiers may contain additional "=" characters as data. resource-id = resource-id-segment *("." resource-id-segment) @@ -43,7 +46,7 @@ atom-char = ALPHA / DIGIT / "#" / "%" / "&" / "'" / "*" / "-" / "/" / "?" / "_" / "`" / "{" / "|" / "}" / "~" -resource-type-char = ALPHA / DIGIT / "-" / "_" +LCALPHA = %x61-7A ; a-z resource-id-char = atom-char / "=" value-char = atom-char / "=" diff --git a/mas-v0.1-spec/reference/mas_parser.py b/mas-v0.1-spec/reference/mas_parser.py index 6b3f8e6..c90f7ea 100755 --- a/mas-v0.1-spec/reference/mas_parser.py +++ b/mas-v0.1-spec/reference/mas_parser.py @@ -3,10 +3,11 @@ import argparse import json +import re from pathlib import Path ATOM = set("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789#%&'*-/ ?_`{|}~".replace(" ", "")) -RTYPE = set("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_") +RTYPE = re.compile(r"[a-z0-9]+(?:[_-][a-z0-9]+)*") # v0.1.1: lower-case, single separators RESERVED = set("+!$^") def _valid_dot_string(s, allowed): @@ -78,7 +79,7 @@ def parse(local_part, profile="p1"): raise ValueError("empty resource type") if not rid: raise ValueError("empty resource id") - if not all(c in RTYPE for c in rtype): + if not RTYPE.fullmatch(rtype): raise ValueError("invalid resource type") if not _valid_dot_string(rid, ATOM | set("=")): raise ValueError("invalid resource id") diff --git a/mas-v0.1-spec/schema/mas-statute-v0.1.schema.json b/mas-v0.1-spec/schema/mas-statute-v0.1.schema.json index 68dc104..983f883 100644 --- a/mas-v0.1-spec/schema/mas-statute-v0.1.schema.json +++ b/mas-v0.1-spec/schema/mas-statute-v0.1.schema.json @@ -46,7 +46,7 @@ "type": "object", "default": {}, "propertyNames": { - "pattern": "^[a-z0-9][a-z0-9_-]*$" + "pattern": "^[a-z0-9]+([_-][a-z0-9]+)*$" }, "additionalProperties": { "type": "object", diff --git a/mas_conformance/addresses.json b/mas_conformance/addresses.json index 00afc69..5347be7 100644 --- a/mas_conformance/addresses.json +++ b/mas_conformance/addresses.json @@ -311,5 +311,43 @@ "profile": "p1", "valid": false, "error": "non-ASCII outside canonical MAS v0.1" + }, + { + "id": "p1-042", + "local_part": "Ticket=PR42", + "profile": "p1", + "valid": false, + "error": "invalid resource type", + "note": "v0.1.1 (SCP-2): resource types are lower-case" + }, + { + "id": "p1-043", + "local_part": "my--type=PR42", + "profile": "p1", + "valid": false, + "error": "invalid resource type", + "note": "v0.1.1 (SCP-2): no doubled separator in resource types" + }, + { + "id": "p1-044", + "local_part": "type-=PR42", + "profile": "p1", + "valid": false, + "error": "invalid resource type", + "note": "v0.1.1 (SCP-2): no trailing separator" + }, + { + "id": "p1-045", + "local_part": "support-case_v2=PR42", + "profile": "p1", + "valid": true, + "parsed": { + "target": "support-case_v2=PR42", + "target_kind": "resource", + "resource_type": "support-case_v2", + "resource_id": "PR42", + "facet": null, + "interaction": null + } } ] diff --git a/mas_conformance/properties.json b/mas_conformance/properties.json index a1529c1..1edbce1 100644 --- a/mas_conformance/properties.json +++ b/mas_conformance/properties.json @@ -256,15 +256,8 @@ "id": "prop-case-001", "local_part": "Ticket=PR42", "profile": "p1", - "valid": true, - "parsed": { - "target": "Ticket=PR42", - "target_kind": "resource", - "facet": null, - "interaction": null, - "resource_type": "Ticket", - "resource_id": "PR42" - }, - "note": "grammar permits upper-case resource types; statutes register lower-case" + "valid": false, + "error": "invalid-resource-type", + "note": "v0.1.1 (SCP-2): resource types are lower-case; was valid in v0.1" } ] diff --git a/mas_core/parser.py b/mas_core/parser.py index a19f902..f21408f 100644 --- a/mas_core/parser.py +++ b/mas_core/parser.py @@ -11,6 +11,8 @@ class (P1 or P2) without re-parsing. from __future__ import annotations +import re + from dataclasses import dataclass from typing import Optional @@ -28,11 +30,16 @@ LOCAL_PART_MAX_OCTETS = 64 _ALNUM = frozenset("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") ATOM_CHARS = _ALNUM | frozenset("#%&'*-/?_`{|}~") -RESOURCE_TYPE_CHARS = _ALNUM | frozenset("-_") +# v0.1.1 (SCP-2): lower-case words joined by single "-" or "_". +_RESOURCE_TYPE = re.compile(r"[a-z0-9]+(?:[_-][a-z0-9]+)*") VALUE_CHARS = ATOM_CHARS | frozenset("=") # resource-id-char and value-char RESERVED_OPERATORS = frozenset("$^") +def is_resource_type(name: str) -> bool: + return _RESOURCE_TYPE.fullmatch(name) is not None + + class MasSyntaxError(ValueError): """A local-part is not canonical MAS v0.1 syntax. @@ -102,7 +109,7 @@ def parse_local_part(value: str) -> ParsedLocalPart: raise MasSyntaxError("empty-resource-type", "resource type is empty") if not rid: raise MasSyntaxError("empty-resource-id", "resource id is empty") - if not all(c in RESOURCE_TYPE_CHARS for c in rtype): + if not is_resource_type(rtype): raise MasSyntaxError("invalid-resource-type", f"invalid resource type {rtype!r}") if not _dot_string(rid, VALUE_CHARS): raise MasSyntaxError("invalid-resource-id", f"invalid resource id {rid!r}") diff --git a/mas_core/schema/mas-statute-v0.1.schema.json b/mas_core/schema/mas-statute-v0.1.schema.json index 68dc104..983f883 100644 --- a/mas_core/schema/mas-statute-v0.1.schema.json +++ b/mas_core/schema/mas-statute-v0.1.schema.json @@ -46,7 +46,7 @@ "type": "object", "default": {}, "propertyNames": { - "pattern": "^[a-z0-9][a-z0-9_-]*$" + "pattern": "^[a-z0-9]+([_-][a-z0-9]+)*$" }, "additionalProperties": { "type": "object", diff --git a/mas_core/statute.py b/mas_core/statute.py index 0d494ec..f8ff380 100644 --- a/mas_core/statute.py +++ b/mas_core/statute.py @@ -15,7 +15,7 @@ from typing import Callable, Mapping, Optional from jsonschema import Draft202012Validator, FormatChecker -from .parser import ATOM_CHARS, RESOURCE_TYPE_CHARS, MasSyntaxError, parse_local_part +from .parser import ATOM_CHARS, MasSyntaxError, is_resource_type, parse_local_part ERROR, WARNING = "error", "warning" SCHEMA, SEMANTIC = "schema", "semantic" @@ -140,7 +140,7 @@ def semantic_findings(doc: Mapping, host: Optional[str] = None, for name, spec in doc.get("resource_types", {}).items(): path = f"/resource_types/{name}" - if not name or not all(c in RESOURCE_TYPE_CHARS for c in name): + if not is_resource_type(name): add(ERROR, "invalid-resource-type-name", path, "name violates MAS resource-type syntax") if spec.get("identifier_policy") == "sequential-allowed": add(WARNING, "enumerable-identifiers", path, diff --git a/workplans/MAS-WP-0005-spec-errata-v0-1-1.md b/workplans/MAS-WP-0005-spec-errata-v0-1-1.md index bfa77e0..600ccc0 100644 --- a/workplans/MAS-WP-0005-spec-errata-v0-1-1.md +++ b/workplans/MAS-WP-0005-spec-errata-v0-1-1.md @@ -70,7 +70,7 @@ SCP-6 split rules), and record which proposals are accepted. ```task id: MAS-WP-0005-T02 -status: todo +status: progress priority: high state_hub_task_id: "eb4c83d0-5b2c-5b7c-a6e3-57b85fe78334" ``` @@ -80,6 +80,24 @@ types), SCP-3 (type intake, `intake_for`), SCP-5 (intake auto-reply bounds), and the SCP-7 recommendation (case-robust identifiers). Add fixtures and update the parser, policy and schema. +Decisions (founder, 2026-10-18): keep the folder `mas-v0.1-spec/` and mark +the revision v0.1.1 inside it (SPECIFICATION §1, README, new CHANGELOG). +For SCP-5, write the spec text now and track the DMARC-gated reply code as +its own task. + +- 2026-10-18 SCP-2 done: + - grammar `resource-type = type-word *(("-" / "_") type-word)`; + - statute schema key pattern `^[a-z0-9]+([_-][a-z0-9]+)*$` (bundle and + vendored copy); + - `mas_core.parser.is_resource_type`, also used by statute lint; + - the bundle's reference parser; + - §6.1 text and the CHANGELOG; + - fixtures p1-042 to p1-045, and prop-case-001 flipped to invalid. + + 141 tests pass, and the reference parser agrees with all 35 bundle + fixtures. SCP-2 text corrected: `Ticket=PR42` becomes non-canonical, + not opaque. + ## Reference service: case-robust ticket ids ```task