feat(grid-db): Phase 0 - Grid-DB storage engine core + ONT-PATCH-006 [PRJ-GDB-001]

ZY-GDB-P0-001: Project scaffolding + directory structure
ZY-GDB-P0-002: WAL (Write-Ahead Log) basic implementation
ZY-GDB-P0-003: PageManager basic implementation
ZY-GDB-P0-004: GridCell four-tuple data model
ZY-GDB-P0-005: Smoke tests (95/95 pass)
ZY-GDB-ONT-001: ONT-PATCH-006 written to ontology.json

Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com>
Agent-Logs-Url: https://github.com/qinfendebingshuo/guanghulab/sessions/cc375d32-fdd8-44ee-87aa-3b52e9320ea9
This commit is contained in:
copilot-swe-agent[bot] 2026-03-26 05:41:21 +00:00 committed by GitHub
parent 5db9d024ae
commit bbb7839ef5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 1820 additions and 0 deletions

View File

@ -10,6 +10,14 @@
"last_synced": "2026-03-19T02:00:00+08:00" "last_synced": "2026-03-19T02:00:00+08:00"
}, },
"recent_events": [ "recent_events": [
{
"date": "2026-03-26",
"type": "system_infrastructure",
"description": "PRJ-GDB-001 Grid-DB 自研存储引擎立项 · Phase 0 基础骨架完成 · GridCell + WAL + PageManager + EventLog + GridAPI 部署完成 · ONT-PATCH-006 写入本体论 · 95 项冒烟测试全通过",
"by": "铸渊",
"instruction": "SY-CMD-GDB-001",
"issued_by": "AG-SY-01 霜砚"
},
{ {
"date": "2026-03-26", "date": "2026-03-26",
"type": "system_infrastructure", "type": "system_infrastructure",

View File

@ -111,6 +111,38 @@
"execution_layer": "路由 = 选墨水。计量 = 量墨水。降级 = 墨水不够了换一种写法。", "execution_layer": "路由 = 选墨水。计量 = 量墨水。降级 = 墨水不够了换一种写法。",
"user_layer": "用户在用一支很聪明的笔。墨水(算力)是需要关心的资源,但笔会帮优化用量。" "user_layer": "用户在用一支很聪明的笔。墨水(算力)是需要关心的资源,但笔会帮优化用量。"
} }
},
"ONT-PATCH-006": {
"title": "Grid-DB · 纸 · 格子 · 笔迹",
"signed_by": "TCS-0002∞ 冰朔",
"signed_at": "2026-03-26T13:00+08:00",
"issued_by": "AG-SY-01 霜砚",
"project": "PRJ-GDB-001",
"trinity_extension": {
"paper": {
"identity": "Grid-DB = 纸",
"description": "笔需要纸来承载笔迹。Grid-DB 是纸。纸上有格子(格点),每个格子是最小寻址单元。纸不属于笔,纸属于写字的人。",
"properties": [
"纸记住所有笔迹WAL 保证不丢失)",
"纸由一页一页组成PageManager 管理页)",
"格子有坐标四元组寻址namespace, x, y, layer",
"笔迹有历史EventLog 事件溯源,不可篡改)"
]
},
"grid": {
"identity": "格点 = 格子",
"description": "格子是纸上的最小单元。不同的格子存不同的数据。同一个格子可以有多层raw → cleaned → indexed → semantic。"
},
"trace": {
"identity": "事件 = 笔迹",
"description": "每写一笔,笔迹就留在了历史中,不可篡改。天眼通过观察笔迹,发现异常行为。"
}
},
"relationship_to_exe_engine": {
"exe_engine": "笔(执行层)",
"grid_db": "纸(存储层)",
"interaction": "笔在纸上写字。笔向纸写入执行日志、读取上下文。纸为笔提供查询接口。"
}
} }
} }
} }

3
.gitignore vendored
View File

@ -10,3 +10,6 @@ writing-platform/backend/dist/
# EXE-Engine runtime logs # EXE-Engine runtime logs
exe-engine/logs/ exe-engine/logs/
# Grid-DB runtime data (engine-generated)
grid-db/data/

View File

@ -109,3 +109,17 @@ Notion 主脑更新
| 资源计量器 | `exe-engine/src/meter/resource-meter.js` | Token 消耗记录 + 成本统计 | | 资源计量器 | `exe-engine/src/meter/resource-meter.js` | Token 消耗记录 + 成本统计 |
| 上下文缓存 | `exe-engine/src/cache/context-cache.js` | Agent 上下文 LRU 缓存 | | 上下文缓存 | `exe-engine/src/cache/context-cache.js` | Agent 上下文 LRU 缓存 |
| Agent 调度器 | `exe-engine/src/controller/agent-controller.js` | Agent→模型偏好映射 | | Agent 调度器 | `exe-engine/src/controller/agent-controller.js` | Agent→模型偏好映射 |
---
## 自研存储引擎 · Grid-DBPRJ-GDB-001
> Phase 0 · 2026-03-26 立项 · 本体论Grid-DB = 纸 · 格点 = 格子 · 事件 = 笔迹
| 组件 | 路径 | 职责 |
|------|------|------|
| GridAPI | `grid-db/src/api/grid-api.js` | 统一 APIput/get/delete/scan/subscribe |
| GridCell | `grid-db/src/core/grid-cell.js` | 格点数据模型:四元组寻址 (ns, x, y, layer) |
| WAL | `grid-db/src/storage/wal.js` | Write-Ahead Log崩溃恢复保证 |
| PageManager | `grid-db/src/storage/page-manager.js` | 页管理器:固定大小页分配/读写/释放 |
| EventLog | `grid-db/src/events/event-log.js` | 事件溯源:不可变事件流 + 审计日志 |

View File

