refactor(persistence): define v3 graph store shell

This commit is contained in:
youzini
2026-05-30 14:14:19 +00:00
parent f59114e403
commit 951fca2c99
4 changed files with 370 additions and 0 deletions

View File

@@ -0,0 +1,97 @@
// ST-BME v3 hard-cut namespace constants.
//
// These constants intentionally do not alias legacy st_bme/st-bme/STBME keys.
// Phase 6 introduces the namespace contract only; live routes are ported later.
export const GRAPH_V3_NAMESPACE_VERSION = 3;
export const GRAPH_V3_MODULE_NAME = "st_bme_v3";
export const GRAPH_V3_METADATA_KEY = `${GRAPH_V3_MODULE_NAME}_graph`;
export const GRAPH_V3_COMMIT_MARKER_KEY = `${GRAPH_V3_MODULE_NAME}_commit_marker`;
export const GRAPH_V3_CHAT_STATE_NAMESPACE = `${GRAPH_V3_MODULE_NAME}_graph_state`;
export const GRAPH_V3_LUKER_MANIFEST_NAMESPACE = `${GRAPH_V3_MODULE_NAME}_graph_manifest`;
export const GRAPH_V3_LUKER_JOURNAL_NAMESPACE = `${GRAPH_V3_MODULE_NAME}_graph_journal`;
export const GRAPH_V3_LUKER_CHECKPOINT_NAMESPACE = `${GRAPH_V3_MODULE_NAME}_graph_checkpoint`;
export const GRAPH_V3_SHADOW_SNAPSHOT_STORAGE_PREFIX = `${GRAPH_V3_MODULE_NAME}:graph-shadow:`;
export const GRAPH_V3_IDENTITY_ALIAS_STORAGE_KEY = `${GRAPH_V3_MODULE_NAME}:chat-identity-aliases`;
export const GRAPH_V3_INDEXEDDB_NAME_PREFIX = "ST_BME_V3";
export const GRAPH_V3_OPFS_ROOT_DIRECTORY_NAME = "stbme-v3";
export const GRAPH_V3_AUTHORITY_TABLES = Object.freeze({
meta: `${GRAPH_V3_MODULE_NAME}_graph_meta`,
nodes: `${GRAPH_V3_MODULE_NAME}_graph_nodes`,
edges: `${GRAPH_V3_MODULE_NAME}_graph_edges`,
tombstones: `${GRAPH_V3_MODULE_NAME}_graph_tombstones`,
});
export const GRAPH_LEGACY_NAMESPACE_VALUES = Object.freeze([
"st_bme",
"st_bme_graph",
"st_bme_commit_marker",
"st_bme_graph_state",
"st_bme_graph_manifest",
"st_bme_graph_journal",
"st_bme_graph_checkpoint",
"st_bme:graph-shadow:",
"st_bme:chat-identity-aliases",
"STBME_",
"st-bme",
"st_bme_graph_meta",
"st_bme_graph_nodes",
"st_bme_graph_edges",
"st_bme_graph_tombstones",
]);
function normalizeNamespaceSegment(value = "") {
return String(value ?? "")
.trim()
.replace(/[^a-zA-Z0-9_-]+/g, "_")
.replace(/^_+|_+$/g, "") || "default";
}
export function buildGraphV3IndexedDbName(chatId = "") {
return `${GRAPH_V3_INDEXEDDB_NAME_PREFIX}_${normalizeNamespaceSegment(chatId)}`;
}
export function buildGraphV3OpfsChatPath(chatId = "") {
return `${GRAPH_V3_OPFS_ROOT_DIRECTORY_NAME}/chats/${normalizeNamespaceSegment(chatId)}`;
}
export function buildGraphV3AuthorityPartition(graphId = "") {
return `${GRAPH_V3_MODULE_NAME}:${normalizeNamespaceSegment(graphId)}`;
}
export function listGraphV3NamespaceValues() {
return Object.freeze([
GRAPH_V3_MODULE_NAME,
GRAPH_V3_METADATA_KEY,
GRAPH_V3_COMMIT_MARKER_KEY,
GRAPH_V3_CHAT_STATE_NAMESPACE,
GRAPH_V3_LUKER_MANIFEST_NAMESPACE,
GRAPH_V3_LUKER_JOURNAL_NAMESPACE,
GRAPH_V3_LUKER_CHECKPOINT_NAMESPACE,
GRAPH_V3_SHADOW_SNAPSHOT_STORAGE_PREFIX,
GRAPH_V3_IDENTITY_ALIAS_STORAGE_KEY,
GRAPH_V3_INDEXEDDB_NAME_PREFIX,
GRAPH_V3_OPFS_ROOT_DIRECTORY_NAME,
...Object.values(GRAPH_V3_AUTHORITY_TABLES),
]);
}
export function validateGraphV3NamespaceIsolation(legacyValues = GRAPH_LEGACY_NAMESPACE_VALUES) {
const legacy = new Set((Array.isArray(legacyValues) ? legacyValues : []).map((value) => String(value)));
const conflicts = listGraphV3NamespaceValues().filter((value) => legacy.has(String(value)));
const unsafePrefixConflicts = [];
if (GRAPH_V3_INDEXEDDB_NAME_PREFIX.startsWith("STBME_")) {
unsafePrefixConflicts.push({ surface: "indexeddb", legacyPrefix: "STBME_" });
}
if (GRAPH_V3_OPFS_ROOT_DIRECTORY_NAME.startsWith("st-bme")) {
unsafePrefixConflicts.push({ surface: "opfs", legacyPrefix: "st-bme" });
}
return {
isolated: conflicts.length === 0 && unsafePrefixConflicts.length === 0,
conflicts,
unsafePrefixConflicts,
namespaceVersion: GRAPH_V3_NAMESPACE_VERSION,
};
}

