diff --git a/.gitignore b/.gitignore index c2d00e5..517740b 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ docs/BME六大功能全景解析.xlsx ST-BME_backup_6f78abcb-9aea-45b1-a8ad-fbbd8e4075f0-cx4dad.json plans/mvu-extra-analysis-guard.md tests/.tmp-*/ +plans/deep-optimization-design.md diff --git a/README.md b/README.md index e64be7c..998d890 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ ST-BME(Bionic Memory Ecology)是一个 **SillyTavern 第三方前端扩展** - [主要配置](#主要配置) - [记忆模型](#记忆模型) - [长聊天与历史安全](#长聊天与历史安全) +- [v3 重生计划与数据格式硬切](#v3-重生计划与数据格式硬切) - [常用操作速查](#常用操作速查) - [数据存储与同步](#数据存储与同步) - [排障指南](#排障指南) @@ -25,6 +26,27 @@ ST-BME(Bionic Memory Ecology)是一个 **SillyTavern 第三方前端扩展** --- +## v3 重生计划与数据格式硬切 + +ST-BME 正在推进一次“克制版重生”:身份解析、持久化确认、GraphHead 数据模型和持久化路由外壳会被重建为更干净的 v3 核心;Luker、OPFS、IndexedDB、Authority、向量等仍在使用的适配能力会平移到新接口。 + +这次重生会采用 **v3 新命名空间 + 新数据格式硬切**: + +- v3 运行时不会把旧 IndexedDB / OPFS / Luker sidecar / metadata-full / 旧 commit marker 当作热路径数据源; +- 不会保留永久的“旧格式读取层”或 dark-read/dual-write 兼容层; +- 如需保留旧图谱,请在切换前通过面板“导出图谱”或服务器备份功能先导出/备份; +- 后续最多提供一次性导入/重置工具,旧数据导入后工具可移除; +- 如果不导入旧图谱,v3 会从聊天历史重新提取/重建,但这会消耗 LLM 调用,且不保证完全复现旧图谱; +- v3 数据可能无法被旧版本读取,切换或回滚前请保留导出/备份。 + +当前阶段的策略盘点 / 切换清单命令(不会扫描浏览器或服务器实际数据): + +```bash +npm run rebirth:inventory +``` + +--- + ## 核心能力 - **自动记忆提取** diff --git a/package.json b/package.json index 1390fd7..29550d0 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "test:triviumdb-poc": "node tests/triviumdb-poc.mjs", "test:runtime-history": "node tests/runtime-history.mjs", "test:graph-persistence": "node tests/graph-persistence.mjs", + "test:rebirth-phase0": "node tests/rebirth-phase0.mjs", "test:hide-engine": "node tests/hide-engine.mjs", "test:maintenance-journal": "node tests/maintenance-journal.mjs", "test:indexeddb-persistence": "node tests/indexeddb-persistence.mjs", @@ -29,7 +30,8 @@ "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: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": { "triviumdb": "0.7.1" diff --git a/runtime/rebirth-policy.mjs b/runtime/rebirth-policy.mjs new file mode 100644 index 0000000..92dd6fe --- /dev/null +++ b/runtime/rebirth-policy.mjs @@ -0,0 +1,128 @@ +// 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"; +} diff --git a/scripts/rebirth-phase0-inventory.mjs b/scripts/rebirth-phase0-inventory.mjs new file mode 100644 index 0000000..b7ea967 --- /dev/null +++ b/scripts/rebirth-phase0-inventory.mjs @@ -0,0 +1,38 @@ +#!/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}`); +} diff --git a/tests/rebirth-phase0.mjs b/tests/rebirth-phase0.mjs new file mode 100644 index 0000000..7df7f38 --- /dev/null +++ b/tests/rebirth-phase0.mjs @@ -0,0 +1,67 @@ +// 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");