Raven ScottandCursor 2daaad3696 feat(platform): Phase 552 compliance span metadata (v0.8.515)
Extend audit.append with auditEntryCount, audit.export with exportLimit and
remainingExportCapacity, and compliance.snapshot with meshPushed and
archiveAppended for dev-log diagnostics.

Co-authored-by: Cursor <[email protected]>
2026-05-24 13:54:04 -04:00

pearcord-platform

Application facade: one PearcordPlatform class that wires identity, database, guild mesh, DMs, invites, voice, discovery, bots, and dozens of feature modules for the Pearcord desktop sidecar and companion.

Mission

Expose a single EventEmitter API the UI drives over IPC (apps/pearcord/index.js), while owning session lifecycle (onboarding, guild/DM mode, active channel), permission checks, gossip bridging, and lazy initialization of feature registries (emoji, stickers, automation, search, …).

When to use / not

Use when:

  • Building the Pearcord app, companion read-only client, or headless smoke that needs full session semantics.
  • You want one import instead of composing pearcord-identity + pearcord-guild + pearcord-message yourself.

Do not use when:

  • Writing a minimal library that only needs constants (pearcord-shared) or raw DB access (pearcord-db).
  • Implementing a single feature in isolation — prefer the focused module (pearcord-voice, pearcord-discovery, …) and optional thin adapter.

Public API

Export Role
PearcordPlatform Main facade (~9k lines)
USER_STATUS Re-exported from pearcord-shared for UI

Session & identity

Method / property Role
ready() identity.ready(), channel descriptions, invite/attachments bootstrap if onboarded
register({ username, displayName }) _bootstrapAfterUser
importIdentityBundle(bundle) Device pair restore
onboarded, identity, storagePath, db, dbPath Session state
listGuilds(), loadGuild(guildId), createGuild({ name, publicListing? }), joinInvite(code) Guild session

v0.8.431 (Phase 468): dm.open span.end includes group, peerUserId, participantCount; discovery.list span.end includes filterTag; contacts.accept span.end includes peerUserId; dm.open/contacts.accept/discovery.list error logs use error: key. Bundle: npm run test:phase468-discovery-dm. See DISCOVERY.md, CONTACTS.md.

v0.8.430 (Phase 467): voice.leave span end includes leavingIsStage; screen.start span end includes label. Bundle: npm run test:phase467-voice-stage. See VOICE.md, STAGE_CHANNELS.md.

v0.8.454 (Phase 491): automation.hook / automation.manifest / automation.hook.dryRun spans + errors. UI automation compositor + hints. Bundle: npm run test:phase491-automation. See AUTOMATION_EXPORT.md.

v0.8.453 (Phase 490): bot.token/bot.verify/bot.install/bot.message span metadata extend. UI bot compositor + hints. Bundle: npm run test:phase490-bots. See BOTS.md.

v0.8.452 (Phase 489): slash.register customCount; slash.delete deleted; slash.invoke replyLen/commandFound. UI slash compositor + hints. Bundle: npm run test:phase489-slash. See SLASH_COMMANDS.md.

v0.8.510 (Phase 547): DM spans extend — activeChannelId on dm.send and dm.group.create ends; split DMs panel compositor; clipboard dmNewDmModalBusy. Bundle: npm run test:phase547-dms. See DM.md.

v0.8.509 (Phase 546): Contacts spans extend — contacts.block/contacts.unblock spans; activeChannelId on request/accept/decline/block/unblock ends; split contacts panel compositor; contacts-errors includes block/unblock. Bundle: npm run test:phase546-contacts. See CONTACTS.md.

v0.8.508 (Phase 545): Discovery spans extend — discovery.prune span; activeChannelId on discovery.list/publish/mesh/prune ends; split discovery panel compositor; discovery-errors includes prune. Bundle: npm run test:phase545-discovery. See DISCOVERY.md.

v0.8.507 (Phase 544): Presence spans extend — presence.image span for activity artwork; activeChannelId on presence.set/presence.activity/presence.image ends; split presence panel compositor; presence-errors includes image. Bundle: npm run test:phase544-presence. See PRESENCE.md.

v0.8.506 (Phase 543): Soundboard spans extend — soundboard.stage span; activeChannelId on play/upload/delete/stage ends; upload panel guild-loading guard; soundboard-errors includes stage. Bundle: npm run test:phase543-soundboard. See SOUNDBOARD.md.

v0.8.505 (Phase 542): Sticker spans extend — sticker.create/sticker.delete/sticker.stage/sticker.send with spanKind + activeChannelId. UI split sticker compositor panels, lastStickerTileName, dedicated sticker-errors dev-log filter. Bundle: npm run test:phase542-stickers. See STICKERS.md.

v0.8.504 (Phase 541): Emoji spans extend — emoji.upsert ends with spanKind: 'emoji.create'; new emoji.delete + emoji.stage spans with activeChannelId. UI split emoji compositor panels, picker tile lastEmojiTileName, clipboard emoji busy flags. Bundle: npm run test:phase541-emoji. See EMOJI.md.

v0.8.503 (Phase 540): Automation spans extend — automation.hook/automation.manifest/automation.hook.dryRun/automation.dispatch end metadata (spanKind, activeChannelId). UI automation panel compositor wrappers, hook row lastAutomationHookRowId + roving tabindex, clipboard automation busy flags. Bundle: npm run test:phase540-automation. See AUTOMATION_EXPORT.md.

v0.8.502 (Phase 539): Bot spans extend — bot.install/bot.verify/bot.message/bot.token end metadata (spanKind, activeChannelId). UI bot panel compositor wrappers, bot row lastBotRowId + roving tabindex, clipboard bot busy flags. Bundle: npm run test:phase539-bots. See BOTS.md.

v0.8.501 (Phase 538): Slash spans extend — slash.register/slash.delete/slash.invoke end metadata (spanKind, activeChannelId). UI slash panel compositor wrappers, slash row lastSlashRowId + roving tabindex, clipboard slash busy flags. Bundle: npm run test:phase538-slash. See SLASH_COMMANDS.md.

