mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-06-14 02:40:45 +08:00
chore: remove abandoned hard-cut v3 namespace cluster
This commit is contained in:
22
README.md
22
README.md
@@ -43,8 +43,7 @@ ST-BME 的记忆图谱以「耐久快照」形式存储在各存储层(Indexed
|
|||||||
- `sync/graph-snapshot-schema.js`:冻结顶层键集合、`schemaVersion`、宽容解析(保留未知嵌套字段、丢弃未知顶层键);
|
- `sync/graph-snapshot-schema.js`:冻结顶层键集合、`schemaVersion`、宽容解析(保留未知嵌套字段、丢弃未知顶层键);
|
||||||
- `sync/graph-snapshot-upgrade.js`:`upgradeGraphSnapshotOnRead` 就地升级链(单调、幂等、不降级、不抛错),已接入 `buildGraphFromSnapshot` 加载路径;
|
- `sync/graph-snapshot-upgrade.js`:`upgradeGraphSnapshotOnRead` 就地升级链(单调、幂等、不降级、不抛错),已接入 `buildGraphFromSnapshot` 加载路径;
|
||||||
- `runtime/identity-resolver.js`:活动身份、图谱身份、排队身份和 marker 身份分离;
|
- `runtime/identity-resolver.js`:活动身份、图谱身份、排队身份和 marker 身份分离;
|
||||||
- `sync/persistence-reducer.js`:accepted / queued / pending 持久化状态机和事件 reducer;
|
- `sync/persistence-reducer.js`:accepted / queued / pending 持久化状态机和事件 reducer。
|
||||||
- `graph/graph-head.js`:GraphHead、ReplicaPointer 和 commit marker 纯模型。
|
|
||||||
|
|
||||||
> 贡献提示:新增图谱数据时,请加进 `meta` / `state` / 记录对象,**不要新增耐久快照顶层键**;只有在加入一个 `upgrade-on-read` 步骤时才提升 `GRAPH_SNAPSHOT_SCHEMA_VERSION`。`tests/graph-snapshot-schema.mjs`、`tests/snapshot-forward-compat.mjs` 是该契约的长期回归保护,请勿删除。
|
> 贡献提示:新增图谱数据时,请加进 `meta` / `state` / 记录对象,**不要新增耐久快照顶层键**;只有在加入一个 `upgrade-on-read` 步骤时才提升 `GRAPH_SNAPSHOT_SCHEMA_VERSION`。`tests/graph-snapshot-schema.mjs`、`tests/snapshot-forward-compat.mjs` 是该契约的长期回归保护,请勿删除。
|
||||||
|
|
||||||
@@ -803,8 +802,6 @@ ST-BME/
|
|||||||
├── graph/ # 图数据模型与领域状态
|
├── graph/ # 图数据模型与领域状态
|
||||||
│ ├── graph.js # 节点/边 CRUD、序列化、迁移
|
│ ├── graph.js # 节点/边 CRUD、序列化、迁移
|
||||||
│ ├── graph-persistence.js # 持久化常量、加载状态、身份别名
|
│ ├── graph-persistence.js # 持久化常量、加载状态、身份别名
|
||||||
│ ├── graph-head.js # GraphHead / ReplicaPointer / commit marker 纯模型
|
|
||||||
│ ├── graph-v3-namespace.js # 控制面命名空间常量
|
|
||||||
│ ├── schema.js # 节点和关系 Schema
|
│ ├── schema.js # 节点和关系 Schema
|
||||||
│ ├── memory-scope.js # 主客观作用域与空间区域
|
│ ├── memory-scope.js # 主客观作用域与空间区域
|
||||||
│ ├── knowledge-state.js # 认知归属、可见性、区域状态
|
│ ├── knowledge-state.js # 认知归属、可见性、区域状态
|
||||||
@@ -857,7 +854,6 @@ ST-BME/
|
|||||||
│ ├── identity-resolver.js # 身份解析核心
|
│ ├── identity-resolver.js # 身份解析核心
|
||||||
│ ├── runtime-state.js
|
│ ├── runtime-state.js
|
||||||
│ ├── reroll-transaction-boundary.js # reroll 召回复用事务边界
|
│ ├── reroll-transaction-boundary.js # reroll 召回复用事务边界
|
||||||
│ ├── rebirth-policy.mjs # v3 重生策略/门禁盘点
|
|
||||||
│ ├── settings-defaults.js
|
│ ├── settings-defaults.js
|
||||||
│ ├── generation-options.js
|
│ ├── generation-options.js
|
||||||
│ ├── planner-tag-utils.js
|
│ ├── planner-tag-utils.js
|
||||||
@@ -874,9 +870,7 @@ ST-BME/
|
|||||||
│ ├── persistence-reducer.js # 持久化 accepted/queued/pending reducer
|
│ ├── persistence-reducer.js # 持久化 accepted/queued/pending reducer
|
||||||
│ ├── legacy-persistence-repair.js # 旧状态安全修复策略
|
│ ├── legacy-persistence-repair.js # 旧状态安全修复策略
|
||||||
│ ├── graph-snapshot-schema.js # 耐久快照契约:冻结顶层键 + 宽容解析
|
│ ├── graph-snapshot-schema.js # 耐久快照契约:冻结顶层键 + 宽容解析
|
||||||
│ ├── graph-snapshot-upgrade.js # 快照 upgrade-on-read 就地升级链
|
│ └── graph-snapshot-upgrade.js # 快照 upgrade-on-read 就地升级链
|
||||||
│ ├── graph-store-contract.js # GraphStore 契约和路由计划
|
|
||||||
│ └── graph-store-v3-adapter.js # GraphStore head/marker 适配包装层
|
|
||||||
│
|
│
|
||||||
├── host/ # SillyTavern 宿主适配
|
├── host/ # SillyTavern 宿主适配
|
||||||
│ ├── event-binding.js
|
│ ├── event-binding.js
|
||||||
@@ -936,14 +930,10 @@ npm run test:p0
|
|||||||
控制面与数据格式专项:
|
控制面与数据格式专项:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run test:rebirth-phase0
|
|
||||||
npm run test:identity-resolver
|
npm run test:identity-resolver
|
||||||
npm run test:persistence-reducer
|
npm run test:persistence-reducer
|
||||||
npm run test:graph-head
|
|
||||||
npm run test:vector-gate
|
npm run test:vector-gate
|
||||||
npm run test:reroll-transaction-boundary
|
npm run test:reroll-transaction-boundary
|
||||||
npm run test:graph-store-contract
|
|
||||||
npm run test:graph-store-v3-adapter
|
|
||||||
npm run test:graph-snapshot-schema
|
npm run test:graph-snapshot-schema
|
||||||
npm run test:graph-snapshot-upgrade
|
npm run test:graph-snapshot-upgrade
|
||||||
npm run test:snapshot-forward-compat
|
npm run test:snapshot-forward-compat
|
||||||
@@ -998,11 +988,11 @@ npm run version:bump-manifest
|
|||||||
- **`tests/graph-persistence.mjs`**
|
- **`tests/graph-persistence.mjs`**
|
||||||
- 图谱持久化基础行为。
|
- 图谱持久化基础行为。
|
||||||
|
|
||||||
- **`tests/identity-resolver.mjs` / `tests/persistence-reducer.mjs` / `tests/graph-head.mjs`**
|
- **`tests/identity-resolver.mjs` / `tests/persistence-reducer.mjs`**
|
||||||
- v3 身份、持久化状态机和 GraphHead 控制面。
|
- 身份解析核心、持久化 accepted/queued/pending 状态机。
|
||||||
|
|
||||||
- **`tests/graph-store-contract.mjs` / `tests/graph-store-v3-adapter.mjs`**
|
- **`tests/graph-snapshot-schema.mjs` / `tests/snapshot-forward-compat.mjs`**
|
||||||
- v3 GraphStore 契约、命名空间隔离和适配器包装层。
|
- 耐久快照契约、宽容解析和真实存储向前兼容往返。
|
||||||
|
|
||||||
- **`tests/indexeddb-persistence.mjs`**
|
- **`tests/indexeddb-persistence.mjs`**
|
||||||
- IndexedDB 快照、增量提交、hydrate。
|
- IndexedDB 快照、增量提交、hydrate。
|
||||||
|
|||||||
@@ -1,261 +0,0 @@
|
|||||||
// ST-BME v3 GraphHead model.
|
|
||||||
//
|
|
||||||
// Pure helpers only. Phase 3 introduces the v3 data shape without switching
|
|
||||||
// storage routes. A GraphHead owns graph identity/revision/counts; replicas and
|
|
||||||
// commit markers are pointers to that head instead of competing authorities.
|
|
||||||
|
|
||||||
import { isAcceptedLegacyPersistenceTier } from "../sync/legacy-persistence-repair.js";
|
|
||||||
import { normalizeIdentityValue } from "../runtime/identity-resolver.js";
|
|
||||||
import { getGraphStats } from "./graph.js";
|
|
||||||
|
|
||||||
export const GRAPH_HEAD_FORMAT_VERSION = 3;
|
|
||||||
export const GRAPH_REPLICA_POINTER_FORMAT_VERSION = 3;
|
|
||||||
export const GRAPH_COMMIT_MARKER_V3_FORMAT_VERSION = 3;
|
|
||||||
|
|
||||||
function normalizeNonNegativeInteger(value = 0) {
|
|
||||||
const numeric = Number(value || 0);
|
|
||||||
if (!Number.isFinite(numeric) || numeric <= 0) return 0;
|
|
||||||
return Math.floor(numeric);
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeFloor(value = -1) {
|
|
||||||
const numeric = Number(value);
|
|
||||||
if (!Number.isFinite(numeric)) return -1;
|
|
||||||
return Math.floor(numeric);
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeUpdatedAt(value = "") {
|
|
||||||
return String(value || new Date().toISOString());
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeCounts(value = {}) {
|
|
||||||
return {
|
|
||||||
nodeCount: normalizeNonNegativeInteger(value.nodeCount ?? value.nodes),
|
|
||||||
edgeCount: normalizeNonNegativeInteger(value.edgeCount ?? value.edges),
|
|
||||||
archivedCount: normalizeNonNegativeInteger(value.archivedCount ?? value.archivedNodes),
|
|
||||||
tombstoneCount: normalizeNonNegativeInteger(value.tombstoneCount ?? value.tombstones),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function firstIdentity(...values) {
|
|
||||||
for (const value of values) {
|
|
||||||
const normalized = normalizeIdentityValue(value);
|
|
||||||
if (normalized) return normalized;
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function normalizeGraphHead(input = null, fallback = {}) {
|
|
||||||
const source = input && typeof input === "object" && !Array.isArray(input) ? input : {};
|
|
||||||
const fallbackSource =
|
|
||||||
fallback && typeof fallback === "object" && !Array.isArray(fallback) ? fallback : {};
|
|
||||||
const counts = normalizeCounts({
|
|
||||||
...(fallbackSource.counts || fallbackSource),
|
|
||||||
...(source.counts || source),
|
|
||||||
});
|
|
||||||
const integrity = firstIdentity(source.integrity, fallbackSource.integrity);
|
|
||||||
const chatId = firstIdentity(source.chatId, fallbackSource.chatId);
|
|
||||||
const graphId = firstIdentity(source.graphId, fallbackSource.graphId, integrity, chatId);
|
|
||||||
|
|
||||||
return {
|
|
||||||
formatVersion: GRAPH_HEAD_FORMAT_VERSION,
|
|
||||||
graphId,
|
|
||||||
chatId,
|
|
||||||
hostChatId: firstIdentity(source.hostChatId, fallbackSource.hostChatId),
|
|
||||||
integrity,
|
|
||||||
revision: normalizeNonNegativeInteger(source.revision ?? fallbackSource.revision),
|
|
||||||
schemaVersion: normalizeNonNegativeInteger(
|
|
||||||
source.schemaVersion ?? fallbackSource.schemaVersion,
|
|
||||||
),
|
|
||||||
lastProcessedAssistantFloor: normalizeFloor(
|
|
||||||
source.lastProcessedAssistantFloor ?? fallbackSource.lastProcessedAssistantFloor,
|
|
||||||
),
|
|
||||||
extractionCount: normalizeNonNegativeInteger(
|
|
||||||
source.extractionCount ?? fallbackSource.extractionCount,
|
|
||||||
),
|
|
||||||
counts,
|
|
||||||
updatedAt: normalizeUpdatedAt(source.updatedAt || fallbackSource.updatedAt),
|
|
||||||
reason: String(source.reason || fallbackSource.reason || ""),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildGraphHeadFromGraph(
|
|
||||||
graph = null,
|
|
||||||
{
|
|
||||||
graphId = "",
|
|
||||||
chatId = "",
|
|
||||||
hostChatId = "",
|
|
||||||
integrity = "",
|
|
||||||
revision = 0,
|
|
||||||
reason = "",
|
|
||||||
updatedAt = "",
|
|
||||||
} = {},
|
|
||||||
) {
|
|
||||||
const stats = graph ? getGraphStats(graph) : null;
|
|
||||||
const historyState = graph?.historyState || {};
|
|
||||||
return normalizeGraphHead({
|
|
||||||
graphId,
|
|
||||||
chatId: firstIdentity(chatId, historyState.chatId),
|
|
||||||
hostChatId,
|
|
||||||
integrity,
|
|
||||||
revision,
|
|
||||||
schemaVersion: graph?.version,
|
|
||||||
lastProcessedAssistantFloor: Number.isFinite(Number(historyState.lastProcessedAssistantFloor))
|
|
||||||
? Number(historyState.lastProcessedAssistantFloor)
|
|
||||||
: Number.isFinite(Number(stats?.lastProcessedSeq))
|
|
||||||
? Number(stats.lastProcessedSeq)
|
|
||||||
: -1,
|
|
||||||
extractionCount: historyState.extractionCount,
|
|
||||||
counts: {
|
|
||||||
nodeCount: stats?.activeNodes,
|
|
||||||
edgeCount: stats?.totalEdges,
|
|
||||||
archivedCount: stats?.archivedNodes,
|
|
||||||
tombstoneCount: stats?.tombstones,
|
|
||||||
},
|
|
||||||
updatedAt,
|
|
||||||
reason,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function normalizeReplicaPointer(input = null, fallback = {}) {
|
|
||||||
const source = input && typeof input === "object" && !Array.isArray(input) ? input : {};
|
|
||||||
const fallbackSource =
|
|
||||||
fallback && typeof fallback === "object" && !Array.isArray(fallback) ? fallback : {};
|
|
||||||
const storageTier = String(source.storageTier || fallbackSource.storageTier || "none")
|
|
||||||
.trim()
|
|
||||||
.toLowerCase() || "none";
|
|
||||||
const revision = normalizeNonNegativeInteger(source.revision ?? fallbackSource.revision);
|
|
||||||
const graphId = firstIdentity(source.graphId, fallbackSource.graphId);
|
|
||||||
const chatId = firstIdentity(source.chatId, fallbackSource.chatId);
|
|
||||||
const integrity = firstIdentity(source.integrity, fallbackSource.integrity);
|
|
||||||
const accepted =
|
|
||||||
source.accepted === true &&
|
|
||||||
revision > 0 &&
|
|
||||||
Boolean(graphId) &&
|
|
||||||
isAcceptedLegacyPersistenceTier(storageTier);
|
|
||||||
|
|
||||||
return {
|
|
||||||
formatVersion: GRAPH_REPLICA_POINTER_FORMAT_VERSION,
|
|
||||||
graphId,
|
|
||||||
revision,
|
|
||||||
storageTier,
|
|
||||||
accepted,
|
|
||||||
chatId,
|
|
||||||
integrity,
|
|
||||||
persistedAt: String(source.persistedAt || source.updatedAt || fallbackSource.persistedAt || ""),
|
|
||||||
source: String(source.source || fallbackSource.source || ""),
|
|
||||||
reason: String(source.reason || fallbackSource.reason || ""),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isReplicaAccepted(pointer = null) {
|
|
||||||
return normalizeReplicaPointer(pointer).accepted === true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildCommitMarkerV3({ head = null, replica = null, reason = "", persistedAt = "" } = {}) {
|
|
||||||
const normalizedHead = normalizeGraphHead(head);
|
|
||||||
const normalizedReplica = normalizeReplicaPointer(replica, {
|
|
||||||
graphId: normalizedHead.graphId,
|
|
||||||
revision: normalizedHead.revision,
|
|
||||||
chatId: normalizedHead.chatId,
|
|
||||||
integrity: normalizedHead.integrity,
|
|
||||||
reason,
|
|
||||||
persistedAt,
|
|
||||||
});
|
|
||||||
const replicaMatchesHead =
|
|
||||||
normalizedReplica.accepted === true &&
|
|
||||||
normalizedReplica.graphId === normalizedHead.graphId &&
|
|
||||||
normalizedReplica.revision === normalizedHead.revision;
|
|
||||||
return {
|
|
||||||
formatVersion: GRAPH_COMMIT_MARKER_V3_FORMAT_VERSION,
|
|
||||||
graphId: normalizedHead.graphId,
|
|
||||||
revision: normalizedHead.revision,
|
|
||||||
accepted: replicaMatchesHead,
|
|
||||||
storageTier: normalizedReplica.storageTier,
|
|
||||||
chatId: normalizedHead.chatId || normalizedReplica.chatId,
|
|
||||||
hostChatId: normalizedHead.hostChatId,
|
|
||||||
integrity: normalizedHead.integrity || normalizedReplica.integrity,
|
|
||||||
nodeCount: normalizedHead.counts.nodeCount,
|
|
||||||
edgeCount: normalizedHead.counts.edgeCount,
|
|
||||||
archivedCount: normalizedHead.counts.archivedCount,
|
|
||||||
tombstoneCount: normalizedHead.counts.tombstoneCount,
|
|
||||||
lastProcessedAssistantFloor: normalizedHead.lastProcessedAssistantFloor,
|
|
||||||
extractionCount: normalizedHead.extractionCount,
|
|
||||||
persistedAt: normalizedReplica.persistedAt || persistedAt || normalizedHead.updatedAt,
|
|
||||||
reason: String(reason || normalizedReplica.reason || normalizedHead.reason || ""),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function normalizeCommitMarkerV3(marker = null) {
|
|
||||||
if (!marker || typeof marker !== "object" || Array.isArray(marker)) return null;
|
|
||||||
const head = normalizeGraphHead({
|
|
||||||
graphId: marker.graphId,
|
|
||||||
chatId: marker.chatId,
|
|
||||||
hostChatId: marker.hostChatId,
|
|
||||||
integrity: marker.integrity,
|
|
||||||
revision: marker.revision,
|
|
||||||
lastProcessedAssistantFloor: marker.lastProcessedAssistantFloor,
|
|
||||||
extractionCount: marker.extractionCount,
|
|
||||||
counts: marker,
|
|
||||||
updatedAt: marker.persistedAt,
|
|
||||||
reason: marker.reason,
|
|
||||||
});
|
|
||||||
const replica = normalizeReplicaPointer({
|
|
||||||
graphId: head.graphId,
|
|
||||||
revision: head.revision,
|
|
||||||
storageTier: marker.storageTier,
|
|
||||||
accepted: marker.accepted,
|
|
||||||
chatId: head.chatId,
|
|
||||||
integrity: head.integrity,
|
|
||||||
persistedAt: marker.persistedAt,
|
|
||||||
reason: marker.reason,
|
|
||||||
});
|
|
||||||
return buildCommitMarkerV3({ head, replica, reason: marker.reason, persistedAt: marker.persistedAt });
|
|
||||||
}
|
|
||||||
|
|
||||||
export function graphHeadFromLegacyPersistenceMeta({ meta = null, graph = null } = {}) {
|
|
||||||
const legacyMeta = meta && typeof meta === "object" && !Array.isArray(meta) ? meta : {};
|
|
||||||
return buildGraphHeadFromGraph(graph, {
|
|
||||||
graphId: legacyMeta.graphId,
|
|
||||||
chatId: legacyMeta.chatId,
|
|
||||||
integrity: legacyMeta.integrity,
|
|
||||||
revision: legacyMeta.revision,
|
|
||||||
reason: legacyMeta.reason,
|
|
||||||
updatedAt: legacyMeta.updatedAt,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function graphHeadFromLegacyCommitMarker(marker = null) {
|
|
||||||
return normalizeGraphHead({
|
|
||||||
graphId: marker?.graphId,
|
|
||||||
chatId: marker?.chatId,
|
|
||||||
integrity: marker?.integrity,
|
|
||||||
revision: marker?.revision,
|
|
||||||
lastProcessedAssistantFloor: marker?.lastProcessedAssistantFloor,
|
|
||||||
extractionCount: marker?.extractionCount,
|
|
||||||
counts: marker,
|
|
||||||
updatedAt: marker?.persistedAt,
|
|
||||||
reason: marker?.reason,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test/importer/diagnostic bridge only. Do not use this in v3 runtime hot paths;
|
|
||||||
// v3 storage routes should write v3 GraphHead/ReplicaPointer directly.
|
|
||||||
export function commitMarkerV3ToLegacyMarker(marker = null) {
|
|
||||||
const normalized = normalizeCommitMarkerV3(marker);
|
|
||||||
if (!normalized) return null;
|
|
||||||
return {
|
|
||||||
revision: normalized.revision,
|
|
||||||
lastProcessedAssistantFloor: normalized.lastProcessedAssistantFloor,
|
|
||||||
extractionCount: normalized.extractionCount,
|
|
||||||
nodeCount: normalized.nodeCount,
|
|
||||||
edgeCount: normalized.edgeCount,
|
|
||||||
archivedCount: normalized.archivedCount,
|
|
||||||
persistedAt: normalized.persistedAt,
|
|
||||||
storageTier: normalized.storageTier,
|
|
||||||
accepted: normalized.accepted,
|
|
||||||
reason: normalized.reason,
|
|
||||||
chatId: normalized.chatId,
|
|
||||||
integrity: normalized.integrity,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
// 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_HEAD_KEY = `${GRAPH_V3_MODULE_NAME}_graph_head`;
|
|
||||||
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_HEAD_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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -6,12 +6,8 @@
|
|||||||
"test:triviumdb-poc": "node tests/triviumdb-poc.mjs",
|
"test:triviumdb-poc": "node tests/triviumdb-poc.mjs",
|
||||||
"test:runtime-history": "node tests/runtime-history.mjs",
|
"test:runtime-history": "node tests/runtime-history.mjs",
|
||||||
"test:graph-persistence": "node tests/graph-persistence.mjs",
|
"test:graph-persistence": "node tests/graph-persistence.mjs",
|
||||||
"test:rebirth-phase0": "node tests/rebirth-phase0.mjs",
|
|
||||||
"test:identity-resolver": "node tests/identity-resolver.mjs",
|
"test:identity-resolver": "node tests/identity-resolver.mjs",
|
||||||
"test:persistence-reducer": "node tests/persistence-reducer.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:graph-store-v3-adapter": "node tests/graph-store-v3-adapter.mjs",
|
|
||||||
"test:graph-snapshot-schema": "node tests/graph-snapshot-schema.mjs",
|
"test:graph-snapshot-schema": "node tests/graph-snapshot-schema.mjs",
|
||||||
"test:graph-snapshot-upgrade": "node tests/graph-snapshot-upgrade.mjs",
|
"test:graph-snapshot-upgrade": "node tests/graph-snapshot-upgrade.mjs",
|
||||||
"test:snapshot-forward-compat": "node tests/snapshot-forward-compat.mjs",
|
"test:snapshot-forward-compat": "node tests/snapshot-forward-compat.mjs",
|
||||||
@@ -41,8 +37,7 @@
|
|||||||
"test:authority:e2e:restore": "node tests/e2e/authority-checkpoint-restore.mjs",
|
"test:authority:e2e:restore": "node tests/e2e/authority-checkpoint-restore.mjs",
|
||||||
"test:authority:e2e:all": "npm run test:authority:e2e && npm run test:authority:e2e:diagnostics && npm run test:authority:e2e:restore",
|
"test:authority:e2e:all": "npm run test:authority:e2e && npm run test:authority:e2e:diagnostics && npm run test:authority:e2e:restore",
|
||||||
"test:all": "npm run test:stable",
|
"test:all": "npm run test:stable",
|
||||||
"check": "node scripts/check-syntax.mjs",
|
"check": "node scripts/check-syntax.mjs"
|
||||||
"rebirth:inventory": "node scripts/rebirth-phase0-inventory.mjs"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"triviumdb": "0.7.1"
|
"triviumdb": "0.7.1"
|
||||||
|
|||||||
@@ -1,128 +0,0 @@
|
|||||||
// ST-BME restrained rebirth policy.
|
|
||||||
//
|
|
||||||
// Phase 0 deliberately keeps this module side-effect free. It records the
|
|
||||||
// project-level cutover contract so later phases cannot quietly reintroduce a
|
|
||||||
// permanent legacy data-format compatibility layer.
|
|
||||||
|
|
||||||
export const REBIRTH_FORMAT_VERSION = 3;
|
|
||||||
|
|
||||||
export const V3_STORAGE_NAMESPACES = Object.freeze({
|
|
||||||
root: "st-bme-v3",
|
|
||||||
graph: "graph-v3",
|
|
||||||
commitMarker: "commit-marker-v3",
|
|
||||||
vectorManifest: "vector-manifest-v3",
|
|
||||||
authorityGraph: "authority-graph-v3",
|
|
||||||
lukerSidecar: "luker-graph-v3",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const LEGACY_DATA_RUNTIME_POLICY = Object.freeze({
|
|
||||||
permanentRuntimeLegacyRead: false,
|
|
||||||
darkReadDualWriteMigration: false,
|
|
||||||
allowedLegacyAccess: Object.freeze(["one-shot-importer", "explicit-export", "manual-reset"]),
|
|
||||||
fallbackWhenNoImporter: "rebuild-from-chat-history",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const LIVE_ADAPTER_TARGETS = Object.freeze([
|
|
||||||
"indexeddb",
|
|
||||||
"opfs",
|
|
||||||
"authority-sql",
|
|
||||||
"luker-chat-state",
|
|
||||||
"vector-manifest",
|
|
||||||
]);
|
|
||||||
|
|
||||||
export const LEGACY_DATA_SOURCES = Object.freeze([
|
|
||||||
Object.freeze({
|
|
||||||
id: "indexeddb-legacy",
|
|
||||||
kind: "graph-store",
|
|
||||||
runtimeAction: "ignore",
|
|
||||||
phase0Action: "inventory-or-export",
|
|
||||||
notes: "Old IndexedDB snapshots/migration stores must not be auto-read by v3 runtime.",
|
|
||||||
}),
|
|
||||||
Object.freeze({
|
|
||||||
id: "opfs-legacy",
|
|
||||||
kind: "graph-store",
|
|
||||||
runtimeAction: "ignore",
|
|
||||||
phase0Action: "inventory-or-export",
|
|
||||||
notes: "Old OPFS v1/v2 graph layouts require explicit import or reset.",
|
|
||||||
}),
|
|
||||||
Object.freeze({
|
|
||||||
id: "authority-sql-legacy",
|
|
||||||
kind: "server-graph-store",
|
|
||||||
runtimeAction: "ignore",
|
|
||||||
phase0Action: "inventory-or-export",
|
|
||||||
notes: "Authority v3 must use a graphId/schema-version namespace and reject old rows by default.",
|
|
||||||
}),
|
|
||||||
Object.freeze({
|
|
||||||
id: "luker-sidecar-legacy",
|
|
||||||
kind: "host-chat-state",
|
|
||||||
runtimeAction: "ignore",
|
|
||||||
phase0Action: "inventory-or-export",
|
|
||||||
notes: "Legacy Luker manifest/journal/checkpoint keys remain inert unless an importer reads them.",
|
|
||||||
}),
|
|
||||||
Object.freeze({
|
|
||||||
id: "metadata-full-legacy",
|
|
||||||
kind: "chat-metadata",
|
|
||||||
runtimeAction: "ignore",
|
|
||||||
phase0Action: "inventory-or-export",
|
|
||||||
notes: "Old full graph blobs in chat metadata are not a v3 runtime source.",
|
|
||||||
}),
|
|
||||||
Object.freeze({
|
|
||||||
id: "commit-marker-legacy",
|
|
||||||
kind: "chat-metadata",
|
|
||||||
runtimeAction: "ignore",
|
|
||||||
phase0Action: "inventory-or-export",
|
|
||||||
notes: "Old commit markers are evidence only for a one-shot importer, not v3 acceptance state.",
|
|
||||||
}),
|
|
||||||
Object.freeze({
|
|
||||||
id: "vector-manifest-legacy",
|
|
||||||
kind: "vector-state",
|
|
||||||
runtimeAction: "ignore",
|
|
||||||
phase0Action: "reset-or-rebuild",
|
|
||||||
notes: "Vectors are rebuildable; legacy vector manifests must not contaminate v3 graphId/vectorSpaceId.",
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
export const PHASE0_BACKUP_CHECKLIST = Object.freeze([
|
|
||||||
Object.freeze({
|
|
||||||
id: "manual-graph-export",
|
|
||||||
label: "Export current graph JSON from the ST-BME panel before enabling v3.",
|
|
||||||
source: "ui-actions-controller:onExportGraphController",
|
|
||||||
}),
|
|
||||||
Object.freeze({
|
|
||||||
id: "server-backup",
|
|
||||||
label: "If Authority/server backup is used, create a server backup envelope first.",
|
|
||||||
source: "sync/bme-sync:backupToServer",
|
|
||||||
}),
|
|
||||||
Object.freeze({
|
|
||||||
id: "authority-reset-plan",
|
|
||||||
label: "Plan an explicit Authority v3 namespace/reset so old SQL/blob/vector rows cannot be selected.",
|
|
||||||
source: "runtime/rebirth-policy:V3_STORAGE_NAMESPACES.authorityGraph",
|
|
||||||
}),
|
|
||||||
Object.freeze({
|
|
||||||
id: "legacy-import-decision",
|
|
||||||
label: "Decide per legacy source: one-shot import, export-only backup, rebuild from chat history, or discard.",
|
|
||||||
source: "runtime/rebirth-policy:LEGACY_DATA_SOURCES",
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
export function getRebirthPhase0Inventory() {
|
|
||||||
return {
|
|
||||||
formatVersion: REBIRTH_FORMAT_VERSION,
|
|
||||||
namespaces: { ...V3_STORAGE_NAMESPACES },
|
|
||||||
policy: {
|
|
||||||
permanentRuntimeLegacyRead: LEGACY_DATA_RUNTIME_POLICY.permanentRuntimeLegacyRead,
|
|
||||||
darkReadDualWriteMigration: LEGACY_DATA_RUNTIME_POLICY.darkReadDualWriteMigration,
|
|
||||||
allowedLegacyAccess: [...LEGACY_DATA_RUNTIME_POLICY.allowedLegacyAccess],
|
|
||||||
fallbackWhenNoImporter: LEGACY_DATA_RUNTIME_POLICY.fallbackWhenNoImporter,
|
|
||||||
},
|
|
||||||
liveAdapterTargets: [...LIVE_ADAPTER_TARGETS],
|
|
||||||
legacyDataSources: LEGACY_DATA_SOURCES.map((source) => ({ ...source })),
|
|
||||||
backupChecklist: PHASE0_BACKUP_CHECKLIST.map((item) => ({ ...item })),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function shouldV3RuntimeReadLegacySource(sourceId) {
|
|
||||||
const source = LEGACY_DATA_SOURCES.find((entry) => entry.id === sourceId);
|
|
||||||
if (!source) return false;
|
|
||||||
return source.runtimeAction === "read";
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
import { getRebirthPhase0Inventory } from "../runtime/rebirth-policy.mjs";
|
|
||||||
|
|
||||||
const inventory = getRebirthPhase0Inventory();
|
|
||||||
|
|
||||||
if (process.argv.includes("--json")) {
|
|
||||||
console.log(JSON.stringify(inventory, null, 2));
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("ST-BME v3 restrained rebirth — Phase 0 policy inventory / cutover checklist");
|
|
||||||
console.log(`formatVersion: ${inventory.formatVersion}`);
|
|
||||||
console.log("\nV3 namespaces:");
|
|
||||||
for (const [key, value] of Object.entries(inventory.namespaces)) {
|
|
||||||
console.log(` - ${key}: ${value}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("\nLegacy runtime policy:");
|
|
||||||
console.log(` permanentRuntimeLegacyRead: ${inventory.policy.permanentRuntimeLegacyRead}`);
|
|
||||||
console.log(` darkReadDualWriteMigration: ${inventory.policy.darkReadDualWriteMigration}`);
|
|
||||||
console.log(` allowedLegacyAccess: ${inventory.policy.allowedLegacyAccess.join(", ")}`);
|
|
||||||
console.log(` fallbackWhenNoImporter: ${inventory.policy.fallbackWhenNoImporter}`);
|
|
||||||
|
|
||||||
console.log("\nLive adapter targets to port (not rewrite):");
|
|
||||||
for (const target of inventory.liveAdapterTargets) {
|
|
||||||
console.log(` - ${target}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("\nLegacy data sources:");
|
|
||||||
for (const source of inventory.legacyDataSources) {
|
|
||||||
console.log(` - ${source.id}: runtime=${source.runtimeAction}, phase0=${source.phase0Action}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("\nBackup / cutover checklist:");
|
|
||||||
for (const item of inventory.backupChecklist) {
|
|
||||||
console.log(` - [${item.id}] ${item.label}`);
|
|
||||||
}
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
// 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",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
// ST-BME v3 GraphStore adapter wrappers.
|
|
||||||
//
|
|
||||||
// These wrappers add v3 head/marker sidecar methods to existing stores without
|
|
||||||
// changing their legacy load/save behavior. Physical namespace cutover is handled
|
|
||||||
// by dedicated constructors/routes later.
|
|
||||||
|
|
||||||
import {
|
|
||||||
GRAPH_V3_COMMIT_MARKER_KEY,
|
|
||||||
GRAPH_V3_HEAD_KEY,
|
|
||||||
} from "../graph/graph-v3-namespace.js";
|
|
||||||
import {
|
|
||||||
normalizeCommitMarkerV3,
|
|
||||||
normalizeGraphHead,
|
|
||||||
} from "../graph/graph-head.js";
|
|
||||||
import {
|
|
||||||
readGraphChatStateNamespaces,
|
|
||||||
writeGraphChatStatePayload,
|
|
||||||
} from "../graph/graph-persistence.js";
|
|
||||||
import { assertGraphStoreContract } from "./graph-store-contract.js";
|
|
||||||
|
|
||||||
const GRAPH_STORE_V3_WRAPPED = Symbol.for("st-bme.graph-store-v3-wrapped");
|
|
||||||
|
|
||||||
function bindStoreMethod(store = null, method = "") {
|
|
||||||
const value = store?.[method];
|
|
||||||
return typeof value === "function" ? value.bind(store) : value;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isGraphStoreV3Wrapped(store = null) {
|
|
||||||
return Boolean(store?.[GRAPH_STORE_V3_WRAPPED]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function wrapDbLikeGraphStoreV3(store = null) {
|
|
||||||
assertGraphStoreContract(store);
|
|
||||||
if (isGraphStoreV3Wrapped(store)) return store;
|
|
||||||
|
|
||||||
const wrapper = Object.create(store);
|
|
||||||
Object.defineProperty(wrapper, GRAPH_STORE_V3_WRAPPED, {
|
|
||||||
value: true,
|
|
||||||
enumerable: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const key of Reflect.ownKeys(store)) {
|
|
||||||
if (key === GRAPH_STORE_V3_WRAPPED) continue;
|
|
||||||
const descriptor = Object.getOwnPropertyDescriptor(store, key);
|
|
||||||
if (descriptor) Object.defineProperty(wrapper, key, descriptor);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const method of [
|
|
||||||
"open",
|
|
||||||
"close",
|
|
||||||
"getMeta",
|
|
||||||
"setMeta",
|
|
||||||
"patchMeta",
|
|
||||||
"commitDelta",
|
|
||||||
"exportSnapshot",
|
|
||||||
"exportSnapshotProbe",
|
|
||||||
"importSnapshot",
|
|
||||||
"isEmpty",
|
|
||||||
"clearAll",
|
|
||||||
]) {
|
|
||||||
if (typeof store[method] === "function") {
|
|
||||||
wrapper[method] = bindStoreMethod(store, method);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
wrapper.readHead = async ({ fallback = null } = {}) => {
|
|
||||||
const raw = await store.getMeta(GRAPH_V3_HEAD_KEY, null);
|
|
||||||
return raw == null ? fallback : normalizeGraphHead(raw, fallback || {});
|
|
||||||
};
|
|
||||||
|
|
||||||
wrapper.writeHead = async (head = null, { fallback = null } = {}) => {
|
|
||||||
const normalized = normalizeGraphHead(head, fallback || {});
|
|
||||||
await store.patchMeta({ [GRAPH_V3_HEAD_KEY]: normalized });
|
|
||||||
return normalized;
|
|
||||||
};
|
|
||||||
|
|
||||||
wrapper.readCommitMarker = async ({ fallback = null } = {}) => {
|
|
||||||
const raw = await store.getMeta(GRAPH_V3_COMMIT_MARKER_KEY, null);
|
|
||||||
return normalizeCommitMarkerV3(raw) || fallback;
|
|
||||||
};
|
|
||||||
|
|
||||||
wrapper.writeCommitMarker = async (marker = null) => {
|
|
||||||
const normalized = normalizeCommitMarkerV3(marker);
|
|
||||||
if (!normalized) {
|
|
||||||
const error = new Error("graph-store-v3-commit-marker-invalid");
|
|
||||||
error.code = "graph_store_v3_commit_marker_invalid";
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
await store.patchMeta({ [GRAPH_V3_COMMIT_MARKER_KEY]: normalized });
|
|
||||||
return normalized;
|
|
||||||
};
|
|
||||||
|
|
||||||
wrapper.deleteAll = async (...args) => {
|
|
||||||
if (typeof store.clearAll !== "function") {
|
|
||||||
const error = new Error("graph-store-v3-delete-all-unavailable");
|
|
||||||
error.code = "graph_store_v3_delete_all_unavailable";
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
return store.clearAll(...args);
|
|
||||||
};
|
|
||||||
|
|
||||||
return wrapper;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createLukerChatStateGraphStoreV3({
|
|
||||||
context = null,
|
|
||||||
chatStateTarget = null,
|
|
||||||
storeKind = "luker-chat-state",
|
|
||||||
storeMode = "luker-chat-state-v3",
|
|
||||||
} = {}) {
|
|
||||||
async function readNamespace(namespace = "", fallback = null) {
|
|
||||||
const payloads = await readGraphChatStateNamespaces(context, [namespace], {
|
|
||||||
target: chatStateTarget,
|
|
||||||
});
|
|
||||||
return payloads.get(namespace) ?? fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function writeNamespace(namespace = "", payload = null) {
|
|
||||||
const result = await writeGraphChatStatePayload(context, namespace, payload, {
|
|
||||||
target: chatStateTarget,
|
|
||||||
});
|
|
||||||
if (result?.ok !== true) {
|
|
||||||
const error = new Error(result?.reason || "luker-graph-store-v3-write-failed");
|
|
||||||
error.code = "luker_graph_store_v3_write_failed";
|
|
||||||
error.result = result;
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
return payload;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
storeKind,
|
|
||||||
storeMode,
|
|
||||||
async open() {
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
async close() {},
|
|
||||||
async getMeta(key = "", fallbackValue = null) {
|
|
||||||
return readNamespace(String(key || ""), fallbackValue);
|
|
||||||
},
|
|
||||||
async patchMeta(record = {}) {
|
|
||||||
const entries = Object.entries(record && typeof record === "object" ? record : {});
|
|
||||||
for (const [key, value] of entries) {
|
|
||||||
await writeNamespace(key, value);
|
|
||||||
}
|
|
||||||
return record;
|
|
||||||
},
|
|
||||||
async readHead({ fallback = null } = {}) {
|
|
||||||
const raw = await readNamespace(GRAPH_V3_HEAD_KEY, null);
|
|
||||||
return raw == null ? fallback : normalizeGraphHead(raw, fallback || {});
|
|
||||||
},
|
|
||||||
async writeHead(head = null, { fallback = null } = {}) {
|
|
||||||
const normalized = normalizeGraphHead(head, fallback || {});
|
|
||||||
await writeNamespace(GRAPH_V3_HEAD_KEY, normalized);
|
|
||||||
return normalized;
|
|
||||||
},
|
|
||||||
async readCommitMarker({ fallback = null } = {}) {
|
|
||||||
const raw = await readNamespace(GRAPH_V3_COMMIT_MARKER_KEY, null);
|
|
||||||
return normalizeCommitMarkerV3(raw) || fallback;
|
|
||||||
},
|
|
||||||
async writeCommitMarker(marker = null) {
|
|
||||||
const normalized = normalizeCommitMarkerV3(marker);
|
|
||||||
if (!normalized) {
|
|
||||||
const error = new Error("luker-graph-store-v3-commit-marker-invalid");
|
|
||||||
error.code = "luker_graph_store_v3_commit_marker_invalid";
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
await writeNamespace(GRAPH_V3_COMMIT_MARKER_KEY, normalized);
|
|
||||||
return normalized;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,180 +0,0 @@
|
|||||||
// ST-BME restrained rebirth — Phase 3 GraphHead model tests.
|
|
||||||
|
|
||||||
import assert from "node:assert/strict";
|
|
||||||
import { createEmptyGraph } from "../graph/graph.js";
|
|
||||||
import {
|
|
||||||
GRAPH_COMMIT_MARKER_V3_FORMAT_VERSION,
|
|
||||||
GRAPH_HEAD_FORMAT_VERSION,
|
|
||||||
buildCommitMarkerV3,
|
|
||||||
buildGraphHeadFromGraph,
|
|
||||||
commitMarkerV3ToLegacyMarker,
|
|
||||||
graphHeadFromLegacyCommitMarker,
|
|
||||||
graphHeadFromLegacyPersistenceMeta,
|
|
||||||
isReplicaAccepted,
|
|
||||||
normalizeCommitMarkerV3,
|
|
||||||
normalizeGraphHead,
|
|
||||||
normalizeReplicaPointer,
|
|
||||||
} from "../graph/graph-head.js";
|
|
||||||
|
|
||||||
const graph = createEmptyGraph();
|
|
||||||
graph.version = 9;
|
|
||||||
graph.historyState.chatId = "chat-a";
|
|
||||||
graph.historyState.lastProcessedAssistantFloor = 8.9;
|
|
||||||
graph.historyState.extractionCount = 3.2;
|
|
||||||
graph.lastProcessedSeq = 7;
|
|
||||||
graph.nodes.push(
|
|
||||||
{ id: "n1", type: "event", archived: false },
|
|
||||||
{ id: "n2", type: "event", archived: true },
|
|
||||||
);
|
|
||||||
graph.edges.push({ id: "e1", from: "n1", to: "n2" });
|
|
||||||
|
|
||||||
const head = buildGraphHeadFromGraph(graph, {
|
|
||||||
graphId: "graph-a",
|
|
||||||
chatId: "chat-a",
|
|
||||||
hostChatId: "host-chat-a",
|
|
||||||
integrity: "integrity-a",
|
|
||||||
revision: 12.7,
|
|
||||||
reason: "unit-test",
|
|
||||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
|
||||||
});
|
|
||||||
|
|
||||||
assert.equal(head.formatVersion, GRAPH_HEAD_FORMAT_VERSION);
|
|
||||||
assert.equal(head.graphId, "graph-a");
|
|
||||||
assert.equal(head.chatId, "chat-a");
|
|
||||||
assert.equal(head.hostChatId, "host-chat-a");
|
|
||||||
assert.equal(head.integrity, "integrity-a");
|
|
||||||
assert.equal(head.revision, 12);
|
|
||||||
assert.equal(head.schemaVersion, 9);
|
|
||||||
assert.equal(head.lastProcessedAssistantFloor, 8);
|
|
||||||
assert.equal(head.extractionCount, 3);
|
|
||||||
assert.deepEqual(head.counts, {
|
|
||||||
nodeCount: 1,
|
|
||||||
edgeCount: 1,
|
|
||||||
archivedCount: 1,
|
|
||||||
tombstoneCount: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(" ✓ GraphHead owns normalized graph identity, revision, and counts");
|
|
||||||
|
|
||||||
const acceptedPointer = normalizeReplicaPointer({
|
|
||||||
graphId: head.graphId,
|
|
||||||
revision: head.revision,
|
|
||||||
storageTier: "authority-sql",
|
|
||||||
accepted: true,
|
|
||||||
chatId: head.chatId,
|
|
||||||
integrity: head.integrity,
|
|
||||||
persistedAt: "2026-01-01T00:00:01.000Z",
|
|
||||||
});
|
|
||||||
assert.equal(isReplicaAccepted(acceptedPointer), true);
|
|
||||||
|
|
||||||
const unsafePointer = normalizeReplicaPointer({
|
|
||||||
graphId: head.graphId,
|
|
||||||
revision: head.revision,
|
|
||||||
storageTier: "metadata-full",
|
|
||||||
accepted: true,
|
|
||||||
});
|
|
||||||
assert.equal(unsafePointer.accepted, false);
|
|
||||||
assert.equal(isReplicaAccepted(unsafePointer), false);
|
|
||||||
|
|
||||||
const missingGraphIdPointer = normalizeReplicaPointer({
|
|
||||||
revision: head.revision,
|
|
||||||
storageTier: "authority-sql",
|
|
||||||
accepted: true,
|
|
||||||
});
|
|
||||||
assert.equal(
|
|
||||||
missingGraphIdPointer.accepted,
|
|
||||||
false,
|
|
||||||
"accepted replica pointers must carry graphId evidence",
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(" ✓ ReplicaPointer accepts only canonical storage tiers");
|
|
||||||
|
|
||||||
const marker = buildCommitMarkerV3({
|
|
||||||
head,
|
|
||||||
replica: acceptedPointer,
|
|
||||||
reason: "accepted-save",
|
|
||||||
});
|
|
||||||
assert.equal(marker.formatVersion, GRAPH_COMMIT_MARKER_V3_FORMAT_VERSION);
|
|
||||||
assert.equal(marker.graphId, "graph-a");
|
|
||||||
assert.equal(marker.revision, 12);
|
|
||||||
assert.equal(marker.accepted, true);
|
|
||||||
assert.equal(marker.storageTier, "authority-sql");
|
|
||||||
assert.equal(marker.nodeCount, 1);
|
|
||||||
assert.equal(marker.edgeCount, 1);
|
|
||||||
assert.equal(marker.archivedCount, 1);
|
|
||||||
assert.equal(marker.lastProcessedAssistantFloor, 8);
|
|
||||||
assert.equal(marker.extractionCount, 3);
|
|
||||||
|
|
||||||
assert.deepEqual(normalizeCommitMarkerV3(marker), marker);
|
|
||||||
|
|
||||||
const mismatchedReplicaMarker = buildCommitMarkerV3({
|
|
||||||
head,
|
|
||||||
replica: {
|
|
||||||
...acceptedPointer,
|
|
||||||
revision: head.revision - 1,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
assert.equal(
|
|
||||||
mismatchedReplicaMarker.accepted,
|
|
||||||
false,
|
|
||||||
"v3 marker must not accept head revision from a mismatched replica pointer",
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(" ✓ v3 commit marker is a small accepted replica pointer plus head diagnostics");
|
|
||||||
|
|
||||||
const legacyMarker = commitMarkerV3ToLegacyMarker(marker);
|
|
||||||
assert.deepEqual(legacyMarker, {
|
|
||||||
revision: 12,
|
|
||||||
lastProcessedAssistantFloor: 8,
|
|
||||||
extractionCount: 3,
|
|
||||||
nodeCount: 1,
|
|
||||||
edgeCount: 1,
|
|
||||||
archivedCount: 1,
|
|
||||||
persistedAt: acceptedPointer.persistedAt,
|
|
||||||
storageTier: "authority-sql",
|
|
||||||
accepted: true,
|
|
||||||
reason: "accepted-save",
|
|
||||||
chatId: "chat-a",
|
|
||||||
integrity: "integrity-a",
|
|
||||||
});
|
|
||||||
|
|
||||||
const headFromLegacyMarker = graphHeadFromLegacyCommitMarker(legacyMarker);
|
|
||||||
assert.equal(headFromLegacyMarker.revision, 12);
|
|
||||||
assert.equal(headFromLegacyMarker.counts.nodeCount, 1);
|
|
||||||
assert.equal(headFromLegacyMarker.counts.edgeCount, 1);
|
|
||||||
assert.equal(headFromLegacyMarker.graphId, "integrity-a");
|
|
||||||
|
|
||||||
const headFromLegacyMeta = graphHeadFromLegacyPersistenceMeta({
|
|
||||||
graph,
|
|
||||||
meta: {
|
|
||||||
revision: 9,
|
|
||||||
chatId: "meta-chat",
|
|
||||||
integrity: "meta-integrity",
|
|
||||||
updatedAt: "2026-01-02T00:00:00.000Z",
|
|
||||||
reason: "legacy-meta",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
assert.equal(headFromLegacyMeta.revision, 9);
|
|
||||||
assert.equal(headFromLegacyMeta.chatId, "meta-chat");
|
|
||||||
assert.equal(headFromLegacyMeta.graphId, "meta-integrity");
|
|
||||||
assert.equal(headFromLegacyMeta.counts.archivedCount, 1);
|
|
||||||
|
|
||||||
console.log(" ✓ legacy marker/meta can be converted without becoming runtime compatibility paths");
|
|
||||||
|
|
||||||
const normalizedHead = normalizeGraphHead({
|
|
||||||
revision: -5,
|
|
||||||
lastProcessedAssistantFloor: "bad",
|
|
||||||
counts: { nodeCount: -1, edgeCount: 2.9 },
|
|
||||||
});
|
|
||||||
assert.equal(normalizedHead.revision, 0);
|
|
||||||
assert.equal(normalizedHead.lastProcessedAssistantFloor, -1);
|
|
||||||
assert.equal(normalizedHead.counts.nodeCount, 0);
|
|
||||||
assert.equal(normalizedHead.counts.edgeCount, 2);
|
|
||||||
assert.equal(
|
|
||||||
head.counts.tombstoneCount,
|
|
||||||
0,
|
|
||||||
"tombstoneCount is reserved until a canonical tombstone collection is introduced",
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(" ✓ GraphHead normalization is safe for malformed inputs");
|
|
||||||
console.log("graph-head tests passed");
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
// 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");
|
|
||||||
@@ -1,190 +0,0 @@
|
|||||||
// ST-BME restrained rebirth — Phase 7 v3 GraphStore adapter tests.
|
|
||||||
|
|
||||||
import assert from "node:assert/strict";
|
|
||||||
import {
|
|
||||||
GRAPH_V3_COMMIT_MARKER_KEY,
|
|
||||||
GRAPH_V3_HEAD_KEY,
|
|
||||||
GRAPH_V3_METADATA_KEY,
|
|
||||||
} from "../graph/graph-v3-namespace.js";
|
|
||||||
import {
|
|
||||||
buildCommitMarkerV3,
|
|
||||||
normalizeGraphHead,
|
|
||||||
normalizeReplicaPointer,
|
|
||||||
} from "../graph/graph-head.js";
|
|
||||||
import { GRAPH_STORE_REQUIRED_METHODS, inspectGraphStoreContract } from "../sync/graph-store-contract.js";
|
|
||||||
import {
|
|
||||||
createLukerChatStateGraphStoreV3,
|
|
||||||
isGraphStoreV3Wrapped,
|
|
||||||
wrapDbLikeGraphStoreV3,
|
|
||||||
} from "../sync/graph-store-v3-adapter.js";
|
|
||||||
|
|
||||||
function createMockDbLikeStore() {
|
|
||||||
const meta = new Map();
|
|
||||||
const calls = [];
|
|
||||||
return {
|
|
||||||
storeKind: "indexeddb",
|
|
||||||
storeMode: "indexeddb-v3-test",
|
|
||||||
meta,
|
|
||||||
calls,
|
|
||||||
async open() {
|
|
||||||
calls.push(["open"]);
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
async close() {
|
|
||||||
calls.push(["close"]);
|
|
||||||
},
|
|
||||||
async getMeta(key, fallbackValue = null) {
|
|
||||||
calls.push(["getMeta", key]);
|
|
||||||
return meta.has(key) ? meta.get(key) : fallbackValue;
|
|
||||||
},
|
|
||||||
async patchMeta(record = {}) {
|
|
||||||
calls.push(["patchMeta", Object.keys(record).sort()]);
|
|
||||||
for (const [key, value] of Object.entries(record)) {
|
|
||||||
meta.set(key, value);
|
|
||||||
}
|
|
||||||
return record;
|
|
||||||
},
|
|
||||||
async commitDelta() {},
|
|
||||||
async exportSnapshot() {},
|
|
||||||
async exportSnapshotProbe() {},
|
|
||||||
async importSnapshot() {},
|
|
||||||
async clearAll() {
|
|
||||||
calls.push(["clearAll"]);
|
|
||||||
meta.clear();
|
|
||||||
return { ok: true };
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const rawStore = createMockDbLikeStore();
|
|
||||||
const wrapped = wrapDbLikeGraphStoreV3(rawStore);
|
|
||||||
assert.equal(isGraphStoreV3Wrapped(wrapped), true);
|
|
||||||
assert.equal(wrapDbLikeGraphStoreV3(wrapped), wrapped);
|
|
||||||
|
|
||||||
const wrappedContract = inspectGraphStoreContract(wrapped);
|
|
||||||
assert.equal(wrappedContract.valid, true);
|
|
||||||
assert.ok(wrappedContract.supportedOptionalMethods.includes("readHead"));
|
|
||||||
assert.ok(wrappedContract.supportedOptionalMethods.includes("writeCommitMarker"));
|
|
||||||
assert.ok(wrappedContract.supportedOptionalMethods.includes("deleteAll"));
|
|
||||||
|
|
||||||
const head = normalizeGraphHead({
|
|
||||||
graphId: "graph-a",
|
|
||||||
chatId: "chat-a",
|
|
||||||
integrity: "integrity-a",
|
|
||||||
revision: 9,
|
|
||||||
counts: { nodeCount: 2, edgeCount: 1 },
|
|
||||||
});
|
|
||||||
const writtenHead = await wrapped.writeHead(head);
|
|
||||||
assert.equal(writtenHead.graphId, "graph-a");
|
|
||||||
assert.deepEqual(await wrapped.readHead(), writtenHead);
|
|
||||||
assert.deepEqual(rawStore.meta.get(GRAPH_V3_HEAD_KEY), writtenHead);
|
|
||||||
assert.equal(rawStore.meta.has(GRAPH_V3_METADATA_KEY), false, "head must use dedicated v3 head key");
|
|
||||||
|
|
||||||
const marker = buildCommitMarkerV3({
|
|
||||||
head,
|
|
||||||
replica: normalizeReplicaPointer({
|
|
||||||
graphId: head.graphId,
|
|
||||||
revision: head.revision,
|
|
||||||
storageTier: "indexeddb",
|
|
||||||
accepted: true,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
const writtenMarker = await wrapped.writeCommitMarker(marker);
|
|
||||||
assert.equal(writtenMarker.accepted, true);
|
|
||||||
assert.deepEqual(await wrapped.readCommitMarker(), writtenMarker);
|
|
||||||
assert.deepEqual(rawStore.meta.get(GRAPH_V3_COMMIT_MARKER_KEY), writtenMarker);
|
|
||||||
assert.equal(rawStore.meta.has("st_bme_commit_marker"), false, "legacy marker key must stay untouched");
|
|
||||||
|
|
||||||
await wrapped.deleteAll();
|
|
||||||
assert.equal(rawStore.meta.size, 0);
|
|
||||||
|
|
||||||
console.log(" ✓ DB-like v3 wrapper adds head/marker methods without legacy key writes");
|
|
||||||
|
|
||||||
class ClassBackedStore {
|
|
||||||
constructor() {
|
|
||||||
this.storeKind = "opfs";
|
|
||||||
this.storeMode = "class-backed-test";
|
|
||||||
this.meta = new Map();
|
|
||||||
this.clearCount = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
async open() {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
async close() {}
|
|
||||||
|
|
||||||
async getMeta(key, fallbackValue = null) {
|
|
||||||
assert.equal(this instanceof ClassBackedStore, true, "wrapped methods must keep class instance this");
|
|
||||||
return this.meta.has(key) ? this.meta.get(key) : fallbackValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
async patchMeta(record = {}) {
|
|
||||||
assert.equal(this instanceof ClassBackedStore, true, "patchMeta must run on the original class instance");
|
|
||||||
for (const [key, value] of Object.entries(record)) {
|
|
||||||
this.meta.set(key, value);
|
|
||||||
}
|
|
||||||
return record;
|
|
||||||
}
|
|
||||||
|
|
||||||
async commitDelta() {}
|
|
||||||
async exportSnapshot() {}
|
|
||||||
async exportSnapshotProbe() {}
|
|
||||||
async importSnapshot() {}
|
|
||||||
|
|
||||||
async clearAll() {
|
|
||||||
assert.equal(this instanceof ClassBackedStore, true, "deleteAll must delegate to class-backed clearAll");
|
|
||||||
this.clearCount += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const classBackedRaw = new ClassBackedStore();
|
|
||||||
const classBackedWrapped = wrapDbLikeGraphStoreV3(classBackedRaw);
|
|
||||||
await classBackedWrapped.writeHead(head);
|
|
||||||
assert.equal((await classBackedWrapped.readHead()).graphId, "graph-a");
|
|
||||||
await classBackedWrapped.deleteAll();
|
|
||||||
assert.equal(classBackedRaw.clearCount, 1);
|
|
||||||
|
|
||||||
console.log(" ✓ DB-like wrapper preserves class-instance method binding");
|
|
||||||
|
|
||||||
const chatState = new Map();
|
|
||||||
const updatedNamespaces = [];
|
|
||||||
const lukerContext = {
|
|
||||||
getChatState(namespace) {
|
|
||||||
return chatState.get(namespace) || null;
|
|
||||||
},
|
|
||||||
getChatStateBatch(namespaces) {
|
|
||||||
return new Map(namespaces.map((namespace) => [namespace, chatState.get(namespace) || null]));
|
|
||||||
},
|
|
||||||
updateChatState(namespace, updater) {
|
|
||||||
updatedNamespaces.push(namespace);
|
|
||||||
const next = updater(chatState.get(namespace) || null);
|
|
||||||
chatState.set(namespace, next);
|
|
||||||
return { ok: true, updated: true };
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const lukerStore = createLukerChatStateGraphStoreV3({ context: lukerContext });
|
|
||||||
const lukerContract = inspectGraphStoreContract(lukerStore, {
|
|
||||||
requiredMethods: ["open", "close", "getMeta", "patchMeta", "readHead", "writeHead", "readCommitMarker", "writeCommitMarker"],
|
|
||||||
});
|
|
||||||
assert.equal(lukerContract.valid, true);
|
|
||||||
for (const requiredMethod of GRAPH_STORE_REQUIRED_METHODS) {
|
|
||||||
if (["commitDelta", "exportSnapshot", "exportSnapshotProbe", "importSnapshot"].includes(requiredMethod)) {
|
|
||||||
assert.equal(
|
|
||||||
typeof lukerStore[requiredMethod],
|
|
||||||
"undefined",
|
|
||||||
`Luker thin wrapper must not claim unsupported DB-like method ${requiredMethod}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await lukerStore.writeHead(head);
|
|
||||||
await lukerStore.writeCommitMarker(marker);
|
|
||||||
assert.deepEqual(await lukerStore.readHead(), head);
|
|
||||||
assert.deepEqual(await lukerStore.readCommitMarker(), marker);
|
|
||||||
assert.deepEqual(updatedNamespaces, [GRAPH_V3_HEAD_KEY, GRAPH_V3_COMMIT_MARKER_KEY]);
|
|
||||||
assert.equal(chatState.has("st_bme_graph_state"), false, "Luker wrapper must not write legacy chat-state namespace");
|
|
||||||
|
|
||||||
console.log(" ✓ Luker v3 wrapper writes only v3 head/marker namespaces");
|
|
||||||
console.log("graph-store-v3-adapter tests passed");
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
// ST-BME restrained rebirth — Phase 0 policy characterization.
|
|
||||||
|
|
||||||
import assert from "node:assert/strict";
|
|
||||||
import {
|
|
||||||
LEGACY_DATA_RUNTIME_POLICY,
|
|
||||||
LEGACY_DATA_SOURCES,
|
|
||||||
PHASE0_BACKUP_CHECKLIST,
|
|
||||||
REBIRTH_FORMAT_VERSION,
|
|
||||||
V3_STORAGE_NAMESPACES,
|
|
||||||
getRebirthPhase0Inventory,
|
|
||||||
shouldV3RuntimeReadLegacySource,
|
|
||||||
} from "../runtime/rebirth-policy.mjs";
|
|
||||||
|
|
||||||
assert.equal(REBIRTH_FORMAT_VERSION, 3);
|
|
||||||
|
|
||||||
for (const [key, namespace] of Object.entries(V3_STORAGE_NAMESPACES)) {
|
|
||||||
assert.match(namespace, /v3/, `${key} namespace must be versioned as v3`);
|
|
||||||
}
|
|
||||||
assert.equal(new Set(Object.values(V3_STORAGE_NAMESPACES)).size, Object.keys(V3_STORAGE_NAMESPACES).length);
|
|
||||||
|
|
||||||
console.log(" ✓ v3 namespaces are explicit and collision-resistant");
|
|
||||||
|
|
||||||
assert.equal(LEGACY_DATA_RUNTIME_POLICY.permanentRuntimeLegacyRead, false);
|
|
||||||
assert.equal(LEGACY_DATA_RUNTIME_POLICY.darkReadDualWriteMigration, false);
|
|
||||||
assert.deepEqual(LEGACY_DATA_RUNTIME_POLICY.allowedLegacyAccess, [
|
|
||||||
"one-shot-importer",
|
|
||||||
"explicit-export",
|
|
||||||
"manual-reset",
|
|
||||||
]);
|
|
||||||
assert.equal(LEGACY_DATA_RUNTIME_POLICY.fallbackWhenNoImporter, "rebuild-from-chat-history");
|
|
||||||
|
|
||||||
for (const source of LEGACY_DATA_SOURCES) {
|
|
||||||
assert.equal(source.runtimeAction, "ignore", `${source.id} must remain inert for v3 runtime`);
|
|
||||||
assert.equal(
|
|
||||||
shouldV3RuntimeReadLegacySource(source.id),
|
|
||||||
false,
|
|
||||||
`${source.id} must not be read by the v3 runtime`,
|
|
||||||
);
|
|
||||||
assert.notEqual(source.phase0Action, "runtime-read", `${source.id} must not plan a runtime read`);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const requiredSource of [
|
|
||||||
"metadata-full-legacy",
|
|
||||||
"commit-marker-legacy",
|
|
||||||
"vector-manifest-legacy",
|
|
||||||
"authority-sql-legacy",
|
|
||||||
]) {
|
|
||||||
assert.ok(
|
|
||||||
LEGACY_DATA_SOURCES.some((source) => source.id === requiredSource),
|
|
||||||
`${requiredSource} must be represented in Phase 0 policy`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(" ✓ legacy sources are inert for the v3 runtime");
|
|
||||||
|
|
||||||
const inventory = getRebirthPhase0Inventory();
|
|
||||||
assert.equal(inventory.formatVersion, 3);
|
|
||||||
assert.equal(inventory.policy.permanentRuntimeLegacyRead, false);
|
|
||||||
assert.equal(inventory.policy.fallbackWhenNoImporter, "rebuild-from-chat-history");
|
|
||||||
assert.equal(inventory.namespaces.authorityGraph, "authority-graph-v3");
|
|
||||||
assert.equal(inventory.namespaces.lukerSidecar, "luker-graph-v3");
|
|
||||||
assert.ok(inventory.legacyDataSources.length >= 6);
|
|
||||||
assert.ok(PHASE0_BACKUP_CHECKLIST.some((item) => item.id === "manual-graph-export"));
|
|
||||||
assert.ok(PHASE0_BACKUP_CHECKLIST.some((item) => item.id === "authority-reset-plan"));
|
|
||||||
|
|
||||||
console.log(" ✓ Phase 0 inventory exposes backup and cutover gates");
|
|
||||||
console.log("rebirth-phase0 tests passed");
|
|
||||||
Reference in New Issue
Block a user