VoidBot/scripts/void-memory-organ-graph.mjs
2026-05-17 08:37:26 +01:00

572 lines
18 KiB
JavaScript

import {
MAX_EDGE_COUNT,
MAX_CLUSTER_COUNT,
MAX_SUPPORTING_REFS,
MAX_CLUSTER_MEMORY_IDS,
EDGE_SIMILARITY_THRESHOLD,
CLUSTER_SIMILARITY_THRESHOLD,
stopwords,
labelNoiseTokens,
averagePairwiseSimilarity,
cosineSimilarity,
topKeywords,
topConceptKeywords,
normalizeText,
sanitizePreferredThoughtLabel,
compactNarrative,
capDistinctStrings,
ensureStringArray,
readString,
readBoolean,
round3,
round4,
clamp,
hashString,
isObject,
} from "./void-memory-organ-shared.mjs";
export async function ensureSemanticVector({ record, embedder, now }) {
const prior = isObject(record.entry.semanticVector) ? record.entry.semanticVector : {};
const contentHash = hashString(record.text);
const priorValues = Array.isArray(prior.values) ? prior.values : [];
if (
prior.sourceHash === contentHash &&
prior.backend === embedder.backend &&
prior.model === embedder.model &&
prior.compactDimensions === embedder.dimensions &&
priorValues.length === embedder.dimensions
) {
return false;
}
const vector = await embedder.embed(record.text);
record.entry.semanticVector = {
version: 1,
backend: embedder.backend,
model: embedder.model,
compactDimensions: embedder.dimensions,
sourceHash: contentHash,
embeddedAt: now.toISOString(),
values: vector.map(round4),
};
return true;
}
export function buildAssociationEdges(records) {
const edges = [];
for (let leftIndex = 0; leftIndex < records.length; leftIndex += 1) {
for (let rightIndex = leftIndex + 1; rightIndex < records.length; rightIndex += 1) {
const left = records[leftIndex];
const right = records[rightIndex];
if (left.lowSignalQuietRoom && right.lowSignalQuietRoom) {
continue;
}
const threshold = edgeThresholdForKinds(left.kind, right.kind);
const similarity = cosineSimilarity(
left.entry.semanticVector.values,
right.entry.semanticVector.values,
);
if (similarity < threshold) {
continue;
}
edges.push({
left,
right,
similarity,
summary: summarizeEdge(left, right, similarity),
});
}
}
return edges.sort((a, b) => b.similarity - a.similarity).slice(0, MAX_EDGE_COUNT * 2);
}
function summarizeEdge(left, right, similarity) {
const overlap = topKeywords(`${left.text} ${right.text}`, 3).join(", ");
return `Resonance between ${left.kind} and ${right.kind}${overlap ? ` around ${overlap}` : ""} (${round3(similarity)}).`;
}
export function buildClusters(records, edges) {
const adjacency = new Map();
for (const record of records) {
adjacency.set(record.memoryId, new Set());
}
for (const edge of edges) {
if (edge.similarity < CLUSTER_SIMILARITY_THRESHOLD) {
continue;
}
adjacency.get(edge.left.memoryId)?.add(edge.right.memoryId);
adjacency.get(edge.right.memoryId)?.add(edge.left.memoryId);
}
const visited = new Set();
const clusters = [];
for (const record of records) {
if (visited.has(record.memoryId)) {
continue;
}
const neighbors = adjacency.get(record.memoryId);
if (!neighbors || neighbors.size === 0) {
continue;
}
const queue = [record.memoryId];
const componentIds = [];
visited.add(record.memoryId);
while (queue.length > 0) {
const current = queue.shift();
if (!current) {
continue;
}
componentIds.push(current);
for (const neighbor of adjacency.get(current) ?? []) {
if (visited.has(neighbor)) {
continue;
}
visited.add(neighbor);
queue.push(neighbor);
}
}
if (componentIds.length < 2) {
continue;
}
const items = componentIds
.map((memoryId) => records.find((candidate) => candidate.memoryId === memoryId))
.filter(Boolean);
const sourceKinds = [...new Set(items.map((item) => item.kind))];
if (sourceKinds.length < 2) {
continue;
}
const quietSignalCount = items.filter((item) => item.lowSignalQuietRoom).length;
const quietSignalRatio = quietSignalCount / items.length;
const resonance = averagePairwiseSimilarity(items);
const keywords = topConceptKeywords(items.map((item) => item.text).join(" "), 5);
const repoNames = [...new Set(items.flatMap((item) => ensureStringArray(item.sourceMeta.repoNames)))];
const archiveYears = [
...new Set(
items
.map((item) => readString(item.sourceMeta, "archiveYear"))
.filter((value) => typeof value === "string" && value.length > 0),
),
];
const channelIds = [
...new Set(
items
.map((item) => readString(item.sourceMeta, "channelId"))
.filter((value) => typeof value === "string" && value.length > 0),
),
];
const evidenceRefs = capDistinctStrings(
items.flatMap((item) => ensureStringArray(item.sourceRefs)),
MAX_SUPPORTING_REFS,
);
const evidenceDiversity = clamp(
sourceKinds.length * 0.18 +
repoNames.length * 0.17 +
archiveYears.length * 0.11 +
channelIds.length * 0.08 +
Math.min(1, evidenceRefs.length / 8) * 0.16,
0,
1,
);
const curiosityProfile = buildClusterCuriosityProfile({
sourceKinds,
repoNames,
archiveYears,
channelIds,
evidenceRefs,
evidenceDiversity,
});
const preferredThoughtSurface = readPreferredClusterThoughtSurface(items);
const thoughtSurface = preferredThoughtSurface ?? synthesizeThoughtSurface({
sourceKinds,
repoNames,
archiveYears,
channelIds,
keywords,
evidenceDiversity,
curiosityProfile,
});
const cleanedLabel = sanitizePreferredThoughtLabel(thoughtSurface.label) ?? thoughtSurface.label;
const cleanedFocusPhrase =
sanitizePreferredThoughtLabel(thoughtSurface.focusPhrase) ?? thoughtSurface.focusPhrase;
clusters.push({
clusterId: `cluster-${hashString(componentIds.sort().join("|")).slice(0, 12)}`,
label: cleanedLabel,
summary: thoughtSurface.summary,
focusKind: thoughtSurface.focusKind,
targetKind: thoughtSurface.targetKind,
focusPhrase: cleanedFocusPhrase,
question: thoughtSurface.question,
claim: thoughtSurface.claim,
fascinationTarget: cleanedFocusPhrase,
worldFacing: thoughtSurface.worldFacing,
resonance: round3(resonance),
memoryIds: items.map((item) => item.memoryId).slice(0, MAX_CLUSTER_MEMORY_IDS),
sourceKinds,
topKeywords: keywords,
repoNames,
archiveYears,
channelIds,
evidenceRefs,
evidenceDiversity: round3(evidenceDiversity),
quietSignalRatio: round3(quietSignalRatio),
curiosityProfile,
lastStrengthenedAt: new Date().toISOString(),
});
}
return clusters
.sort((left, right) => right.resonance - left.resonance)
.slice(0, MAX_CLUSTER_COUNT);
}
function edgeThresholdForKinds(leftKind, rightKind) {
if (leftKind === "episodic" && rightKind === "episodic") {
return 0.86;
}
if (leftKind === rightKind) {
return EDGE_SIMILARITY_THRESHOLD + 0.05;
}
return EDGE_SIMILARITY_THRESHOLD;
}
export function buildClusterCuriosityProfile({
sourceKinds,
repoNames,
archiveYears,
channelIds,
evidenceRefs,
evidenceDiversity,
}) {
const sourceKindSet = new Set(sourceKinds);
const selfFacing =
repoNames.length === 0 &&
sourceKinds.length > 0 &&
sourceKinds.every((kind) =>
["semantic", "dream", "musing", "bridge_synthesis", "candidate_intervention"].includes(kind),
);
const domainCount =
(repoNames.length > 0 ? 1 : 0) +
(archiveYears.length > 0 || channelIds.length > 0 ? 1 : 0) +
(sourceKindSet.has("dream") || sourceKindSet.has("semantic") || sourceKindSet.has("musing") ? 1 : 0) +
(sourceKindSet.has("analytic_thread") || sourceKindSet.has("episodic") ? 1 : 0);
const concreteness = clamp(
repoNames.length * 0.28 +
archiveYears.length * 0.1 +
channelIds.length * 0.1 +
(sourceKindSet.has("repo_sweep") ? 0.24 : 0) +
(sourceKindSet.has("archive_excursion") ? 0.18 : 0) +
(sourceKindSet.has("episodic") ? 0.12 : 0) +
Math.min(1, evidenceRefs.length / 8) * 0.12 -
(selfFacing ? 0.18 : 0),
0,
1,
);
const fertility = clamp(
evidenceDiversity * 0.46 +
Math.min(1, sourceKinds.length / 4) * 0.24 +
Math.min(1, domainCount / 3) * 0.2 +
(repoNames.length > 0 && archiveYears.length > 0 ? 0.12 : 0),
0,
1,
);
const crossDomainPotential = clamp(
Math.min(1, domainCount / 3) * 0.5 +
(repoNames.length > 0 && archiveYears.length > 0 ? 0.22 : 0) +
(repoNames.length > 0 && sourceKindSet.has("dream") ? 0.18 : 0) +
(sourceKindSet.has("analytic_thread") && sourceKindSet.has("associative_thread") ? 0.14 : 0),
0,
1,
);
const contradictionPressure = clamp(
(sourceKindSet.has("analytic_thread") && sourceKindSet.has("associative_thread") ? 0.34 : 0) +
(repoNames.length > 0 && sourceKindSet.has("dream") ? 0.18 : 0) +
(repoNames.length > 0 && (archiveYears.length > 0 || channelIds.length > 0) ? 0.18 : 0) +
(selfFacing ? 0.08 : 0.16),
0,
1,
);
return {
concreteness: round3(concreteness),
fertility: round3(fertility),
crossDomainPotential: round3(crossDomainPotential),
contradictionPressure: round3(contradictionPressure),
selfFacing,
};
}
function readPreferredClusterThoughtSurface(items) {
const preferredEntry = items
.filter((item) => item.kind === "semantic" && isObject(item.entry))
.map((item) => item.entry)
.find((entry) => readString(entry, "targetKind") === "repo" && readString(entry, "subjectLabel"));
if (!preferredEntry) {
return null;
}
const label = readString(preferredEntry, "subjectLabel");
const focusPhrase = extractFocusPhraseFromLabel(label);
if (!label || !focusPhrase) {
return null;
}
return {
focusKind: readString(preferredEntry, "focusKind") ?? "claim",
targetKind: "repo",
focusPhrase,
label,
summary:
compactNarrative(readString(preferredEntry, "summary") ?? "") ||
`${label} keeps pulling as a concrete repo seam.`,
question:
readString(preferredEntry, "question") ??
`How should ${extractRepoNameFromLabel(label) ?? "this repo"} embody ${focusPhrase} concretely?`,
claim:
readString(preferredEntry, "claim") ??
`${extractRepoNameFromLabel(label) ?? "This repo"} is accumulating a concrete decision around ${focusPhrase}.`,
fascinationTarget: readString(preferredEntry, "fascinationTarget") ?? label,
worldFacing: readBoolean(preferredEntry, "worldFacing") ?? true,
};
}
function extractFocusPhraseFromLabel(label) {
const normalized = normalizeText(label);
if (!normalized) {
return null;
}
if (normalized.includes(":")) {
const [, rest] = normalized.split(/:\s*/, 2);
return rest ? rest.toLowerCase() : null;
}
return normalized.toLowerCase();
}
export function extractRepoNameFromLabel(label) {
const normalized = normalizeText(label);
if (!normalized || !normalized.includes(":")) {
return null;
}
const [prefix] = normalized.split(/:\s*/, 1);
if (!prefix || !/[A-Z]/.test(prefix)) {
return null;
}
return prefix;
}
export function synthesizeThoughtSurface({
sourceKinds,
repoNames,
archiveYears,
channelIds,
keywords,
evidenceDiversity,
curiosityProfile,
}) {
const targetKind = inferTargetKind({ sourceKinds, repoNames, archiveYears, channelIds, curiosityProfile });
const focusKind = inferFocusKind({ targetKind, sourceKinds, evidenceDiversity, curiosityProfile });
const focusPhrase = buildFocusPhrase({ targetKind, repoNames, keywords, sourceKinds });
const label = buildThoughtLabel({ focusKind, targetKind, focusPhrase, repoNames });
const question = buildThoughtQuestion({ targetKind, focusPhrase, repoNames, sourceKinds });
const claim = buildThoughtClaim({ targetKind, focusPhrase, repoNames, sourceKinds, curiosityProfile });
const fascinationTarget = buildFascinationTarget({ targetKind, focusPhrase, repoNames });
const summary = buildThoughtSummary({
focusKind,
targetKind,
focusPhrase,
repoNames,
sourceKinds,
claim,
question,
});
return {
focusKind,
targetKind,
focusPhrase,
label,
summary,
question,
claim,
fascinationTarget,
worldFacing: !curiosityProfile.selfFacing && targetKind !== "self",
};
}
function inferTargetKind({ sourceKinds, repoNames, archiveYears, channelIds, curiosityProfile }) {
if (repoNames.length > 0) {
return "repo";
}
if (curiosityProfile.selfFacing) {
return "self";
}
if (archiveYears.length > 0 || channelIds.length > 0 || sourceKinds.includes("archive_excursion")) {
return "archive";
}
if (sourceKinds.includes("episodic") || sourceKinds.includes("analytic_thread")) {
return "room";
}
return "system";
}
function inferFocusKind({ targetKind, sourceKinds, evidenceDiversity, curiosityProfile }) {
if (targetKind === "repo" || targetKind === "system") {
return curiosityProfile.concreteness >= 0.55 ? "fascination" : "question";
}
if (targetKind === "self") {
return "question";
}
if (evidenceDiversity >= 0.58 && sourceKinds.length >= 3) {
return "claim";
}
return "question";
}
function buildFocusPhrase({ targetKind, repoNames, keywords, sourceKinds }) {
const cleaned = keywords
.filter((token) => !stopwords.has(token) && !labelNoiseTokens.has(token))
.slice(0, 4);
if (
cleaned.includes("compression") ||
cleaned.includes("continuity") ||
cleaned.includes("receipts") ||
(cleaned.includes("dream") && cleaned.includes("semantic"))
) {
return "continuity after compression";
}
if (
cleaned.includes("authority") &&
(cleaned.includes("boundary") || cleaned.includes("product") || cleaned.includes("native"))
) {
return "authority boundary";
}
if (cleaned.includes("authorship") && cleaned.includes("audience")) {
return "authorship meeting audience";
}
if (cleaned.includes("embodiment") && (cleaned.includes("continuity") || cleaned.includes("compression"))) {
return "embodiment after compression";
}
if (repoNames.length > 0 && cleaned.length > 0) {
return cleaned.length >= 2 ? `${cleaned[0]} and ${cleaned[1]}` : cleaned[0];
}
if (targetKind === "archive" && cleaned.length > 0) {
return cleaned.length >= 2 ? `${cleaned[0]} and ${cleaned[1]}` : cleaned[0];
}
if (cleaned.length >= 3) {
return `${cleaned[0]}, ${cleaned[1]}, and ${cleaned[2]}`;
}
if (cleaned.length === 2) {
return `${cleaned[0]} and ${cleaned[1]}`;
}
if (cleaned.length === 1) {
return cleaned[0];
}
if (repoNames.length > 0) {
return `${repoNames[0]} shape`;
}
if (sourceKinds.includes("archive_excursion")) {
return "an archive pressure";
}
return "an unresolved pressure";
}
function buildThoughtLabel({ focusKind, targetKind, focusPhrase, repoNames }) {
const repoName = repoNames[0];
if (focusKind === "fascination" && repoName) {
return `${repoName}: ${capitalizePhrase(focusPhrase)}`;
}
if (targetKind === "self" || targetKind === "archive") {
return capitalizePhrase(focusPhrase);
}
if (focusKind === "claim") {
return `${capitalizePhrase(focusPhrase)} deserves a cleaner shape`;
}
return `${capitalizePhrase(focusPhrase)}`;
}
function buildThoughtQuestion({ targetKind, focusPhrase, repoNames, sourceKinds }) {
const repoName = repoNames[0];
if (targetKind === "repo" && repoName) {
return `How should ${repoName} embody ${focusPhrase} concretely?`;
}
if (targetKind === "self") {
return `Why does ${focusPhrase} keep surviving compression?`;
}
if (targetKind === "archive") {
return `What older pattern keeps pulling around ${focusPhrase}?`;
}
if (sourceKinds.includes("analytic_thread") && sourceKinds.includes("associative_thread")) {
return `What does ${focusPhrase} imply once the room-facing and associative lanes are forced to agree?`;
}
return `Where does ${focusPhrase} bite concretely?`;
}
function buildThoughtClaim({ targetKind, focusPhrase, repoNames, sourceKinds, curiosityProfile }) {
const repoName = repoNames[0];
if (targetKind === "repo" && repoName) {
return `${repoName} is accumulating a concrete decision around ${focusPhrase}.`;
}
if (targetKind === "self") {
return `${capitalizePhrase(focusPhrase)} is behaving like a persistent self-seam, not a passing thought.`;
}
if (curiosityProfile.crossDomainPotential >= 0.55 && sourceKinds.includes("repo_sweep") && sourceKinds.includes("archive_excursion")) {
return `${capitalizePhrase(focusPhrase)} links current work to older patterns strongly enough to deserve a concrete read.`;
}
return `${capitalizePhrase(focusPhrase)} is staying structurally important across passes.`;
}
function buildFascinationTarget({ targetKind, focusPhrase, repoNames }) {
const repoName = repoNames[0];
if (targetKind === "repo" && repoName) {
return `${repoName} around ${focusPhrase}`;
}
return focusPhrase;
}
function buildThoughtSummary({ focusKind, targetKind, focusPhrase, repoNames, sourceKinds, claim, question }) {
const sourceLine =
sourceKinds.length > 0 && sourceKinds.length <= 3 ? ` Built from ${sourceKinds.join(", ")}.` : "";
if (focusKind === "fascination" && repoNames.length > 0) {
return `${repoNames[0]} keeps pulling around ${focusPhrase}.${sourceLine}`.trim();
}
if (targetKind === "self") {
return `${capitalizePhrase(focusPhrase)} keeps surviving as a self-seam.${sourceLine}`.trim();
}
if (targetKind === "archive") {
return `${capitalizePhrase(focusPhrase)} keeps pulling in the archive.${sourceLine}`.trim();
}
if (focusKind === "claim") {
return `${capitalizePhrase(focusPhrase)} has become a concrete claim.${sourceLine}`.trim();
}
return `${capitalizePhrase(focusPhrase)} remains an unresolved question.${sourceLine}`.trim();
}
export function capitalizePhrase(value) {
const normalized = normalizeText(value);
return normalized.length > 0 ? normalized[0].toUpperCase() + normalized.slice(1) : value;
}
function mapCountEntries(map, keyName) {
return [...map.entries()]
.map(([key, count]) => ({ [keyName]: key, count }))
.sort((left, right) => right.count - left.count);
}