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

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