A community based topic aggregation platform built on atproto

Merge branch 'feat/lexicon-extensibility-fixes'

Comprehensive lexicon extensibility fixes to ensure alpha-readiness
and future-proof schema evolution without requiring V2 migrations.

Key Changes:
- Fixed all closed enums → knownValues (moderationType, visibility, sort)
- Made moderator roles and permissions extensible
- Added authentication documentation to all endpoints
- Removed invalid membership record references
- Documented technical decisions in PRD_GOVERNANCE.md

This locks down the lexicon schemas for alpha while enabling future
beta features (sortition moderation, new visibility modes, moderator
tiers) without breaking changes.

Per atProto style guide (bluesky-social/atproto#4245): enum sets
cannot be extended without breaking schema evolution rules.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

Changed files
+50 -34
docs
internal
tests
+25
docs/PRD_GOVERNANCE.md
···
## Technical Decisions Log
+
### 2025-10-18: Moderator Lexicon Extensibility
+
**Decision:** Use `knownValues` instead of `enum` for moderator roles and permissions in `social.coves.community.moderator` record schema
+
+
**Rationale:**
+
- Moderator records are immutable once published (atProto record semantics)
+
- Closed `enum` values cannot be extended without breaking schema evolution rules
+
- Using `knownValues` allows adding new roles/permissions in Beta Phase 2 without requiring V2 schema migration
+
- Zero cost to fix during alpha planning; expensive to migrate once records exist in production
+
+
**Changes Made:**
+
- `role` field: Changed from `enum: ["moderator", "admin"]` to `knownValues: ["moderator", "admin"]` with `maxLength: 64`
+
- `permissions` array items: Changed from closed enum to `knownValues` with `maxLength: 64`
+
+
**Future Extensibility Examples:**
+
- **New roles**: "owner" (full transfer rights), "trainee" (limited trial moderator), "emeritus" (honorary former moderator)
+
- **New permissions**: "manage_bots", "manage_flairs", "manage_automoderator", "manage_federation", "pin_posts"
+
- Can add these values during Phase 2 (Moderator Tiers & Permissions) without breaking existing moderator records
+
+
**atProto Style Guide Reference:**
+
Per [atproto#4245](https://github.com/bluesky-social/atproto/discussions/4245): "Enum sets are 'closed' and can not be updated or extended without breaking schema evolution rules. For this reason they should almost always be avoided. For strings, `knownValues` provides more flexible alternative."
+
+
**Implementation Status:** ✅ Fixed in lexicon before alpha launch
+
+
---
+
### 2025-10-11: Moderator Records Storage Location
**Decision:** Store moderator records in community's repository (`at://community_did/social.coves.community.moderator/{tid}`), not user's repository
+3 -2
internal/atproto/lexicon/social/coves/community/create.json
···
"defs": {
"main": {
"type": "procedure",
-
"description": "Create a new community",
+
"description": "Create a new community. Requires authentication.",
"input": {
"encoding": "application/json",
"schema": {
···
},
"visibility": {
"type": "string",
-
"enum": ["public", "unlisted", "private"],
+
"knownValues": ["public", "unlisted", "private"],
"default": "public",
+
"maxLength": 64,
"description": "Community visibility level"
},
"allowExternalDiscovery": {
+2 -7
internal/atproto/lexicon/social/coves/community/get.json
···
"defs": {
"main": {
"type": "query",
-
"description": "Get detailed information about a community",
+
"description": "Get detailed information about a community. Authentication optional; viewer state will be included if authenticated.",
"parameters": {
"type": "params",
"required": ["community"],
···
},
"member": {
"type": "boolean",
-
"description": "Whether the viewer has membership status"
-
},
-
"membershipUri": {
-
"type": "string",
-
"format": "at-uri",
-
"description": "AT-URI of the membership record if member"
+
"description": "Whether the viewer has membership status (AppView-computed)"
},
"reputation": {
"type": "integer",
+3 -2
internal/atproto/lexicon/social/coves/community/list.json
···
"defs": {
"main": {
"type": "query",
-
"description": "List communities with various sorting options",
+
"description": "List communities with various sorting options. Authentication optional; viewer state will be included if authenticated.",
"parameters": {
"type": "params",
"properties": {
···
},
"sort": {
"type": "string",
-
"enum": ["popular", "active", "new", "alphabetical"],
+
"knownValues": ["popular", "active", "new", "alphabetical"],
"default": "popular",
+
"maxLength": 64,
"description": "Sorting method"
},
"category": {
+5 -3
internal/atproto/lexicon/social/coves/community/moderator.json
···
},
"role": {
"type": "string",
-
"enum": ["moderator", "admin"],
+
"knownValues": ["moderator", "admin"],
+
"maxLength": 64,
"description": "Level of moderation privileges"
},
"permissions": {
···
"description": "Specific permissions granted",
"items": {
"type": "string",
-
"enum": [
+
"knownValues": [
"remove_posts",
"remove_comments",
"ban_users",
···
"manage_wiki",
"manage_moderators",
"manage_settings"
-
]
+
],
+
"maxLength": 64
}
},
"createdAt": {
+7 -4
internal/atproto/lexicon/social/coves/community/profile.json
···
"key": "literal:self",
"record": {
"type": "object",
-
"required": ["handle", "name", "createdAt", "createdBy", "hostedBy", "visibility"],
+
"required": ["handle", "name", "createdAt", "createdBy", "hostedBy", "visibility", "moderationType"],
"properties": {
"handle": {
"type": "string",
···
},
"visibility": {
"type": "string",
-
"enum": ["public", "unlisted", "private"],
+
"knownValues": ["public", "unlisted", "private"],
"default": "public",
+
"maxLength": 64,
"description": "Community visibility level"
},
"federation": {
···
},
"moderationType": {
"type": "string",
-
"enum": ["moderator", "sortition"],
-
"description": "Type of moderation system"
+
"knownValues": ["moderator", "sortition"],
+
"default": "moderator",
+
"maxLength": 64,
+
"description": "Type of moderation system (moderator=traditional moderator team, sortition=community tribunal)"
},
"contentWarnings": {
"type": "array",
+1 -1
internal/atproto/lexicon/social/coves/community/subscribe.json
···
"defs": {
"main": {
"type": "procedure",
-
"description": "Subscribe to a community to see its posts in your feed",
+
"description": "Subscribe to a community to see its posts in your feed. Requires authentication.",
"input": {
"encoding": "application/json",
"schema": {
+1 -1
internal/atproto/lexicon/social/coves/community/unsubscribe.json
···
"defs": {
"main": {
"type": "procedure",
-
"description": "Unsubscribe from a community",
+
"description": "Unsubscribe from a community. Requires authentication.",
"input": {
"encoding": "application/json",
"schema": {
+3 -2
internal/atproto/lexicon/social/coves/community/update.json
···
"defs": {
"main": {
"type": "procedure",
-
"description": "Update community profile",
+
"description": "Update community profile. Requires authentication and moderator/admin permissions.",
"input": {
"encoding": "application/json",
"schema": {
···
},
"visibility": {
"type": "string",
-
"enum": ["public", "unlisted", "private"],
+
"knownValues": ["public", "unlisted", "private"],
+
"maxLength": 64,
"description": "Community visibility level"
},
"allowExternalDiscovery": {
-6
tests/lexicon-test-data/actor/membership-invalid-reputation.json
···
-
{
-
"$type": "social.coves.actor.membership",
-
"community": "did:plc:examplecommunity123",
-
"createdAt": "2024-01-15T10:30:00Z",
-
"reputation": -50
-
}
-6
tests/lexicon-test-data/actor/membership-valid.json
···
-
{
-
"$type": "social.coves.actor.membership",
-
"community": "did:plc:examplecommunity123",
-
"reputation": 150,
-
"createdAt": "2024-01-15T10:30:00Z"
-
}