Phase 435: search query filters, pin logging, and guild.search errors (v0.8.398).

parseSearchQuery integration via _applySearchQueryFilters; channel search structured filters; pin.message and pin.unpin spans with error logs; guild.search error logging on failure.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Raven Scott
2026-05-23 18:21:22 -04:00
co-authored by Cursor
parent da8541dd0c
commit 9fc0c3a90c
2 changed files with 134 additions and 35 deletions
+2
View File
@@ -137,6 +137,8 @@ Primary consumer: `apps/pearcord/index.js` sidecar reading pear-pipe JSON.
**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.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`.
+102 -5
View File
@@ -1712,25 +1712,50 @@ class PearcordPlatform extends EventEmitter {
return []
}
const qLower = String(q).toLowerCase()
const { parseSearchQuery, rankAndEnrichSearchHits } = require('pearcord-search')
const parsed = parseSearchQuery(q)
const textNeedle = String(parsed.text || '').toLowerCase()
let rows = []
if (this.messages?.channelId === channelId) {
rows = await this.messages.history(200)
} else {
rows = await this.db.find(COLLECTIONS.MESSAGES, { channelId })
}
const pinRows = await this.db.find(COLLECTIONS.PINS, { channelId })
const pinnedIds = new Set(pinRows.map((p) => p.messageId))
const members = this.guild ? await this.guild.listMembers() : []
const usersById = await this._usersById(members)
const chRow = await this.db.get(COLLECTIONS.CHANNELS, { guildId: this.guild.guild.id, id: channelId })
const rawHits = []
for (const row of rows) {
if (row.channelId && row.channelId !== channelId) continue
const enriched = enrichMessageRow(row)
const plain = this._messagePlaintext(enriched)
if (textNeedle && !plain.toLowerCase().includes(textNeedle)) continue
if (!textNeedle && !parsed.from && !parsed.has && !parsed.before && !parsed.after) {
if (!plain.toLowerCase().includes(qLower)) continue
}
const mem = members.find((m) => m.userId === enriched.authorId)
const u = usersById[enriched.authorId] || {}
const { messageMatchesSearchFilters } = require('pearcord-search')
if (
!messageMatchesSearchFilters(enriched, parsed, {
plaintext: plain,
authorTokens: [mem?.nickname, u.username, u.displayName].filter(Boolean),
authorId: enriched.authorId,
channelName: chRow?.name,
isPinned: pinnedIds.has(enriched.id),
attachmentIds: enriched.attachmentIds
})
) {
continue
}
rawHits.push({
...enriched,
channelId
})
}
const { rankAndEnrichSearchHits } = require('pearcord-search')
const hits = rankAndEnrichSearchHits(rawHits, q, { limit: 50 })
const hits = rankAndEnrichSearchHits(rawHits, parsed.text || q, { limit: 50 })
const searchMs = Date.now() - searchStarted
this._lastSearchMeshMeta = {
mesh: false,
@@ -1751,6 +1776,7 @@ class PearcordPlatform extends EventEmitter {
limit: 50,
mesh: this._searchIncludeMesh
})
const filtered = await this._applySearchQueryFilters(out.hits, q, this.guild.guild.id)
this._lastSearchMeshMeta = {
mesh: out.mesh,
meshHitCount: out.meshHitCount || 0,
@@ -1763,18 +1789,22 @@ class PearcordPlatform extends EventEmitter {
const searchMs = Date.now() - searchStarted
this._lastSearchMeshMeta = { ...this._lastSearchMeshMeta, searchMs }
span.end({
count: out.hits?.length || 0,
count: filtered?.length || 0,
scope: 'guild',
mesh: !!out.mesh,
searchMs
})
this._commitSearchCache(cacheKey, out.hits)
return out.hits
this._commitSearchCache(cacheKey, filtered)
return filtered
}
span.end({ count: 0, scope: this._searchScope })
this._commitSearchCache(cacheKey, [])
return []
} catch (err) {
this.log.error('guild.search error', {
scope: this._searchScope,
error: err?.message || String(err)
})
span.fail(err)
throw err
}
@@ -2318,6 +2348,51 @@ class PearcordPlatform extends EventEmitter {
return rankAndEnrichSearchHits(mapped, q, { limit })
}
async _applySearchQueryFilters (hits, rawQuery, guildId) {
const {
parseSearchQuery,
messageMatchesSearchFilters,
hasStructuredSearchFilters
} = require('pearcord-search')
const filters = parseSearchQuery(rawQuery)
if (!hasStructuredSearchFilters(filters)) return hits
const gId = guildId || this.guild?.guild?.id
const members = this.guild
? await this.guild.listMembers()
: gId
? await this.db.find(COLLECTIONS.MEMBERS, { guildId: gId })
: []
const usersById = await this._usersById(members)
const channels = gId ? await this.db.find(COLLECTIONS.CHANNELS, { guildId: gId }) : []
const channelById = new Map(channels.map((c) => [c.id, c]))
let pinnedKeys = null
if (filters.has === 'pin' && gId) {
const pins = await this.db.find(COLLECTIONS.PINS, { guildId: gId })
pinnedKeys = new Set(pins.map((p) => `${p.channelId}:${p.messageId}`))
}
const out = []
for (const hit of hits || []) {
const mem = members.find((m) => m.userId === hit.authorId)
const u = usersById[hit.authorId] || {}
const ch = channelById.get(hit.channelId)
const plain = this._messagePlaintext(hit)
if (
!messageMatchesSearchFilters(hit, filters, {
plaintext: plain,
authorTokens: [mem?.nickname, u.username, u.displayName].filter(Boolean),
authorId: hit.authorId,
channelName: ch?.name || hit.channelName,
isPinned: pinnedKeys ? pinnedKeys.has(`${hit.channelId}:${hit.id}`) : false,
attachmentIds: hit.attachmentIds
})
) {
continue
}
out.push(hit)
}
return out
}
getGuildSearchIndexMeta (guildId) {
const id = guildId || this.guild?.guild?.id
if (!id) return { persisted: false, rowCount: 0, savedAt: null }
@@ -10254,6 +10329,8 @@ class PearcordPlatform extends EventEmitter {
}
async pinMessage (messageId) {
const span = this.log.time('pin.message', { messageId })
try {
const user = this.identity.user
if (!user || !this.guild?.guild) throw new Error('no guild')
const roles = await this._memberRoles()
@@ -10275,10 +10352,21 @@ class PearcordPlatform extends EventEmitter {
await this.db.insert(COLLECTIONS.PINS, pin)
this.guild.gossipPin(pin)
this.emit('pin', pin)
span.end({ channelId: pin.channelId })
return pin
} catch (err) {
this.log.error('pin.message error', {
messageId,
error: err?.message || String(err)
})
span.fail(err)
throw err
}
}
async unpinMessage (messageId) {
const span = this.log.time('pin.unpin', { messageId })
try {
const roles = await this._memberRoles()
if (!roleHasPermission(roles, PERMISSION.MANAGE_MESSAGES)) {
throw new Error('no permission to unpin')
@@ -10288,6 +10376,15 @@ class PearcordPlatform extends EventEmitter {
messageId
})
this.emit('pin', { channelId: this.activeChannelId, messageId, removed: true })
span.end({ channelId: this.activeChannelId })
} catch (err) {
this.log.error('pin.unpin error', {
messageId,
error: err?.message || String(err)
})
span.fail(err)
throw err
}
}
async kickMember ({ userId, reason }) {