From dd80e9c2af4b11ef675601d00ab523e2856cb02b Mon Sep 17 00:00:00 2001 From: Chris Guidry Date: Tue, 9 Jun 2026 07:35:00 -0400 Subject: [PATCH] Add the v1 pub.openletter.* lexicon schemas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The five records from the design doc as lexicon JSON: petition, draft, endorsement, objection, and contribution. Petitions are immutable markdown letters; endorsements and objections are CID-pinned signatures (objections have to give a reason); contribution is the co-author's half of the mutually-attested authorship. Also trims DESIGN.md to match — dropped the listed flag (it implied a privacy control that doesn't exist on public records) and closesAt (deferred). Co-Authored-By: Claude Opus 4.8 (1M context) --- DESIGN.md | 24 ++--- .../lexicons/pub/openletter/contribution.json | 26 +++++ .../lexicons/pub/openletter/draft.json | 63 ++++++++++++ .../lexicons/pub/openletter/endorsement.json | 32 +++++++ .../lexicons/pub/openletter/objection.json | 33 +++++++ .../lexicons/pub/openletter/petition.json | 95 +++++++++++++++++++ 6 files changed, 262 insertions(+), 11 deletions(-) create mode 100644 openletter.pub/lexicons/pub/openletter/contribution.json create mode 100644 openletter.pub/lexicons/pub/openletter/draft.json create mode 100644 openletter.pub/lexicons/pub/openletter/endorsement.json create mode 100644 openletter.pub/lexicons/pub/openletter/objection.json create mode 100644 openletter.pub/lexicons/pub/openletter/petition.json diff --git a/DESIGN.md b/DESIGN.md index f1f9385..75b3411 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -19,8 +19,8 @@ These fall out of ATProto's grain, and everything below follows from them: modeled as handshakes between repos, never shared writes. - **The AppView interprets; it never owns or gatekeeps.** `openletter.vote` aggregates records off the network and computes the views below. Counts are - eventually-consistent, and "closing" a petition is advisory — anyone can - still write an endorsement to their own repo. + eventually-consistent, and the author can't truly prevent a signature — + anyone can write an endorsement to their own repo. ## Records — `pub.openletter.*` @@ -35,19 +35,20 @@ These fall out of ATProto's grain, and everything below follows from them: **petition** — `title`, markdown `body` (CommonMark, no raw HTML), `recipients[]` (each a `name` plus an optional ATProto identity and/or URL), `createdAt`, `contributors[]` (DIDs the author claims as co-authors), and -optional `summary` / `closesAt` / `langs` / `tags` / `banner`. Once published -it is never edited (see Immutability). +optional `summary` / `langs` / `tags` / `banner`. Once published it is never +edited (see Immutability). **draft** — the same content fields, mutable and edited in place. Publishing creates a `petition`, so the `petition` collection only ever holds finished, signable letters. A draft lives on the PDS, so it is *unlisted, not private*. **endorsement / objection** — a CID-pinned reference to a petition, -`createdAt`, an optional reason-for-signing `comment`, and a `listed` flag (a -display hint; the signer's DID is always on the record). The record key is -derived from the petition, so a signer holds at most one of each per petition -and re-signing is idempotent. Holding both on the same petition is resolved in -the AppView by recency. +`createdAt`, and a reason-for-signing `comment` (optional on an endorsement, +required on an objection — dissent has to say why). The signer's DID is always +on the record, so there is no "anonymous" signature. The record key is derived +from the petition, so a signer holds at most one of each per petition and +re-signing is idempotent. Holding both on the same petition is resolved in the +AppView by recency. **contribution** — published by a claimed co-author: a CID-pinned reference to the petition they affirm. A co-author is *confirmed* only where the author's @@ -81,5 +82,6 @@ below). Petition versioning and amendments (editing a published letter while keeping its signatures; first-class amendments as their own endorsable records), comments/discussion, suggestion-based collaborative editing, signature -`goal`s, structured target responses, and real-time co-editing. Each is a -clean additive extension; none requires reworking the above. +`goal`s, advisory close dates, structured target responses, and real-time +co-editing. Each is a clean additive extension; none requires reworking the +above. diff --git a/openletter.pub/lexicons/pub/openletter/contribution.json b/openletter.pub/lexicons/pub/openletter/contribution.json new file mode 100644 index 0000000..5ee2845 --- /dev/null +++ b/openletter.pub/lexicons/pub/openletter/contribution.json @@ -0,0 +1,26 @@ +{ + "lexicon": 1, + "id": "pub.openletter.contribution", + "defs": { + "main": { + "type": "record", + "description": "A co-author's attestation that they helped write a petition, paired with the petition's contributors list.", + "key": "any", + "record": { + "type": "object", + "required": ["subject", "createdAt"], + "properties": { + "subject": { + "type": "ref", + "ref": "com.atproto.repo.strongRef", + "description": "The petition being co-authored." + }, + "createdAt": { + "type": "string", + "format": "datetime" + } + } + } + } + } +} diff --git a/openletter.pub/lexicons/pub/openletter/draft.json b/openletter.pub/lexicons/pub/openletter/draft.json new file mode 100644 index 0000000..580a7ec --- /dev/null +++ b/openletter.pub/lexicons/pub/openletter/draft.json @@ -0,0 +1,63 @@ +{ + "lexicon": 1, + "id": "pub.openletter.draft", + "defs": { + "main": { + "type": "record", + "description": "A work-in-progress letter, published as a pub.openletter.petition when ready.", + "key": "tid", + "record": { + "type": "object", + "required": ["createdAt"], + "properties": { + "title": { + "type": "string", + "maxGraphemes": 300, + "maxLength": 3000 + }, + "body": { + "type": "string", + "description": "The draft text, as CommonMark markdown.", + "maxGraphemes": 50000, + "maxLength": 100000 + }, + "recipients": { + "type": "array", + "items": {"type": "ref", "ref": "pub.openletter.petition#recipient"}, + "maxLength": 50 + }, + "summary": { + "type": "string", + "maxGraphemes": 300, + "maxLength": 3000 + }, + "contributors": { + "type": "array", + "description": "DIDs the author intends as co-authors.", + "items": {"type": "string", "format": "did"}, + "maxLength": 100 + }, + "langs": { + "type": "array", + "items": {"type": "string", "format": "language"}, + "maxLength": 3 + }, + "tags": { + "type": "array", + "items": {"type": "string", "maxGraphemes": 64, "maxLength": 640}, + "maxLength": 10 + }, + "banner": { + "type": "blob", + "accept": ["image/png", "image/jpeg", "image/webp"], + "maxSize": 1000000 + }, + "createdAt": { + "type": "string", + "format": "datetime" + } + } + } + } + } +} diff --git a/openletter.pub/lexicons/pub/openletter/endorsement.json b/openletter.pub/lexicons/pub/openletter/endorsement.json new file mode 100644 index 0000000..a0c57d4 --- /dev/null +++ b/openletter.pub/lexicons/pub/openletter/endorsement.json @@ -0,0 +1,32 @@ +{ + "lexicon": 1, + "id": "pub.openletter.endorsement", + "defs": { + "main": { + "type": "record", + "description": "A signer's support for a petition.", + "key": "any", + "record": { + "type": "object", + "required": ["subject", "createdAt"], + "properties": { + "subject": { + "type": "ref", + "ref": "com.atproto.repo.strongRef", + "description": "The petition being endorsed." + }, + "comment": { + "type": "string", + "description": "A reason for signing.", + "maxGraphemes": 500, + "maxLength": 5000 + }, + "createdAt": { + "type": "string", + "format": "datetime" + } + } + } + } + } +} diff --git a/openletter.pub/lexicons/pub/openletter/objection.json b/openletter.pub/lexicons/pub/openletter/objection.json new file mode 100644 index 0000000..aad56b4 --- /dev/null +++ b/openletter.pub/lexicons/pub/openletter/objection.json @@ -0,0 +1,33 @@ +{ + "lexicon": 1, + "id": "pub.openletter.objection", + "defs": { + "main": { + "type": "record", + "description": "A signer's dissent against a petition.", + "key": "any", + "record": { + "type": "object", + "required": ["subject", "comment", "createdAt"], + "properties": { + "subject": { + "type": "ref", + "ref": "com.atproto.repo.strongRef", + "description": "The petition being objected to." + }, + "comment": { + "type": "string", + "description": "The reason for objecting.", + "minLength": 1, + "maxGraphemes": 500, + "maxLength": 5000 + }, + "createdAt": { + "type": "string", + "format": "datetime" + } + } + } + } + } +} diff --git a/openletter.pub/lexicons/pub/openletter/petition.json b/openletter.pub/lexicons/pub/openletter/petition.json new file mode 100644 index 0000000..5e0d370 --- /dev/null +++ b/openletter.pub/lexicons/pub/openletter/petition.json @@ -0,0 +1,95 @@ +{ + "lexicon": 1, + "id": "pub.openletter.petition", + "defs": { + "main": { + "type": "record", + "description": "An open letter that others can endorse or object to.", + "key": "tid", + "record": { + "type": "object", + "required": ["title", "body", "createdAt"], + "properties": { + "title": { + "type": "string", + "description": "The headline of the open letter.", + "minLength": 1, + "maxGraphemes": 300, + "maxLength": 3000 + }, + "body": { + "type": "string", + "description": "The full text of the letter, as CommonMark markdown.", + "minLength": 1, + "maxGraphemes": 50000, + "maxLength": 100000 + }, + "recipients": { + "type": "array", + "description": "The parties the letter is addressed to.", + "items": {"type": "ref", "ref": "#recipient"}, + "maxLength": 50 + }, + "summary": { + "type": "string", + "description": "A short tagline shown in previews and listings.", + "maxGraphemes": 300, + "maxLength": 3000 + }, + "contributors": { + "type": "array", + "description": "DIDs the author claims as co-authors; each is confirmed by a matching pub.openletter.contribution.", + "items": {"type": "string", "format": "did"}, + "maxLength": 100 + }, + "langs": { + "type": "array", + "description": "The language(s) the letter is written in.", + "items": {"type": "string", "format": "language"}, + "maxLength": 3 + }, + "tags": { + "type": "array", + "description": "Free-form tags for browsing.", + "items": {"type": "string", "maxGraphemes": 64, "maxLength": 640}, + "maxLength": 10 + }, + "banner": { + "type": "blob", + "description": "A banner image for the letter.", + "accept": ["image/png", "image/jpeg", "image/webp"], + "maxSize": 1000000 + }, + "createdAt": { + "type": "string", + "format": "datetime" + } + } + } + }, + "recipient": { + "type": "object", + "description": "A party an open letter is addressed to.", + "required": ["name"], + "properties": { + "name": { + "type": "string", + "description": "The recipient's name, e.g. \"The City Council of Springfield\".", + "minLength": 1, + "maxGraphemes": 256, + "maxLength": 2560 + }, + "did": { + "type": "string", + "format": "did", + "description": "The recipient's ATProto identity, if they have one." + }, + "url": { + "type": "string", + "format": "uri", + "description": "A web page for the recipient." + } + } + } + } +} -- 2.51.2