refactor(platform): Phase 752 forum search & view consistency mixins (v0.8.728)

Extract forum post local/mesh search, partition forum index heal, and channel
view pin/reaction consistency audit into three prototype mixins (manifest 101
rows, runtime registry 41). Non-breaking: identical forum search, partition heal,
and view consistency audit behavior; platform class ~31564 lines (~311 net drop).

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Raven Scott
2026-06-03 11:54:22 -04:00
co-authored by Cursor
parent 713658b6bc
commit 5d242d5da9
7 changed files with 359 additions and 317 deletions
+2
View File
@@ -54,6 +54,8 @@ Application facade: one `PearcordPlatform` class that wires identity, database,
**Phase 723 (v0.8.699):** Dedicated slash registry & invoke audit export JSON — `slash-registry-json-mixin.js`, `slash-invoke-audit-json-mixin.js`, spans `slash-registry.json` / `slash-invoke-audit.json`, `getSlashInvokeAuditExportJsonExport`, `clearSlashInvokeAuditExportJsonExports`, deep links `openSlashRegistryJson` / `openSlashInvokeAuditJson`. Bundle: `npm run test:ci-phase723`.
**Phase 752 (v0.8.728):** `platform-forum-search-mixin.js`, `platform-forum-partition-heal-mixin.js`, `platform-view-channel-consistency-mixin.js` (101 mixin rows, 41 runtime mixins); forum post local/mesh search, partition forum index heal, channel view consistency audit. Bundle: `npm run test:ci-phase752`.
**Phase 751 (v0.8.727):** `platform-search-guild-messages-mixin.js`, `platform-search-query-filters-mixin.js`, `platform-search-mesh-resolve-mixin.js` (98 mixin rows, 38 runtime mixins); guild message search, structured query filters, index meta, mesh hit merge. Bundle: `npm run test:ci-phase751`.
**Phase 750 (v0.8.726):** `platform-search-index-store-mixin.js`, `platform-search-index-rebuild-mixin.js`, `platform-search-mesh-gossip-mixin.js`, `platform-discovery-mesh-refresh-mixin.js` (95 mixin rows, 35 runtime mixins); guild search index store/persist/rebuild, mesh search gossip, discovery listing refresh after mesh. Bundle: `npm run test:ci-phase750`.
+19
View File
@@ -0,0 +1,19 @@
'use strict'
const platformForumPartitionHealMixin = {
async _healForumSearchIndexOnPartition (guildId) {
const gid = guildId || this.guild?.guild?.id
if (!gid) return { forumRows: 0, searchUpdated: false }
const channels = (await this.guild?.listChannels?.().catch(() => [])) || []
const forumRows = await this._exportForumIndexForSyncBundle(gid, channels, 0)
const forumApplied = await this._ingestForumIndexBundle(gid, forumRows)
let searchUpdated = false
if (typeof this.rebuildGuildSearchIndex === 'function') {
await this.rebuildGuildSearchIndex(gid).catch(() => null)
searchUpdated = true
}
return { forumRows: forumRows.length, forumApplied, searchUpdated }
}
}
module.exports = { platformForumPartitionHealMixin }
+134
View File
@@ -0,0 +1,134 @@
'use strict'
const sharedScope = require('pearcord-shared')
const threadsScope = require('pearcord-threads')
const searchScope = require('pearcord-search')
const enrichmentScope = require('./platform-message-enrichment')
const { forumPostHaystack } = require('./platform-class-imports')
const platformForumSearchMixin = {
async searchForumPosts (guildId, forumChannelId, query, opts = {}) {
const q = String(query || '').trim().toLowerCase()
if (!q || !guildId || !forumChannelId) return []
const limit = opts.limit || 50
const includeArchived = opts.includeArchived !== false
const channels = await this.guild?.listChannels?.() || await this.db.find(sharedScope.COLLECTIONS.CHANNELS, { guildId })
const posts = threadsScope.listForumPostsForParent(channels, forumChannelId, { includeArchived })
const forumCh = channels.find((c) => c.id === forumChannelId)
const hits = []
for (const post of posts) {
const tags = this.forum ? await this.forum.getPostTags(guildId, post.id) : []
let content = ''
if (post.rootMessageId) {
const root = await this.db.get(sharedScope.COLLECTIONS.MESSAGES, {
channelId: post.id,
id: post.rootMessageId
})
content = root ? this._messagePlaintext(enrichmentScope.enrichMessageRow(root)) : ''
} else {
const rows = await this.db.find(sharedScope.COLLECTIONS.MESSAGES, { channelId: post.id })
if (rows[0]) content = this._messagePlaintext(enrichmentScope.enrichMessageRow(rows[0]))
}
const haystack = forumPostHaystack({
title: post.name,
content,
tags,
archived: post.archived
})
if (!haystack.includes(q)) continue
hits.push({
...post,
tags,
forumChannelId,
forumChannelName: forumCh?.name || 'forum',
searchHaystack: haystack,
snippet: String(content || '').slice(0, 160)
})
}
return searchScope.rankAndEnrichForumSearchHits(hits, q, { limit })
},
async _forumPostsFromMeshMessages (guildId, forumChannelId, messages, query) {
const q = String(query || '').trim()
if (!q || !guildId || !forumChannelId) return []
const channels =
(await this.guild?.listChannels?.()) ||
(await this.db.find(sharedScope.COLLECTIONS.CHANNELS, { guildId }))
const forumCh = channels.find((c) => c.id === forumChannelId)
const posts = threadsScope.listForumPostsForParent(channels, forumChannelId, {
includeArchived: true
})
const byThread = new Map(posts.map((p) => [p.id, p]))
const hits = []
for (const m of messages || []) {
const post = byThread.get(m.channelId)
if (!post) continue
let content = String(m.content || '')
if (!content && post.rootMessageId) {
const root = await this.db.get(sharedScope.COLLECTIONS.MESSAGES, {
channelId: post.id,
id: post.rootMessageId
})
if (root) content = this._messagePlaintext(enrichmentScope.enrichMessageRow(root))
}
const tags = this.forum
? await this.forum.getPostTags(guildId, post.id).catch(() => [])
: []
hits.push({
...post,
tags,
forumChannelId,
forumChannelName: forumCh?.name || 'forum',
searchHaystack: forumPostHaystack({
title: post.name,
content,
tags,
archived: post.archived
}),
snippet: String(content || '').slice(0, 160),
fromMesh: true
})
}
return searchScope.rankAndEnrichForumSearchHits(hits, q, { limit: 50 })
},
async searchForumPostsWithMesh (guildId, forumChannelId, query, opts = {}) {
const local = await this.searchForumPosts(guildId, forumChannelId, query, opts)
if (opts.mesh === false || !this.guild?.guild) {
return {
hits: local,
mesh: false,
meshHitCount: 0,
localHitCount: local.length
}
}
try {
const meshMsgs = await this._fetchGuildSearchFromMeshOnce(guildId, query, opts)
const meshPosts = await this._forumPostsFromMeshMessages(
guildId,
forumChannelId,
meshMsgs.map((m) => enrichmentScope.enrichMessageRow(m)),
query
)
const merged = searchScope.mergeForumSearchHits(local, meshPosts, {
limit: opts.limit || 50,
query: String(query || '').trim()
})
return {
hits: merged,
mesh: true,
meshHitCount: meshPosts.length,
localHitCount: local.length
}
} catch {
return {
hits: local,
mesh: false,
meshHitCount: 0,
localHitCount: local.length
}
}
}
}
module.exports = { platformForumSearchMixin }
+4 -1
View File
@@ -4,7 +4,7 @@
* Ordered PearcordPlatform prototype mixin registration (Phase 730).
* Preserves assign order from index.js / apply-platform-mixins (Phase 727).
*/
const PLATFORM_MIXIN_ASSIGNMENT_COUNT = 98
const PLATFORM_MIXIN_ASSIGNMENT_COUNT = 101
const PLATFORM_MIXIN_ASSIGNMENTS = [
{ module: './polls-scheduling', export: 'pollSchedulingMixin' },
@@ -104,6 +104,9 @@ const PLATFORM_MIXIN_ASSIGNMENTS = [
{ module: './platform-search-guild-messages-mixin', export: 'platformSearchGuildMessagesMixin' },
{ module: './platform-search-query-filters-mixin', export: 'platformSearchQueryFiltersMixin' },
{ module: './platform-search-mesh-resolve-mixin', export: 'platformSearchMeshResolveMixin' },
{ module: './platform-forum-search-mixin', export: 'platformForumSearchMixin' },
{ module: './platform-forum-partition-heal-mixin', export: 'platformForumPartitionHealMixin' },
{ module: './platform-view-channel-consistency-mixin', export: 'platformViewChannelConsistencyMixin' },
{ module: './platform-diagnostics-mixin', export: 'platformDiagnosticsMixin' }
]
+4 -314
View File
@@ -420,19 +420,7 @@ class PearcordPlatform extends EventEmitter {
return { links, roleCount: roles.length }
}
async _healForumSearchIndexOnPartition (guildId) {
const gid = guildId || this.guild?.guild?.id
if (!gid) return { forumRows: 0, searchUpdated: false }
const channels = (await this.guild?.listChannels?.().catch(() => [])) || []
const forumRows = await this._exportForumIndexForSyncBundle(gid, channels, 0)
const forumApplied = await this._ingestForumIndexBundle(gid, forumRows)
let searchUpdated = false
if (typeof this.rebuildGuildSearchIndex === 'function') {
await this.rebuildGuildSearchIndex(gid).catch(() => null)
searchUpdated = true
}
return { forumRows: forumRows.length, forumApplied, searchUpdated }
}
async _healSettingsMeshDeviceSync (guildId) {
const gid = guildId || this.guild?.guild?.id
@@ -964,188 +952,7 @@ class PearcordPlatform extends EventEmitter {
* Compare messages, reactions, and pins for the active channel during view build.
* Invalidates pin/reaction list caches when view-layer data disagrees with DB rows.
*/
async _auditChannelViewConsistency (opts = {}) {
const channelId = opts.channelId || this.activeChannelId
const guildId =
opts.guildId ??
(opts.mode === 'dm' ? dmScope.DM_GUILD_ID : this.guild?.guild?.id ?? null)
const skipped = { ok: true, skipped: true, channelId: null, issues: [], counts: {} }
if (!channelId) return skipped
const messages = opts.messages || []
const messageById = opts.messageById || {}
const reactions = opts.reactions || {}
const pins = opts.pins || []
const light = opts.light === true
const detail = opts.detail || 'full'
const issues = []
const counts = {
messageCount: messages.length,
reactionMessageIds: 0,
pinCount: pins.length,
reactionsOutsideHistory: 0,
pinsOutsideHistory: 0,
orphanReactionRows: 0,
orphanPinRows: 0,
reactionCacheMismatches: 0,
pinCacheMismatches: 0,
repaired: 0
}
const historyIds = new Set(messages.map((m) => m.id))
for (const messageId of Object.keys(reactions)) {
counts.reactionMessageIds++
if (!messageById[messageId]) {
counts.reactionsOutsideHistory++
if (detail === 'diagnostic') {
issues.push({
kind: 'reaction-outside-history',
messageId,
severity: 'info'
})
}
}
}
for (const pin of pins) {
const mid = pin.messageId
if (!mid) continue
if (!pin.message && !messageById[mid]) {
issues.push({
kind: 'pin-missing-embedded-message',
messageId: mid,
severity: 'warn'
})
}
if (!historyIds.has(mid)) counts.pinsOutsideHistory++
}
let repaired = false
if (!light && this.messages?.channelId === channelId) {
const reactionRows = await this.messages.listReactions().catch(() => [])
const reactionMsgIds = new Set(reactionRows.map((r) => r.messageId))
for (const messageId of Object.keys(reactions)) {
if (!reactionMsgIds.has(messageId)) {
counts.reactionCacheMismatches++
issues.push({
kind: 'reaction-phantom-in-view',
messageId,
severity: 'warn'
})
}
}
for (const mid of reactionMsgIds) {
if (!reactions[mid]) {
counts.reactionCacheMismatches++
issues.push({
kind: 'reaction-missing-in-view',
messageId: mid,
severity: 'warn'
})
}
}
for (const r of reactionRows) {
const msg = await this.db.get(sharedScope.COLLECTIONS.MESSAGES, {
channelId,
id: r.messageId
})
if (!msg) {
counts.orphanReactionRows++
issues.push({
kind: 'orphan-reaction-row',
messageId: r.messageId,
severity: 'error'
})
}
}
const pinRows = await this.db.find(sharedScope.COLLECTIONS.PINS, { channelId })
counts.pinRowCount = pinRows.length
const pinViewIds = new Set(pins.map((p) => p.messageId).filter(Boolean))
for (const pin of pinRows) {
const msg = await this.db.get(sharedScope.COLLECTIONS.MESSAGES, {
channelId: pin.channelId || channelId,
id: pin.messageId
})
if (!msg) {
counts.orphanPinRows++
issues.push({
kind: 'orphan-pin-row',
messageId: pin.messageId,
severity: 'error'
})
const removed = await this._repairOrphanPinRow(pin, {
channelId,
guildId,
source: 'view-consistency-audit'
})
if (removed) {
counts.repaired += 1
repaired = true
}
continue
}
if (!pinViewIds.has(pin.messageId)) {
counts.pinCacheMismatches++
issues.push({
kind: 'pin-missing-in-view',
messageId: pin.messageId,
severity: 'warn'
})
}
}
}
if (counts.reactionCacheMismatches > 0) {
this._invalidateReactionListCache()
repaired = true
counts.repaired += 1
}
if (counts.pinCacheMismatches > 0) {
this._invalidatePinListCache()
repaired = true
counts.repaired += 1
}
const bad = issues.filter((i) => i.severity === 'warn' || i.severity === 'error')
const ok = bad.length === 0 && !counts.orphanReactionRows && !counts.orphanPinRows
const span = this.log.time('view.channel-consistency', {
spanKind: 'view.channel-consistency',
channelId,
guildId
})
span.end({
ok,
repaired,
...counts,
issueCount: issues.length,
logLevel: ok ? 'debug' : 'info'
})
if (!ok) {
this.log.info('view.channel-consistency issues', {
spanKind: 'view.channel-consistency',
channelId,
guildId,
ok,
repaired,
counts,
issues: issues.slice(0, 8)
})
}
return {
ok,
channelId,
guildId,
counts,
issues: issues.slice(0, 32),
repaired
}
}
/**
* Remove a pin row whose message body no longer exists (P410-46).
@@ -4597,128 +4404,11 @@ class PearcordPlatform extends EventEmitter {
this.emit('forum-filter')
}
async searchForumPosts (guildId, forumChannelId, query, opts = {}) {
const q = String(query || '').trim().toLowerCase()
if (!q || !guildId || !forumChannelId) return []
const limit = opts.limit || 50
const includeArchived = opts.includeArchived !== false
const channels = await this.guild?.listChannels?.() || await this.db.find(sharedScope.COLLECTIONS.CHANNELS, { guildId })
const posts = threadsScope.listForumPostsForParent(channels, forumChannelId, { includeArchived })
const forumCh = channels.find((c) => c.id === forumChannelId)
const hits = []
for (const post of posts) {
const tags = this.forum ? await this.forum.getPostTags(guildId, post.id) : []
let content = ''
if (post.rootMessageId) {
const root = await this.db.get(sharedScope.COLLECTIONS.MESSAGES, {
channelId: post.id,
id: post.rootMessageId
})
content = root ? this._messagePlaintext(enrichmentScope.enrichMessageRow(root)) : ''
} else {
const rows = await this.db.find(sharedScope.COLLECTIONS.MESSAGES, { channelId: post.id })
if (rows[0]) content = this._messagePlaintext(enrichmentScope.enrichMessageRow(rows[0]))
}
const haystack = forumPostHaystack({
title: post.name,
content,
tags,
archived: post.archived
})
if (!haystack.includes(q)) continue
hits.push({
...post,
tags,
forumChannelId,
forumChannelName: forumCh?.name || 'forum',
searchHaystack: haystack,
snippet: String(content || '').slice(0, 160)
})
}
return searchScope.rankAndEnrichForumSearchHits(hits, q, { limit })
}
async _forumPostsFromMeshMessages (guildId, forumChannelId, messages, query) {
const q = String(query || '').trim()
if (!q || !guildId || !forumChannelId) return []
const channels =
(await this.guild?.listChannels?.()) ||
(await this.db.find(sharedScope.COLLECTIONS.CHANNELS, { guildId }))
const forumCh = channels.find((c) => c.id === forumChannelId)
const posts = threadsScope.listForumPostsForParent(channels, forumChannelId, {
includeArchived: true
})
const byThread = new Map(posts.map((p) => [p.id, p]))
const hits = []
for (const m of messages || []) {
const post = byThread.get(m.channelId)
if (!post) continue
let content = String(m.content || '')
if (!content && post.rootMessageId) {
const root = await this.db.get(sharedScope.COLLECTIONS.MESSAGES, {
channelId: post.id,
id: post.rootMessageId
})
if (root) content = this._messagePlaintext(enrichmentScope.enrichMessageRow(root))
}
const tags = this.forum
? await this.forum.getPostTags(guildId, post.id).catch(() => [])
: []
hits.push({
...post,
tags,
forumChannelId,
forumChannelName: forumCh?.name || 'forum',
searchHaystack: forumPostHaystack({
title: post.name,
content,
tags,
archived: post.archived
}),
snippet: String(content || '').slice(0, 160),
fromMesh: true
})
}
return searchScope.rankAndEnrichForumSearchHits(hits, q, { limit: 50 })
}
async searchForumPostsWithMesh (guildId, forumChannelId, query, opts = {}) {
const local = await this.searchForumPosts(guildId, forumChannelId, query, opts)
if (opts.mesh === false || !this.guild?.guild) {
return {
hits: local,
mesh: false,
meshHitCount: 0,
localHitCount: local.length
}
}
try {
const meshMsgs = await this._fetchGuildSearchFromMeshOnce(guildId, query, opts)
const meshPosts = await this._forumPostsFromMeshMessages(
guildId,
forumChannelId,
meshMsgs.map((m) => enrichmentScope.enrichMessageRow(m)),
query
)
const merged = searchScope.mergeForumSearchHits(local, meshPosts, {
limit: opts.limit || 50,
query: String(query || '').trim()
})
return {
hits: merged,
mesh: true,
meshHitCount: meshPosts.length,
localHitCount: local.length
}
} catch {
return {
hits: local,
mesh: false,
meshHitCount: 0,
localHitCount: local.length
}
}
}
+5 -2
View File
@@ -1,6 +1,6 @@
'use strict'
/** Prototype mixins extracted from PearcordPlatform class body (Phases 738751). */
/** Prototype mixins extracted from PearcordPlatform class body (Phases 738752). */
const PLATFORM_RUNTIME_MIXIN_MODULES = [
'./platform-hyperswarm-runtime-mixin',
'./platform-session-startup-mixin',
@@ -39,7 +39,10 @@ const PLATFORM_RUNTIME_MIXIN_MODULES = [
'./platform-discovery-mesh-refresh-mixin',
'./platform-search-guild-messages-mixin',
'./platform-search-query-filters-mixin',
'./platform-search-mesh-resolve-mixin'
'./platform-search-mesh-resolve-mixin',
'./platform-forum-search-mixin',
'./platform-forum-partition-heal-mixin',
'./platform-view-channel-consistency-mixin'
]
const PLATFORM_RUNTIME_MIXIN_COUNT = PLATFORM_RUNTIME_MIXIN_MODULES.length
+191
View File
@@ -0,0 +1,191 @@
'use strict'
const dmScope = require('pearcord-dm')
const sharedScope = require('pearcord-shared')
const platformViewChannelConsistencyMixin = {
async _auditChannelViewConsistency (opts = {}) {
const channelId = opts.channelId || this.activeChannelId
const guildId =
opts.guildId ??
(opts.mode === 'dm' ? dmScope.DM_GUILD_ID : this.guild?.guild?.id ?? null)
const skipped = { ok: true, skipped: true, channelId: null, issues: [], counts: {} }
if (!channelId) return skipped
const messages = opts.messages || []
const messageById = opts.messageById || {}
const reactions = opts.reactions || {}
const pins = opts.pins || []
const light = opts.light === true
const detail = opts.detail || 'full'
const issues = []
const counts = {
messageCount: messages.length,
reactionMessageIds: 0,
pinCount: pins.length,
reactionsOutsideHistory: 0,
pinsOutsideHistory: 0,
orphanReactionRows: 0,
orphanPinRows: 0,
reactionCacheMismatches: 0,
pinCacheMismatches: 0,
repaired: 0
}
const historyIds = new Set(messages.map((m) => m.id))
for (const messageId of Object.keys(reactions)) {
counts.reactionMessageIds++
if (!messageById[messageId]) {
counts.reactionsOutsideHistory++
if (detail === 'diagnostic') {
issues.push({
kind: 'reaction-outside-history',
messageId,
severity: 'info'
})
}
}
}
for (const pin of pins) {
const mid = pin.messageId
if (!mid) continue
if (!pin.message && !messageById[mid]) {
issues.push({
kind: 'pin-missing-embedded-message',
messageId: mid,
severity: 'warn'
})
}
if (!historyIds.has(mid)) counts.pinsOutsideHistory++
}
let repaired = false
if (!light && this.messages?.channelId === channelId) {
const reactionRows = await this.messages.listReactions().catch(() => [])
const reactionMsgIds = new Set(reactionRows.map((r) => r.messageId))
for (const messageId of Object.keys(reactions)) {
if (!reactionMsgIds.has(messageId)) {
counts.reactionCacheMismatches++
issues.push({
kind: 'reaction-phantom-in-view',
messageId,
severity: 'warn'
})
}
}
for (const mid of reactionMsgIds) {
if (!reactions[mid]) {
counts.reactionCacheMismatches++
issues.push({
kind: 'reaction-missing-in-view',
messageId: mid,
severity: 'warn'
})
}
}
for (const r of reactionRows) {
const msg = await this.db.get(sharedScope.COLLECTIONS.MESSAGES, {
channelId,
id: r.messageId
})
if (!msg) {
counts.orphanReactionRows++
issues.push({
kind: 'orphan-reaction-row',
messageId: r.messageId,
severity: 'error'
})
}
}
const pinRows = await this.db.find(sharedScope.COLLECTIONS.PINS, { channelId })
counts.pinRowCount = pinRows.length
const pinViewIds = new Set(pins.map((p) => p.messageId).filter(Boolean))
for (const pin of pinRows) {
const msg = await this.db.get(sharedScope.COLLECTIONS.MESSAGES, {
channelId: pin.channelId || channelId,
id: pin.messageId
})
if (!msg) {
counts.orphanPinRows++
issues.push({
kind: 'orphan-pin-row',
messageId: pin.messageId,
severity: 'error'
})
const removed = await this._repairOrphanPinRow(pin, {
channelId,
guildId,
source: 'view-consistency-audit'
})
if (removed) {
counts.repaired += 1
repaired = true
}
continue
}
if (!pinViewIds.has(pin.messageId)) {
counts.pinCacheMismatches++
issues.push({
kind: 'pin-missing-in-view',
messageId: pin.messageId,
severity: 'warn'
})
}
}
}
if (counts.reactionCacheMismatches > 0) {
this._invalidateReactionListCache()
repaired = true
counts.repaired += 1
}
if (counts.pinCacheMismatches > 0) {
this._invalidatePinListCache()
repaired = true
counts.repaired += 1
}
const bad = issues.filter((i) => i.severity === 'warn' || i.severity === 'error')
const ok = bad.length === 0 && !counts.orphanReactionRows && !counts.orphanPinRows
const span = this.log.time('view.channel-consistency', {
spanKind: 'view.channel-consistency',
channelId,
guildId
})
span.end({
ok,
repaired,
...counts,
issueCount: issues.length,
logLevel: ok ? 'debug' : 'info'
})
if (!ok) {
this.log.info('view.channel-consistency issues', {
spanKind: 'view.channel-consistency',
channelId,
guildId,
ok,
repaired,
counts,
issues: issues.slice(0, 8)
})
}
return {
ok,
channelId,
guildId,
counts,
issues: issues.slice(0, 32),
repaired
}
}
}
module.exports = { platformViewChannelConsistencyMixin }