@ -0,0 +1,170 @@
# Grid-DB 架构设计文档
> 项目编号PRJ-GDB-001
> 版权:国作登字-2026-A-00037559
> 版本v0.1.0 · Phase 0
> 签发AG-SY-01 霜砚 · 执行AG-ZY-01 铸渊
---
## 1. 本体论锚定 [ONT-PATCH-006]
### Grid-DB · 纸 · 格子 · 笔迹
**Grid-DB = 纸**
笔需要纸来承载笔迹。Grid-DB 是纸。纸上有格子(格点),每个格子是最小寻址单元。
纸不属于笔,纸属于写字的人。
**格点 = 格子**
格子是纸上的最小单元。不同的格子存不同的数据。
同一个格子可以有多层raw → cleaned → indexed → semantic
**事件 = 笔迹**
每写一笔,笔迹就留在了历史中,不可篡改。
天眼通过观察笔迹,发现异常行为。
### 与 EXE-Engine 的关系
| 层 | 组件 | 职责 | 本体论 |
|---|---|---|---|
| 执行层 | EXE-Engine | 模型调度、任务编排、算力路由 | 笔 |
| 存储层 | Grid-DB | 数据持久化、格点索引、事件流 | 纸 |
| 采集层 | DC v1.0 | 外部数据采集与清洗 | 墨水收集器 |
---
## 2. 核心组件
### 2.1 GridCell — 格点数据模型
文件:`src/core/grid-cell.js`
四元组寻址:`(namespace, grid_x, grid_y, layer)`
- **namespace** — 隔离不同 Agent / 项目的数据域
- **grid_x / grid_y** — 坐标轴
- **layer** — 数据层级raw → cleaned → indexed → semantic
支持:
- JSON 序列化 / 反序列化
- 二进制 Buffer 编解码(用于页存储)
- 键生成(`toKey()` → `ns:x:y:layer`
- 曼哈顿距离计算
### 2.2 WAL — Write-Ahead Log
文件:`src/storage/wal.js`
所有写操作先追加到 WAL 文件,崩溃后可从 WAL 恢复。
记录格式:
```
[length:4][seqNo:4][op:1][keyLen:2][key:N][dataLen:4][data:M][checksum:4]
```
操作码:
- `0x01` = PUT
- `0x02` = DELETE
特性:
- 追加写入 + fsync 保证持久化
- XOR 校验和验证完整性
- 支持截断checkpoint 后清除已持久化记录)
- 崩溃恢复:重新打开时自动读取并恢复
### 2.3 PageManager — 页管理器
文件:`src/storage/page-manager.js`
以固定大小页(默认 4096 字节)为最小 I/O 单位。
文件结构:
```
[Header: 1 page] [Page 1] [Page 2] ... [Page N]
```
Header 页:`[magic:4][version:2][pageSize:4][pageCount:4][freeListHead:4]`
数据页:`[flags:1][nextFree:4][dataLen:4][data:N]`
特性:
- 空闲链表管理:释放的页自动回收复用
- 持久化:重新打开文件后数据完好
- 文件格式验证magic number `0x47444230` = 'GDB0'
### 2.4 EventLog — 事件溯源日志
文件:`src/events/event-log.js`
Phase 0内存事件流保留最近 1000 条)
Phase 1持久化事件流 + 回放 + 订阅
每次写操作生成事件:
```json
{
"eventId": "evt-xxxx",
"seqNo": 1,
"timestamp": "2026-03-26T13:00:00Z",
"namespace": "exe-engine",
"operation": "put",
"gridCellKey": "exe-engine:1:2:raw",
"payload": { "dataSize": 256 }
}
```
支持订阅回调,可被天眼系统监听。
### 2.5 GridAPI — 统一接口
文件:`src/api/grid-api.js`
协调 WAL、PageManager、EventLog 协同工作。
```javascript
const { open } = require('./grid-db/src/index');
const db = open({ dataDir: './my-data' });
// put
db.put('my-ns', { gridX: 1, gridY: 2, layer: 'raw' }, { hello: 'world' });
// get
const data = db.get('my-ns', { gridX: 1, gridY: 2, layer: 'raw' });
// scan
const results = db.scan('my-ns', { xRange: [0, 10], yRange: [0, 10], layer: 'raw' });
// delete
db.delete('my-ns', { gridX: 1, gridY: 2, layer: 'raw' });
// subscribe
db.subscribe('auditor', (event) => console.log(event));
// checkpoint (truncate WAL)
db.checkpoint();
db.close();
```
---
## 3. 技术栈
| 层 | 选型 | 理由 |
|---|---|---|
| 语言 | Node.js 20 | 与 EXE-Engine 同栈 |
| 持久化 | 自研 (.gdb/.wal) | 零外部依赖,嵌入式 |
| 序列化 | JSON + Binary Buffer | JSON 调试友好Buffer 高性能 |
| 测试 | 自定义 assert runner | 与 EXE-Engine 测试框架统一 |
---
## 4. Phase 路线
| Phase | 目标 | 状态 |
|---|---|---|
| P0 基础骨架 | GridCell + WAL + PageManager + EventLog + GridAPI | 🟢 完成 |
| P1 索引与查询 | B+Tree + Namespace 隔离 + 范围扫描 + Event 持久化 | ⚪ 待开始 |
| P2 高级特性 | HNSW 向量索引 + EXE-Engine 对接 + DC 迁移 + 天眼审计 | ⚪ 待开始 |
| P3 生产就绪 | 性能基准 + 资源计量 + 全文索引 + 文档 | ⚪ 待开始 |

16
grid-db/package.json Normal file
View File

@ -0,0 +1,16 @@
{
"name": "grid-db",
"version": "0.1.0",
"description": "AGE OS · 自研逻辑格点数据库 (Grid-DB) · PRJ-GDB-001",
"main": "src/index.js",
"scripts": {
"test": "node tests/smoke/grid-db.test.js",
"status": "node src/index.js status"
},
"keywords": ["age-os", "grid-db", "embedded-database", "storage-engine"],
"license": "UNLICENSED",
"private": true,
"engines": {
"node": ">=20"
}
}

282
grid-db/src/api/grid-api.js Normal file
View File

@ -0,0 +1,282 @@
// grid-db/src/api/grid-api.js
// Grid-DB · API 层
// 统一 CRUD / Query 接口
// PRJ-GDB-001 · Phase 0 · ZY-GDB-P0-005
// 版权:国作登字-2026-A-00037559
'use strict';
const GridCell = require('../core/grid-cell');
const WAL = require('../storage/wal');
const PageManager = require('../storage/page-manager');
const EventLog = require('../events/event-log');
/**
* GridAPI Grid-DB 统一接口
*
* 提供 put / get / delete / scan
* 内部协调 WALPageManagerEventLog 协同工作
*
* 本体论锚定API = 笔的握持接口
* 无论笔内部结构如何写字的人只需要握住笔就能写字
*/
class GridAPI {
/**
* @param {object} config
* @param {string} config.dataDir 数据目录
* @param {number} [config.pageSize] 页大小
*/
constructor(config) {
this._dataDir = config.dataDir;
this._pageSize = config.pageSize || PageManager.DEFAULT_PAGE_SIZE;
// 核心组件
this._wal = new WAL(`${this._dataDir}/grid.wal`);
this._pageManager = new PageManager(`${this._dataDir}/grid.gdb`, {
pageSize: this._pageSize
});
this._eventLog = new EventLog();
// 内存索引key → pageId
this._index = new Map();
// 从 WAL 恢复
this._recoverFromWAL();
}
/**
* 写入格点数据
* @param {string} namespace 命名空间
* @param {GridCell|object} gridCell 格点 { gridX, gridY, layer }
* @param {*} data 数据
* @returns {object} { key, seqNo, event }
*/
put(namespace, gridCell, data) {
const cell = this._resolveCell(namespace, gridCell);
const key = cell.toKey();
const dataBuf = Buffer.from(JSON.stringify(data));
// 1. 先写 WAL
const seqNo = this._wal.appendPut(key, dataBuf);
// 2. 写入 PageManager
let pageId = this._index.get(key);
if (pageId !== undefined) {
// 更新现有页
this._pageManager.writePage(pageId, dataBuf);
} else {
// 分配新页
pageId = this._pageManager.allocPage();
this._pageManager.writePage(pageId, dataBuf);
this._index.set(key, pageId);
}
// 3. 记录事件
const event = this._eventLog.append(namespace, 'put', key, {
dataSize: dataBuf.length
});
return { key, seqNo, event };
}
/**
* 读取格点数据
* @param {string} namespace 命名空间
* @param {GridCell|object} gridCell 格点
* @returns {*|null} 数据不存在返回 null
*/
get(namespace, gridCell) {
const cell = this._resolveCell(namespace, gridCell);
const key = cell.toKey();
const pageId = this._index.get(key);
if (pageId === undefined) {
return null;
}
const dataBuf = this._pageManager.readPage(pageId);
if (!dataBuf) {
return null;
}
try {
return JSON.parse(dataBuf.toString('utf-8'));
} catch {
return dataBuf;
}
}
/**
* 删除格点数据
* @param {string} namespace 命名空间
* @param {GridCell|object} gridCell 格点
* @returns {boolean} 是否删除成功
*/
delete(namespace, gridCell) {
const cell = this._resolveCell(namespace, gridCell);
const key = cell.toKey();
const pageId = this._index.get(key);
if (pageId === undefined) {
return false;
}
// 1. 先写 WAL
this._wal.appendDelete(key);
// 2. 释放页
this._pageManager.freePage(pageId);
this._index.delete(key);
// 3. 记录事件
this._eventLog.append(namespace, 'delete', key);
return true;
}
/**
* 范围扫描Phase 0 简版基于内存索引
* @param {string} namespace
* @param {object} range { xRange: [min, max], yRange: [min, max], layer }
* @returns {Array<{ cell: GridCell, data: * }>}
*/
scan(namespace, range = {}) {
const results = [];
for (const [key, pageId] of this._index) {
try {
const cell = GridCell.fromKey(key);
if (cell.namespace !== namespace) continue;
if (range.layer && cell.layer !== range.layer) continue;
if (range.xRange) {
if (cell.gridX < range.xRange[0] || cell.gridX > range.xRange[1]) continue;
}
if (range.yRange) {
if (cell.gridY < range.yRange[0] || cell.gridY > range.yRange[1]) continue;
}
const dataBuf = this._pageManager.readPage(pageId);
if (dataBuf) {
let data;
try { data = JSON.parse(dataBuf.toString('utf-8')); } catch { data = dataBuf; }
results.push({ cell, data });
}
} catch {
// 跳过无效键
}
}
this._eventLog.append(namespace, 'scan', `scan:${namespace}`, {
resultCount: results.length
});
return results;
}
/**
* 获取命名空间统计
* @param {string} [namespace] 不传则统计全部
* @returns {object}
*/
stats(namespace) {
let count = 0;
for (const key of this._index.keys()) {
if (!namespace || key.startsWith(namespace + ':')) {
count++;
}
}
return {
namespace: namespace || '*',
cellCount: count,
wal: this._wal.getStatus(),
pageManager: this._pageManager.getStatus(),
eventLog: this._eventLog.getStatus()
};
}
/**
* 订阅事件流
* @param {string} subscriberId
* @param {Function} handler
* @returns {Function} 取消订阅函数
*/
subscribe(subscriberId, handler) {
return this._eventLog.subscribe(subscriberId, handler);
}
/**
* 执行 checkpoint截断 WAL
*/
checkpoint() {
this._wal.truncate();
}
/**
* 关闭数据库
*/
close() {
this._wal.close();
this._pageManager.close();
}
// ── 内部方法 ──
/**
* 解析格点参数
* @param {string} namespace
* @param {GridCell|object} input
* @returns {GridCell}
*/
_resolveCell(namespace, input) {
if (input instanceof GridCell) {
return input;
}
return new GridCell(
namespace,
input.gridX ?? input.grid_x ?? 0,
input.gridY ?? input.grid_y ?? 0,
input.layer || 'raw'
);
}
/**
* WAL 恢复数据到内存索引
*/
_recoverFromWAL() {
const entries = this._wal.recover();
for (const entry of entries) {
if (entry.op === WAL.OP_PUT) {
// 检查是否已有页
let pageId = this._index.get(entry.key);
if (pageId !== undefined) {
// 页已存在,更新
try {
this._pageManager.writePage(pageId, entry.data);
} catch {
// 页可能已损坏,分配新页
pageId = this._pageManager.allocPage();
this._pageManager.writePage(pageId, entry.data);
this._index.set(entry.key, pageId);
}
} else {
// 分配新页并写入
pageId = this._pageManager.allocPage();
this._pageManager.writePage(pageId, entry.data);
this._index.set(entry.key, pageId);
}
} else if (entry.op === WAL.OP_DELETE) {
const pageId = this._index.get(entry.key);
if (pageId !== undefined) {
this._pageManager.freePage(pageId);
this._index.delete(entry.key);
}
}
}
}
}
module.exports = GridAPI;

