Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ schema = get_contribution_schema()

`validate_record`, `record_errors` and `get_record_schema` handle expanded Registry records. Pass `--record` to the CLI for the corresponding record format.

The package bundles the MAP 0.1 and Registry schemas, the Content Review 0.1 request schema and its canonical type contract. It uses the established `jsonschema` and `referencing` libraries for validation and performs no network requests.
The package bundles the MAP 0.1 and Registry schemas plus the Content Review 0.1 and 0.2 request schemas and canonical type contracts. Unversioned Content Review helpers use 0.2; `*_content_review_01_*` helpers remain available for implementations pinned to 0.1. It uses the established `jsonschema` and `referencing` libraries for validation and performs no network requests.

## Trust boundary

Expand Down
13 changes: 10 additions & 3 deletions UPSTREAM.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
# Release provenance

Version `0.1.2` was prepared from [`mailschema/mailschema@eddc745`](https://github.com/mailschema/mailschema/commit/eddc7456910bf6a3c33de51c46ebf50f61ee7342).
Version `0.1.3` derives from [`mailschema/mailschema@f630ec5`](https://github.com/mailschema/mailschema/commit/f630ec5569e7167114fb2a39a10548febfa6b979).

The included contribution schema has SHA-256 digest `a8a241b4837971a6251bcc135c704283536f4fb825fbbaedaf1598c6e688d564`. The canonical schemas live in the main MailSchema repository; this repository owns the Python package surface and its release history.
The bundled files are exact projections of that source commit and do not independently define MAP:

The unreleased MAP contract projection is derived from [`mailschema/mailschema@e2f2cc6`](https://github.com/mailschema/mailschema/commit/e2f2cc6983173e5aba1da27f1f8bff37336efd77). Its MAP schema digest is `c39befeecf8643e8beda1f53a8d1411482b68ddf14974f3806cfa95776103f85`; its Content Review schema digest is `10e2263c820ba3a6ef0381c377078e2c1f324c3b142a6f8caf92b298cc66c692`; and its Content Review contract byte digest is `754ab7245ddb09ff4da000fccc60d746724379d4146ea227973e5d87f105f800`. These files are exact projections of that source commit and do not independently define MAP.
- MAP 0.1 schema: `c39befeecf8643e8beda1f53a8d1411482b68ddf14974f3806cfa95776103f85`
- Content Review 0.1 schema: `10e2263c820ba3a6ef0381c377078e2c1f324c3b142a6f8caf92b298cc66c692`
- Content Review 0.1 contract: `754ab7245ddb09ff4da000fccc60d746724379d4146ea227973e5d87f105f800`
- Content Review 0.2 schema: `d2286ffe410cceae56f9caa145634fd9613f5ce51e11ba089cad67b9f62259b7`
- Content Review 0.2 contract: `3bd1cd4a86e00d00590eeb150b7b29cebece589058d6094a40aa9d497dc8efe0`
- Contribution schema: `a8a241b4837971a6251bcc135c704283536f4fb825fbbaedaf1598c6e688d564`

The canonical contracts live in the main MailSchema repository. This repository owns the Python package surface and its release history.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "mailschema"
version = "0.1.2"
version = "0.1.3"
description = "Schemas and validation tools for Mail Action Protocol and the MailSchema Registry"
readme = "README.md"
license = "MIT"
Expand Down
39 changes: 33 additions & 6 deletions src/mailschema/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from jsonschema import Draft202012Validator, FormatChecker
from referencing import Registry, Resource

__version__ = "0.1.2"
__version__ = "0.1.3"


def _bundled(name: str) -> dict[str, Any]:
Expand All @@ -25,12 +25,22 @@ def get_map_schema() -> dict[str, Any]:


def get_content_review_schema() -> dict[str, Any]:
"""Return a fresh copy of the Content Review 0.1 request schema."""
return _bundled("content-review-0.1.schema.json")
"""Return a fresh copy of the current Content Review 0.2 request schema."""
return _bundled("content-review-0.2.schema.json")


def get_content_review_contract() -> dict[str, Any]:
"""Return a fresh copy of the canonical Content Review 0.1 type contract."""
"""Return a fresh copy of the canonical Content Review 0.2 type contract."""
return _bundled("content-review-0.2.contract.json")


def get_content_review_01_schema() -> dict[str, Any]:
"""Return a fresh copy of the immutable Content Review 0.1 request schema."""
return _bundled("content-review-0.1.schema.json")


def get_content_review_01_contract() -> dict[str, Any]:
"""Return a fresh copy of the immutable Content Review 0.1 type contract."""
return _bundled("content-review-0.1.contract.json")


Expand All @@ -57,6 +67,11 @@ def get_record_schema() -> dict[str, Any]:
registry=Registry().with_resource(_map_schema["$id"], Resource.from_contents(_map_schema)),
format_checker=FormatChecker(),
)
_content_review_01_validator = Draft202012Validator(
get_content_review_01_schema(),
registry=Registry().with_resource(_map_schema["$id"], Resource.from_contents(_map_schema)),
format_checker=FormatChecker(),
)


def _errors(validator: Draft202012Validator, value: Any) -> list[str]:
Expand Down Expand Up @@ -85,10 +100,15 @@ def map_errors(value: Any) -> list[str]:


def content_review_request_errors(value: Any) -> list[str]:
"""Return errors for a Content Review 0.1 MAP request."""
"""Return errors for a Content Review 0.2 MAP request."""
return _errors(_content_review_validator, value)


def content_review_01_request_errors(value: Any) -> list[str]:
"""Return errors for an immutable Content Review 0.1 MAP request."""
return _errors(_content_review_01_validator, value)


def validate_contribution(value: Any) -> None:
"""Raise ValueError if a contribution does not match the schema."""
errors = contribution_errors(value)
Expand All @@ -111,7 +131,14 @@ def validate_map_document(value: Any) -> None:


def validate_content_review_request(value: Any) -> None:
"""Raise ValueError if a value is not a Content Review 0.1 request."""
"""Raise ValueError if a value is not a Content Review 0.2 request."""
errors = content_review_request_errors(value)
if errors:
raise ValueError("Invalid Content Review 0.2 request:\n" + "\n".join(errors))


def validate_content_review_01_request(value: Any) -> None:
"""Raise ValueError if a value is not an immutable Content Review 0.1 request."""
errors = content_review_01_request_errors(value)
if errors:
raise ValueError("Invalid Content Review 0.1 request:\n" + "\n".join(errors))
91 changes: 91 additions & 0 deletions src/mailschema/content-review-0.2.contract.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
{
"kind": "MapTypeContract",
"id": "https://mailschema.org/types/content-review",
"version": "0.2",
"profile": "https://mailschema.org/profiles/map/0.1",
"target": "Service-owned content at the exact revision and digest named in the request.",
"requestSchema": {
"url": "https://mailschema.org/schemas/content-review-0.2.schema.json",
"canonicalDigest": "sha-256:43a01e620e0730e47755694bdf5d086f8d0a1d3d8328fb0c7d2a4b70c6e62e25"
},
"operations": [
{
"id": "request-changes",
"effect": "Record review feedback on the named revision.",
"results": [
{
"state": "accepted",
"outputSchema": {
"type": "object",
"additionalProperties": false,
"properties": {
"feedbackRecorded": {
"const": true
}
},
"required": [
"feedbackRecorded"
]
}
}
]
},
{
"id": "approve",
"effect": "Record approval of the named revision, or record why a proposed approval ended without approval.",
"results": [
{
"state": "approval-required",
"outputSchema": {
"type": "object",
"additionalProperties": false,
"properties": {
"approvalUrl": {
"type": "string",
"format": "uri",
"pattern": "^https://"
}
},
"required": [
"approvalUrl"
]
}
},
{
"state": "completed",
"outputSchema": {
"type": "object",
"additionalProperties": false,
"properties": {
"decision": {
"const": "approved"
}
},
"required": [
"decision"
]
}
},
{
"state": "failed",
"outputSchema": {
"type": "object",
"additionalProperties": false,
"properties": {
"reason": {
"enum": [
"declined",
"stale-target",
"expired"
]
}
},
"required": [
"reason"
]
}
}
]
}
]
}
54 changes: 54 additions & 0 deletions src/mailschema/content-review-0.2.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://mailschema.org/schemas/content-review-0.2.schema.json",
"title": "Content Review 0.2 MAP request binding",
"allOf": [
{ "$ref": "https://mailschema.org/schemas/map-0.1.schema.json#/$defs/request" },
{
"type": "object",
"properties": {
"type": {
"type": "object",
"properties": {
"id": { "const": "https://mailschema.org/types/content-review" },
"version": { "const": "0.2" }
}
}
}
},
{
"oneOf": [
{
"type": "object",
"properties": {
"operation": { "const": "request-changes" },
"input": {
"type": "object",
"additionalProperties": false,
"properties": {
"feedback": {
"type": "string",
"minLength": 1,
"maxLength": 12000,
"pattern": "\\S"
}
},
"required": ["feedback"]
}
}
},
{
"type": "object",
"properties": {
"operation": { "const": "approve" },
"input": {
"type": "object",
"additionalProperties": false,
"maxProperties": 0
}
}
}
]
}
]
}
22 changes: 13 additions & 9 deletions tests/content-review.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{
"slug": "content-review",
"name": "Content Review",
"summary": "Request changes or approve a specific revision of content.",
"summary": "Request changes or decide approval for a specific revision of content.",
"category": "Review",
"status": "Draft",
"version": "0.1",
"version": "0.2",
"origin": "MailSchema draft",
"profile": "https://mailschema.org/profiles/map/0.1",
"overview": "A service sends content for review. The recipient can submit feedback or record approval of the revision they received. Editing the draft and sending or publishing it remain separate service operations.",
"overview": "A service sends content for review. The recipient can submit feedback or decide whether to approve the revision they received. A proposed approval may require an authorized human decision and ends as approved, declined, stale or expired. Editing the draft and sending or publishing it remain separate service operations.",
"target": "Service-owned content identified by its exact revision.",
"operations": [
{
Expand All @@ -18,12 +18,12 @@
{
"id": "approve",
"name": "Approve",
"description": "Record approval of that revision."
"description": "Propose or record approval of that revision."
}
],
"inputs": "The target revision and, for a change request, review feedback.",
"results": "Feedback recorded or approval recorded, with the revision identified. Requests without permission or against stale revisions leave the current review decision unchanged.",
"permissions": "The service checks the caller’s review permission. Editing and any later sending or publication require their own permissions.",
"results": "Feedback recorded; approval recorded; or a proposed approval closed as declined, stale or expired. Every result identifies the request and exact revision.",
"permissions": "The service checks permission when the request is proposed and again when a human decides it. An authorized teammate may decide for the same service tenant. An unauthorized attempt does not change the proposed request. Editing and any later sending or publication require their own permissions.",
"humanRoute": "A review page showing the same content and revision.",
"example": {
"title": "Review a campaign draft",
Expand All @@ -37,8 +37,8 @@
},
"references": [
{
"title": "Content Review 0.1 contract",
"href": "https://mailschema.org/contracts/content-review-0.1.json",
"title": "Content Review 0.2 contract",
"href": "https://mailschema.org/contracts/content-review-0.2.json",
"description": "Normative canonical type contract used by MAP wire messages."
},
{
Expand All @@ -53,10 +53,14 @@
}
],
"openQuestions": [
"Which independent services and clients can reproduce the 0.1 conformance cases?",
"Which independent services and clients can reproduce the 0.2 conformance cases?",
"Which content digest representations need bindings beyond SHA-256?"
],
"history": [
{
"label": "0.2 · Working draft",
"description": "Declares completed and terminal failed outcomes for supervised approval, including declined, stale and expired decisions. Human decisions are authorized independently of the proposing client."
},
{
"label": "0.1 · Working draft",
"description": "The MAP 0.1 profile defines review feedback, revision approval, exact target binding, retries and recovery."
Expand Down
12 changes: 7 additions & 5 deletions tests/map-description.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
"profile": "https://mailschema.org/profiles/map/0.1",
"type": {
"id": "https://mailschema.org/types/content-review",
"version": "0.1",
"contractDigest": "sha-256:deba8fc588c83c7cdf9d4e47d23b114861aea35c3daaf71854dc6c626c8a1d05"
"version": "0.2",
"contractDigest": "sha-256:6ee5b086138799f70f2489e851e1db6e11ff91cd1141bd36fe202a47d8c1e637"
},
"describedAt": "2026-09-23T01:00:00Z",
"expiresAt": "2026-09-30T01:00:00Z",
Expand All @@ -24,7 +24,9 @@
"humanUrl": "https://reviews.example/reviews/campaign-42/revision/4",
"authorization": {
"kind": "service-configured",
"schemes": ["oauth2"],
"schemes": [
"oauth2"
],
"audience": "https://reviews.example/"
}
},
Expand All @@ -39,13 +41,13 @@
"id": "request-changes",
"name": "Request changes",
"description": "Record feedback on revision 4.",
"inputSchema": "https://mailschema.org/schemas/content-review-0.1.schema.json"
"inputSchema": "https://mailschema.org/schemas/content-review-0.2.schema.json"
},
{
"id": "approve",
"name": "Approve",
"description": "Record approval of revision 4.",
"inputSchema": "https://mailschema.org/schemas/content-review-0.1.schema.json"
"inputSchema": "https://mailschema.org/schemas/content-review-0.2.schema.json"
}
]
}
4 changes: 2 additions & 2 deletions tests/map-request.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
"interactionId": "urn:uuid:018f47a2-5d7c-7b11-9a3d-4d2160b85b10",
"type": {
"id": "https://mailschema.org/types/content-review",
"version": "0.1",
"contractDigest": "sha-256:deba8fc588c83c7cdf9d4e47d23b114861aea35c3daaf71854dc6c626c8a1d05"
"version": "0.2",
"contractDigest": "sha-256:6ee5b086138799f70f2489e851e1db6e11ff91cd1141bd36fe202a47d8c1e637"
},
"operation": "approve",
"target": {
Expand Down
4 changes: 2 additions & 2 deletions tests/map-result.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
"interactionId": "urn:uuid:018f47a2-5d7c-7b11-9a3d-4d2160b85b10",
"type": {
"id": "https://mailschema.org/types/content-review",
"version": "0.1",
"contractDigest": "sha-256:deba8fc588c83c7cdf9d4e47d23b114861aea35c3daaf71854dc6c626c8a1d05"
"version": "0.2",
"contractDigest": "sha-256:6ee5b086138799f70f2489e851e1db6e11ff91cd1141bd36fe202a47d8c1e637"
},
"operation": "approve",
"state": "completed",
Expand Down
3 changes: 3 additions & 0 deletions tests/test_package.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
contribution_errors,
get_contribution_schema,
get_content_review_contract,
get_content_review_01_contract,
get_record_schema,
get_map_schema,
validate_contribution,
Expand Down Expand Up @@ -46,6 +47,8 @@ def test_schema_copies_and_record_reference(self):
self.assertEqual(get_record_schema()["$ref"], "#/$defs/record")
self.assertEqual(get_map_schema()["$id"], "https://mailschema.org/schemas/map-0.1.schema.json")
self.assertEqual(get_content_review_contract()["id"], "https://mailschema.org/types/content-review")
self.assertEqual(get_content_review_contract()["version"], "0.2")
self.assertEqual(get_content_review_01_contract()["version"], "0.1")

def test_map_documents(self):
validate_map_document(self.description)
Expand Down
Loading