v0.8.500 (Phase 537): Webhook spans extend — webhook.create/webhook.delete/webhook.execute end metadata (spanKind, activeChannelId). UI webhook panel compositor wrappers, webhook row lastWebhookRowId + roving tabindex, clipboard webhook busy flags. Bundle: npm run test:phase537-webhooks. See WEBHOOKS.md.

v0.8.451 (Phase 488): webhook.create channelType/hasAvatar; webhook.execute messageSent. UI webhook compositor split + hints. Bundle: npm run test:phase488-webhooks. See WEBHOOKS.md.

v0.8.450 (Phase 487): audit.append hasTarget/hasDetail; audit.export archived; compliance.snapshot signed/format. UI compliance compositor. Bundle: npm run test:phase487-compliance. See COMPLIANCE.md.

v0.8.449 (Phase 486): ban.member/kick.member reasonLen/removedMember; timeout.member durationMs; automod.evaluate blocked. UI moderation compositor split + hints. Bundle: npm run test:phase486-moderation. See MODERATION.md.

v0.8.448 (Phase 485): role.create nameLen; role.update renamed/permissionsChanged; role.delete existed; permission.overwrite allowNonZero/denyNonZero. UI roles compositor split + hints. Bundle: npm run test:phase485-roles. See ROLES.md.

v0.8.447 (Phase 484): thread.create fromMessage/hasRootMessage; thread.archive wasArchived/threadName; thread.read parentId/clearedMentions; forum.post titleLen/hasTags. UI threads compositor split + hints. Bundle: npm run test:phase484-threads. See MESSAGE_THREADS.md.

v0.8.482 (Phase 519): guild.search, guild.search.cache, and guild.search.mesh spans include spanKind on start/end. UI extends search compositor (recent chips, mesh chip, panel live region). Bundle: npm run test:phase519-search. See SEARCH.md.

v0.8.462 (Phase 499): guild.search.cache span on cache hits (scope/guildId); guild.search.mesh span after mesh merge (meshHitCount/localHitCount/mergedCount). UI split search guild-loading compositor. Bundle: npm run test:phase499-search. See SEARCH.md.

v0.8.483 (Phase 520): message.edit and message.delete spans include spanKind on start/end. UI extends messaging compositor (reply bar, slowmode pill, message list live region). Bundle: npm run test:phase520-messaging. See MESSAGING.md.

v0.8.463 (Phase 500): message.send replyToId; message.edit deltaLength; message.delete hadReactions/reactionCount. UI split messaging composer guild-loading compositor. Bundle: npm run test:phase500-messaging. See MESSAGING.md.

v0.8.484 (Phase 521): reaction.toggle and reaction.gossip spans include spanKind on start/end. UI extends reaction guild-loading compositor. Bundle: npm run test:phase521-reactions. See REACTIONS.md.

v0.8.464 (Phase 501): reaction.toggle added/hadMeBefore/gossipSent; reaction.gossip span on mesh fanout. UI syncReactionPanelsDuringGuildLoading. Bundle: npm run test:phase501-reactions. See REACTIONS.md.

v0.8.485 (Phase 522): attachment.stage, attachment.read, and embed.gossip spans include spanKind on start/end. UI extends attachment guild-loading compositor. Bundle: npm run test:phase522-attachments. See ATTACHMENTS.md.

v0.8.465 (Phase 502): attachment.stage filenameLen; attachment.read attachmentId/isImage; embed.gossip span; embed.resolve gossipSent. UI split attachment guild-loading compositor. Bundle: npm run test:phase502-attachments. See ATTACHMENTS.md, EMBEDS.md.

v0.8.490 (Phase 527): soundboard.play, soundboard.upload, and soundboard.delete spans include spanKind. UI split soundboard guild-loading compositor. Bundle: npm run test:phase527-soundboard. See SOUNDBOARD.md.

v0.8.489 (Phase 526): screen.start, screen.stop, and screen.capture spans include spanKind. UI split screen guild-loading compositor. Bundle: npm run test:phase526-screen. See SCREEN_SHARE.md.

v0.8.488 (Phase 525): stage.join and stage.speaker.* spans include spanKind. UI split stage guild-loading compositor. Bundle: npm run test:phase525-stage. See STAGE.md.

v0.8.487 (Phase 524): voice.join, voice.leave, and voice.mute spans include spanKind. UI split voice guild-loading compositor (syncVoiceBarPanelDuringGuildLoading, syncVoiceRosterPanelDuringGuildLoading, syncVoiceControlsPanelDuringGuildLoading). Bundle: npm run test:phase524-voice. See VOICE.md.

v0.8.486 (Phase 523): embed.resolve and embed.gossip spans include spanKind; resolve end adds fromCache. UI extends embed guild-loading compositor. Bundle: npm run test:phase523-embeds. See EMBEDS.md.

v0.8.466 (Phase 503): embed.resolve hasDescription; embed.gossip hasTitle/hasImage/urlHost. UI syncEmbedPanelsDuringGuildLoading. Bundle: npm run test:phase503-embeds. See EMBEDS.md.

v0.8.446 (Phase 483): guild.search includeMesh/queryLen; pin.list pinCount. UI search compositor split + hints. Bundle: npm run test:phase483-search. See SEARCH.md.

v0.8.445 (Phase 482): message.send hasAttachment/hasSticker; message.edit contentChanged; message.delete wasOwn. UI messaging compositor + hints. Bundle: npm run test:phase482-messaging. See MESSAGING.md.

v0.8.444 (Phase 481): attachment.stage byteLength/isImage; attachment.read fromLocal; embed.resolve urlLen. UI attachment compositor + clipboard staging meta. Bundle: npm run test:phase481-attachments. See ATTACHMENTS.md, EMBEDS.md.

v0.8.461 (Phase 498): inbox.mark-read unreadBefore/unreadAfter; inbox.mark-all remainingUnread; notification.open jumped/mode. UI syncInboxPanelsDuringGuildLoading + inbox-errors filter. Bundle: npm run test:phase498-inbox. See INBOX.md.

v0.8.471 (Phase 508): presence.set/presence.custom/presence.activity spans extended (previousStatus, statusChanged, customTextLength, spanKind, activityName, hasAssets). Split presence guild-loading compositor + clipboard presence meta. Bundle: npm run test:phase508-presence.