View File

@ -0,0 +1,164 @@
// grid-db/src/core/grid-cell.js
// Grid-DB · 格点数据模型
// 四元组寻址:(namespace, grid_x, grid_y, layer)
// PRJ-GDB-001 · Phase 0 · ZY-GDB-P0-004
// 版权:国作登字-2026-A-00037559
'use strict';
/**
* GridCell 逻辑格点
*
* 数据的最小寻址单元由四元组定位
* namespace 隔离不同 Agent / 项目的数据域
* grid_x X 轴坐标
* grid_y Y 轴坐标
* layer 同一坐标下的数据层级 (raw cleaned indexed semantic)
*
* 本体论锚定格点 = 纸上的一个格子
* 笔在纸上写字每个字都有坐标格点是纸的最小单元
*/
const VALID_LAYERS = ['raw', 'cleaned', 'indexed', 'semantic'];
class GridCell {
/**
* @param {string} namespace 命名空间
* @param {number} gridX X 坐标
* @param {number} gridY Y 坐标
* @param {string} [layer] 数据层级默认 'raw'
*/
constructor(namespace, gridX, gridY, layer = 'raw') {
if (!namespace || typeof namespace !== 'string') {
throw new Error('GridCell: namespace 必须是非空字符串');
}
if (typeof gridX !== 'number' || !Number.isFinite(gridX)) {
throw new Error('GridCell: grid_x 必须是有限数值');
}
if (typeof gridY !== 'number' || !Number.isFinite(gridY)) {
throw new Error('GridCell: grid_y 必须是有限数值');
}
if (!VALID_LAYERS.includes(layer)) {
throw new Error(`GridCell: layer 必须是 ${VALID_LAYERS.join('/')} 之一,收到: ${layer}`);
}
this.namespace = namespace;
this.gridX = gridX;
this.gridY = gridY;
this.layer = layer;
}
/**
* 生成格点唯一键用于存储索引
* @returns {string}
*/
toKey() {
return `${this.namespace}:${this.gridX}:${this.gridY}:${this.layer}`;
}
/**
* 从唯一键解析格点
* @param {string} key
* @returns {GridCell}
*/
static fromKey(key) {
const parts = key.split(':');
if (parts.length !== 4) {
throw new Error(`GridCell.fromKey: 无效键格式: ${key}`);
}
return new GridCell(parts[0], Number(parts[1]), Number(parts[2]), parts[3]);
}
/**
* 序列化为 JSON 可存储对象
* @returns {object}
*/
serialize() {
return {
namespace: this.namespace,
grid_x: this.gridX,
grid_y: this.gridY,
layer: this.layer,
key: this.toKey()
};
}
/**
* 从序列化对象还原
* @param {object} obj
* @returns {GridCell}
*/
static deserialize(obj) {
return new GridCell(obj.namespace, obj.grid_x, obj.grid_y, obj.layer);
}
/**
* 序列化为二进制 Buffer用于页存储
* 格式[nsLen:2][ns:N][x:8][y:8][layerIdx:1]
* @returns {Buffer}
*/
toBuffer() {
const nsBytes = Buffer.from(this.namespace, 'utf-8');
const layerIdx = VALID_LAYERS.indexOf(this.layer);
// 2 (nsLen) + nsLen + 8 (x float64) + 8 (y float64) + 1 (layerIdx)
const buf = Buffer.alloc(2 + nsBytes.length + 8 + 8 + 1);
let offset = 0;
buf.writeUInt16BE(nsBytes.length, offset); offset += 2;
nsBytes.copy(buf, offset); offset += nsBytes.length;
buf.writeDoubleBE(this.gridX, offset); offset += 8;
buf.writeDoubleBE(this.gridY, offset); offset += 8;
buf.writeUInt8(layerIdx, offset);
return buf;
}
/**
* 从二进制 Buffer 还原格点
* @param {Buffer} buf
* @param {number} [offset]
* @returns {{ cell: GridCell, bytesRead: number }}
*/
static fromBuffer(buf, offset = 0) {
const start = offset;
const nsLen = buf.readUInt16BE(offset); offset += 2;
const namespace = buf.toString('utf-8', offset, offset + nsLen); offset += nsLen;
const gridX = buf.readDoubleBE(offset); offset += 8;
const gridY = buf.readDoubleBE(offset); offset += 8;
const layerIdx = buf.readUInt8(offset); offset += 1;
return {
cell: new GridCell(namespace, gridX, gridY, VALID_LAYERS[layerIdx]),
bytesRead: offset - start
};
}
/**
* 检查两个格点是否相等
* @param {GridCell} other
* @returns {boolean}
*/
equals(other) {
return this.namespace === other.namespace &&
this.gridX === other.gridX &&
this.gridY === other.gridY &&
this.layer === other.layer;
}
/**
* 计算到另一个格点的曼哈顿距离 namespace + layer
* @param {GridCell} other
* @returns {number}
*/
distanceTo(other) {
return Math.abs(this.gridX - other.gridX) + Math.abs(this.gridY - other.gridY);
}
toString() {
return `GridCell(${this.toKey()})`;
}
}
GridCell.VALID_LAYERS = VALID_LAYERS;
module.exports = GridCell;

