diff --git a/cmd/validate-lexicon/main.go b/cmd/validate-lexicon/main.go index ab93d46..3b8fffa 100644 --- a/cmd/validate-lexicon/main.go +++ b/cmd/validate-lexicon/main.go @@ -1,6 +1,7 @@ package main import ( + "bytes" "encoding/json" "flag" "fmt" @@ -270,12 +271,17 @@ func validateTestData(catalog *lexicon.BaseCatalog, testDataPath string, verbose 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) @@ -438,3 +444,34 @@ func validateCrossReferences(catalog *lexicon.BaseCatalog, verbose bool) error { 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 + } +} diff --git a/internal/atproto/lexicon/social/coves/actor/profile.json b/internal/atproto/lexicon/social/coves/actor/profile.json index b13fcca..90da1ca 100644 --- a/internal/atproto/lexicon/social/coves/actor/profile.json +++ b/internal/atproto/lexicon/social/coves/actor/profile.json @@ -12,6 +12,7 @@ "properties": { "handle": { "type": "string", + "format": "handle", "maxLength": 253, "description": "User's handle" }, diff --git a/internal/atproto/lexicon/social/coves/community/profile.json b/internal/atproto/lexicon/social/coves/community/profile.json index 8d9e75c..84123ce 100644 --- a/internal/atproto/lexicon/social/coves/community/profile.json +++ b/internal/atproto/lexicon/social/coves/community/profile.json @@ -52,7 +52,7 @@ }, "moderationType": { "type": "string", - "knownValues": ["moderator", "sortition"], + "enum": ["moderator", "sortition"], "description": "Type of moderation system" }, "contentWarnings": { diff --git a/internal/atproto/lexicon/social/coves/interaction/comment.json b/internal/atproto/lexicon/social/coves/interaction/comment.json index 915937f..12482ef 100644 --- a/internal/atproto/lexicon/social/coves/interaction/comment.json +++ b/internal/atproto/lexicon/social/coves/interaction/comment.json @@ -60,7 +60,7 @@ "properties": { "image": { "type": "ref", - "ref": "social.coves.embed.image" + "ref": "social.coves.embed.images#image" }, "caption": { "type": "string", diff --git a/internal/atproto/lexicon/social/coves/interaction/tag.json b/internal/atproto/lexicon/social/coves/interaction/tag.json index dd0632f..bcc616b 100644 --- a/internal/atproto/lexicon/social/coves/interaction/tag.json +++ b/internal/atproto/lexicon/social/coves/interaction/tag.json @@ -17,6 +17,8 @@ }, "tag": { "type": "string", + "minLength": 1, + "maxLength": 50, "knownValues": ["helpful", "insightful", "spam", "hostile", "offtopic", "misleading"], "description": "Predefined tag or custom community tag" }, diff --git a/internal/atproto/lexicon/social/coves/moderation/ruleProposal.json b/internal/atproto/lexicon/social/coves/moderation/ruleProposal.json index 768a989..cd8be1b 100644 --- a/internal/atproto/lexicon/social/coves/moderation/ruleProposal.json +++ b/internal/atproto/lexicon/social/coves/moderation/ruleProposal.json @@ -17,7 +17,7 @@ }, "proposalType": { "type": "string", - "knownValues": [ + "enum": [ "addTag", "removeTag", "blockDomain", @@ -60,7 +60,7 @@ }, "status": { "type": "string", - "knownValues": ["active", "passed", "failed", "cancelled", "implemented"], + "enum": ["active", "passed", "failed", "cancelled", "implemented"], "default": "active" }, "votingStartsAt": { diff --git a/internal/atproto/lexicon/social/coves/moderation/tribunalVote.json b/internal/atproto/lexicon/social/coves/moderation/tribunalVote.json index 1d051dc..6cefe2e 100644 --- a/internal/atproto/lexicon/social/coves/moderation/tribunalVote.json +++ b/internal/atproto/lexicon/social/coves/moderation/tribunalVote.json @@ -22,7 +22,7 @@ }, "decision": { "type": "string", - "knownValues": ["remove", "keep", "warn", "ban", "timeout"], + "enum": ["remove", "keep", "warn", "ban", "timeout"], "description": "Tribunal decision" }, "duration": { diff --git a/internal/atproto/lexicon/social/coves/moderation/vote.json b/internal/atproto/lexicon/social/coves/moderation/vote.json index 79de493..0ea633c 100644 --- a/internal/atproto/lexicon/social/coves/moderation/vote.json +++ b/internal/atproto/lexicon/social/coves/moderation/vote.json @@ -17,7 +17,7 @@ }, "vote": { "type": "string", - "knownValues": ["approve", "reject", "abstain"] + "enum": ["approve", "reject", "abstain"] }, "reason": { "type": "string", diff --git a/tests/lexicon-test-data/actor/block-invalid-did.json b/tests/lexicon-test-data/actor/block-invalid-did.json new file mode 100644 index 0000000..812b065 --- /dev/null +++ b/tests/lexicon-test-data/actor/block-invalid-did.json @@ -0,0 +1,5 @@ +{ + "$type": "social.coves.actor.block", + "subject": "not-a-valid-did", + "createdAt": "2025-01-05T09:15:00Z" +} \ No newline at end of file diff --git a/tests/lexicon-test-data/actor/block-valid.json b/tests/lexicon-test-data/actor/block-valid.json new file mode 100644 index 0000000..131517b --- /dev/null +++ b/tests/lexicon-test-data/actor/block-valid.json @@ -0,0 +1,6 @@ +{ + "$type": "social.coves.actor.block", + "subject": "did:plc:blockeduser123", + "createdAt": "2025-01-05T09:15:00Z", + "reason": "Repeated harassment and spam" +} \ No newline at end of file diff --git a/tests/lexicon-test-data/actor/membership-invalid-reputation.json b/tests/lexicon-test-data/actor/membership-invalid-reputation.json new file mode 100644 index 0000000..c7f7b52 --- /dev/null +++ b/tests/lexicon-test-data/actor/membership-invalid-reputation.json @@ -0,0 +1,6 @@ +{ + "$type": "social.coves.actor.membership", + "community": "did:plc:examplecommunity123", + "createdAt": "2024-01-15T10:30:00Z", + "reputation": -50 +} \ No newline at end of file diff --git a/tests/lexicon-test-data/actor/membership-valid.json b/tests/lexicon-test-data/actor/membership-valid.json new file mode 100644 index 0000000..327540c --- /dev/null +++ b/tests/lexicon-test-data/actor/membership-valid.json @@ -0,0 +1,6 @@ +{ + "$type": "social.coves.actor.membership", + "community": "did:plc:examplecommunity123", + "reputation": 150, + "createdAt": "2024-01-15T10:30:00Z" +} \ No newline at end of file diff --git a/tests/lexicon-test-data/actor/preferences-invalid-enum.json b/tests/lexicon-test-data/actor/preferences-invalid-enum.json new file mode 100644 index 0000000..69581c2 --- /dev/null +++ b/tests/lexicon-test-data/actor/preferences-invalid-enum.json @@ -0,0 +1,7 @@ +{ + "$type": "social.coves.actor.preferences", + "feedPreferences": { + "defaultFeed": "invalid-feed-type", + "defaultSort": "hot" + } +} \ No newline at end of file diff --git a/tests/lexicon-test-data/actor/preferences-valid.json b/tests/lexicon-test-data/actor/preferences-valid.json new file mode 100644 index 0000000..d392c5d --- /dev/null +++ b/tests/lexicon-test-data/actor/preferences-valid.json @@ -0,0 +1,40 @@ +{ + "$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 + } +} \ No newline at end of file diff --git a/tests/lexicon-test-data/actor/profile-invalid-handle-format.json b/tests/lexicon-test-data/actor/profile-invalid-handle-format.json new file mode 100644 index 0000000..3e11b95 --- /dev/null +++ b/tests/lexicon-test-data/actor/profile-invalid-handle-format.json @@ -0,0 +1,6 @@ +{ + "$type": "social.coves.actor.profile", + "handle": "invalid handle with spaces", + "displayName": "Test User", + "createdAt": "2024-01-01T00:00:00Z" +} \ No newline at end of file diff --git a/tests/lexicon-test-data/actor/saved-invalid-type.json b/tests/lexicon-test-data/actor/saved-invalid-type.json new file mode 100644 index 0000000..fc0cca3 --- /dev/null +++ b/tests/lexicon-test-data/actor/saved-invalid-type.json @@ -0,0 +1,6 @@ +{ + "$type": "social.coves.actor.saved", + "subject": "at://did:plc:exampleuser/social.coves.post.record/3k7a3dmb5bk2c", + "type": "article", + "createdAt": "2025-01-09T14:30:00Z" +} \ No newline at end of file diff --git a/tests/lexicon-test-data/actor/saved-valid.json b/tests/lexicon-test-data/actor/saved-valid.json new file mode 100644 index 0000000..f41fabe --- /dev/null +++ b/tests/lexicon-test-data/actor/saved-valid.json @@ -0,0 +1,7 @@ +{ + "$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" +} \ No newline at end of file diff --git a/tests/lexicon-test-data/actor/subscription-invalid-visibility.json b/tests/lexicon-test-data/actor/subscription-invalid-visibility.json new file mode 100644 index 0000000..02df450 --- /dev/null +++ b/tests/lexicon-test-data/actor/subscription-invalid-visibility.json @@ -0,0 +1,6 @@ +{ + "$type": "social.coves.actor.subscription", + "community": "did:plc:programmingcommunity", + "createdAt": "2024-06-01T08:00:00Z", + "contentVisibility": 10 +} \ No newline at end of file diff --git a/tests/lexicon-test-data/actor/subscription-valid.json b/tests/lexicon-test-data/actor/subscription-valid.json new file mode 100644 index 0000000..537d57a --- /dev/null +++ b/tests/lexicon-test-data/actor/subscription-valid.json @@ -0,0 +1,6 @@ +{ + "$type": "social.coves.actor.subscription", + "community": "did:plc:programmingcommunity", + "createdAt": "2024-06-01T08:00:00Z", + "contentVisibility": 3 +} \ No newline at end of file diff --git a/tests/lexicon-test-data/community/moderator-invalid-permissions.json b/tests/lexicon-test-data/community/moderator-invalid-permissions.json new file mode 100644 index 0000000..f540a35 --- /dev/null +++ b/tests/lexicon-test-data/community/moderator-invalid-permissions.json @@ -0,0 +1,9 @@ +{ + "$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" +} \ No newline at end of file diff --git a/tests/lexicon-test-data/community/moderator-valid.json b/tests/lexicon-test-data/community/moderator-valid.json new file mode 100644 index 0000000..2185635 --- /dev/null +++ b/tests/lexicon-test-data/community/moderator-valid.json @@ -0,0 +1,9 @@ +{ + "$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" +} \ No newline at end of file diff --git a/tests/lexicon-test-data/community/profile-invalid-moderation-type.json b/tests/lexicon-test-data/community/profile-invalid-moderation-type.json new file mode 100644 index 0000000..45033de --- /dev/null +++ b/tests/lexicon-test-data/community/profile-invalid-moderation-type.json @@ -0,0 +1,9 @@ +{ + "$type": "social.coves.community.profile", + "name": "testcommunity", + "displayName": "Test Community", + "creator": "did:plc:creator123", + "moderationType": "anarchy", + "federatedFrom": "coves", + "createdAt": "2023-12-01T08:00:00Z" +} \ No newline at end of file diff --git a/tests/lexicon-test-data/community/rules-invalid-sortition.json b/tests/lexicon-test-data/community/rules-invalid-sortition.json new file mode 100644 index 0000000..6a17e8d --- /dev/null +++ b/tests/lexicon-test-data/community/rules-invalid-sortition.json @@ -0,0 +1,8 @@ +{ + "$type": "social.coves.community.rules", + "sortitionConfig": { + "tagThreshold": 5, + "tribunalThreshold": 30, + "jurySize": 9 + } +} \ No newline at end of file diff --git a/tests/lexicon-test-data/community/rules-valid.json b/tests/lexicon-test-data/community/rules-valid.json new file mode 100644 index 0000000..9bbdf9d --- /dev/null +++ b/tests/lexicon-test-data/community/rules-valid.json @@ -0,0 +1,44 @@ +{ + "$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 + } +} \ No newline at end of file diff --git a/tests/lexicon-test-data/community/wiki-invalid-slug.json b/tests/lexicon-test-data/community/wiki-invalid-slug.json new file mode 100644 index 0000000..a286069 --- /dev/null +++ b/tests/lexicon-test-data/community/wiki-invalid-slug.json @@ -0,0 +1,7 @@ +{ + "$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" +} \ No newline at end of file diff --git a/tests/lexicon-test-data/community/wiki-valid.json b/tests/lexicon-test-data/community/wiki-valid.json new file mode 100644 index 0000000..5c9d2cb --- /dev/null +++ b/tests/lexicon-test-data/community/wiki-valid.json @@ -0,0 +1,13 @@ +{ + "$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"] +} \ No newline at end of file diff --git a/tests/lexicon-test-data/interaction/comment-invalid-content.json b/tests/lexicon-test-data/interaction/comment-invalid-content.json new file mode 100644 index 0000000..8619cbe --- /dev/null +++ b/tests/lexicon-test-data/interaction/comment-invalid-content.json @@ -0,0 +1,5 @@ +{ + "$type": "social.coves.interaction.comment", + "post": "at://did:plc:author123/social.coves.post.record/3k7a3dmb5bk2c", + "createdAt": "2025-01-09T16:45:00Z" +} \ No newline at end of file diff --git a/tests/lexicon-test-data/interaction/comment-valid-sticker.json b/tests/lexicon-test-data/interaction/comment-valid-sticker.json new file mode 100644 index 0000000..1e1e24c --- /dev/null +++ b/tests/lexicon-test-data/interaction/comment-valid-sticker.json @@ -0,0 +1,10 @@ +{ + "$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" +} \ No newline at end of file diff --git a/tests/lexicon-test-data/interaction/comment-valid-text.json b/tests/lexicon-test-data/interaction/comment-valid-text.json new file mode 100644 index 0000000..a71c434 --- /dev/null +++ b/tests/lexicon-test-data/interaction/comment-valid-text.json @@ -0,0 +1,23 @@ +{ + "$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" +} \ No newline at end of file diff --git a/tests/lexicon-test-data/interaction/share-valid-no-community.json b/tests/lexicon-test-data/interaction/share-valid-no-community.json new file mode 100644 index 0000000..f631297 --- /dev/null +++ b/tests/lexicon-test-data/interaction/share-valid-no-community.json @@ -0,0 +1,5 @@ +{ + "$type": "social.coves.interaction.share", + "subject": "at://did:plc:originalauthor/social.coves.post.record/3k7a3dmb5bk2c", + "createdAt": "2025-01-09T17:00:00Z" +} \ No newline at end of file diff --git a/tests/lexicon-test-data/interaction/share-valid.json b/tests/lexicon-test-data/interaction/share-valid.json new file mode 100644 index 0000000..6488ec5 --- /dev/null +++ b/tests/lexicon-test-data/interaction/share-valid.json @@ -0,0 +1,6 @@ +{ + "$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" +} \ No newline at end of file diff --git a/tests/lexicon-test-data/interaction/tag-invalid-empty.json b/tests/lexicon-test-data/interaction/tag-invalid-empty.json new file mode 100644 index 0000000..ca15543 --- /dev/null +++ b/tests/lexicon-test-data/interaction/tag-invalid-empty.json @@ -0,0 +1,6 @@ +{ + "$type": "social.coves.interaction.tag", + "subject": "at://did:plc:author123/social.coves.post.record/3k7a3dmb5bk2c", + "tag": "", + "createdAt": "2025-01-09T17:15:00Z" +} \ No newline at end of file diff --git a/tests/lexicon-test-data/interaction/tag-valid-custom.json b/tests/lexicon-test-data/interaction/tag-valid-custom.json new file mode 100644 index 0000000..b3967f5 --- /dev/null +++ b/tests/lexicon-test-data/interaction/tag-valid-custom.json @@ -0,0 +1,6 @@ +{ + "$type": "social.coves.interaction.tag", + "subject": "at://did:plc:author123/social.coves.post.record/3k7a3dmb5bk2c", + "tag": "beginner-friendly", + "createdAt": "2025-01-09T17:15:00Z" +} \ No newline at end of file diff --git a/tests/lexicon-test-data/interaction/tag-valid-known.json b/tests/lexicon-test-data/interaction/tag-valid-known.json new file mode 100644 index 0000000..54bf67e --- /dev/null +++ b/tests/lexicon-test-data/interaction/tag-valid-known.json @@ -0,0 +1,6 @@ +{ + "$type": "social.coves.interaction.tag", + "subject": "at://did:plc:author123/social.coves.post.record/3k7a3dmb5bk2c", + "tag": "nsfw", + "createdAt": "2025-01-09T17:15:00Z" +} \ No newline at end of file diff --git a/tests/lexicon-test-data/moderation/rule-proposal-invalid-status.json b/tests/lexicon-test-data/moderation/rule-proposal-invalid-status.json new file mode 100644 index 0000000..4ff7282 --- /dev/null +++ b/tests/lexicon-test-data/moderation/rule-proposal-invalid-status.json @@ -0,0 +1,9 @@ +{ + "$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" +} \ No newline at end of file diff --git a/tests/lexicon-test-data/moderation/rule-proposal-invalid-threshold.json b/tests/lexicon-test-data/moderation/rule-proposal-invalid-threshold.json new file mode 100644 index 0000000..29d9573 --- /dev/null +++ b/tests/lexicon-test-data/moderation/rule-proposal-invalid-threshold.json @@ -0,0 +1,9 @@ +{ + "$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" +} \ No newline at end of file diff --git a/tests/lexicon-test-data/moderation/rule-proposal-invalid-type.json b/tests/lexicon-test-data/moderation/rule-proposal-invalid-type.json new file mode 100644 index 0000000..722a091 --- /dev/null +++ b/tests/lexicon-test-data/moderation/rule-proposal-invalid-type.json @@ -0,0 +1,8 @@ +{ + "$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" +} \ No newline at end of file diff --git a/tests/lexicon-test-data/moderation/rule-proposal-valid.json b/tests/lexicon-test-data/moderation/rule-proposal-valid.json new file mode 100644 index 0000000..b0d38c7 --- /dev/null +++ b/tests/lexicon-test-data/moderation/rule-proposal-valid.json @@ -0,0 +1,13 @@ +{ + "$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" +} \ No newline at end of file diff --git a/tests/lexicon-test-data/moderation/tribunal-vote-invalid-decision.json b/tests/lexicon-test-data/moderation/tribunal-vote-invalid-decision.json new file mode 100644 index 0000000..bb78a31 --- /dev/null +++ b/tests/lexicon-test-data/moderation/tribunal-vote-invalid-decision.json @@ -0,0 +1,7 @@ +{ + "$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" +} \ No newline at end of file diff --git a/tests/lexicon-test-data/moderation/tribunal-vote-valid.json b/tests/lexicon-test-data/moderation/tribunal-vote-valid.json new file mode 100644 index 0000000..b6087a4 --- /dev/null +++ b/tests/lexicon-test-data/moderation/tribunal-vote-valid.json @@ -0,0 +1,13 @@ +{ + "$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" +} \ No newline at end of file diff --git a/tests/lexicon-test-data/moderation/vote-invalid-option.json b/tests/lexicon-test-data/moderation/vote-invalid-option.json new file mode 100644 index 0000000..e6ff44f --- /dev/null +++ b/tests/lexicon-test-data/moderation/vote-invalid-option.json @@ -0,0 +1,6 @@ +{ + "$type": "social.coves.moderation.vote", + "subject": "at://did:plc:community123/social.coves.moderation.ruleProposal/3k7a3dmb5bk2c", + "vote": "strongly-approve", + "createdAt": "2025-01-09T18:30:00Z" +} \ No newline at end of file diff --git a/tests/lexicon-test-data/moderation/vote-valid-approve.json b/tests/lexicon-test-data/moderation/vote-valid-approve.json new file mode 100644 index 0000000..d3e1361 --- /dev/null +++ b/tests/lexicon-test-data/moderation/vote-valid-approve.json @@ -0,0 +1,6 @@ +{ + "$type": "social.coves.moderation.vote", + "subject": "at://did:plc:community123/social.coves.moderation.ruleProposal/3k7a3dmb5bk2c", + "vote": "approve", + "createdAt": "2025-01-09T18:30:00Z" +} \ No newline at end of file diff --git a/tests/lexicon-test-data/post/post-invalid-missing-community.json b/tests/lexicon-test-data/post/post-invalid-missing-community.json new file mode 100644 index 0000000..34ba2b5 --- /dev/null +++ b/tests/lexicon-test-data/post/post-invalid-missing-community.json @@ -0,0 +1,10 @@ +{ + "$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" +} \ No newline at end of file -- 2.51.2 From 991902263d708a7574b86f525111714421f678a3 Mon Sep 17 00:00:00 2001 From: Bretton Date: Wed, 9 Jul 2025 16:59:54 -0700 Subject: [PATCH 2/2] Updating CLAUDE.md --- CLAUDE.md | 285 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 146 insertions(+), 139 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index a8978b0..cb93ba7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,141 +1,148 @@ -Project: -You are a distinguished developer helping build Coves, a forum like atProto social media platform (think reddit / lemmy). +# CLAUDE-BUILD.md -Human & LLM Readability Guidelines: -- Clear Module Boundaries: Each feature is a self-contained module with explicit interfaces +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: - 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 - -Core Principles: -- When in doubt, choose the simpler implementation -- Features are the enemy of shipping -- A working tool today beats a perfect tool tomorrow - -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 - -## atProto Guidelines - -For comprehensive AT Protocol implementation details, see [ATPROTO_GUIDE.md](./ATPROTO_GUIDE.md). - -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 - -# Architecture Guidelines - -## 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 - - -## Directory Structure - -For a detailed project structure with file-level details and implementation status, see [PROJECT_STRUCTURE.md](./PROJECT_STRUCTURE.md). - -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 - -## 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 - -## Testing Requirements -- Services must be easily mockable (use interfaces) -- Integration tests should test the full stack -- Unit tests should test individual layers in isolation - -Test File Naming: -- Unit tests: `[file]_test.go` in same directory -- Integration tests: `[feature]_integration_test.go` in tests/ directory - -## Claude Code Instructions - -### 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/ - -### 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 - -## Database Migrations -- Use golang-goose for version control -- Migrations in db/migrations/ -- Never modify existing migrations -- Always provide rollback migrations - -## Dependency Injection -- Use constructor functions for all components -- Pass interfaces, not concrete types -- Wire dependencies in main.go or cmd/server/main.go - -Example dependency wiring: -```go -// main.go -userWriteRepo := carstore.NewUserWriteRepository(carStore) -userReadRepo := appview.NewUserReadRepository(db) -userService := users.NewUserService(userWriteRepo, userReadRepo) -userHandler := xrpc.NewUserHandler(userService) -``` - -## 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 - -### 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 - -## XRPC Implementation - -For detailed XRPC patterns and Lexicon examples, see [ATPROTO_GUIDE.md](./ATPROTO_GUIDE.md#xrpc). - -### 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 - -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. \ No newline at end of file + +## 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`) + + - 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`) + + - Implement to pass tests + - **Validate all inputs before processing** + - **Check permissions before operations** + - Handle transactions +5. **Repository Implementations** + + - **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`) + + - **Verify auth tokens/DIDs** + - Parse XRPC request + - Call service + - **Sanitize errors before responding** + +### Phase 3: Integration + +**WIRE IT UP:** + +- [ ] 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 + +### Every Feature MUST: + +- [ ] **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) + +### Red Flags to Avoid: + +- `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 + +## Quick Decision Guide + +### "Should I use X?" + +1. Does Indigo have it? → Use it +2. Can PostgreSQL + Go do it securely? → Build it simple +3. Requires external dependency? → Check Context7 first + +### "How should I structure this?" + +1. One domain, one package +2. Interfaces for testability +3. Services coordinate repos +4. Handlers only handle XRPC + +## Pre-Production Advantages + +Since we're pre-production: + +- **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** + +## Success Metrics + +Your code is ready when: + +- [ ] Tests pass (including security tests) +- [ ] Follows atProto patterns +- [ ] No security checklist items missed +- [ ] Handles errors gracefully +- [ ] Works end-to-end with auth + +## Quick Checks Before Committing + +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) + +Remember: We're building a working product. Perfect is the enemy of shipped. \ No newline at end of file