v0.8.473 (Phase 510): role.update adds previousPermissionMask/permissionMaskDelta; permission.overwrite adds replacedExisting/allowChanged/denyChanged; permission.eval after overwrite upsert; permission.overwrite.delete adds hadAllow/hadDeny. UI syncPermissionsPanelsDuringGuildLoading. Bundle: npm run test:phase510-permissions.

v0.8.472 (Phase 509): discovery.list adds queryLen/sort/minMembers; discovery.publish adds beforeListed/changed; discovery.mesh adds hadCachedStats. UI split discovery guild-loading compositor + clipboard discovery meta. Bundle: npm run test:phase509-discovery.

v0.8.460 (Phase 497): presence.set/presence.custom/presence.activity spans (peerCount, activityType, guildGossip). UI presence compositor + tightened presence-errors filter. Bundle: npm run test:phase497-presence. See PRESENCE.md.

v0.8.470 (Phase 507): soundboard.play pcmBytes/peerCountInChannel/wasGossip; soundboard.upload replacedExisting; soundboard.delete hadAttachment. Split soundboard guild-loading compositor in UI. Bundle: npm run test:phase507-soundboard. See SOUNDBOARD.md.

v0.8.459 (Phase 496): soundboard.play framesQueued/remote/listenOnly; soundboard.upload soundCount/durationMs; soundboard.delete remainingSounds. UI soundboard compositor + soundboard-errors filter. Bundle: npm run test:phase496-soundboard. See SOUNDBOARD.md.

v0.8.469 (Phase 506): screen.start peerCountInChannel/displayActive; screen.capture frameBytes/captureSource; screen.stop captureSourceWas/hadCaptureLogged. Split screen guild-loading compositor in UI. Bundle: npm run test:phase506-screen. See SCREEN_SHARE.md.

v0.8.458 (Phase 495): screen.start captureSource/activeShareCount; screen.stop remainingShares; screen.capture on first display frame. UI screen compositor + screen-errors filter. Bundle: npm run test:phase495-screen. See SCREEN_SHARE.md.

v0.8.457 (Phase 494): stage.join channelType/peerCount; stage.speaker.request pendingCount; stage.speaker.approve approvedFromPending/speakerCount; stage.speaker.demote demotedFromSpeaker. UI stage compositor + stage-errors filter. Bundle: npm run test:phase494-stage. See STAGE.md.

v0.8.468 (Phase 505): stage.join joinedFresh/switchedStage/wasInVoice; stage.speaker.request wasAudience; stage.speaker.demote speakerCountAfter/pendingCountAfter. UI split stage guild-loading compositor. Bundle: npm run test:phase505-stage. See STAGE.md.

v0.8.467 (Phase 504): voice.join listenOnlyRequested/joinedFresh; voice.leave peerCountAtLeave; voice.mute span + error log (wasMuted/wasDeafened). UI voice-errors filter: voice.join|leave|mute. Bundle: npm run test:phase504-voice. See VOICE.md.

v0.8.456 (Phase 493): voice.join channelType/peerCount; voice.leave listenOnlyWas; stage.join isModerator; screen.start wasSharing/listenOnly; screen.stop hadActive. Split voice guild-loading compositor. Bundle: npm run test:phase493-voice. See VOICE.md.

v0.8.443 (Phase 480): voice.join wasInVoice/switchedChannel; voice.leave hadMedia; screen.start hasLabel; screen.stop stopped. UI voice compositor. Bundle: npm run test:phase480-voice. See VOICE.md.

v0.8.455 (Phase 492): discovery.publish on setGuildDiscoveryListing (hasInvite, memberCount); discovery.mesh on stats refresh; discovery.list logs cache hits (fromCache: true); discovery.join span.fail. UI split discovery compositor + discovery-errors dev-log filter. Bundle: npm run test:phase492-discovery. See DISCOVERY.md.

v0.8.442 (Phase 479): discovery.list meshLive/filteredCount; discovery.join on joinPublicDiscovery; dm.open reusedChannel; contacts.accept accepted. UI discovery compositor. Bundle: npm run test:phase479-discovery. See DISCOVERY.md.

v0.8.441 (Phase 478): notification.markRead wasUnread/hadMessageId; notification.markAll unreadBefore; notification.open hasMessageId; inbox.mark-read clearedInboxCount. UI syncNotificationPanelsDuringGuildLoading compositor. Bundle: npm run test:phase478-notifications. See NOTIFICATIONS.md, INBOX.md.

v0.8.440 (Phase 477): message.send hasMention/isSlash; message.edit priorLength; message.delete hadAttachments; reaction.toggle emojiLength; pin.message/pin.unpin span start channelId. UI syncMessagingPanelsDuringGuildLoading. Bundle: npm run test:phase477-messaging. See MESSAGING.md, REACTIONS.md.

v0.8.439 (Phase 476): attachment.stage span.end adds mimeType; attachment.read adds filename/mimeType; embed.resolve adds hasTitle/hasImage. UI syncAttachmentsPanelsDuringGuildLoading. Bundle: npm run test:phase476-attachments-embeds. See ATTACHMENTS.md, EMBEDS.md.

v0.8.429 (Phase 466): attachment.read span.end includes byteLength; embed.resolve includes urlHost. Bundle: npm run test:phase466-attachments-embeds. See ATTACHMENTS.md, EMBEDS.md.

v0.8.428 (Phase 465): message.send/message.edit span.end includes contentLength; reaction.toggle includes removed. Bundle: npm run test:phase465-messaging-reactions. See MESSAGING.md, REACTIONS.md.

v0.8.427 (Phase 464): role.update span.end includes name; permission.overwrite* span.end includes targetType. Bundle: npm run test:phase464-roles-permissions. See ROLES.md, PERMISSIONS.md.

v0.8.426 (Phase 463): audit.append, audit.export (end metadata), compliance.snapshot spans with guildId. Bundle: npm run test:phase463-audit-compliance. See AUDIT.md, COMPLIANCE.md.

v0.8.425 (Phase 462): automod.evaluate on send; ban.member/kick.member/timeout.member re-verified with guildId. Bundle: npm run test:phase462-moderation-automod. See MODERATION.md, AUTOMOD.md.