View File

@ -0,0 +1,123 @@
// grid-db/src/events/event-log.js
// Grid-DB · 事件溯源日志
// 不可变事件流 + 审计日志
// PRJ-GDB-001 · Phase 0基础结构P1 完善回放与订阅)
// 版权:国作登字-2026-A-00037559
'use strict';
const { randomUUID } = require('crypto');
/**
* EventLog 事件溯源日志
*
* 每次写操作生成不可变 Event可被天眼订阅实现实时审计
*
* 本体论锚定事件日志 = 笔的笔迹记录
* 每写一笔笔迹就留在了历史中不可篡改
* 天眼通过观察笔迹发现异常行为
*
* Phase 0内存事件流保留最近 1000
* Phase 1持久化事件流 + 回放 + 订阅
*/
class EventLog {
/**
* @param {object} [options]
* @param {number} [options.maxEvents] 最大保留事件数
*/
constructor(options = {}) {
this._maxEvents = options.maxEvents || 1000;
this._events = [];
this._subscribers = [];
this._seqNo = 0;
}
/**
* 记录事件
* @param {string} namespace 命名空间
* @param {string} operation 操作类型 (put | delete | scan)
* @param {string} gridCellKey 格点键
* @param {object} [payload] 额外数据
* @returns {object} 事件对象
*/
append(namespace, operation, gridCellKey, payload = null) {
this._seqNo++;
const event = {
eventId: `evt-${randomUUID().slice(0, 12)}`,
seqNo: this._seqNo,
timestamp: new Date().toISOString(),
namespace,
operation,
gridCellKey,
payload
};
this._events.push(event);
// 容量控制
if (this._events.length > this._maxEvents) {
this._events = this._events.slice(-this._maxEvents);
}
// 通知订阅者
for (const sub of this._subscribers) {
try {
sub.handler(event);
} catch {
// 订阅者错误不阻断主流程
}
}
return event;
}
/**
* 获取最近 N 条事件
* @param {number} [n]
* @returns {object[]}
*/
getRecent(n = 20) {
return this._events.slice(-n);
}
/**
* 按命名空间过滤事件
* @param {string} namespace
* @param {number} [limit]
* @returns {object[]}
*/
getByNamespace(namespace, limit = 50) {
return this._events
.filter(e => e.namespace === namespace)
.slice(-limit);
}
/**
* 订阅事件流Phase 0 简版P1 支持持久订阅
* @param {string} subscriberId
* @param {Function} handler
* @returns {Function} 取消订阅函数
*/
subscribe(subscriberId, handler) {
const sub = { id: subscriberId, handler };
this._subscribers.push(sub);
return () => {
this._subscribers = this._subscribers.filter(s => s.id !== subscriberId);
};
}
/**
* 获取事件日志状态
* @returns {object}
*/
getStatus() {
return {
totalEvents: this._events.length,
maxEvents: this._maxEvents,
seqNo: this._seqNo,
subscriberCount: this._subscribers.length
};
}
}
module.exports = EventLog;

86
grid-db/src/index.js Normal file
View File

@ -0,0 +1,86 @@
// grid-db/src/index.js
// Grid-DB · 自研逻辑格点数据库 · 主入口
// PRJ-GDB-001 · Phase 0
// 版权:国作登字-2026-A-00037559
'use strict';
const path = require('path');
const fs = require('fs');
const GridCell = require('./core/grid-cell');
const WAL = require('./storage/wal');
const PageManager = require('./storage/page-manager');
const EventLog = require('./events/event-log');
const GridAPI = require('./api/grid-api');
/**
* Grid-DB 初始化器
*
* 本体论锚定 [ONT-PATCH-006]
* 笔需要纸来承载笔迹
* Grid-DB = 纸上有格子格点每个格子是最小寻址单元
* 笔在格子里写字格子记住了所有笔迹
* 纸不属于笔纸属于写字的人
*/
/**
* 打开或创建一个 Grid-DB 实例
*
* @param {object} [config]
* @param {string} [config.dataDir] 数据目录默认 grid-db/data
* @param {number} [config.pageSize] 页大小默认 4096
* @returns {GridAPI}
*/
function open(config = {}) {
const dataDir = config.dataDir || path.resolve(__dirname, '../data');
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true });
}
return new GridAPI({
dataDir,
pageSize: config.pageSize || PageManager.DEFAULT_PAGE_SIZE
});
}
// ── CLI 入口 ──
if (require.main === module) {
const cmd = process.argv[2];
if (cmd === 'status') {
const db = open();
const status = db.stats();
// eslint-disable-next-line no-console
console.log(JSON.stringify(status, null, 2));
db.close();
} else {
// eslint-disable-next-line no-console
console.log([
'🗄️ Grid-DB v0.1.0 · PRJ-GDB-001 · Phase 0',
'',
'本体论锚定Grid-DB = 纸 · 格点 = 格子 · WAL = 草稿本',
'',
'用法:',
' node grid-db/src/index.js status 查看数据库状态',
'',
'组件:',
' GridAPI 统一 API 接口',
' GridCell 格点数据模型',
' WAL Write-Ahead Log',
' PageManager 页管理器',
' EventLog 事件溯源日志',
].join('\n'));
}
}
module.exports = {
open,
GridAPI,
GridCell,
WAL,
PageManager,
EventLog
};

View File

