diff --git a/sync/bme-db.js b/sync/bme-db.js index d70533a..ba72860 100644 --- a/sync/bme-db.js +++ b/sync/bme-db.js @@ -1,4 +1,9 @@ import { createEmptyGraph, deserializeGraph } from "../graph/graph.js"; +import { normalizeMemoryScope } from "../graph/memory-scope.js"; +import { + normalizeStoryTime, + normalizeStoryTimeSpan, +} from "../graph/story-timeline.js"; import { buildVectorCollectionId, cloneGraphPersistDirtyState, @@ -508,6 +513,49 @@ function cloneHydrateSnapshotEdgeRecords(records = []) { return output; } +function isNormalizedSnapshotNodeRecord(record = null) { + if (!record || typeof record !== "object" || Array.isArray(record)) { + return false; + } + if (!Array.isArray(record.seqRange) || record.seqRange.length < 2) { + return false; + } + if (!Array.isArray(record.childIds) || !Array.isArray(record.clusters)) { + return false; + } + if (normalizeMemoryScope(record.scope) !== record.scope) { + return false; + } + if (normalizeStoryTime(record.storyTime) !== record.storyTime) { + return false; + } + if (normalizeStoryTimeSpan(record.storyTimeSpan) !== record.storyTimeSpan) { + return false; + } + return true; +} + +function isNormalizedSnapshotEdgeRecord(record = null) { + if (!record || typeof record !== "object" || Array.isArray(record)) { + return false; + } + return normalizeMemoryScope(record.scope) === record.scope; +} + +function areSnapshotRecordsNormalized(snapshotView = {}) { + for (const node of toArray(snapshotView?.nodes)) { + if (!isNormalizedSnapshotNodeRecord(node)) { + return false; + } + } + for (const edge of toArray(snapshotView?.edges)) { + if (!isNormalizedSnapshotEdgeRecord(edge)) { + return false; + } + } + return true; +} + function toMetaMap(rows = []) { const output = {}; for (const row of rows) { @@ -2906,7 +2954,8 @@ export function buildGraphFromSnapshot(snapshot, options = {}) { {}, ); const snapshotRecordsNormalized = - snapshotMeta?.[BME_RUNTIME_RECORDS_NORMALIZED_META_KEY] === true; + snapshotMeta?.[BME_RUNTIME_RECORDS_NORMALIZED_META_KEY] === true && + areSnapshotRecordsNormalized(snapshotView); const nativeHydrateGate = options?.useNativeHydrate === true ? evaluateNativeHydrateGate(snapshotView, options) diff --git a/tests/indexeddb-persistence.mjs b/tests/indexeddb-persistence.mjs index a5876fd..71a82b7 100644 --- a/tests/indexeddb-persistence.mjs +++ b/tests/indexeddb-persistence.mjs @@ -734,6 +734,16 @@ async function testGraphSnapshotConverters() { const rebuiltLegacyCompatible = buildGraphFromSnapshot(legacyCompatibleSnapshot, { chatId: "chat-a", }); + const malformedButFlaggedSnapshot = { + ...legacyCompatibleSnapshot, + meta: { + ...legacyCompatibleSnapshot.meta, + [BME_RUNTIME_RECORDS_NORMALIZED_META_KEY]: true, + }, + }; + const rebuiltMalformedButFlagged = buildGraphFromSnapshot(malformedButFlaggedSnapshot, { + chatId: "chat-a", + }); assert.equal(rebuilt.historyState.lastProcessedAssistantFloor, 9); assert.equal(rebuilt.historyState.extractionCount, 4); assert.equal(rebuilt.nodes.length, 1); @@ -751,6 +761,9 @@ async function testGraphSnapshotConverters() { assert.equal(rebuiltLegacyCompatible.nodes[0].scope?.layer, "objective"); assert.equal(rebuiltLegacyCompatible.nodes[0].storyTime?.tense, "unknown"); assert.equal(rebuiltLegacyCompatible.nodes[0].storyTimeSpan?.mixed, false); + assert.equal(rebuiltMalformedButFlagged.nodes[0].scope?.layer, "objective"); + assert.equal(rebuiltMalformedButFlagged.nodes[0].storyTime?.tense, "unknown"); + assert.equal(rebuiltMalformedButFlagged.nodes[0].storyTimeSpan?.mixed, false); rebuilt.nodes[0].fields.title = "Mutated Converter Node"; rebuilt.nodes[0].embedding[0] = 99; diff --git a/vector/vector-index.js b/vector/vector-index.js index b7fde57..fd0b19f 100644 --- a/vector/vector-index.js +++ b/vector/vector-index.js @@ -302,14 +302,18 @@ export function buildNodeVectorText(node) { const scope = normalizeMemoryScope(node?.scope); const scopeText = describeMemoryScope(scope); + const regionPath = Array.isArray(scope?.regionPath) ? scope.regionPath : []; + const regionSecondary = Array.isArray(scope?.regionSecondary) + ? scope.regionSecondary + : []; if (scopeText) { parts.push(`memory_scope: ${scopeText}`); } - if (scope.regionPath.length > 0) { - parts.push(`memory_region_path: ${scope.regionPath.join(" / ")}`); + if (regionPath.length > 0) { + parts.push(`memory_region_path: ${regionPath.join(" / ")}`); } - if (scope.regionSecondary.length > 0) { - parts.push(`memory_region_secondary: ${scope.regionSecondary.join(", ")}`); + if (regionSecondary.length > 0) { + parts.push(`memory_region_secondary: ${regionSecondary.join(", ")}`); } return parts.join(" | ").trim();