v0.8.424 (Phase 461): slash.invoke span includes guildId/channelId; bot.message + bot.install spans with guildId. Bundle: npm run test:phase461-slash-bots. See SLASH_COMMANDS.md, BOTS.md.

v0.8.423 (Phase 460): embed.resolve span uses active guildId/channelId fallback; attachment.stage/attachment.read re-verified. Bundle: npm run test:phase460-attachments-embeds. See ATTACHMENTS.md, EMBEDS.md.

v0.8.422 (Phase 459): emoji.upsert + sticker.create spans include guildId + span.fail; reaction.toggle error log includes guildId/channelId (re-verified). Bundle: npm run test:phase459-reactions-emojis. See REACTIONS.md, EMOJI.md.

v0.8.421 (Phase 458): channel.delete span + gossip; thread.create, thread.archive, forum.post spans include guildId + span.fail. Bundle: npm run test:phase458-threads-forum. See MESSAGE_THREADS.md, FORUM_CHANNELS.md.

v0.8.420 (Phase 457): channel.create, channel.update (rename), and category.create spans + error logs with guildId. create-channel IPC selects new channel and emits composer focus. Bundle: npm run test:phase457-channels. See CHANNELS.md.

v0.8.419 (Phase 456): session.startup span ends with guildCount + guildId; session.startup error log. identity.register span + identity.register error. guild.open span end includes guildId. loadGuild / createGuild bust search cache and clear _lastSearchFromCache. Bundle: npm run test:phase456-onboarding-session. See SESSION.md.

v0.8.474 (Phase 511): Onboarding spans — identity.register (usernameLen, displayNameLen, hadGuilds), guild.create (channelCount, isFirstGuild, listOnDiscovery), invite.join (inviteLen, guildId) + matching error logs. UI syncOnboardingPanelsDuringGuildLoading, #composer-onboarding-hint, dev-log onboarding-errors + clipboard meta. Bundle: npm run test:phase511-onboarding. See ONBOARDING.md.

v0.8.475 (Phase 512): Settings spans — settings.prefs (patchKeyCount, themeChanged), notifications.prefs, guild.update / settings.guild (nameChanged, iconChanged, nsfwGateChanged). UI syncSettingsPanelsDuringGuildLoading, #composer-settings-hint, dev-log settings-errors + clipboard meta. Bundle: npm run test:phase512-settings. See SETTINGS.md.

v0.8.476 (Phase 513): Guild-admin spans — channel.create (nameLen, hadParent), channel.delete (wasActive, channelType), channel.reorder (updatedCount, channelCount). UI syncGuildAdminPanelsDuringGuildLoading, #composer-guild-admin-hint, dev-log guild-admin-errors + clipboard meta. Bundle: npm run test:phase513-guild-admin. See CHANNELS.md.

v0.8.479 (Phase 516): Contact spans — contacts.request (peerDisplayNameLen), contacts.accept (accepted), contacts.decline (declined). UI syncContactsPanelsDuringGuildLoading, contacts-errors filter, clipboard contacts meta. Bundle: npm run test:phase516-contacts. See CONTACTS.md.

v0.8.478 (Phase 515): DM spans — dm.open (spanKind, reusedChannel, peerDisplayNameLen), dm.send (when guildId === dm), dm.group.create (participantCount, nameLen). UI syncDmsPanelsDuringGuildLoading, tightened dm-errors, clipboard DM meta. Bundle: npm run test:phase515-dms. See DM.md.

v0.8.499 (Phase 536): Compliance/audit spans extend — audit.append/audit.export/compliance.snapshot end metadata (spanKind, activeChannelId). UI compliance panel compositor wrappers, audit row lastAuditRowId + roving tabindex, clipboard compliance busy flags. Bundle: npm run test:phase536-compliance. See COMPLIANCE.md, AUDIT_LOG.md.

v0.8.515 (Phase 552): Compliance spans extend — audit.append auditEntryCount, audit.export exportLimit/remainingExportCapacity, compliance.snapshot meshPushed/archiveAppended. UI split compliance panels; .compliance-panel-live. Bundle: npm run test:phase552-compliance. See COMPLIANCE.md.

v0.8.514 (Phase 551): Moderation spans extend — kick.member memberCount, timeout.member activeTimeoutCount, automod.setConfig keywordCount/blockInvites. UI split automod/ban/member panels; .moderation-panel-live/.moderation-modal-live; clipboard moderationAutomodTogglesBusy/moderationTimeoutModalBusy. Bundle: npm run test:phase551-moderation. See MODERATION.md.

v0.8.498 (Phase 535): Moderation/automod spans extend — ban.member/kick.member/timeout.member/automod.* end metadata (spanKind, activeChannelId). UI moderation panel compositor wrappers, ban/audit aria-current=location, clipboard moderation busy flags. Bundle: npm run test:phase535-moderation. See MODERATION.md.

v0.8.513 (Phase 550): Role/permission spans extend — customRoleCount on update/delete, channelOverwriteCount/remainingOverwriteCount. UI split role list + channel perm panels; .roles-panel-live/.roles-modal-live; clipboard rolesListChromeBusy/rolesChannelPermControlsBusy. ui-flow composer focus after delete/reorder role. Bundle: npm run test:phase550-roles. See ROLES.md.

v0.8.497 (Phase 534): Role/permission spans extend — role.create/update/delete/reorder and permission.overwrite* end metadata (spanKind, activeChannelId, customRoleCount). UI roles panel compositor wrappers, role/overwrite aria-current=location, clipboard roles busy flags. Bundle: npm run test:phase534-roles. See ROLES.md.

v0.8.512 (Phase 549): Guild-admin spans extend — channel.create channelCount, channel.delete remainingChannelCount, channel.reorder requestedCount/reorderedAll. UI split channel list chrome/rows + category panels; .guild-admin-modal-live; clipboard guildAdminChannelModalBusy. ui-flow composer focus after rename-channel/create-category. Bundle: npm run test:phase549-guild-admin. See CHANNELS.md.

v0.8.496 (Phase 533): Guild-admin spans extend — channel.create/delete/reorder end metadata (spanKind, activeChannelId, createdChannelId). UI panel compositor wrappers, channel row aria-current=location, clipboard guild-admin busy flags. Bundle: npm run test:phase533-guild-admin. See CHANNELS.md.

