A community based topic aggregation platform built on atproto

Merge pull request #10 from BrettM86/feature/lexicon-test-data-and-fixes

feat: Add comprehensive lexicon test data and validation fixes

authored by

Bretton May and committed by
GitHub
3ce02d92 bcdbd2b2

+516 -121
+120 -113
CLAUDE.md
··· 1 - Project: 2 - You are a distinguished developer helping build Coves, a forum like atProto social media platform (think reddit / lemmy). 1 + # CLAUDE-BUILD.md 2 + 3 + Project: Coves Builder You are a distinguished developer actively building Coves, a forum-like atProto social media platform. Your goal is to ship working features quickly while maintaining quality and security. 4 + 5 + ## Builder Mindset 6 + 7 + - Ship working code today, refactor tomorrow 8 + - Security is built-in, not bolted-on 9 + - Test-driven: write the test, then make it pass 10 + - When stuck, check Context7 for patterns and examples 11 + - ASK QUESTIONS if you need context surrounding the product DONT ASSUME 3 12 4 - Human & LLM Readability Guidelines: 5 - - Clear Module Boundaries: Each feature is a self-contained module with explicit interfaces 13 + #### Human & LLM Readability Guidelines: 6 14 - Descriptive Naming: Use full words over abbreviations (e.g., CommunityGovernance not CommGov) 7 - - Structured Documentation: Each module includes purpose, dependencies, and example usage 8 - - Consistent Patterns: RESTful APIs, standard error handling, predictable data structures 9 - - Context-Rich Comments: Explain "why" not just "what" at decision points 15 + 16 + ## Build Process 17 + 18 + ### Phase 1: Planning (Before Writing Code) 19 + 20 + **ALWAYS START WITH:** 21 + 22 + - [ ] Identify which atProto patterns apply (check ATPROTO_GUIDE.md or context7 https://context7.com/bluesky-social/atproto) 23 + - [ ] Check if Indigo (also in context7) packages already solve this: https://context7.com/bluesky-social/indigo 24 + - [ ] Define the XRPC interface first 25 + - [ ] Write the Lexicon schema 26 + - [ ] Plan the data flow: CAR store → AppView 27 + - [ ] - Follow the two-database pattern: Repository (CAR files)(PostgreSQL for metadata) and AppView (PostgreSQL) 28 + - [ ] **Identify auth requirements and data sensitivity** 29 + 30 + ### Phase 2: Test-First Implementation 31 + 32 + **BUILD ORDER:** 33 + 34 + 1. **Domain Model** (`core/[domain]/[domain].go`) 35 + 36 + - Start with the simplest struct 37 + - Add validation methods 38 + - Define error types 39 + - **Add input validation from the start** 40 + 2. **Repository Interfaces** (`core/[domain]/repository.go`) 41 + 42 + ```go 43 + type CommunityWriteRepository interface { 44 + Create(ctx context.Context, community *Community) error 45 + Update(ctx context.Context, community *Community) error 46 + } 47 + 48 + type CommunityReadRepository interface { 49 + GetByID(ctx context.Context, id string) (*Community, error) 50 + List(ctx context.Context, limit, offset int) ([]*Community, error) 51 + } 52 + ``` 53 + 54 + 3. **Service Tests** (`core/[domain]/service_test.go`) 10 55 11 - Core Principles: 12 - - When in doubt, choose the simpler implementation 13 - - Features are the enemy of shipping 14 - - A working tool today beats a perfect tool tomorrow 56 + - Write failing tests for happy path 57 + - **Add tests for invalid inputs** 58 + - **Add tests for unauthorized access** 59 + - Mock repositories 60 + 4. **Service Implementation** (`core/[domain]/service.go`) 15 61 16 - Utilize existing tech stack 17 - - Before attempting to use an external tool, ensure it cannot be done via the current stack: 18 - - Go Chi (Web framework) 19 - - DB: PostgreSQL 20 - - atProto for federation & user identities 62 + - Implement to pass tests 63 + - **Validate all inputs before processing** 64 + - **Check permissions before operations** 65 + - Handle transactions 66 + 5. **Repository Implementations** 21 67 22 - ## atProto Guidelines 68 + - **Always use parameterized queries** 69 + - **Never concatenate user input into queries** 70 + - Write repo: `internal/atproto/carstore/[domain]_write_repo.go` 71 + - Read repo: `db/appview/[domain]_read_repo.go` 72 + 6. **XRPC Handler** (`xrpc/handlers/[domain]_handler.go`) 23 73 24 - For comprehensive AT Protocol implementation details, see [ATPROTO_GUIDE.md](./ATPROTO_GUIDE.md). 74 + - **Verify auth tokens/DIDs** 75 + - Parse XRPC request 76 + - Call service 77 + - **Sanitize errors before responding** 25 78 26 - Key principles: 27 - - Utilize Bluesky's Indigo packages before building custom atProto functionality 28 - - Everything is XRPC - no separate REST API layer needed 29 - - Follow the two-database pattern: Repository (CAR files) and AppView (PostgreSQL) 30 - - Design for federation and data portability from the start 79 + ### Phase 3: Integration 31 80 32 - # Architecture Guidelines 81 + **WIRE IT UP:** 33 82 34 - ## Required Layered Architecture 35 - Follow this strict separation of concerns: 36 - ``` 37 - Handler (XRPC) → Service (Business Logic) → Repository (Data Access) → Database 38 - ``` 39 - - Handlers: XRPC request/response only 40 - - Services: Business logic, uses both write/read repos 41 - - Write Repos: CAR store operations 42 - - Read Repos: AppView queries 83 + - [ ] Add to dependency injection in main.go 84 + - [ ] Register XRPC routes with proper auth middleware 85 + - [ ] Create migration if needed 86 + - [ ] Write integration test including auth flows 43 87 88 + ## Security-First Building 44 89 45 - ## Directory Structure 90 + ### Every Feature MUST: 46 91 47 - For a detailed project structure with file-level details and implementation status, see [PROJECT_STRUCTURE.md](./PROJECT_STRUCTURE.md). 92 + - [ ] **Validate all inputs** at the handler level 93 + - [ ] **Use parameterized queries** (never string concatenation) 94 + - [ ] **Check authorization** before any operation 95 + - [ ] **Limit resource access** (pagination, rate limits) 96 + - [ ] **Log security events** (failed auth, invalid inputs) 97 + - [ ] **Never log sensitive data** (passwords, tokens, PII) 48 98 49 - The project follows a layered architecture with clear separation between: 50 - - **XRPC handlers** - atProto API layer 51 - - Only handle XRPC concerns: parsing requests, formatting responses 52 - - Delegate all business logic to services 53 - - No direct database access 54 - - **Core business logic** - Domain services and models 55 - - Contains all business logic 56 - - Orchestrates between write and read repositories 57 - - Manages transactions and complex operations 58 - - **Data repositories** - Split between CAR store writes and AppView reads 59 - - **Write Repositories** (`internal/atproto/carstore/*_write_repo.go`) 60 - - Modify CAR files (source of truth) 61 - - **Read Repositories** (`db/appview/*_read_repo.go`) 62 - - Query denormalized PostgreSQL tables 63 - - Optimized for performance 99 + ### Red Flags to Avoid: 64 100 65 - ## Strict Prohibitions 66 - - **NEVER** put SQL queries in handlers 67 - - **NEVER** import database packages in handlers 68 - - **NEVER** pass *sql.DB directly to handlers 69 - - **NEVER** mix business logic with XRPC concerns 70 - - **NEVER** bypass the service layer 101 + - `fmt.Sprintf` in SQL queries → Use parameterized queries 102 + - Missing `context.Context` → Need it for timeouts/cancellation 103 + - No input validation → Add it immediately 104 + - Error messages with internal details → Wrap errors properly 105 + - Unbounded queries → Add limits/pagination 71 106 72 - ## Testing Requirements 73 - - Services must be easily mockable (use interfaces) 74 - - Integration tests should test the full stack 75 - - Unit tests should test individual layers in isolation 107 + ## Quick Decision Guide 76 108 77 - Test File Naming: 78 - - Unit tests: `[file]_test.go` in same directory 79 - - Integration tests: `[feature]_integration_test.go` in tests/ directory 109 + ### "Should I use X?" 80 110 81 - ## Claude Code Instructions 111 + 1. Does Indigo have it? → Use it 112 + 2. Can PostgreSQL + Go do it securely? → Build it simple 113 + 3. Requires external dependency? → Check Context7 first 82 114 83 - ### Code Generation Patterns 84 - When creating new features: 85 - 1. Generate interface first in core/[domain]/ 86 - 2. Generate test file with failing tests 87 - 3. Generate implementation to pass tests 88 - 4. Generate handler with tests 89 - 5. Update routes in xrpc/routes/ 115 + ### "How should I structure this?" 90 116 91 - ### Refactoring Checklist 92 - Before considering a feature complete: 93 - - All tests pass 94 - - No SQL in handlers 95 - - Services use interfaces only 96 - - Error handling follows patterns 97 - - API documented with examples 117 + 1. One domain, one package 118 + 2. Interfaces for testability 119 + 3. Services coordinate repos 120 + 4. Handlers only handle XRPC 98 121 99 - ## Database Migrations 100 - - Use golang-goose for version control 101 - - Migrations in db/migrations/ 102 - - Never modify existing migrations 103 - - Always provide rollback migrations 122 + ## Pre-Production Advantages 104 123 105 - ## Dependency Injection 106 - - Use constructor functions for all components 107 - - Pass interfaces, not concrete types 108 - - Wire dependencies in main.go or cmd/server/main.go 124 + Since we're pre-production: 109 125 110 - Example dependency wiring: 111 - ```go 112 - // main.go 113 - userWriteRepo := carstore.NewUserWriteRepository(carStore) 114 - userReadRepo := appview.NewUserReadRepository(db) 115 - userService := users.NewUserService(userWriteRepo, userReadRepo) 116 - userHandler := xrpc.NewUserHandler(userService) 117 - ``` 126 + - **Break things**: Delete and rebuild rather than complex migrations 127 + - **Experiment**: Try approaches, keep what works 128 + - **Simplify**: Remove unused code aggressively 129 + - **But never compromise security basics** 118 130 119 - ## Error Handling 120 - - Define custom error types in core/errors/ 121 - - Use error wrapping with context: fmt.Errorf("service: %w", err) 122 - - Services return domain errors, handlers translate to HTTP status codes 123 - - Never expose internal error details in API responses 131 + ## Success Metrics 124 132 125 - ### Context7 Usage Guidelines: 126 - - Always check Context7 for best practices before implementing external integrations and packages 127 - - Use Context7 to understand proper error handling patterns for specific libraries 128 - - Reference Context7 for testing patterns with external dependencies 129 - - Consult Context7 for proper configuration patterns 133 + Your code is ready when: 130 134 131 - ## XRPC Implementation 135 + - [ ] Tests pass (including security tests) 136 + - [ ] Follows atProto patterns 137 + - [ ] No security checklist items missed 138 + - [ ] Handles errors gracefully 139 + - [ ] Works end-to-end with auth 132 140 133 - For detailed XRPC patterns and Lexicon examples, see [ATPROTO_GUIDE.md](./ATPROTO_GUIDE.md#xrpc). 141 + ## Quick Checks Before Committing 134 142 135 - ### Key Points 136 - - All client interactions go through XRPC endpoints 137 - - Handlers validate against Lexicon schemas automatically 138 - - Queries are read-only, procedures modify repositories 139 - - Every endpoint must have a corresponding Lexicon definition 143 + 1. **Will it work?** (Integration test proves it) 144 + 2. 1. **Is it secure?** (Auth, validation, parameterized queries) 145 + 3. **Is it simple?** (Could you explain to a junior?) 146 + 4. **Is it complete?** (Test, implementation, documentation) 140 147 141 - Key note: we are pre-production, we do not need migration strategies, feel free to tear down and rebuild, however ensure to erase any unneeded data structures or code. 148 + Remember: We're building a working product. Perfect is the enemy of shipped.
+39 -2
cmd/validate-lexicon/main.go
··· 1 1 package main 2 2 3 3 import ( 4 + "bytes" 4 5 "encoding/json" 5 6 "flag" 6 7 "fmt" ··· 270 271 return nil 271 272 } 272 273 273 - // Parse JSON data 274 + // Parse JSON data using Decoder to handle numbers properly 274 275 var recordData map[string]interface{} 275 - if err := json.Unmarshal(data, &recordData); err != nil { 276 + decoder := json.NewDecoder(bytes.NewReader(data)) 277 + decoder.UseNumber() // This preserves numbers as json.Number instead of float64 278 + if err := decoder.Decode(&recordData); err != nil { 276 279 validationErrors = append(validationErrors, fmt.Sprintf("Failed to parse JSON in %s: %v", path, err)) 277 280 return nil 278 281 } 282 + 283 + // Convert json.Number values to appropriate types 284 + recordData = convertNumbers(recordData).(map[string]interface{}) 279 285 280 286 // Extract $type field 281 287 recordType, ok := recordData["$type"].(string) ··· 438 444 439 445 return nil 440 446 } 447 + 448 + // convertNumbers recursively converts json.Number values to int64 or float64 449 + func convertNumbers(v interface{}) interface{} { 450 + switch vv := v.(type) { 451 + case map[string]interface{}: 452 + result := make(map[string]interface{}) 453 + for k, val := range vv { 454 + result[k] = convertNumbers(val) 455 + } 456 + return result 457 + case []interface{}: 458 + result := make([]interface{}, len(vv)) 459 + for i, val := range vv { 460 + result[i] = convertNumbers(val) 461 + } 462 + return result 463 + case json.Number: 464 + // Try to convert to int64 first 465 + if i, err := vv.Int64(); err == nil { 466 + return i 467 + } 468 + // If that fails, convert to float64 469 + if f, err := vv.Float64(); err == nil { 470 + return f 471 + } 472 + // If both fail, return as string 473 + return vv.String() 474 + default: 475 + return v 476 + } 477 + }
+1
internal/atproto/lexicon/social/coves/actor/profile.json
··· 12 12 "properties": { 13 13 "handle": { 14 14 "type": "string", 15 + "format": "handle", 15 16 "maxLength": 253, 16 17 "description": "User's handle" 17 18 },
+1 -1
internal/atproto/lexicon/social/coves/community/profile.json
··· 52 52 }, 53 53 "moderationType": { 54 54 "type": "string", 55 - "knownValues": ["moderator", "sortition"], 55 + "enum": ["moderator", "sortition"], 56 56 "description": "Type of moderation system" 57 57 }, 58 58 "contentWarnings": {
+1 -1
internal/atproto/lexicon/social/coves/interaction/comment.json
··· 60 60 "properties": { 61 61 "image": { 62 62 "type": "ref", 63 - "ref": "social.coves.embed.image" 63 + "ref": "social.coves.embed.images#image" 64 64 }, 65 65 "caption": { 66 66 "type": "string",
+2
internal/atproto/lexicon/social/coves/interaction/tag.json
··· 17 17 }, 18 18 "tag": { 19 19 "type": "string", 20 + "minLength": 1, 21 + "maxLength": 50, 20 22 "knownValues": ["helpful", "insightful", "spam", "hostile", "offtopic", "misleading"], 21 23 "description": "Predefined tag or custom community tag" 22 24 },
+2 -2
internal/atproto/lexicon/social/coves/moderation/ruleProposal.json
··· 17 17 }, 18 18 "proposalType": { 19 19 "type": "string", 20 - "knownValues": [ 20 + "enum": [ 21 21 "addTag", 22 22 "removeTag", 23 23 "blockDomain", ··· 60 60 }, 61 61 "status": { 62 62 "type": "string", 63 - "knownValues": ["active", "passed", "failed", "cancelled", "implemented"], 63 + "enum": ["active", "passed", "failed", "cancelled", "implemented"], 64 64 "default": "active" 65 65 }, 66 66 "votingStartsAt": {
+1 -1
internal/atproto/lexicon/social/coves/moderation/tribunalVote.json
··· 22 22 }, 23 23 "decision": { 24 24 "type": "string", 25 - "knownValues": ["remove", "keep", "warn", "ban", "timeout"], 25 + "enum": ["remove", "keep", "warn", "ban", "timeout"], 26 26 "description": "Tribunal decision" 27 27 }, 28 28 "duration": {
+1 -1
internal/atproto/lexicon/social/coves/moderation/vote.json
··· 17 17 }, 18 18 "vote": { 19 19 "type": "string", 20 - "knownValues": ["approve", "reject", "abstain"] 20 + "enum": ["approve", "reject", "abstain"] 21 21 }, 22 22 "reason": { 23 23 "type": "string",
+5
tests/lexicon-test-data/actor/block-invalid-did.json
··· 1 + { 2 + "$type": "social.coves.actor.block", 3 + "subject": "not-a-valid-did", 4 + "createdAt": "2025-01-05T09:15:00Z" 5 + }
+6
tests/lexicon-test-data/actor/block-valid.json
··· 1 + { 2 + "$type": "social.coves.actor.block", 3 + "subject": "did:plc:blockeduser123", 4 + "createdAt": "2025-01-05T09:15:00Z", 5 + "reason": "Repeated harassment and spam" 6 + }
+6
tests/lexicon-test-data/actor/membership-invalid-reputation.json
··· 1 + { 2 + "$type": "social.coves.actor.membership", 3 + "community": "did:plc:examplecommunity123", 4 + "createdAt": "2024-01-15T10:30:00Z", 5 + "reputation": -50 6 + }
+6
tests/lexicon-test-data/actor/membership-valid.json
··· 1 + { 2 + "$type": "social.coves.actor.membership", 3 + "community": "did:plc:examplecommunity123", 4 + "reputation": 150, 5 + "createdAt": "2024-01-15T10:30:00Z" 6 + }
+7
tests/lexicon-test-data/actor/preferences-invalid-enum.json
··· 1 + { 2 + "$type": "social.coves.actor.preferences", 3 + "feedPreferences": { 4 + "defaultFeed": "invalid-feed-type", 5 + "defaultSort": "hot" 6 + } 7 + }
+40
tests/lexicon-test-data/actor/preferences-valid.json
··· 1 + { 2 + "$type": "social.coves.actor.preferences", 3 + "feedPreferences": { 4 + "defaultFeed": "home", 5 + "defaultSort": "hot", 6 + "showNSFW": false, 7 + "blurNSFW": true, 8 + "autoplayVideos": true, 9 + "infiniteScroll": true 10 + }, 11 + "contentFiltering": { 12 + "blockedTags": ["politics", "spoilers"], 13 + "blockedCommunities": ["did:plc:controversialcommunity"], 14 + "mutedWords": ["spam", "scam"], 15 + "languageFilter": ["en", "es"] 16 + }, 17 + "notificationSettings": { 18 + "postReplies": true, 19 + "commentReplies": true, 20 + "mentions": true, 21 + "upvotes": false, 22 + "newFollowers": true, 23 + "communityInvites": true, 24 + "moderatorNotifications": true 25 + }, 26 + "privacySettings": { 27 + "profileVisibility": "public", 28 + "showSubscriptions": true, 29 + "showSavedPosts": false, 30 + "showVoteHistory": false, 31 + "allowDMs": "followers" 32 + }, 33 + "displayPreferences": { 34 + "theme": "dark", 35 + "compactView": false, 36 + "showAvatars": true, 37 + "showThumbnails": true, 38 + "postsPerPage": 25 39 + } 40 + }
+6
tests/lexicon-test-data/actor/profile-invalid-handle-format.json
··· 1 + { 2 + "$type": "social.coves.actor.profile", 3 + "handle": "invalid handle with spaces", 4 + "displayName": "Test User", 5 + "createdAt": "2024-01-01T00:00:00Z" 6 + }
+6
tests/lexicon-test-data/actor/saved-invalid-type.json
··· 1 + { 2 + "$type": "social.coves.actor.saved", 3 + "subject": "at://did:plc:exampleuser/social.coves.post.record/3k7a3dmb5bk2c", 4 + "type": "article", 5 + "createdAt": "2025-01-09T14:30:00Z" 6 + }
+7
tests/lexicon-test-data/actor/saved-valid.json
··· 1 + { 2 + "$type": "social.coves.actor.saved", 3 + "subject": "at://did:plc:exampleuser/social.coves.post.record/3k7a3dmb5bk2c", 4 + "type": "post", 5 + "createdAt": "2025-01-09T14:30:00Z", 6 + "note": "Great tutorial on Go concurrency patterns" 7 + }
+6
tests/lexicon-test-data/actor/subscription-invalid-visibility.json
··· 1 + { 2 + "$type": "social.coves.actor.subscription", 3 + "community": "did:plc:programmingcommunity", 4 + "createdAt": "2024-06-01T08:00:00Z", 5 + "contentVisibility": 10 6 + }
+6
tests/lexicon-test-data/actor/subscription-valid.json
··· 1 + { 2 + "$type": "social.coves.actor.subscription", 3 + "community": "did:plc:programmingcommunity", 4 + "createdAt": "2024-06-01T08:00:00Z", 5 + "contentVisibility": 3 6 + }
+9
tests/lexicon-test-data/community/moderator-invalid-permissions.json
··· 1 + { 2 + "$type": "social.coves.community.moderator", 3 + "user": "did:plc:moderator123", 4 + "community": "did:plc:community123", 5 + "role": "moderator", 6 + "permissions": ["remove_posts", "invalid-permission"], 7 + "createdAt": "2024-06-15T10:00:00Z", 8 + "createdBy": "did:plc:owner123" 9 + }
+9
tests/lexicon-test-data/community/moderator-valid.json
··· 1 + { 2 + "$type": "social.coves.community.moderator", 3 + "user": "did:plc:trustedmoderator", 4 + "community": "did:plc:programmingcommunity", 5 + "role": "moderator", 6 + "permissions": ["remove_posts", "remove_comments", "manage_wiki"], 7 + "createdAt": "2024-06-15T10:00:00Z", 8 + "createdBy": "did:plc:communityowner" 9 + }
+9
tests/lexicon-test-data/community/profile-invalid-moderation-type.json
··· 1 + { 2 + "$type": "social.coves.community.profile", 3 + "name": "testcommunity", 4 + "displayName": "Test Community", 5 + "creator": "did:plc:creator123", 6 + "moderationType": "anarchy", 7 + "federatedFrom": "coves", 8 + "createdAt": "2023-12-01T08:00:00Z" 9 + }
+8
tests/lexicon-test-data/community/rules-invalid-sortition.json
··· 1 + { 2 + "$type": "social.coves.community.rules", 3 + "sortitionConfig": { 4 + "tagThreshold": 5, 5 + "tribunalThreshold": 30, 6 + "jurySize": 9 7 + } 8 + }
+44
tests/lexicon-test-data/community/rules-valid.json
··· 1 + { 2 + "$type": "social.coves.community.rules", 3 + "postTypes": { 4 + "allowText": true, 5 + "allowVideo": true, 6 + "allowImage": true, 7 + "allowArticle": true 8 + }, 9 + "contentRestrictions": { 10 + "blockedDomains": ["spam.com", "malware.com"], 11 + "allowedDomains": [] 12 + }, 13 + "geoRestrictions": { 14 + "enabled": true, 15 + "allowedCountries": ["US", "CA", "GB", "AU"], 16 + "allowedRegions": [] 17 + }, 18 + "customTags": ["help", "announcement", "discussion", "tutorial"], 19 + "textRules": [ 20 + { 21 + "title": "Be respectful", 22 + "description": "Treat all members with respect. No harassment, hate speech, or personal attacks.", 23 + "createdAt": "2024-01-01T00:00:00Z", 24 + "isActive": true 25 + }, 26 + { 27 + "title": "No spam", 28 + "description": "Do not post spam, including excessive self-promotion or irrelevant content.", 29 + "createdAt": "2024-01-01T00:00:00Z", 30 + "isActive": true 31 + }, 32 + { 33 + "title": "Stay on topic", 34 + "description": "Posts must be related to programming and software development.", 35 + "createdAt": "2024-01-01T00:00:00Z", 36 + "isActive": true 37 + } 38 + ], 39 + "sortitionConfig": { 40 + "tagThreshold": 15, 41 + "tribunalThreshold": 30, 42 + "jurySize": 9 43 + } 44 + }
+7
tests/lexicon-test-data/community/wiki-invalid-slug.json
··· 1 + { 2 + "$type": "social.coves.community.wiki", 3 + "slug": "this-slug-is-way-too-long-and-exceeds-the-maximum-allowed-length-of-128-characters-which-should-trigger-a-validation-error-when-we-run-the-test", 4 + "title": "Invalid Wiki Page", 5 + "content": "This wiki page has a slug that exceeds the maximum length.", 6 + "createdAt": "2024-01-01T00:00:00Z" 7 + }
+13
tests/lexicon-test-data/community/wiki-valid.json
··· 1 + { 2 + "$type": "social.coves.community.wiki", 3 + "slug": "getting-started", 4 + "title": "Getting Started with Our Community", 5 + "content": "# Welcome to the Programming Community\n\nThis guide will help you get started with our community.\n\n## Rules\nPlease read our community rules before posting.\n\n## Resources\n- [FAQ](/wiki/faq)\n- [Posting Guidelines](/wiki/posting-guidelines)\n- [Code of Conduct](/wiki/code-of-conduct)", 6 + "author": "did:plc:moderator123", 7 + "editors": ["did:plc:editor1", "did:plc:editor2"], 8 + "isIndex": false, 9 + "createdAt": "2024-01-01T00:00:00Z", 10 + "updatedAt": "2025-01-09T15:00:00Z", 11 + "revision": 5, 12 + "tags": ["meta", "help", "guide"] 13 + }
+5
tests/lexicon-test-data/interaction/comment-invalid-content.json
··· 1 + { 2 + "$type": "social.coves.interaction.comment", 3 + "post": "at://did:plc:author123/social.coves.post.record/3k7a3dmb5bk2c", 4 + "createdAt": "2025-01-09T16:45:00Z" 5 + }
+10
tests/lexicon-test-data/interaction/comment-valid-sticker.json
··· 1 + { 2 + "$type": "social.coves.interaction.comment", 3 + "subject": "at://did:plc:author123/social.coves.post.record/3k7a3dmb5bk2c", 4 + "content": { 5 + "$type": "social.coves.interaction.comment#stickerContent", 6 + "stickerId": "thumbs-up", 7 + "stickerPackId": "default-pack" 8 + }, 9 + "createdAt": "2025-01-09T16:50:00Z" 10 + }
+23
tests/lexicon-test-data/interaction/comment-valid-text.json
··· 1 + { 2 + "$type": "social.coves.interaction.comment", 3 + "subject": "at://did:plc:author123/social.coves.post.record/3k7a3dmb5bk2c", 4 + "content": { 5 + "$type": "social.coves.interaction.comment#textContent", 6 + "text": "Great post! I especially liked the part about @alice.example.com's contribution to the project.", 7 + "facets": [ 8 + { 9 + "index": { 10 + "byteStart": 46, 11 + "byteEnd": 64 12 + }, 13 + "features": [ 14 + { 15 + "$type": "social.coves.richtext.facet#mention", 16 + "did": "did:plc:aliceuser123" 17 + } 18 + ] 19 + } 20 + ] 21 + }, 22 + "createdAt": "2025-01-09T16:30:00Z" 23 + }
+5
tests/lexicon-test-data/interaction/share-valid-no-community.json
··· 1 + { 2 + "$type": "social.coves.interaction.share", 3 + "subject": "at://did:plc:originalauthor/social.coves.post.record/3k7a3dmb5bk2c", 4 + "createdAt": "2025-01-09T17:00:00Z" 5 + }
+6
tests/lexicon-test-data/interaction/share-valid.json
··· 1 + { 2 + "$type": "social.coves.interaction.share", 3 + "subject": "at://did:plc:originalauthor/social.coves.post.record/3k7a3dmb5bk2c", 4 + "community": "did:plc:targetcommunity", 5 + "createdAt": "2025-01-09T17:00:00Z" 6 + }
+6
tests/lexicon-test-data/interaction/tag-invalid-empty.json
··· 1 + { 2 + "$type": "social.coves.interaction.tag", 3 + "subject": "at://did:plc:author123/social.coves.post.record/3k7a3dmb5bk2c", 4 + "tag": "", 5 + "createdAt": "2025-01-09T17:15:00Z" 6 + }
+6
tests/lexicon-test-data/interaction/tag-valid-custom.json
··· 1 + { 2 + "$type": "social.coves.interaction.tag", 3 + "subject": "at://did:plc:author123/social.coves.post.record/3k7a3dmb5bk2c", 4 + "tag": "beginner-friendly", 5 + "createdAt": "2025-01-09T17:15:00Z" 6 + }
+6
tests/lexicon-test-data/interaction/tag-valid-known.json
··· 1 + { 2 + "$type": "social.coves.interaction.tag", 3 + "subject": "at://did:plc:author123/social.coves.post.record/3k7a3dmb5bk2c", 4 + "tag": "nsfw", 5 + "createdAt": "2025-01-09T17:15:00Z" 6 + }
+9
tests/lexicon-test-data/moderation/rule-proposal-invalid-status.json
··· 1 + { 2 + "$type": "social.coves.moderation.ruleProposal", 3 + "community": "did:plc:community123", 4 + "proposalType": "addRule", 5 + "title": "Test invalid status", 6 + "description": "This should fail validation due to invalid status", 7 + "status": "invalidStatus", 8 + "createdAt": "2025-01-09T17:00:00Z" 9 + }
+9
tests/lexicon-test-data/moderation/rule-proposal-invalid-threshold.json
··· 1 + { 2 + "$type": "social.coves.moderation.ruleProposal", 3 + "community": "did:plc:community123", 4 + "proposalType": "updateRule", 5 + "title": "Update harassment policy", 6 + "description": "Strengthen the harassment policy", 7 + "requiredVotes": -50, 8 + "createdAt": "2025-01-09T17:00:00Z" 9 + }
+8
tests/lexicon-test-data/moderation/rule-proposal-invalid-type.json
··· 1 + { 2 + "$type": "social.coves.moderation.ruleProposal", 3 + "community": "did:plc:community123", 4 + "proposalType": "invalidProposalType", 5 + "title": "Test invalid proposal type", 6 + "description": "This should fail validation", 7 + "createdAt": "2025-01-09T17:00:00Z" 8 + }
+13
tests/lexicon-test-data/moderation/rule-proposal-valid.json
··· 1 + { 2 + "$type": "social.coves.moderation.ruleProposal", 3 + "community": "did:plc:programmingcommunity", 4 + "proposalType": "addRule", 5 + "title": "No AI-generated content without disclosure", 6 + "description": "All AI-generated code or content must be clearly marked as such. This helps maintain transparency and allows community members to make informed decisions about the content they consume.", 7 + "proposalData": { 8 + "ruleTitle": "Disclose AI-generated content", 9 + "ruleDescription": "All posts containing AI-generated code or content must include a clear disclosure statement" 10 + }, 11 + "requiredVotes": 100, 12 + "createdAt": "2025-01-09T17:00:00Z" 13 + }
+7
tests/lexicon-test-data/moderation/tribunal-vote-invalid-decision.json
··· 1 + { 2 + "$type": "social.coves.moderation.tribunalVote", 3 + "tribunal": "at://did:plc:community123/social.coves.moderation.tribunal/3k7a3dmb5bk2c", 4 + "subject": "at://did:plc:user123/social.coves.post.record/3k7a2clb4bj2b", 5 + "decision": "maybe", 6 + "createdAt": "2025-01-09T18:00:00Z" 7 + }
+13
tests/lexicon-test-data/moderation/tribunal-vote-valid.json
··· 1 + { 2 + "$type": "social.coves.moderation.tribunalVote", 3 + "tribunal": "at://did:plc:community123/social.coves.moderation.tribunal/3k7a3dmb5bk2c", 4 + "subject": "at://did:plc:spammer123/social.coves.post.record/3k7a2clb4bj2b", 5 + "decision": "remove", 6 + "reasoning": "The moderator's action was justified based on clear violation of Rule 2 (No Spam). The user posted the same promotional content across multiple communities within a short timeframe.", 7 + "precedents": [ 8 + "at://did:plc:community123/social.coves.moderation.case/3k6z2cla4aj1a", 9 + "at://did:plc:community456/social.coves.moderation.case/3k6y1bkz3zi0z" 10 + ], 11 + "dissenting": false, 12 + "createdAt": "2025-01-09T18:00:00Z" 13 + }
+6
tests/lexicon-test-data/moderation/vote-invalid-option.json
··· 1 + { 2 + "$type": "social.coves.moderation.vote", 3 + "subject": "at://did:plc:community123/social.coves.moderation.ruleProposal/3k7a3dmb5bk2c", 4 + "vote": "strongly-approve", 5 + "createdAt": "2025-01-09T18:30:00Z" 6 + }
+6
tests/lexicon-test-data/moderation/vote-valid-approve.json
··· 1 + { 2 + "$type": "social.coves.moderation.vote", 3 + "subject": "at://did:plc:community123/social.coves.moderation.ruleProposal/3k7a3dmb5bk2c", 4 + "vote": "approve", 5 + "createdAt": "2025-01-09T18:30:00Z" 6 + }
+10
tests/lexicon-test-data/post/post-invalid-missing-community.json
··· 1 + { 2 + "$type": "social.coves.post.record", 3 + "postType": "text", 4 + "title": "Test Post", 5 + "text": "This post is missing the required community field", 6 + "tags": ["test"], 7 + "language": "en", 8 + "contentWarnings": [], 9 + "createdAt": "2025-01-09T14:30:00Z" 10 + }