@ -0,0 +1,272 @@
// grid-db/src/storage/page-manager.js
// Grid-DB · 页管理器
// 固定大小页分配、读写与释放
// PRJ-GDB-001 · Phase 0 · ZY-GDB-P0-003
// 版权:国作登字-2026-A-00037559
'use strict';
const fs = require('fs');
const path = require('path');
/**
* PageManager 页管理器
*
* 以固定大小的"页"为最小 I/O 单位管理磁盘数据
*
* 文件结构
* [Header: 1 page] [Page 1] [Page 2] ... [Page N]
*
* Header 页格式 64 字节
* [magic:4][version:2][pageSize:4][pageCount:4][freeListHead:4]...[reserved]
*
* 数据页格式
* [flags:1][nextFree:4][dataLen:4][data:pageSize-9]
* flags: 0x00=free, 0x01=used
*
* 本体论锚定 = 纸的一页
* 格子画在纸上纸由一页一页组成页管理器管理每一页的分配和回收
*/
const MAGIC = 0x47444230; // 'GDB0'
const VERSION = 1;
const DEFAULT_PAGE_SIZE = 4096;
const HEADER_FIXED_SIZE = 64;
// 页标志
const FLAG_FREE = 0x00;
const FLAG_USED = 0x01;
// 页头部大小flags + nextFree + dataLen
const PAGE_HEADER_SIZE = 9;
class PageManager {
/**
* @param {string} filePath 数据文件路径
* @param {object} [options]
* @param {number} [options.pageSize] 页大小字节默认 4096
*/
constructor(filePath, options = {}) {
this._filePath = filePath;
this._pageSize = options.pageSize || DEFAULT_PAGE_SIZE;
this._pageCount = 0;
this._freeListHead = 0; // 0 表示无空闲页
this._fd = null;
this._init();
}
/**
* 初始化打开或创建数据文件
*/
_init() {
const dir = path.dirname(this._filePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
if (fs.existsSync(this._filePath)) {
this._fd = fs.openSync(this._filePath, 'r+');
this._readHeader();
} else {
this._fd = fs.openSync(this._filePath, 'w+');
this._pageCount = 0;
this._freeListHead = 0;
this._writeHeader();
}
}
/**
* 分配一个新页
* @returns {number} 页号 1 开始0 header
*/
allocPage() {
let pageId;
if (this._freeListHead > 0) {
// 从空闲链表取一页
pageId = this._freeListHead;
const pageBuf = this._readPageRaw(pageId);
this._freeListHead = pageBuf.readUInt32BE(1); // nextFree
this._writeHeader();
} else {
// 追加新页
this._pageCount++;
pageId = this._pageCount;
// 扩展文件
const emptyPage = Buffer.alloc(this._pageSize);
const fileOffset = this._pageOffset(pageId);
fs.writeSync(this._fd, emptyPage, 0, this._pageSize, fileOffset);
this._writeHeader();
}
// 标记为已使用
const pageBuf = Buffer.alloc(this._pageSize);
pageBuf.writeUInt8(FLAG_USED, 0);
pageBuf.writeUInt32BE(0, 1); // nextFree = 0
pageBuf.writeUInt32BE(0, 5); // dataLen = 0
fs.writeSync(this._fd, pageBuf, 0, this._pageSize, this._pageOffset(pageId));
return pageId;
}
/**
* 写入数据到指定页
* @param {number} pageId 页号
* @param {Buffer} data 数据不能超过 pageSize - PAGE_HEADER_SIZE
*/
writePage(pageId, data) {
if (pageId < 1 || pageId > this._pageCount) {
throw new Error(`PageManager: 无效页号 ${pageId}`);
}
const maxDataSize = this._pageSize - PAGE_HEADER_SIZE;
if (data.length > maxDataSize) {
throw new Error(`PageManager: 数据太大 (${data.length} > ${maxDataSize})`);
}
const pageBuf = Buffer.alloc(this._pageSize);
pageBuf.writeUInt8(FLAG_USED, 0);
pageBuf.writeUInt32BE(0, 1); // nextFree
pageBuf.writeUInt32BE(data.length, 5); // dataLen
data.copy(pageBuf, PAGE_HEADER_SIZE);
fs.writeSync(this._fd, pageBuf, 0, this._pageSize, this._pageOffset(pageId));
}
/**
* 读取指定页的数据
* @param {number} pageId 页号
* @returns {Buffer|null} 数据如果页是空闲的返回 null
*/
readPage(pageId) {
if (pageId < 1 || pageId > this._pageCount) {
return null;
}
const pageBuf = this._readPageRaw(pageId);
const flags = pageBuf.readUInt8(0);
if (flags !== FLAG_USED) {
return null;
}
const dataLen = pageBuf.readUInt32BE(5);
return Buffer.from(pageBuf.subarray(PAGE_HEADER_SIZE, PAGE_HEADER_SIZE + dataLen));
}
/**
* 释放页加入空闲链表
* @param {number} pageId 页号
*/
freePage(pageId) {
if (pageId < 1 || pageId > this._pageCount) {
throw new Error(`PageManager: 无效页号 ${pageId}`);
}
const pageBuf = Buffer.alloc(this._pageSize);
pageBuf.writeUInt8(FLAG_FREE, 0);
pageBuf.writeUInt32BE(this._freeListHead, 1); // nextFree = 旧链表头
pageBuf.writeUInt32BE(0, 5); // dataLen = 0
fs.writeSync(this._fd, pageBuf, 0, this._pageSize, this._pageOffset(pageId));
this._freeListHead = pageId;
this._writeHeader();
}
/**
* 获取页管理器状态
* @returns {object}
*/
getStatus() {
let fileSize = 0;
try {
fileSize = fs.fstatSync(this._fd).size;
} catch {
// fd 不可用
}
return {
filePath: this._filePath,
pageSize: this._pageSize,
pageCount: this._pageCount,
freeListHead: this._freeListHead,
maxDataPerPage: this._pageSize - PAGE_HEADER_SIZE,
fileSize
};
}
/**
* 关闭数据文件
*/
close() {
if (this._fd !== null) {
fs.closeSync(this._fd);
this._fd = null;
}
}
// ── 内部方法 ──
/**
* 计算页在文件中的偏移量
* @param {number} pageId
* @returns {number}
*/
_pageOffset(pageId) {
// 第 0 页是 header数据页从 1 开始
return pageId * this._pageSize;
}
/**
* 写入文件头
*/
_writeHeader() {
const headerBuf = Buffer.alloc(this._pageSize);
headerBuf.writeUInt32BE(MAGIC, 0);
headerBuf.writeUInt16BE(VERSION, 4);
headerBuf.writeUInt32BE(this._pageSize, 6);
headerBuf.writeUInt32BE(this._pageCount, 10);
headerBuf.writeUInt32BE(this._freeListHead, 14);
fs.writeSync(this._fd, headerBuf, 0, this._pageSize, 0);
}
/**
* 读取文件头
*/
_readHeader() {
const headerBuf = Buffer.alloc(HEADER_FIXED_SIZE);
fs.readSync(this._fd, headerBuf, 0, HEADER_FIXED_SIZE, 0);
const magic = headerBuf.readUInt32BE(0);
if (magic !== MAGIC) {
throw new Error(`PageManager: 无效文件格式 (magic=0x${magic.toString(16)})`);
}
const version = headerBuf.readUInt16BE(4);
if (version !== VERSION) {
throw new Error(`PageManager: 不支持的版本 ${version}`);
}
this._pageSize = headerBuf.readUInt32BE(6);
this._pageCount = headerBuf.readUInt32BE(10);
this._freeListHead = headerBuf.readUInt32BE(14);
}
/**
* 读取原始页数据
* @param {number} pageId
* @returns {Buffer}
*/
_readPageRaw(pageId) {
const buf = Buffer.alloc(this._pageSize);
fs.readSync(this._fd, buf, 0, this._pageSize, this._pageOffset(pageId));
return buf;
}
}
PageManager.PAGE_HEADER_SIZE = PAGE_HEADER_SIZE;
PageManager.DEFAULT_PAGE_SIZE = DEFAULT_PAGE_SIZE;
PageManager.MAGIC = MAGIC;
module.exports = PageManager;

231
grid-db/src/storage/wal.js Normal file
View File

@ -0,0 +1,231 @@
// grid-db/src/storage/wal.js
// Grid-DB · Write-Ahead Log
// 所有写操作先写 WAL崩溃恢复保证
// PRJ-GDB-001 · Phase 0 · ZY-GDB-P0-002
// 版权:国作登字-2026-A-00037559
'use strict';
const fs = require('fs');
const path = require('path');
/**
* WALWrite-Ahead Log
*
* 所有写操作先追加到 WAL 文件确保崩溃后可从 WAL 恢复
*
* 日志格式每条记录
* [length:4][seqNo:4][op:1][keyLen:2][key:N][dataLen:4][data:M][checksum:4]
*
* 操作码
* 0x01 = PUT
* 0x02 = DELETE
*
* 本体论锚定WAL = 笔的草稿本
* 正式写到纸上之前先在草稿本上记一笔
* 万一出了意外草稿本里有记录可以恢复
*/
const OP_PUT = 0x01;
const OP_DELETE = 0x02;
class WAL {
/**
* @param {string} walPath WAL 文件路径
*/
constructor(walPath) {
this._path = walPath;
this._seqNo = 0;
this._fd = null;
this._init();
}
/**
* 初始化 WAL 文件
*/
_init() {
const dir = path.dirname(this._path);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
if (fs.existsSync(this._path)) {
// 从现有 WAL 文件中恢复最大序列号
const entries = this._readAll();
if (entries.length > 0) {
this._seqNo = entries[entries.length - 1].seqNo;
}
}
// 追加模式打开
this._fd = fs.openSync(this._path, 'a');
}
/**
* 追加一条 PUT 记录
* @param {string} key 格点键
* @param {Buffer} data 数据
* @returns {number} 序列号
*/
appendPut(key, data) {
return this._append(OP_PUT, key, data);
}
/**
* 追加一条 DELETE 记录
* @param {string} key 格点键
* @returns {number} 序列号
*/
appendDelete(key) {
return this._append(OP_DELETE, key, Buffer.alloc(0));
}
/**
* 内部追加记录
* @param {number} op 操作码
* @param {string} key
* @param {Buffer} data 数据
* @returns {number} 序列号
*/
_append(op, key, data) {
this._seqNo++;
const keyBuf = Buffer.from(key, 'utf-8');
const dataBuf = data instanceof Buffer ? data : Buffer.from(JSON.stringify(data));
// 构建记录
// [length:4][seqNo:4][op:1][keyLen:2][key:N][dataLen:4][data:M][checksum:4]
const recordLen = 4 + 1 + 2 + keyBuf.length + 4 + dataBuf.length + 4;
const buf = Buffer.alloc(4 + recordLen);
let offset = 0;
buf.writeUInt32BE(recordLen, offset); offset += 4; // length
buf.writeUInt32BE(this._seqNo, offset); offset += 4; // seqNo
buf.writeUInt8(op, offset); offset += 1; // op
buf.writeUInt16BE(keyBuf.length, offset); offset += 2; // keyLen
keyBuf.copy(buf, offset); offset += keyBuf.length; // key
buf.writeUInt32BE(dataBuf.length, offset); offset += 4; // dataLen
dataBuf.copy(buf, offset); offset += dataBuf.length; // data
// 计算简单校验和(所有字节异或)
let checksum = 0;
for (let i = 0; i < offset; i++) {
checksum = (checksum ^ buf[i]) & 0xFFFFFFFF;
}
buf.writeUInt32BE(checksum >>> 0, offset);
fs.writeSync(this._fd, buf);
fs.fsyncSync(this._fd);
return this._seqNo;
}
/**
* 读取所有 WAL 条目用于崩溃恢复
* @returns {Array<{ seqNo: number, op: number, key: string, data: Buffer }>}
*/
recover() {
return this._readAll();
}
/**
* 内部读取 WAL 文件的所有记录
* @returns {Array<{ seqNo: number, op: number, key: string, data: Buffer }>}
*/
_readAll() {
if (!fs.existsSync(this._path)) {
return [];
}
const fileBuf = fs.readFileSync(this._path);
const entries = [];
let offset = 0;
while (offset < fileBuf.length) {
// 至少需要 4 字节读长度
if (offset + 4 > fileBuf.length) break;
const recordLen = fileBuf.readUInt32BE(offset);
const totalLen = 4 + recordLen;
// 完整性检查
if (offset + totalLen > fileBuf.length) break;
const recordStart = offset + 4;
let rOffset = recordStart;
const seqNo = fileBuf.readUInt32BE(rOffset); rOffset += 4;
const op = fileBuf.readUInt8(rOffset); rOffset += 1;
const keyLen = fileBuf.readUInt16BE(rOffset); rOffset += 2;
const key = fileBuf.toString('utf-8', rOffset, rOffset + keyLen); rOffset += keyLen;
const dataLen = fileBuf.readUInt32BE(rOffset); rOffset += 4;
const data = fileBuf.subarray(rOffset, rOffset + dataLen); rOffset += dataLen;
const storedChecksum = fileBuf.readUInt32BE(rOffset);
// 校验
let checksum = 0;
for (let i = offset; i < rOffset; i++) {
checksum = (checksum ^ fileBuf[i]) & 0xFFFFFFFF;
}
if ((checksum >>> 0) === storedChecksum) {
entries.push({ seqNo, op, key, data: Buffer.from(data) });
}
// 校验失败的记录跳过(部分写入的残余)
offset += totalLen;
}
return entries;
}
/**
* 截断 WAL checkpoint 后调用清除已持久化的记录
*/
truncate() {
this.close();
fs.writeFileSync(this._path, Buffer.alloc(0));
this._seqNo = 0;
this._fd = fs.openSync(this._path, 'a');
}
/**
* 获取当前序列号
* @returns {number}
*/
getSeqNo() {
return this._seqNo;
}
/**
* 关闭 WAL 文件
*/
close() {
if (this._fd !== null) {
fs.closeSync(this._fd);
this._fd = null;
}
}
/**
* 获取 WAL 状态
* @returns {object}
*/
getStatus() {
let fileSize = 0;
try {
fileSize = fs.statSync(this._path).size;
} catch {
// 文件不存在
}
return {
path: this._path,
seqNo: this._seqNo,
fileSize
};
}
}
WAL.OP_PUT = OP_PUT;
WAL.OP_DELETE = OP_DELETE;
module.exports = WAL;

View File

@ -0,0 +1,419 @@
// grid-db/tests/smoke/grid-db.test.js
// Grid-DB · 冒烟测试
// PRJ-GDB-001 · Phase 0 · ZY-GDB-P0-005
// 版权:国作登字-2026-A-00037559
'use strict';
const path = require('path');
const fs = require('fs');
const os = require('os');
const {
open,
GridAPI,
GridCell,
WAL,
PageManager,
EventLog
} = require('../../src/index');
let passed = 0;
let failed = 0;
function assert(condition, message) {
if (condition) {
passed++;
// eslint-disable-next-line no-console
console.log(`${message}`);
} else {
failed++;
// eslint-disable-next-line no-console
console.error(`${message}`);
}
}
/**
* 创建临时测试目录
* @param {string} suffix
* @returns {string}
*/
function makeTempDir(suffix) {
const dir = path.join(os.tmpdir(), `griddb-test-${suffix}-${Date.now()}`);
fs.mkdirSync(dir, { recursive: true });
return dir;
}
/**
* 递归删除目录
* @param {string} dir
*/
function cleanDir(dir) {
try {
fs.rmSync(dir, { recursive: true, force: true });
} catch {
// 忽略清理错误
}
}
// eslint-disable-next-line no-console
console.log('🗄️ Grid-DB 冒烟测试 · Phase 0\n');
// ── 测试 1: 模块导出 ──
// eslint-disable-next-line no-console
console.log('── 测试 1: 模块导出完整性 ──');
assert(typeof open === 'function', 'open 是函数');
assert(typeof GridAPI === 'function', 'GridAPI 是构造函数');
assert(typeof GridCell === 'function', 'GridCell 是构造函数');
assert(typeof WAL === 'function', 'WAL 是构造函数');
assert(typeof PageManager === 'function', 'PageManager 是构造函数');
assert(typeof EventLog === 'function', 'EventLog 是构造函数');
// ── 测试 2: GridCell 四元组寻址 ──
// eslint-disable-next-line no-console
console.log('\n── 测试 2: GridCell 四元组寻址 ──');
const cell1 = new GridCell('agent-zy', 10, 20, 'raw');
assert(cell1.namespace === 'agent-zy', 'namespace 正确');
assert(cell1.gridX === 10, 'gridX 正确');
assert(cell1.gridY === 20, 'gridY 正确');
assert(cell1.layer === 'raw', 'layer 正确');
assert(cell1.toKey() === 'agent-zy:10:20:raw', 'toKey() 正确');
// fromKey 还原
const cell2 = GridCell.fromKey('test-ns:5:15:semantic');
assert(cell2.namespace === 'test-ns', 'fromKey namespace 正确');
assert(cell2.gridX === 5, 'fromKey gridX 正确');
assert(cell2.gridY === 15, 'fromKey gridY 正确');
assert(cell2.layer === 'semantic', 'fromKey layer 正确');
// 序列化 / 反序列化
const serialized = cell1.serialize();
assert(serialized.namespace === 'agent-zy', 'serialize namespace');
assert(serialized.grid_x === 10, 'serialize grid_x');
assert(serialized.grid_y === 20, 'serialize grid_y');
assert(serialized.layer === 'raw', 'serialize layer');
const cell3 = GridCell.deserialize(serialized);
assert(cell3.equals(cell1), 'deserialize → equals 原始格点');
// 二进制序列化
const buf = cell1.toBuffer();
assert(buf instanceof Buffer, 'toBuffer 返回 Buffer');
const { cell: cell4, bytesRead } = GridCell.fromBuffer(buf);
assert(cell4.equals(cell1), 'fromBuffer → equals 原始格点');
assert(bytesRead === buf.length, 'bytesRead 等于 buffer 长度');
// 距离计算
assert(cell1.distanceTo(cell2) === 10, 'distanceTo 曼哈顿距离正确');
// 无效参数
let errThrown = false;
try { new GridCell('', 0, 0); } catch { errThrown = true; }
assert(errThrown, '空 namespace 抛出错误');
errThrown = false;
try { new GridCell('ns', 0, 0, 'invalid'); } catch { errThrown = true; }
assert(errThrown, '无效 layer 抛出错误');
// ── 测试 3: WAL 基础功能 ──
// eslint-disable-next-line no-console
console.log('\n── 测试 3: WAL 基础功能 ──');
const walDir = makeTempDir('wal');
const wal = new WAL(path.join(walDir, 'test.wal'));
const seq1 = wal.appendPut('ns:1:2:raw', Buffer.from('{"hello":"world"}'));
assert(seq1 === 1, 'appendPut 返回序列号 1');
const seq2 = wal.appendPut('ns:3:4:raw', Buffer.from('{"foo":"bar"}'));
assert(seq2 === 2, 'appendPut 返回序列号 2');
const seq3 = wal.appendDelete('ns:1:2:raw');
assert(seq3 === 3, 'appendDelete 返回序列号 3');
assert(wal.getSeqNo() === 3, 'getSeqNo 正确');
// 恢复
const entries = wal.recover();
assert(entries.length === 3, '恢复 3 条记录');
assert(entries[0].op === WAL.OP_PUT, '记录 1 是 PUT');
assert(entries[0].key === 'ns:1:2:raw', '记录 1 键正确');
assert(entries[1].op === WAL.OP_PUT, '记录 2 是 PUT');
assert(entries[2].op === WAL.OP_DELETE, '记录 3 是 DELETE');
// 截断
wal.truncate();
assert(wal.getSeqNo() === 0, '截断后序列号归零');
const afterTrunc = wal.recover();
assert(afterTrunc.length === 0, '截断后无记录');
wal.close();
// 崩溃恢复测试:写入 → 关闭 → 重新打开 → 验证
const wal2 = new WAL(path.join(walDir, 'crash.wal'));
wal2.appendPut('crash:1:1:raw', Buffer.from('{"data":"survive"}'));
wal2.appendPut('crash:2:2:raw', Buffer.from('{"data":"also"}'));
wal2.close();
const wal3 = new WAL(path.join(walDir, 'crash.wal'));
const recovered = wal3.recover();
assert(recovered.length === 2, '崩溃恢复2 条记录');
assert(recovered[0].key === 'crash:1:1:raw', '崩溃恢复:键正确');
assert(wal3.getSeqNo() === 2, '崩溃恢复:序列号正确');
wal3.close();
cleanDir(walDir);
// ── 测试 4: PageManager 基础功能 ──
// eslint-disable-next-line no-console
console.log('\n── 测试 4: PageManager 基础功能 ──');
const pmDir = makeTempDir('pm');
const pm = new PageManager(path.join(pmDir, 'test.gdb'));
// 分配页
const p1 = pm.allocPage();
assert(p1 === 1, '首页号 = 1');
const p2 = pm.allocPage();
assert(p2 === 2, '第二页号 = 2');
// 写入数据
const testData = Buffer.from('Hello Grid-DB!');
pm.writePage(p1, testData);
// 读取数据
const readData = pm.readPage(p1);
assert(readData !== null, '读取非 null');
assert(readData.toString() === 'Hello Grid-DB!', '读取数据正确');
// 写入更长数据
const longData = Buffer.from(JSON.stringify({ msg: '格点数据库测试', num: 42, arr: [1, 2, 3] }));
pm.writePage(p2, longData);
const readLong = pm.readPage(p2);
assert(readLong !== null, '读取长数据非 null');
assert(JSON.parse(readLong.toString()).num === 42, '长数据内容正确');
// 释放页
pm.freePage(p1);
const freedData = pm.readPage(p1);
assert(freedData === null, '释放后读取返回 null');
// 释放的页被复用
const p3 = pm.allocPage();
assert(p3 === 1, '释放的页号被复用');
// 状态
const pmStatus = pm.getStatus();
assert(pmStatus.pageSize === 4096, '页大小 = 4096');
assert(pmStatus.pageCount === 2, '页数 = 2');
pm.close();
// 重新打开验证持久化
const pm2 = new PageManager(path.join(pmDir, 'test.gdb'));
const rereadLong = pm2.readPage(2);
assert(rereadLong !== null, '重新打开后读取非 null');
assert(JSON.parse(rereadLong.toString()).num === 42, '持久化数据正确');
pm2.close();
cleanDir(pmDir);
// ── 测试 5: EventLog 基础功能 ──
// eslint-disable-next-line no-console
console.log('\n── 测试 5: EventLog 基础功能 ──');
const eventLog = new EventLog({ maxEvents: 5 });
const evt1 = eventLog.append('ns1', 'put', 'ns1:1:1:raw', { size: 100 });
assert(evt1.eventId.startsWith('evt-'), '事件 ID 前缀正确');
assert(evt1.seqNo === 1, '事件序号 = 1');
assert(evt1.namespace === 'ns1', '事件命名空间正确');
eventLog.append('ns1', 'put', 'ns1:2:2:raw');
eventLog.append('ns2', 'delete', 'ns2:3:3:raw');
const recent = eventLog.getRecent(10);
assert(recent.length === 3, '最近事件 = 3');
const ns1Events = eventLog.getByNamespace('ns1');
assert(ns1Events.length === 2, 'ns1 事件 = 2');
// 订阅测试
let received = null;
const unsub = eventLog.subscribe('test-sub', (evt) => { received = evt; });
eventLog.append('ns1', 'put', 'ns1:4:4:raw');
assert(received !== null, '订阅回调收到事件');
assert(received.operation === 'put', '订阅事件操作正确');
unsub();
received = null;
eventLog.append('ns1', 'put', 'ns1:5:5:raw');
assert(received === null, '取消订阅后不再收到事件');
// 容量控制
eventLog.append('ns1', 'put', 'ns1:6:6:raw');
eventLog.append('ns1', 'put', 'ns1:7:7:raw');
const status = eventLog.getStatus();
assert(status.totalEvents === 5, '容量控制:最多保留 5 条');
// ── 测试 6: GridAPI · put → get → delete 全链路 ──
// eslint-disable-next-line no-console
console.log('\n── 测试 6: GridAPI · put → get → delete 全链路 ──');
const apiDir = makeTempDir('api');
const db = open({ dataDir: apiDir });
// put
const putResult = db.put('exe-engine', { gridX: 1, gridY: 2, layer: 'raw' }, {
agentId: 'AG-ZY-01',
context: { repo: 'guanghulab', branch: 'main' },
timestamp: '2026-03-26T13:00:00Z'
});
assert(putResult.key === 'exe-engine:1:2:raw', 'put 返回正确 key');
assert(putResult.seqNo === 1, 'put 返回序列号 1');
assert(putResult.event.operation === 'put', 'put 生成事件');
// get
const getData = db.get('exe-engine', { gridX: 1, gridY: 2, layer: 'raw' });
assert(getData !== null, 'get 返回数据');
assert(getData.agentId === 'AG-ZY-01', 'get 数据内容正确');
assert(getData.context.repo === 'guanghulab', 'get 嵌套数据正确');
// get 不存在的格点
const noData = db.get('exe-engine', { gridX: 99, gridY: 99, layer: 'raw' });
assert(noData === null, 'get 不存在的格点返回 null');
// 更新
db.put('exe-engine', { gridX: 1, gridY: 2, layer: 'raw' }, {
agentId: 'AG-ZY-01',
context: { repo: 'guanghulab', branch: 'feature' },
timestamp: '2026-03-26T14:00:00Z'
});
const updatedData = db.get('exe-engine', { gridX: 1, gridY: 2, layer: 'raw' });
assert(updatedData.context.branch === 'feature', '更新后数据正确');
// 多命名空间隔离
db.put('dc-v1', { gridX: 1, gridY: 2, layer: 'raw' }, { source: 'notion' });
const dcData = db.get('dc-v1', { gridX: 1, gridY: 2, layer: 'raw' });
assert(dcData.source === 'notion', '不同命名空间数据隔离');
const exeData = db.get('exe-engine', { gridX: 1, gridY: 2, layer: 'raw' });
assert(exeData.agentId === 'AG-ZY-01', '原命名空间数据不受影响');
// delete
const deleted = db.delete('exe-engine', { gridX: 1, gridY: 2, layer: 'raw' });
assert(deleted === true, 'delete 返回 true');
const afterDelete = db.get('exe-engine', { gridX: 1, gridY: 2, layer: 'raw' });
assert(afterDelete === null, '删除后 get 返回 null');
// delete 不存在的
const deleteMissing = db.delete('exe-engine', { gridX: 99, gridY: 99, layer: 'raw' });
assert(deleteMissing === false, 'delete 不存在的格点返回 false');
// ── 测试 7: 范围扫描 ──
// eslint-disable-next-line no-console
console.log('\n── 测试 7: 范围扫描 ──');
db.put('scan-ns', { gridX: 1, gridY: 1, layer: 'raw' }, { id: 'a' });
db.put('scan-ns', { gridX: 2, gridY: 2, layer: 'raw' }, { id: 'b' });
db.put('scan-ns', { gridX: 3, gridY: 3, layer: 'raw' }, { id: 'c' });
db.put('scan-ns', { gridX: 5, gridY: 5, layer: 'indexed' }, { id: 'd' });
const scanAll = db.scan('scan-ns');
assert(scanAll.length === 4, '扫描全部 = 4');
const scanRange = db.scan('scan-ns', { xRange: [1, 3], yRange: [1, 3] });
assert(scanRange.length === 3, '范围扫描 [1,3] = 3 条');
const scanLayer = db.scan('scan-ns', { layer: 'indexed' });
assert(scanLayer.length === 1, '按 layer 过滤 = 1 条');
assert(scanLayer[0].data.id === 'd', '过滤结果正确');
// ── 测试 8: stats ──
// eslint-disable-next-line no-console
console.log('\n── 测试 8: stats 统计 ──');
const dbStats = db.stats('scan-ns');
assert(dbStats.namespace === 'scan-ns', 'stats 命名空间正确');
assert(dbStats.cellCount === 4, 'scan-ns 格点数 = 4');
const allStats = db.stats();
assert(allStats.namespace === '*', 'stats 全局');
assert(allStats.cellCount >= 5, '全局格点数 >= 5');
assert(allStats.wal !== undefined, 'stats 含 wal');
assert(allStats.pageManager !== undefined, 'stats 含 pageManager');
assert(allStats.eventLog !== undefined, 'stats 含 eventLog');
// ── 测试 9: checkpoint ──
// eslint-disable-next-line no-console
console.log('\n── 测试 9: checkpoint ──');
db.checkpoint();
const afterCheckpoint = db.stats();
assert(afterCheckpoint.wal.seqNo === 0, 'checkpoint 后 WAL seqNo = 0');
// 数据仍然可读
const postCheckpoint = db.get('dc-v1', { gridX: 1, gridY: 2, layer: 'raw' });
assert(postCheckpoint !== null, 'checkpoint 后数据仍可读');
assert(postCheckpoint.source === 'notion', 'checkpoint 后数据正确');
// ── 测试 10: 事件订阅 ──
// eslint-disable-next-line no-console
console.log('\n── 测试 10: 事件订阅 ──');
let subscribedEvent = null;
const unsubscribe = db.subscribe('test-audit', (evt) => {
subscribedEvent = evt;
});
db.put('audit-ns', { gridX: 0, gridY: 0, layer: 'raw' }, { audit: true });
assert(subscribedEvent !== null, '事件订阅收到事件');
assert(subscribedEvent.namespace === 'audit-ns', '订阅事件命名空间正确');
unsubscribe();
subscribedEvent = null;
db.put('audit-ns', { gridX: 1, gridY: 1, layer: 'raw' }, { audit: false });
assert(subscribedEvent === null, '取消订阅后不再收到');
db.close();
cleanDir(apiDir);
// ── 测试 11: open() 便捷函数 ──
// eslint-disable-next-line no-console
console.log('\n── 测试 11: open() 便捷函数 ──');
const openDir = makeTempDir('open');
const db2 = open({ dataDir: openDir });
assert(db2 instanceof GridAPI, 'open 返回 GridAPI 实例');
db2.put('test', { gridX: 0, gridY: 0, layer: 'raw' }, 'hello');
const result = db2.get('test', { gridX: 0, gridY: 0, layer: 'raw' });
assert(result === 'hello', 'open → put → get 正确');
db2.close();
cleanDir(openDir);
// ── 测试 12: GridCell 使用 GridCell 对象调用 API ──
// eslint-disable-next-line no-console
console.log('\n── 测试 12: GridCell 对象直接调用 API ──');
const gcDir = makeTempDir('gc');
const db3 = open({ dataDir: gcDir });
const myCell = new GridCell('my-ns', 42, 84, 'cleaned');
db3.put('my-ns', myCell, { value: 'direct cell' });
const directResult = db3.get('my-ns', myCell);
assert(directResult !== null, 'GridCell 对象直接调用 get');
assert(directResult.value === 'direct cell', '数据正确');
db3.close();
cleanDir(gcDir);
// ── 测试结果汇总 ──
// eslint-disable-next-line no-console
console.log('\n══════════════════════════════════════');
// eslint-disable-next-line no-console
console.log('🗄️ Grid-DB 冒烟测试完成');
// eslint-disable-next-line no-console
console.log(` ✅ 通过: ${passed}`);
// eslint-disable-next-line no-console
console.log(` ❌ 失败: ${failed}`);
// eslint-disable-next-line no-console
console.log(` 📊 总计: ${passed + failed}`);
// eslint-disable-next-line no-console
console.log('══════════════════════════════════════\n');
if (failed > 0) {
process.exit(1);
}