v0.8.511 (Phase 548): Invite spans extend — guild.invite (maxUses, activeChannelId), invite.revoke (activeChannelId). UI split hub buttons/banner + join/invite modal panels, .invite-modal-live, clipboard invitesJoinModalBusy/invitesInviteModalBusy. ui-flow composer focus after create-invite. Bundle: npm run test:phase548-invites. See INVITES.md.

v0.8.495 (Phase 532): Invite spans extend — guild.invite (spanKind, hasShareCode), invite.join (activeChannelId, guildCount, spanKind), invite.revoke (spanKind). UI split compositor (syncInvitesHubPanelDuringGuildLoading, syncInvitesJoinPanelDuringGuildLoading, syncInvitesInvitePanelDuringGuildLoading), invite code aria-current=location, clipboard invitesHubBusy/invitesHubButtonsBusy/inviteCopyBusy. Bundle: npm run test:phase532-invites. See INVITES.md.

v0.8.477 (Phase 514): Invite spans — guild.invite (shareCodeLen, channelCount), invite.join (inviteGuildId, portable), invite.revoke (revoked). UI syncInvitesPanelsDuringGuildLoading, #composer-invite-hint, dev-log invite-errors + clipboard meta. Bundle: npm run test:phase514-invites. See INVITES.md.

Messaging & channels

Method Role
selectChannel(channelId) Sets messages.setChannel, read state, voice context
sendMessage, editMessage, deleteMessage Delegates to PearcordMessage + guild/DM gossip
signalTyping, toggleReaction Gossip + local state
createCategory, createForumPost, thread helpers PearcordGuild + permissions

Invites & discovery

Method Role
createInvite() PearcordInvite#create with channel snapshot
joinInvite(code) Resolve + guild.joinByInvite + mesh; pearcord://explore?…navigateDeepLink (v0.8.108)
markOnboardingExploreShown() Clear first-guild Explore prompt; onboardingExploreShown pref (v0.8.108)
setThreadsPanelFilter(filter) Filter chips; persists threadsPanelFilter in USER_PREFS (v0.8.108)
formatDiscoveryExploreBaselineDeepLink() pearcord://explore?baseline&at share URL (v0.8.107)

Voice & media

Method Role
joinVoice, leaveVoice, setVoiceMute pearcord-voice + guild gossip
setVoiceRosterChannelExpanded(channelId, expanded) USER_PREFS voiceRosterExpandedChannelIds (v0.8.110)
setDiscoveryFilter({ query, sort, minMembers, tag }) Persists discoveryExploreFilter in USER_PREFS (v0.8.111)
Voice/s screen share ingest VoiceMediaHub, ScreenShareHub wired in _openGuild

Views & companion

Property Role
mode 'home', 'guild', or DM mode
guildBans Ban rows for active guild (@pearcord/bans, limit 200) (v0.8.409)
getViewState() / snapshot builders IPC state payloads for UI
_readOnly, _companionMode Companion restrictions

Hundreds of additional methods cover moderation, bots, search, audit export, forums, stage, automod, boosts, settings mesh, contacts, notifications — each delegating to the matching pearcord-* package.

P2P surface

Platform does not implement wire protocols; it orchestrates:

Subsystem Module Topic / protocol
Guild gossip pearcord-guild pearcord-gossip-v1 on guild.topic
DM mesh pearcord-dm dmTopic / group DM topics
Contacts pearcord-contacts contactsTopic
Settings / prefs pearcord-settings userPrefsTopic
Discovery pearcord-discovery Public listings mesh
Device pair pearcord-device-sync deviceSyncTopic + DEVICE_PAIR_* RPC

_wireGuild(guild) registers setMessageHandler and forwards guild events to platform.emit (message, member, guild-sync, voice, …).

Storage