View File

@@ -10,6 +10,7 @@
"test:identity-resolver": "node tests/identity-resolver.mjs",
"test:persistence-reducer": "node tests/persistence-reducer.mjs",
"test:graph-head": "node tests/graph-head.mjs",
"test:graph-store-contract": "node tests/graph-store-contract.mjs",
"test:reroll-transaction-boundary": "node tests/reroll-transaction-boundary.mjs",
"test:vector-gate": "node tests/vector-gate.mjs",
"test:hide-engine": "node tests/hide-engine.mjs",

View File

@@ -0,0 +1,140 @@
// ST-BME v3 GraphStore contract and pure router shell.
//
// Phase 6 only defines/validates the contract and route plans. Live adapters are
// ported in Phase 7 so durable routing is not switched accidentally.
export const GRAPH_STORE_CONTRACT_VERSION = 3;
export const GRAPH_STORE_KINDS = Object.freeze({
AUTHORITY: "authority",
OPFS: "opfs",
INDEXEDDB: "indexeddb",
LUKER_CHAT_STATE: "luker-chat-state",
NONE: "none",
});
export const GRAPH_STORE_REQUIRED_METHODS = Object.freeze([
"open",
"close",
"getMeta",
"patchMeta",
"commitDelta",
"exportSnapshot",
"exportSnapshotProbe",
"importSnapshot",
]);
export const GRAPH_STORE_OPTIONAL_METHODS = Object.freeze([
"readHead",
"writeHead",
"readCommitMarker",
"writeCommitMarker",
"isEmpty",
"deleteAll",
]);
function normalizeStoreKind(value = "") {
const kind = String(value || "").trim().toLowerCase();
if (Object.values(GRAPH_STORE_KINDS).includes(kind)) return kind;
return GRAPH_STORE_KINDS.NONE;
}
function methodExists(store = null, method = "") {
return store && typeof store[method] === "function";
}
export function inspectGraphStoreContract(store = null, options = {}) {
const requiredMethods = Array.isArray(options.requiredMethods)
? options.requiredMethods
: GRAPH_STORE_REQUIRED_METHODS;
const optionalMethods = Array.isArray(options.optionalMethods)
? options.optionalMethods
: GRAPH_STORE_OPTIONAL_METHODS;
const missingMethods = requiredMethods.filter((method) => !methodExists(store, method));
const supportedOptionalMethods = optionalMethods.filter((method) => methodExists(store, method));
return {
contractVersion: GRAPH_STORE_CONTRACT_VERSION,
valid: missingMethods.length === 0,
storeKind: normalizeStoreKind(store?.storeKind || store?.kind),
storeMode: String(store?.storeMode || store?.mode || ""),
missingMethods,
supportedOptionalMethods,
};
}
export function assertGraphStoreContract(store = null, options = {}) {
const inspection = inspectGraphStoreContract(store, options);
if (!inspection.valid) {
const error = new Error(`graph-store-contract-invalid:${inspection.missingMethods.join(",")}`);
error.code = "graph_store_contract_invalid";
error.contract = inspection;
throw error;
}
return inspection;
}
function normalizeBoolean(value) {
return value === true;
}
function normalizePreference(value = "") {
const normalized = String(value || "").trim().toLowerCase();
if (normalized === "authority-sql") return GRAPH_STORE_KINDS.AUTHORITY;
if (normalized === "opfs-primary" || normalized === "opfs-shadow") return GRAPH_STORE_KINDS.OPFS;
if (normalized === "indexeddb") return GRAPH_STORE_KINDS.INDEXEDDB;
if (normalized === "luker-chat-state") return GRAPH_STORE_KINDS.LUKER_CHAT_STATE;
return "auto";
}
function pushUniqueRoute(routes, kind, reason = "") {
const normalizedKind = normalizeStoreKind(kind);
if (!normalizedKind || normalizedKind === GRAPH_STORE_KINDS.NONE) return;
if (routes.some((route) => route.kind === normalizedKind)) return;
routes.push({ kind: normalizedKind, reason: String(reason || normalizedKind) });
}
export function planGraphStoreRoute(input = {}) {
const preference = normalizePreference(input.preference || input.primaryStorageTier || input.localStoreMode);
const capabilities = input.capabilities && typeof input.capabilities === "object" ? input.capabilities : {};
const environment = input.environment && typeof input.environment === "object" ? input.environment : {};
const hardCutNamespace = input.hardCutNamespace && typeof input.hardCutNamespace === "object"
? input.hardCutNamespace
: null;
const routes = [];
const authorityReady = normalizeBoolean(capabilities.authoritySqlReady || capabilities.storagePrimaryReady);
const opfsReady = normalizeBoolean(capabilities.opfsReady || capabilities.opfsAvailable);
const indexedDbReady =
normalizeBoolean(capabilities.indexedDbReady) || normalizeBoolean(capabilities.indexedDbAvailable);
const lukerReady = normalizeBoolean(environment.lukerChatStateReady || capabilities.lukerChatStateReady);
if (preference === GRAPH_STORE_KINDS.AUTHORITY && authorityReady) {
pushUniqueRoute(routes, GRAPH_STORE_KINDS.AUTHORITY, "preferred-authority-sql");
}
if (preference === GRAPH_STORE_KINDS.OPFS && opfsReady) {
pushUniqueRoute(routes, GRAPH_STORE_KINDS.OPFS, "preferred-opfs");
}
if (preference === GRAPH_STORE_KINDS.INDEXEDDB && indexedDbReady) {
pushUniqueRoute(routes, GRAPH_STORE_KINDS.INDEXEDDB, "preferred-indexeddb");
}
if (preference === GRAPH_STORE_KINDS.LUKER_CHAT_STATE && lukerReady) {
pushUniqueRoute(routes, GRAPH_STORE_KINDS.LUKER_CHAT_STATE, "preferred-luker-chat-state");
}
if (authorityReady) pushUniqueRoute(routes, GRAPH_STORE_KINDS.AUTHORITY, "authority-sql-ready");
if (opfsReady) pushUniqueRoute(routes, GRAPH_STORE_KINDS.OPFS, "opfs-ready");
if (indexedDbReady) pushUniqueRoute(routes, GRAPH_STORE_KINDS.INDEXEDDB, "indexeddb-ready");
if (lukerReady) pushUniqueRoute(routes, GRAPH_STORE_KINDS.LUKER_CHAT_STATE, "luker-chat-state-ready");
return {
contractVersion: GRAPH_STORE_CONTRACT_VERSION,
hardCut: true,
hotPathReadsLegacy: false,
namespace: hardCutNamespace,
primary: routes[0]?.kind || GRAPH_STORE_KINDS.NONE,
fallback: routes.slice(1).map((route) => route.kind),
routes,
blocked: routes.length === 0,
reason: routes.length ? routes[0].reason : "no-graph-store-route-ready",
};
}

