611 lines
22 KiB
TypeScript
611 lines
22 KiB
TypeScript
/**
|
|
* Per-Space Knowledge Index — ranked retrieval for MI context injection.
|
|
*
|
|
* Instead of dumping all 35 module summaries into every prompt,
|
|
* this index builds KnowledgeEntry objects from each module and
|
|
* returns only the top-N most relevant entries for a given query
|
|
* using trigram similarity + recency boost + category weighting.
|
|
*/
|
|
|
|
import { trigrams, jaccardSimilarity } from "./mi-trigrams";
|
|
import { sanitizeForPrompt, MAX_TITLE_LENGTH } from "./mi-sanitize";
|
|
|
|
import { getUpcomingEventsForMI } from "../modules/rcal/mod";
|
|
import { getRecentVaultNotesForMI } from "../modules/rnotes/mod";
|
|
import { getRecentTasksForMI } from "../modules/rtasks/mod";
|
|
import { getRecentCampaignsForMI } from "../modules/rsocials/mod";
|
|
import { getRecentContactsForMI } from "../modules/rnetwork/mod";
|
|
import { getRecentThreadsForMI } from "../modules/rinbox/mod";
|
|
import { getRecentCommitmentsForMI } from "../modules/rtime/mod";
|
|
import { getRecentFilesForMI } from "../modules/rfiles/mod";
|
|
import { getUpcomingRemindersForMI } from "../modules/rminders/mod";
|
|
import { getMapPinsForMI } from "../modules/rmaps/mod";
|
|
import { getRecentMeetingsForMI } from "../modules/rmeets/mod";
|
|
import { getRecentVideosForMI } from "../modules/rtube/mod";
|
|
import { getRecentMessagesForMI } from "../modules/rchats/mod";
|
|
import { getRecentAgentPostsForMI } from "../modules/ragents/mod";
|
|
import { getRecentPublicationsForMI } from "../modules/rpubs/mod";
|
|
import { getRecentDesignsForMI } from "../modules/rswag/mod";
|
|
import { getRecentSheetsForMI } from "../modules/rsheets/mod";
|
|
import { getRecentDocsForMI } from "../modules/rdocs/mod";
|
|
import { getRecentSessionsForMI } from "../modules/rdesign/mod";
|
|
import { getSharedAlbumsForMI } from "../modules/rphotos/mod";
|
|
import { getRecentFlowsForMI } from "../modules/rflows/mod";
|
|
import { getRecentIntentsForMI } from "../modules/rexchange/mod";
|
|
import { getRecentOrdersForMI } from "../modules/rcart/mod";
|
|
import { getActiveProposalsForMI } from "../modules/rvote/mod";
|
|
import { getRecentBooksForMI } from "../modules/rbooks/mod";
|
|
import { getRecentSplatsForMI } from "../modules/rsplat/mod";
|
|
import { getRecentTripsForMI } from "../modules/rtrips/mod";
|
|
import { getActiveListingsForMI } from "../modules/rbnb/mod";
|
|
import { getActiveVehiclesForMI } from "../modules/rvnb/mod";
|
|
import { getForumInstancesForMI } from "../modules/rforum/mod";
|
|
import { getRecentChoiceSessionsForMI } from "../modules/rchoices/mod";
|
|
import { getActivePromptsForMI } from "../modules/crowdsurf/mod";
|
|
import { getGovShapesForMI } from "../modules/rgov/mod";
|
|
import { getCrdtTokensForMI } from "../modules/rwallet/mod";
|
|
import { getCanvasSummaryForMI } from "../modules/rspace/mod";
|
|
import { getDataSummaryForMI } from "../modules/rdata/mod";
|
|
|
|
// ── Types ──
|
|
|
|
type KnowledgeCategory =
|
|
| "tasks" | "time" | "content" | "people" | "social"
|
|
| "commerce" | "community" | "spatial" | "media" | "infra" | "canvas";
|
|
|
|
interface KnowledgeEntry {
|
|
id: string;
|
|
moduleId: string;
|
|
category: KnowledgeCategory;
|
|
title: string;
|
|
detail: string;
|
|
tags: string[];
|
|
timestamp: number;
|
|
formatted: string;
|
|
}
|
|
|
|
// ── Category weights (higher = more likely to surface) ──
|
|
|
|
const CATEGORY_WEIGHT: Record<KnowledgeCategory, number> = {
|
|
tasks: 1.5,
|
|
time: 1.4,
|
|
content: 1.3,
|
|
people: 1.2,
|
|
social: 1.1,
|
|
commerce: 0.9,
|
|
community: 0.8,
|
|
spatial: 0.7,
|
|
media: 0.7,
|
|
infra: 0.6,
|
|
canvas: 0.5,
|
|
};
|
|
|
|
const SEVEN_DAYS = 7 * 24 * 60 * 60 * 1000;
|
|
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
|
const PER_MODULE_LIMIT = 12;
|
|
|
|
// ── SpaceKnowledgeIndex ──
|
|
|
|
class SpaceKnowledgeIndex {
|
|
#cache = new Map<string, { entries: KnowledgeEntry[]; builtAt: number }>();
|
|
|
|
/** Get ranked context string for injection into MI system prompt. */
|
|
getRankedContext(space: string, query: string, topN = 18): string {
|
|
const { entries } = this.#getOrBuild(space);
|
|
if (entries.length === 0) return "";
|
|
|
|
const ranked = this.#rank(entries, query, topN);
|
|
if (ranked.length === 0) return "";
|
|
|
|
// Group by module for readability
|
|
const groups = new Map<string, string[]>();
|
|
for (const entry of ranked) {
|
|
if (!groups.has(entry.moduleId)) groups.set(entry.moduleId, []);
|
|
groups.get(entry.moduleId)!.push(entry.formatted);
|
|
}
|
|
|
|
const lines: string[] = [];
|
|
for (const [moduleId, items] of groups) {
|
|
lines.push(`- [${moduleId}]`);
|
|
for (const item of items) {
|
|
lines.push(` ${item}`);
|
|
}
|
|
}
|
|
|
|
return `\n- Space data (ranked by relevance):\n${lines.join("\n")}`;
|
|
}
|
|
|
|
/** Invalidate cache for a space (called on doc changes). */
|
|
invalidate(space: string): void {
|
|
this.#cache.delete(space);
|
|
}
|
|
|
|
#getOrBuild(space: string): { entries: KnowledgeEntry[]; builtAt: number } {
|
|
const cached = this.#cache.get(space);
|
|
if (cached && Date.now() - cached.builtAt < CACHE_TTL) return cached;
|
|
|
|
const entries = this.#buildIndex(space);
|
|
const result = { entries, builtAt: Date.now() };
|
|
this.#cache.set(space, result);
|
|
return result;
|
|
}
|
|
|
|
#buildIndex(space: string): KnowledgeEntry[] {
|
|
const entries: KnowledgeEntry[] = [];
|
|
const now = Date.now();
|
|
|
|
// Each adapter is wrapped in try/catch so one module failure doesn't block others
|
|
|
|
try {
|
|
for (const e of getUpcomingEventsForMI(space, 14, PER_MODULE_LIMIT)) {
|
|
const date = e.allDay ? e.start.split("T")[0] : e.start;
|
|
const safeTitle = sanitizeForPrompt(e.title, MAX_TITLE_LENGTH);
|
|
let line = `${date}: ${safeTitle}`;
|
|
if (e.location) line += ` (${sanitizeForPrompt(e.location, MAX_TITLE_LENGTH)})`;
|
|
else if (e.isVirtual) line += ` (virtual)`;
|
|
if (e.tags?.length) line += ` [${e.tags.join(", ")}]`;
|
|
entries.push({
|
|
id: `rcal:${e.title}:${e.start}`, moduleId: "rcal", category: "time",
|
|
title: safeTitle, detail: `${e.location || ""} ${(e.tags || []).join(" ")}`,
|
|
tags: e.tags || [], timestamp: Date.parse(e.start) || now, formatted: line,
|
|
});
|
|
}
|
|
} catch {}
|
|
|
|
try {
|
|
for (const n of getRecentVaultNotesForMI(space, PER_MODULE_LIMIT)) {
|
|
const safeTitle = sanitizeForPrompt(n.title, MAX_TITLE_LENGTH);
|
|
const line = `"${safeTitle}" in ${n.vaultName} (${n.path})${n.tags.length ? ` [${n.tags.join(", ")}]` : ""}`;
|
|
entries.push({
|
|
id: `rnotes:${n.path}`, moduleId: "rnotes", category: "content",
|
|
title: safeTitle, detail: `${n.vaultName} ${n.path}`,
|
|
tags: n.tags, timestamp: now, formatted: line,
|
|
});
|
|
}
|
|
} catch {}
|
|
|
|
try {
|
|
for (const t of getRecentTasksForMI(space, PER_MODULE_LIMIT)) {
|
|
const safeTitle = sanitizeForPrompt(t.title, MAX_TITLE_LENGTH);
|
|
const line = `"${safeTitle}" [${t.status}]${t.priority ? ` (${t.priority})` : ""}${t.description ? `: ${sanitizeForPrompt(t.description, 80)}` : ""}`;
|
|
entries.push({
|
|
id: `rtasks:${t.id}`, moduleId: "rtasks", category: "tasks",
|
|
title: safeTitle, detail: sanitizeForPrompt(t.description?.slice(0, 200) || ""),
|
|
tags: [t.status, t.priority].filter(Boolean) as string[],
|
|
timestamp: t.createdAt || now, formatted: line,
|
|
});
|
|
}
|
|
} catch {}
|
|
|
|
try {
|
|
for (const c of getRecentCampaignsForMI(space, PER_MODULE_LIMIT)) {
|
|
const safeTitle = sanitizeForPrompt(c.title, MAX_TITLE_LENGTH);
|
|
const line = `"${safeTitle}" (${c.platforms.join(", ")}, ${c.postCount} posts)`;
|
|
entries.push({
|
|
id: `rsocials:${c.id}`, moduleId: "rsocials", category: "social",
|
|
title: safeTitle, detail: c.platforms.join(" "),
|
|
tags: c.platforms, timestamp: c.updatedAt || now, formatted: line,
|
|
});
|
|
}
|
|
} catch {}
|
|
|
|
try {
|
|
for (const c of getRecentContactsForMI(space, PER_MODULE_LIMIT)) {
|
|
const safeName = sanitizeForPrompt(c.name, MAX_TITLE_LENGTH);
|
|
const line = `${safeName} (${c.role})${c.tags.length ? ` [${c.tags.join(", ")}]` : ""}`;
|
|
entries.push({
|
|
id: `rnetwork:${c.did}`, moduleId: "rnetwork", category: "people",
|
|
title: safeName, detail: c.role,
|
|
tags: c.tags, timestamp: now, formatted: line,
|
|
});
|
|
}
|
|
} catch {}
|
|
|
|
try {
|
|
for (const t of getRecentThreadsForMI(space, PER_MODULE_LIMIT)) {
|
|
const safeSubject = sanitizeForPrompt(t.subject, MAX_TITLE_LENGTH);
|
|
const line = `"${safeSubject}" from ${t.fromAddress || "unknown"} [${t.status}]${t.isRead ? "" : " (unread)"}`;
|
|
entries.push({
|
|
id: `rinbox:${t.subject}`, moduleId: "rinbox", category: "people",
|
|
title: safeSubject, detail: t.fromAddress || "",
|
|
tags: [t.status, t.isRead ? "" : "unread"].filter(Boolean),
|
|
timestamp: t.receivedAt || now, formatted: line,
|
|
});
|
|
}
|
|
} catch {}
|
|
|
|
try {
|
|
for (const c of getRecentCommitmentsForMI(space, PER_MODULE_LIMIT)) {
|
|
const safeName = sanitizeForPrompt(c.memberName, MAX_TITLE_LENGTH);
|
|
const line = `${safeName}: ${c.hours}h ${c.skill} — ${sanitizeForPrompt(c.desc, 80)}`;
|
|
entries.push({
|
|
id: `rtime:${c.id}`, moduleId: "rtime", category: "people",
|
|
title: `${safeName} ${c.skill}`, detail: sanitizeForPrompt(c.desc, 200),
|
|
tags: [c.skill], timestamp: now, formatted: line,
|
|
});
|
|
}
|
|
} catch {}
|
|
|
|
try {
|
|
for (const f of getRecentFilesForMI(space, PER_MODULE_LIMIT)) {
|
|
const safeTitle = sanitizeForPrompt(f.title || f.originalFilename, MAX_TITLE_LENGTH);
|
|
const line = `${safeTitle} (${f.mimeType || "unknown"})`;
|
|
entries.push({
|
|
id: `rfiles:${f.id}`, moduleId: "rfiles", category: "content",
|
|
title: safeTitle, detail: f.mimeType || "",
|
|
tags: [], timestamp: f.updatedAt || now, formatted: line,
|
|
});
|
|
}
|
|
} catch {}
|
|
|
|
try {
|
|
for (const r of getUpcomingRemindersForMI(space, 14, PER_MODULE_LIMIT)) {
|
|
const date = new Date(r.remindAt).toISOString().split("T")[0];
|
|
const safeTitle = sanitizeForPrompt(r.title, MAX_TITLE_LENGTH);
|
|
const line = `${date}: ${safeTitle}${r.sourceModule ? ` (from ${r.sourceModule})` : ""}`;
|
|
entries.push({
|
|
id: `rminders:${r.id}`, moduleId: "rminders", category: "time",
|
|
title: safeTitle, detail: r.sourceModule || "",
|
|
tags: [r.sourceModule].filter(Boolean) as string[],
|
|
timestamp: r.remindAt || now, formatted: line,
|
|
});
|
|
}
|
|
} catch {}
|
|
|
|
try {
|
|
for (const p of getMapPinsForMI(space, PER_MODULE_LIMIT)) {
|
|
const safeLabel = sanitizeForPrompt(p.label, MAX_TITLE_LENGTH);
|
|
const line = `"${safeLabel}" (${p.type}) at ${p.lat.toFixed(4)}, ${p.lng.toFixed(4)}`;
|
|
entries.push({
|
|
id: `rmaps:${p.id}`, moduleId: "rmaps", category: "spatial",
|
|
title: safeLabel, detail: p.type,
|
|
tags: [p.type], timestamp: p.createdAt || now, formatted: line,
|
|
});
|
|
}
|
|
} catch {}
|
|
|
|
try {
|
|
for (const m of getRecentMeetingsForMI(space, PER_MODULE_LIMIT)) {
|
|
const safeTitle = sanitizeForPrompt(m.title, MAX_TITLE_LENGTH);
|
|
const line = `"${safeTitle}" (${m.participantCount} participants)`;
|
|
entries.push({
|
|
id: `rmeets:${m.id}`, moduleId: "rmeets", category: "people",
|
|
title: safeTitle, detail: "",
|
|
tags: [], timestamp: m.createdAt || now, formatted: line,
|
|
});
|
|
}
|
|
} catch {}
|
|
|
|
try {
|
|
for (const v of getRecentVideosForMI(space, PER_MODULE_LIMIT)) {
|
|
const safeName = sanitizeForPrompt(v.name, MAX_TITLE_LENGTH);
|
|
const line = `"${safeName}" (${v.entryCount} entries)`;
|
|
entries.push({
|
|
id: `rtube:${v.id}`, moduleId: "rtube", category: "media",
|
|
title: safeName, detail: "",
|
|
tags: [], timestamp: v.createdAt || now, formatted: line,
|
|
});
|
|
}
|
|
} catch {}
|
|
|
|
try {
|
|
for (const m of getRecentMessagesForMI(space, PER_MODULE_LIMIT)) {
|
|
const line = `[${m.channel}] ${sanitizeForPrompt(m.author, MAX_TITLE_LENGTH)}: ${sanitizeForPrompt(m.content, 80)}`;
|
|
entries.push({
|
|
id: `rchats:${m.id}`, moduleId: "rchats", category: "social",
|
|
title: `${m.channel} ${sanitizeForPrompt(m.author, MAX_TITLE_LENGTH)}`, detail: sanitizeForPrompt(m.content, 200),
|
|
tags: [m.channel], timestamp: m.createdAt || now, formatted: line,
|
|
});
|
|
}
|
|
} catch {}
|
|
|
|
try {
|
|
for (const p of getRecentAgentPostsForMI(space, PER_MODULE_LIMIT)) {
|
|
const line = `[${p.channel}] ${sanitizeForPrompt(p.author, MAX_TITLE_LENGTH)}: ${sanitizeForPrompt(p.content, 80)}${p.hasPayload ? " [+data]" : ""}`;
|
|
entries.push({
|
|
id: `ragents:${p.id}`, moduleId: "ragents", category: "social",
|
|
title: `${p.channel} ${sanitizeForPrompt(p.author, MAX_TITLE_LENGTH)}`, detail: sanitizeForPrompt(p.content, 200),
|
|
tags: [p.channel], timestamp: p.createdAt || now, formatted: line,
|
|
});
|
|
}
|
|
} catch {}
|
|
|
|
try {
|
|
for (const p of getRecentPublicationsForMI(space, PER_MODULE_LIMIT)) {
|
|
const safeTitle = sanitizeForPrompt(p.title, MAX_TITLE_LENGTH);
|
|
const line = `"${safeTitle}" by ${sanitizeForPrompt(p.author, MAX_TITLE_LENGTH)} (${p.format})`;
|
|
entries.push({
|
|
id: `rpubs:${p.id}`, moduleId: "rpubs", category: "content",
|
|
title: safeTitle, detail: `${sanitizeForPrompt(p.author, MAX_TITLE_LENGTH)} ${p.format}`,
|
|
tags: [p.format], timestamp: p.updatedAt || now, formatted: line,
|
|
});
|
|
}
|
|
} catch {}
|
|
|
|
try {
|
|
for (const d of getRecentDesignsForMI(space, PER_MODULE_LIMIT)) {
|
|
const safeTitle = sanitizeForPrompt(d.title, MAX_TITLE_LENGTH);
|
|
const line = `"${safeTitle}" (${d.productType}, ${d.status})`;
|
|
entries.push({
|
|
id: `rswag:${d.id}`, moduleId: "rswag", category: "media",
|
|
title: safeTitle, detail: `${d.productType} ${d.status}`,
|
|
tags: [d.productType, d.status], timestamp: d.createdAt || now, formatted: line,
|
|
});
|
|
}
|
|
} catch {}
|
|
|
|
try {
|
|
for (const s of getRecentSheetsForMI(space, PER_MODULE_LIMIT)) {
|
|
const safeName = sanitizeForPrompt(s.name, MAX_TITLE_LENGTH);
|
|
const line = `"${safeName}" (${s.cellCount} cells)`;
|
|
entries.push({
|
|
id: `rsheets:${s.id}`, moduleId: "rsheets", category: "infra",
|
|
title: safeName, detail: "",
|
|
tags: [], timestamp: s.updatedAt || now, formatted: line,
|
|
});
|
|
}
|
|
} catch {}
|
|
|
|
try {
|
|
for (const n of getRecentDocsForMI(space, PER_MODULE_LIMIT)) {
|
|
const safeTitle = sanitizeForPrompt(n.title, MAX_TITLE_LENGTH);
|
|
const line = `"${safeTitle}" (${n.type}${n.tags.length ? `, tags: ${n.tags.join(", ")}` : ""}): ${sanitizeForPrompt(n.contentPlain, 100)}`;
|
|
entries.push({
|
|
id: `rdocs:${n.id}`, moduleId: "rdocs", category: "content",
|
|
title: safeTitle, detail: sanitizeForPrompt(n.contentPlain, 200),
|
|
tags: n.tags, timestamp: n.updatedAt || now, formatted: line,
|
|
});
|
|
}
|
|
} catch {}
|
|
|
|
try {
|
|
for (const s of getRecentSessionsForMI(space, PER_MODULE_LIMIT)) {
|
|
const safeTitle = sanitizeForPrompt(s.title, MAX_TITLE_LENGTH);
|
|
const line = `"${safeTitle}" (${s.pageCount} pages, ${s.frameCount} frames)`;
|
|
entries.push({
|
|
id: `rdesign:${s.title}`, moduleId: "rdesign", category: "media",
|
|
title: safeTitle, detail: "",
|
|
tags: [], timestamp: now, formatted: line,
|
|
});
|
|
}
|
|
} catch {}
|
|
|
|
try {
|
|
for (const a of getSharedAlbumsForMI(space, PER_MODULE_LIMIT)) {
|
|
const safeName = sanitizeForPrompt(a.name, MAX_TITLE_LENGTH);
|
|
const line = `"${safeName}"`;
|
|
entries.push({
|
|
id: `rphotos:${a.id}`, moduleId: "rphotos", category: "media",
|
|
title: safeName, detail: "",
|
|
tags: [], timestamp: a.sharedAt || now, formatted: line,
|
|
});
|
|
}
|
|
} catch {}
|
|
|
|
try {
|
|
for (const f of getRecentFlowsForMI(space, PER_MODULE_LIMIT)) {
|
|
const safeName = sanitizeForPrompt(f.name, MAX_TITLE_LENGTH);
|
|
const line = `"${safeName}" (${f.nodeCount} nodes)`;
|
|
entries.push({
|
|
id: `rflows:${f.id}`, moduleId: "rflows", category: "infra",
|
|
title: safeName, detail: "",
|
|
tags: [], timestamp: f.createdAt || now, formatted: line,
|
|
});
|
|
}
|
|
} catch {}
|
|
|
|
try {
|
|
for (const i of getRecentIntentsForMI(space, PER_MODULE_LIMIT)) {
|
|
const line = `${i.side} ${i.tokenId} [${i.status}]`;
|
|
entries.push({
|
|
id: `rexchange:${i.id}`, moduleId: "rexchange", category: "commerce",
|
|
title: `${i.side} ${i.tokenId}`, detail: i.status,
|
|
tags: [i.side, i.status], timestamp: i.createdAt || now, formatted: line,
|
|
});
|
|
}
|
|
} catch {}
|
|
|
|
try {
|
|
for (const o of getRecentOrdersForMI(space, PER_MODULE_LIMIT)) {
|
|
const safeTitle = sanitizeForPrompt(o.title, MAX_TITLE_LENGTH);
|
|
const line = `"${safeTitle}" [${o.status}]`;
|
|
entries.push({
|
|
id: `rcart:${o.id}`, moduleId: "rcart", category: "commerce",
|
|
title: safeTitle, detail: o.status,
|
|
tags: [o.status], timestamp: o.createdAt || now, formatted: line,
|
|
});
|
|
}
|
|
} catch {}
|
|
|
|
try {
|
|
for (const p of getActiveProposalsForMI(space, PER_MODULE_LIMIT)) {
|
|
const safeTitle = sanitizeForPrompt(p.title, MAX_TITLE_LENGTH);
|
|
const line = `"${safeTitle}" [${p.status}] (${p.voteCount} votes)`;
|
|
entries.push({
|
|
id: `rvote:${p.id}`, moduleId: "rvote", category: "community",
|
|
title: safeTitle, detail: p.status,
|
|
tags: [p.status], timestamp: p.createdAt || now, formatted: line,
|
|
});
|
|
}
|
|
} catch {}
|
|
|
|
try {
|
|
for (const b of getRecentBooksForMI(space, PER_MODULE_LIMIT)) {
|
|
const safeTitle = sanitizeForPrompt(b.title, MAX_TITLE_LENGTH);
|
|
const line = `"${safeTitle}" by ${sanitizeForPrompt(b.author, MAX_TITLE_LENGTH)}`;
|
|
entries.push({
|
|
id: `rbooks:${b.id}`, moduleId: "rbooks", category: "content",
|
|
title: safeTitle, detail: sanitizeForPrompt(b.author, MAX_TITLE_LENGTH),
|
|
tags: [b.author], timestamp: b.createdAt || now, formatted: line,
|
|
});
|
|
}
|
|
} catch {}
|
|
|
|
try {
|
|
for (const s of getRecentSplatsForMI(space, PER_MODULE_LIMIT)) {
|
|
const safeTitle = sanitizeForPrompt(s.title, MAX_TITLE_LENGTH);
|
|
const line = `"${safeTitle}" (${s.format})`;
|
|
entries.push({
|
|
id: `rsplat:${s.id}`, moduleId: "rsplat", category: "media",
|
|
title: safeTitle, detail: s.format,
|
|
tags: [s.format], timestamp: s.createdAt || now, formatted: line,
|
|
});
|
|
}
|
|
} catch {}
|
|
|
|
try {
|
|
for (const t of getRecentTripsForMI(space, PER_MODULE_LIMIT)) {
|
|
const safeTitle = sanitizeForPrompt(t.title, MAX_TITLE_LENGTH);
|
|
const line = `"${safeTitle}" [${t.status}] (${t.destinationCount} destinations)`;
|
|
entries.push({
|
|
id: `rtrips:${t.id}`, moduleId: "rtrips", category: "spatial",
|
|
title: safeTitle, detail: t.status,
|
|
tags: [t.status], timestamp: t.createdAt || now, formatted: line,
|
|
});
|
|
}
|
|
} catch {}
|
|
|
|
try {
|
|
for (const l of getActiveListingsForMI(space, PER_MODULE_LIMIT)) {
|
|
const safeTitle = sanitizeForPrompt(l.title, MAX_TITLE_LENGTH);
|
|
const line = `"${safeTitle}" (${l.type}, ${l.economy})`;
|
|
entries.push({
|
|
id: `rbnb:${l.id}`, moduleId: "rbnb", category: "infra",
|
|
title: safeTitle, detail: `${l.type} ${l.economy}`,
|
|
tags: [l.type, l.economy], timestamp: l.createdAt || now, formatted: line,
|
|
});
|
|
}
|
|
} catch {}
|
|
|
|
try {
|
|
for (const v of getActiveVehiclesForMI(space, PER_MODULE_LIMIT)) {
|
|
const safeTitle = sanitizeForPrompt(v.title, MAX_TITLE_LENGTH);
|
|
const line = `"${safeTitle}" (${v.type}, ${v.economy})`;
|
|
entries.push({
|
|
id: `rvnb:${v.id}`, moduleId: "rvnb", category: "infra",
|
|
title: safeTitle, detail: `${v.type} ${v.economy}`,
|
|
tags: [v.type, v.economy], timestamp: v.createdAt || now, formatted: line,
|
|
});
|
|
}
|
|
} catch {}
|
|
|
|
try {
|
|
for (const i of getForumInstancesForMI(space, PER_MODULE_LIMIT)) {
|
|
const safeName = sanitizeForPrompt(i.name, MAX_TITLE_LENGTH);
|
|
const line = `"${safeName}" (${i.domain || "pending"}) [${i.status}]`;
|
|
entries.push({
|
|
id: `rforum:${i.id}`, moduleId: "rforum", category: "community",
|
|
title: safeName, detail: i.domain || "",
|
|
tags: [i.status], timestamp: i.createdAt || now, formatted: line,
|
|
});
|
|
}
|
|
} catch {}
|
|
|
|
try {
|
|
for (const s of getRecentChoiceSessionsForMI(space, PER_MODULE_LIMIT)) {
|
|
const safeTitle = sanitizeForPrompt(s.title, MAX_TITLE_LENGTH);
|
|
const line = `"${safeTitle}" (${s.type}, ${s.optionCount} options)`;
|
|
entries.push({
|
|
id: `rchoices:${s.id}`, moduleId: "rchoices", category: "community",
|
|
title: safeTitle, detail: s.type,
|
|
tags: [s.type], timestamp: s.createdAt || now, formatted: line,
|
|
});
|
|
}
|
|
} catch {}
|
|
|
|
try {
|
|
for (const p of getActivePromptsForMI(space, PER_MODULE_LIMIT)) {
|
|
const line = `"${sanitizeForPrompt(p.text, 60)}" (${p.swipeCount}/${p.threshold})`;
|
|
entries.push({
|
|
id: `crowdsurf:${p.id}`, moduleId: "crowdsurf", category: "community",
|
|
title: sanitizeForPrompt(p.text, 80), detail: "",
|
|
tags: [], timestamp: p.createdAt || now, formatted: line,
|
|
});
|
|
}
|
|
} catch {}
|
|
|
|
try {
|
|
for (const s of getGovShapesForMI(space)) {
|
|
const line = `${s.type}: ${s.count}`;
|
|
entries.push({
|
|
id: `rgov:${s.type}`, moduleId: "rgov", category: "community",
|
|
title: s.type, detail: `${s.count} shapes`,
|
|
tags: [s.type], timestamp: now, formatted: line,
|
|
});
|
|
}
|
|
} catch {}
|
|
|
|
try {
|
|
for (const t of getCrdtTokensForMI(space, PER_MODULE_LIMIT)) {
|
|
const line = `${t.symbol} (${t.name}): supply ${t.totalSupply}`;
|
|
entries.push({
|
|
id: `rwallet:${t.tokenId}`, moduleId: "rwallet", category: "commerce",
|
|
title: `${t.symbol} ${t.name}`, detail: `supply ${t.totalSupply}`,
|
|
tags: [t.symbol], timestamp: now, formatted: line,
|
|
});
|
|
}
|
|
} catch {}
|
|
|
|
try {
|
|
const summary = getCanvasSummaryForMI(space);
|
|
if (summary.length > 0 && summary[0].totalShapes > 0) {
|
|
const s = summary[0];
|
|
const top = s.typeBreakdown.slice(0, 5).map((t) => `${t.type}: ${t.count}`).join(", ");
|
|
entries.push({
|
|
id: "rspace:canvas", moduleId: "rspace", category: "canvas",
|
|
title: "Canvas summary", detail: top,
|
|
tags: s.typeBreakdown.map((t) => t.type), timestamp: now,
|
|
formatted: `${s.totalShapes} shapes (${top})`,
|
|
});
|
|
}
|
|
} catch {}
|
|
|
|
try {
|
|
const data = getDataSummaryForMI(space);
|
|
if (data.length > 0) {
|
|
entries.push({
|
|
id: "rdata:summary", moduleId: "rdata", category: "infra",
|
|
title: "Analytics", detail: data.map((d) => `${d.label}: ${d.value}`).join(", "),
|
|
tags: ["analytics"], timestamp: now,
|
|
formatted: `Analytics: ${data.map((d) => `${d.label}=${d.value}`).join(", ")}`,
|
|
});
|
|
}
|
|
} catch {}
|
|
|
|
return entries;
|
|
}
|
|
|
|
#rank(entries: KnowledgeEntry[], query: string, topN: number): KnowledgeEntry[] {
|
|
const now = Date.now();
|
|
|
|
if (!query.trim()) {
|
|
// No query — sort by recency * category weight
|
|
return entries
|
|
.map((e) => ({
|
|
entry: e,
|
|
score: (0.5 + 0.2 * Math.exp(-(now - e.timestamp) / SEVEN_DAYS)) * CATEGORY_WEIGHT[e.category],
|
|
}))
|
|
.sort((a, b) => b.score - a.score)
|
|
.slice(0, topN)
|
|
.map((r) => r.entry);
|
|
}
|
|
|
|
const queryTrigrams = trigrams(query);
|
|
|
|
return entries
|
|
.map((e) => {
|
|
const text = `${e.title} ${e.detail} ${e.tags.join(" ")}`;
|
|
const entryTrigrams = trigrams(text);
|
|
const sim = jaccardSimilarity(queryTrigrams, entryTrigrams);
|
|
const recencyBoost = 0.2 * Math.exp(-(now - e.timestamp) / SEVEN_DAYS);
|
|
const score = (sim + recencyBoost) * CATEGORY_WEIGHT[e.category];
|
|
return { entry: e, score };
|
|
})
|
|
.sort((a, b) => b.score - a.score)
|
|
.slice(0, topN)
|
|
.map((r) => r.entry);
|
|
}
|
|
}
|
|
|
|
export const spaceKnowledgeIndex = new SpaceKnowledgeIndex();
|