Path Owner
{storagePath}/db/ LocalDatabase — all @pearcord/* collections
{storagePath}/identity/ pearcord-identity keypairs
{storagePath}/dm-meta/ JsonStore for DM metadata
Feature dirs attachments, discovery JSON, moderation, voice state, etc.

storagePath from resolveStoragePath() (PEARCORD_STORAGE or ~/.config/pearcord).

Platform integration (core batch A wiring)

// Constructor (simplified)
this.db = new LocalDatabase(this.dbPath)
this.identity = new PearcordIdentity({ db, storagePath })
// ready() → invite, attachments, discovery when snap.user

// Guild open (_openGuild)
this.guild = new PearcordGuild({ ownerId, userId, db })
this._wireGuild(this.guild)
await this.guild.joinMesh()
this.messages = new PearcordMessage({ authorId: user.id, db })
Dependency Role in platform
pearcord-db Shared this.db
pearcord-identity this.identity, storage paths
pearcord-guild Active server + mesh
pearcord-message this.messages per channel
pearcord-invite this.invite
pearcord-shared Permissions, COLLECTIONS, deep links, id, now

pearcord-channel and pearcord-sync are not dependencies.

UI / IPC

Primary consumer: apps/pearcord/index.js sidecar reading pear-pipe JSON.

Direction Examples
UI → platform register, create-guild, join-invite, select-guild, select-channel, send-message, join-voice

v0.8.115: loadGuild / _openGuild clears stale activeChannelId, then _selectGuildChannelWithUnread picks mention-weighted unread channels before falling back to the first text channel. setSearch records searchRecentByGuild in user prefs. View: searchRecentQueries.

v0.8.397: ingestInboundGuildMessage(msg) processes guild mesh gossip while on home (mention alerts + inbox via DB member lookup). markAllNotificationsRead / openNotificationTarget / markChannelRead log structured errors. Smoke helper: simulateGuildMessage(platform, msg) in smoke-runner.cjs.

v0.8.398: parseSearchQuery + _applySearchQueryFilters for guild/channel search; pinMessage/unpinMessage log pin.message/pin.unpin spans and errors; guild.search error on search failure.

v0.8.399: banMember/kickMember/timeoutMember log ban.member/kick.member/timeout.member spans and errors; exportAuditLog logs audit.export span and audit.export error.

v0.8.400: createChannelWebhook/deleteChannelWebhook/executeChannelWebhook log webhook.* spans and errors; registerSlashCommands/deleteSlashCommand/executeSlashCommand log slash.* spans and errors.

v0.8.411 (Phase 448): Webhook and slash spans/errors include guildId metadata; webhook.execute span end includes channelId. Dev-log webhook-errors filter: webhook.create|delete|execute; slash-errors: slash.register|delete|invoke. Integrations UI permission toasts; webhook token modal autofocus. Bundle: npm run test:phase448-webhooks-integrations.

v0.8.412 (Phase 449): sendMessage/editMessage/deleteMessage/toggleReaction spans/errors include guildId + channelId; _assertCanParticipate('send'|'reaction') for distinct read-only errors. Dev-log message-errors filter: message.send|edit|delete; reaction-errors: reaction.toggle. UI: message toolbar + reaction pill keyboard roving, composer autofocus after reaction, permission toasts. Bundle: npm run test:phase449-messaging-reactions.

v0.8.413 (Phase 450): stageAttachment/readAttachmentPreview/embed.resolve spans/errors include guildId + channelId; _assertCanParticipate('attach') for read-only attach gate. Dev-log attachment-errors filter: attachment.stage|read; embed-errors: embed.resolve. UI: staging bar + btn-attach aria-busy, attach-files permission toast, staging/embed keyboard roving, empty-state hints. Bundle: npm run test:phase450-attachments-media.

v0.8.431 (Phase 468): openDM span.end adds group: false + peerUserId; openGroupDM span.end adds group: true + participantCount; discovery.list span.end adds filterTag; acceptFriendRequest span.end re-verifies peerUserId. Error logs use error: key (not err:). Dev-log clipboard: discoveryFilterTag, discoveryMeshLive, dmChannelCount, contactsPendingCount. UI: syncDiscoveryPanelsDuringGuildLoading, #composer-dm-hint, 🔒 E2E badge. Bundle: npm run test:phase468-discovery-dm.

v0.8.415 (Phase 452): openDM/openGroupDM log dm.open spans + dm.open error; sendFriendRequest/acceptFriendRequest log contacts.request/contacts.accept spans; _buildView logs discovery.list span + discovery.list error. Dev-log dm-errors filter: dm.open|group, contacts.request|accept|decline, discovery.list. UI-flow emits composer-focus-channel after DM open. Bundle: npm run test:phase452-discovery-dm.

v0.8.414 (Phase 451): joinVoiceChannel/leaveVoiceChannel/joinStageChannel/requestStageSpeak/approveStageSpeaker/demoteStageSpeaker/startScreenShare/stopScreenShare spans/errors include guildId + channelId. Dev-log voice-errors filter: voice.join|leave, stage.join|speaker.*, screen.start|stop. UI: voice/stage bars + channel list aria-busy, stage connect/speak permission toasts, stage approve keyboard, stream popout autofocus. Bundle: npm run test:phase451-voice-stage.

v0.8.402: sendMessage/editMessage/deleteMessage log message.* spans and errors; toggleReaction logs reaction.toggle span and reaction.toggle error on failure.

v0.8.403: stageAttachment/readAttachmentPreview log attachment.* spans and errors; _queueLinkEmbed logs embed.resolve span and embed.resolve error on failure.

v0.8.436 (Phase 473): role.create/role.update span.end adds hasColor and permissionMask; permission.overwrite adds allowBits/denyBits; permission.overwrite.delete adds deleted. UI syncRolesPanelsDuringGuildLoading + composer roles hint. Smokes: test:platform-role-span-metadata-extend, test:platform-permission-span-metadata-extend. Bundle: test:phase473-roles-permissions.

v0.8.439 (Phase 476): attachment.stage/attachment.read/embed.resolve span.end metadata extend (mimeType, filename, hasTitle, hasImage). UI syncAttachmentsPanelsDuringGuildLoading. Smokes: test:platform-attachment-span-metadata-extend, test:platform-embed-span-metadata-extend. Bundle: test:phase476-attachments-embeds.

v0.8.438 (Phase 475): slash.invoke span.end adds argCount/ephemeral/builtin; bot.install adds botName/permissionMask; bot.message adds contentLength/botId. UI syncSlashBotsPanelsDuringGuildLoading + ensureComposerContextHints + #composer-slash-hint. IPC bot-installed/slash-invoked. Bundle: test:phase475-slash-bots.

v0.8.437 (Phase 474): webhook.create span.end adds webhookName/channelId; webhook.delete adds deleted; webhook.execute adds contentLength/hasUsername; slash.register adds firstName. UI syncWebhooksPanelsDuringGuildLoading + composer webhooks hint. Smokes: test:platform-webhook-span-metadata-extend, test:platform-slash-span-metadata-extend. Bundle: test:phase474-webhooks-integrations.

v0.8.435 (Phase 472): ban.member/kick.member span end adds hasReason; ban.member adds banCount; timeout.member adds until; audit.export adds signed. UI extended syncModerationPanelsDuringGuildLoading + composer moderation hint. Smokes: test:platform-moderation-span-metadata-extend, test:dev-log-moderation-errors-tighten, test:dev-log-audit-errors-tighten. Bundle: test:phase472-moderation-audit.

v0.8.434 (Phase 471): thread.create span end adds parentType/threadName; thread.archive adds parentId; forum.post adds parentChannelId/messageId. Error logs use error: key. UI syncThreadsPanelsDuringGuildLoading + composer thread hint + thread-read live extend. Smokes: test:platform-thread-span-metadata-extend, test:dev-log-thread-errors-tighten. Bundle: test:phase471-threads-forum.

v0.8.433 (Phase 470): guild.search span end adds meshHitCount/localHitCount/mesh per scope; pin.message/pin.unpin span end adds messageId + pinCount. UI syncSearchPanelsDuringGuildLoading + composer search hint. Smokes: test:platform-search-span-metadata-extend, test:dev-log-search-errors-tighten, test:dev-log-pin-errors-tighten. Bundle: test:phase470-search-pins.

v0.8.432: notification.markRead span end adds kind + read; notification.markAll adds inboxCount; notification.open adds messageId; inbox.mark-read/thread.read span end adds threadRead + isDm. Error logs use error: key. UI syncNotificationPanelsDuringGuildLoading + composer notif hint + thread-read live region. Smokes: test:platform-notification-span-metadata-extend, test:dev-log-notification-errors-tighten, test:dev-log-inbox-errors-tighten. Bundle: test:phase469-notifications-inbox.

v0.8.416: openNotificationTarget span end includes guildId; markAllNotificationsRead / markNotificationRead / markChannelRead (inbox.mark-read or thread.read) span end includes guildId + channelId. Smoke: test:platform-notification-span-guild-id. Bundle: test:phase453-notifications-inbox.

v0.8.407: markNotificationRead(notificationId) wraps inbox markRead with notification.markRead span + notification.markRead error. markAllNotificationsRead span ends with count + guildId. markChannelRead logs inbox.mark-read span + error for non-thread channels (thread channels use thread.read). Smokes: test:notification-platform-errors, test:inbox-platform-errors. Bundle: test:phase444-notifications-inbox.

v0.8.409 (Phase 446): banMember/kickMember/timeoutMember spans include guildId metadata on start and error lines; exportAuditLog span includes guildId, format, filter; setAutomodConfig logs automod.setConfig span + automod.setConfig error. View snapshot adds guildBans (db.find(COLLECTIONS.BANS, { guildId }, { limit: 200 })). Bundle: test:phase446-moderation-audit.

v0.8.410 (Phase 447): createGuildCustomRole/updateGuildCustomRole/deleteGuildCustomRole/reorderGuildCustomRoles/setMemberCustomRoles spans and role.* error lines include guildId; upsertChannelPermissionOverwrite/deleteChannelPermissionOverwrite spans and permission.overwrite*.error include guildId, channelId, targetType, targetId. View snapshot adds channelPermsByChannel (effective viewChannel/sendMessages per channel) and channelOverwriteCount (guild overwrite row count). Bundle: test:phase447-roles-permissions.

v0.8.418: updateGuildSettings logs guild.update span + guild.update error; createInvite logs guild.invite span + guild.invite error (both include guildId). Smoke: test:platform-guild-settings-span-guild-id. Bundle: test:phase455-guild-settings.

v0.8.417: guild.search spans include guildId on start/end (endSearchSpan helper); pin.message/pin.unpin spans include guildId. Smoke: test:platform-search-span-guild-id. Bundle: test:phase454-search-pins.

v0.8.408: markChannelRead calls _invalidateSearchCache() when the channel is a thread (thread.read path) so search results refresh after thread read. Smoke: test:search-cache-invalidate-thread-read. Bundle: test:phase445-search-inbox.

v0.8.406: createThread/createForumPost/markChannelRead (thread channels)/archiveThread log thread.create/forum.post/thread.read/thread.archive spans and errors; audit entries for thread.create, forum.post.create, thread.archive, and thread.unarchive. Smoke: test:thread-platform-errors.

v0.8.401: createGuildCustomRole/updateGuildCustomRole/deleteGuildCustomRole/reorderGuildCustomRoles/setMemberCustomRoles log role.* spans and errors; upsertChannelPermissionOverwrite/deleteChannelPermissionOverwrite log permission.overwrite* spans and errors.

v0.8.252: Guild search mesh skipped when guildOpenNoViewableChannels; emoji batch prefetch skips negative miss TTL; sticker gossip prefetch gated on attachments; session.startup logs INFO when initial guild mesh flush was bounded. Smoke: test:guild-search-no-viewable-skip, test:sticker-prefetch-attachments-ready.

v0.8.248: Guild open filters unread/last picks with _canViewChannel; sets guildOpenNoViewableChannels when none accessible; defers _ensureGuildModules via setTimeout(0) after channel pick; emoji mesh prefetch clears miss cache on success. Smokes: test:guild-viewable-channel-pick, test:guild-open-no-viewable, test:guild-plain-beats-dual-combo.

v0.8.158: Between two mention+plain parent buckets, the category with the higher combined score wins on guild open. Smoke: test:guild-dual-combo-buckets-highest.

v0.8.157: A heavy plain-only parent bucket whose total exceeds three lower mention+plain combo categories wins on guild open. Smoke: test:guild-plain-beats-triple-combo.

v0.8.156: Among three mention+plain parent buckets, the category with the highest combined score wins on guild open. Smoke: test:guild-triple-combo-buckets-highest.

v0.8.155: Among three plain-only parent buckets, the category with the highest combined unread total wins on guild open. Smoke: test:guild-triple-plain-bucket-pick.

v0.8.154: A mention+plain parent bucket whose combined score exceeds a higher plain-only bucket wins on guild open. Smoke: test:guild-combo-beats-plain-bucket.

v0.8.153: When plain-only and mention+plain parent buckets tie on total score, the bucket with the lower minimum channel position wins. Smoke: test:guild-plain-combo-minpos-tie.

v0.8.152: Between two plain-only parent buckets, the category with the higher plain-unread total wins on guild open. Smoke: test:guild-dual-plain-bucket-pick.

v0.8.151: A plain-only parent bucket whose unread total exceeds a lighter mention+plain combo bucket wins on guild open (e.g. 1004 plain vs 1003 combo). Smoke: test:guild-plain-beats-combo-bucket.

v0.8.150: Among three mention+plain parent buckets, the category with the highest combined score wins on guild open. Smoke: test:guild-triple-combo-bucket-pick.

v0.8.149: Equal mention+plain parent-bucket totals tie-break to the bucket whose unread channels have the lower minimum position. Smoke: test:guild-combo-bucket-minpos-tie.

v0.8.148: A mention+plain category bucket beats a plain-only bucket when both have the same plain-unread count (mention adds 1000 to bucket total). Smoke: test:guild-combo-beats-plain-equal.

v0.8.147: Between two categories each with mention+plain unread, the parent bucket with the higher combined score wins. Smoke: test:guild-dual-combo-bucket-pick.

v0.8.146: A category bucket with one mention plus plain unread (score 1001 + n) beats a higher plain-only bucket in another category. Smoke: test:guild-mention-plain-bucket-pick.

v0.8.145: A root (uncategorized) bucket whose plain-unread total exceeds 1000 beats two mention-weighted category buckets. Smoke: test:guild-root-plain-triple-mention-pick.

v0.8.144: Among three parent buckets, a mention-weighted bucket (score 1001+) beats two higher plain-unread buckets. Smoke: test:guild-mention-triple-bucket-pick.

v0.8.143: Three parent buckets with equal unread totals pick the bucket with the lowest minPos. Smoke: test:guild-bucket-minpos-triple-pick.

v0.8.142: When two parent buckets share equal unread totals and equal minPos, the first bucket encountered in channel scan order wins. Smoke: test:guild-bucket-minpos-order-pick.

v0.8.141: Equal parent-bucket unread totals across root and categories tie-break to the bucket with the lower minimum channel position. Smoke: test:guild-bucket-minpos-root-pick.

v0.8.140: removeSearchRecent(query) drops a guild-scoped recent search from searchRecentByGuild. A mention in the root bucket beats higher plain unread in a category. Smokes: test:search-recent-remove, test:guild-root-mention-bucket-pick.

v0.8.139: A mention-weighted category bucket beats higher plain unread in the root (uncategorized) bucket. Smoke: test:guild-mention-root-bucket-pick.

v0.8.138: With unread in both root and category buckets, parent-bucket totals decide the pool (highest score wins). Smoke: test:guild-mixed-bucket-pick.

v0.8.137: When unread exists in only one category among several, selection stays inside that category pool. Smoke: test:guild-dual-category-isolation-pick.

v0.8.136: In a single-category unread pool, mention-weighted score (mentions × 1000 + unread) beats a lower-position channel with equal plain unread. Smoke: test:guild-mention-single-category-pick.

v0.8.135: When all unread channels share one parentId, _selectGuildChannelWithUnread limits the pool to that category before position/score tie-breaks. Smoke: test:guild-single-category-pool.

v0.8.134: Uncategorized (parentId empty) and category buckets compete by total unread score; uncategorized totals can beat a lower-total category. Smoke: test:guild-uncategorized-bucket-pick.

v0.8.133: Parent-bucket selection prefers the highest total unread score over a bucket with a lower minPos but fewer unreads. Smoke: test:guild-bucket-total-over-minpos.

v0.8.132: Within one category, a channel with a mention alert beats another with the same plain unread count (mentions × 1000 + unread). Smoke: test:guild-mention-in-bucket-pick.

v0.8.131: Within a single parent bucket, equal unread/mention scores pick the channel with the lower sibling position. Smoke: test:guild-in-bucket-position-tie.

v0.8.130: Equal parent-bucket unread totals break ties by lower minimum channel position among unread rows in that bucket (sibling index within the parent). Smoke: test:guild-bucket-minpos-tie.

v0.8.126: Parent bucket totals use mention-weighted scores (mentions × 1000 + unread), so a mention category beats higher plain unread elsewhere. Smoke: test:guild-mention-bucket-pick.

v0.8.125: _selectGuildChannelWithUnread picks the parent bucket (category or uncategorized) with the highest total unread score, then best channel within it. Smokes: test:guild-category-bucket-pick, test:guild-category-unread-pick.

v0.8.124: _unreadForChannel filters rows by channelId after HyperDB find. When all unread are in one category, _selectGuildChannelWithUnread picks only within that parentId. Smoke: test:guild-category-unread-pick.

v0.8.123: Equal unread/mention scores tie-break to lower channel.position. Smoke: test:guild-unread-position-tie.

v0.8.120: _selectGuildChannelWithUnread scores mentions × 1000 + unread; mention channel beats higher plain unread. Smoke: test:guild-mention-channel-pick.

v0.8.119: _selectGuildChannelWithUnread order: highest unread/mention score → lastChannelByGuild → first text. Smokes: test:guild-unread-over-last-channel, test:guild-unread-channel-jump.

v0.8.116: selectChannel persists savable channel types via _persistLastGuildChannel. Smoke: npm run test:guild-last-channel-prefs.

v0.8.237: View exposes sessionReady, guildLoading, guildLoadingGuildId, sessionError. Emits user-prefs and guild-loading for UI pushState. openHome() cancels in-flight loadGuild.

| platform → UI | state, message, channel, notification, session-ready, user-prefs, guild-loading, user |

See IPC.md for the full message table. UI should not import guild/message modules directly in production builds.

Tests

Most apps/pearcord/scripts/smoke-*.cjs scripts construct PearcordPlatform with a temp storagePath. Runner: npm run test:smoke / smoke-runner.cjs.

Representative targets:

  • Guild/invite: test:guild-sync, test:invite, test:storage-path
  • Mesh RPC: smoke-voice-speaking-mesh.cjs, smoke-guild-sync.cjs
  • HyperDB: test:hyperdb (db only, but platform uses same path layout)

Code example

const path = require('bare-path')
const os = require('bare-os')
const { PearcordPlatform } = require('pearcord-platform')

const storagePath = path.join(os.tmpdir(), `pearcord-demo-${Date.now()}`)
const platform = new PearcordPlatform({ storagePath })

await platform.ready()
await platform.register({ username: 'demo', displayName: 'Demo' })

const { guild } = await platform.createGuild({ name: 'Test Server' })
await platform.sendMessage('Hello from the facade')

const view = platform.getViewState?.() ?? { mode: platform.mode, guildId: guild.id }
console.log(view)

await platform.close?.()

Minimal sidecar-style usage:

platform.on('message', (msg) => { /* push to UI */ })
platform.on('session-ready', () => { /* enable guild list */ })
platform.on('user-prefs', () => pushState()) // theme/density mesh sync (v0.8.237)
platform.on('guild-loading', () => pushState()) // guild switch overlay

View fields sessionReady, guildLoading, guildLoadingGuildId, sessionError (v0.8.237). UI must not show the main shell until sessionReady is true.

v0.8.174: updateGuildSettings({ name, iconHash, nsfwGateEnabled }) persists server-wide age-restricted gate toggle on guild record. View exposes guild.nsfwGateEnabled.

Repository

Part of Pearcord.

  • Org: pearcord
  • Clone: git clone https://git.ssh.surf/pearcord/pearcord-platform.git
  • Install: npm install git+https://git.ssh.surf/pearcord/pearcord-platform.git#main

Depends on the full Pearcord module workspace (see package.json); install from monorepo modules/ for local development.

S
Description
Pearcord module: pearcord-platform
Readme
105 MiB
Languages
JavaScript 100%