View File

@@ -0,0 +1,132 @@
// ST-BME restrained rebirth — Phase 6 GraphStore contract/router shell tests.
import assert from "node:assert/strict";
import {
GRAPH_LEGACY_NAMESPACE_VALUES,
GRAPH_V3_AUTHORITY_TABLES,
GRAPH_V3_CHAT_STATE_NAMESPACE,
GRAPH_V3_COMMIT_MARKER_KEY,
GRAPH_V3_INDEXEDDB_NAME_PREFIX,
GRAPH_V3_LUKER_CHECKPOINT_NAMESPACE,
GRAPH_V3_LUKER_JOURNAL_NAMESPACE,
GRAPH_V3_LUKER_MANIFEST_NAMESPACE,
GRAPH_V3_METADATA_KEY,
GRAPH_V3_MODULE_NAME,
GRAPH_V3_OPFS_ROOT_DIRECTORY_NAME,
buildGraphV3AuthorityPartition,
buildGraphV3IndexedDbName,
buildGraphV3OpfsChatPath,
listGraphV3NamespaceValues,
validateGraphV3NamespaceIsolation,
} from "../graph/graph-v3-namespace.js";
import {
GRAPH_STORE_CONTRACT_VERSION,
GRAPH_STORE_KINDS,
assertGraphStoreContract,
inspectGraphStoreContract,
planGraphStoreRoute,
} from "../sync/graph-store-contract.js";
const v3Values = listGraphV3NamespaceValues();
assert.ok(v3Values.includes(GRAPH_V3_MODULE_NAME));
assert.ok(v3Values.includes(GRAPH_V3_METADATA_KEY));
assert.ok(v3Values.includes(GRAPH_V3_COMMIT_MARKER_KEY));
assert.ok(v3Values.includes(GRAPH_V3_CHAT_STATE_NAMESPACE));
assert.ok(v3Values.includes(GRAPH_V3_LUKER_MANIFEST_NAMESPACE));
assert.ok(v3Values.includes(GRAPH_V3_LUKER_JOURNAL_NAMESPACE));
assert.ok(v3Values.includes(GRAPH_V3_LUKER_CHECKPOINT_NAMESPACE));
assert.ok(v3Values.includes(GRAPH_V3_INDEXEDDB_NAME_PREFIX));
assert.ok(v3Values.includes(GRAPH_V3_OPFS_ROOT_DIRECTORY_NAME));
assert.ok(v3Values.includes(GRAPH_V3_AUTHORITY_TABLES.meta));
const isolation = validateGraphV3NamespaceIsolation();
assert.equal(isolation.isolated, true);
assert.deepEqual(isolation.conflicts, []);
assert.deepEqual(isolation.unsafePrefixConflicts, []);
for (const value of v3Values) {
assert.equal(
GRAPH_LEGACY_NAMESPACE_VALUES.includes(value),
false,
`v3 namespace must not reuse legacy value: ${value}`,
);
}
assert.equal(buildGraphV3IndexedDbName("chat/a b"), "ST_BME_V3_chat_a_b");
assert.equal(buildGraphV3OpfsChatPath("chat/a b"), "stbme-v3/chats/chat_a_b");
assert.equal(buildGraphV3IndexedDbName("chat").startsWith("STBME_"), false);
assert.equal(buildGraphV3OpfsChatPath("chat").startsWith("st-bme"), false);
assert.equal(buildGraphV3AuthorityPartition("graph/a b"), "st_bme_v3:graph_a_b");
console.log(" ✓ v3 hard-cut namespaces are isolated from legacy keys");
function createMockStore(extra = {}) {
return {
storeKind: "authority",
storeMode: "authority-sql-primary",
async open() {},
async close() {},
async getMeta() {},
async patchMeta() {},
async commitDelta() {},
async exportSnapshot() {},
async exportSnapshotProbe() {},
async importSnapshot() {},
...extra,
};
}
const contract = inspectGraphStoreContract(createMockStore({ async readHead() {} }));
assert.equal(contract.contractVersion, GRAPH_STORE_CONTRACT_VERSION);
assert.equal(contract.valid, true);
assert.equal(contract.storeKind, GRAPH_STORE_KINDS.AUTHORITY);
assert.deepEqual(contract.missingMethods, []);
assert.ok(contract.supportedOptionalMethods.includes("readHead"));
assert.doesNotThrow(() => assertGraphStoreContract(createMockStore()));
assert.throws(
() => assertGraphStoreContract(createMockStore({ commitDelta: undefined })),
/graph-store-contract-invalid:commitDelta/,
);
console.log(" ✓ GraphStore contract validates existing adapter-shaped stores");
const authorityPlan = planGraphStoreRoute({
preference: "authority-sql",
capabilities: { authoritySqlReady: true, opfsReady: true, indexedDbReady: true },
environment: { lukerChatStateReady: true },
hardCutNamespace: { moduleName: GRAPH_V3_MODULE_NAME },
});
assert.equal(authorityPlan.hardCut, true);
assert.equal(authorityPlan.hotPathReadsLegacy, false);
assert.equal(authorityPlan.primary, GRAPH_STORE_KINDS.AUTHORITY);
assert.deepEqual(authorityPlan.fallback, [
GRAPH_STORE_KINDS.OPFS,
GRAPH_STORE_KINDS.INDEXEDDB,
GRAPH_STORE_KINDS.LUKER_CHAT_STATE,
]);
assert.equal(authorityPlan.namespace.moduleName, GRAPH_V3_MODULE_NAME);
const lukerPlan = planGraphStoreRoute({
primaryStorageTier: "luker-chat-state",
capabilities: { authoritySqlReady: false, opfsReady: false, indexedDbReady: false },
environment: { lukerChatStateReady: true },
});
assert.equal(lukerPlan.primary, GRAPH_STORE_KINDS.LUKER_CHAT_STATE);
const blockedPlan = planGraphStoreRoute({
capabilities: { authoritySqlReady: false, opfsReady: false, indexedDbReady: false },
environment: { lukerChatStateReady: false },
});
assert.equal(blockedPlan.blocked, true);
assert.equal(blockedPlan.reason, "no-graph-store-route-ready");
const emptyCapabilityPlan = planGraphStoreRoute({});
assert.equal(
emptyCapabilityPlan.blocked,
true,
"Phase 6 shell must not assume IndexedDB readiness when callers omit capabilities",
);
console.log(" ✓ v3 router shell plans routes without switching live persistence");
console.log("graph-store-contract tests passed");