HyperMind Lab M1 架构地基 Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: 在现有思维实验室原型上落地 M1 架构地基:Zustand 状态层 + 命令模式撤销栈(含 30s 软删除恢复)、SQLite 22 表全量建库、REST /api/v1 + SSE 流式、设计令牌/交互参数配置中心、深度滑块元交互、项目工作台向导、事件溯源基座——同时保持现有 30+ LLM API 与 18 个面板可用。
Architecture: 前后端分层:server/(Express 路由 + better-sqlite3 repos + SSE TaskManager)、src/store/(Zustand 5 store + 命令模式 undoStore,唯一数据变更通道)、src/design/ + src/config/(设计令牌与交互参数,单一事实源)、src/app/(router + 四区布局外壳,深度滑块驱动面板显隐)。现有 server.ts 与 src/App.tsx 组件保持兼容:新布局中央嵌入现有画布(视图投影适配),遗留 API 仅加埋点适配层。
Tech Stack: React 19 + Vite 6 + TypeScript(现有)、Zustand(新增)、better-sqlite3(新增)、react-router-dom v7(新增)、Express 4(现有)、tsx(现有)、vitest + supertest + @testing-library/react(新增,测试)。
Spec: docs/superpowers/specs/2026-08-11-mindgraph-m1-design.md
Global Constraints
- 数据库:SQLite(WAL 模式、外键开启),文件
data/hypermind.db(gitignore);22 张表一次性全量建库(schema.sql幂等,CREATE TABLE IF NOT EXISTS);嵌套结构用 JSON 文本列(JSON1);频繁过滤字段建普通索引。 - ID 规范:UUID v7 + 语义前缀,20 个前缀与 spec §3.3 表一致(
node_/hyph_/knot_/proj_/egg_/wmhl_/ethc_/aest_/elgm_/rsnc_/pks_/fedc_/cnss_/hdrc_/grav_/rpwl_/trrn_/oper_/evnt_/vals_)。 - 撤销栈:深度上限 50(超出出栈最旧命令并提示"历史记录已压缩");剪枝/删除软删除 + 30s 恢复期(
deleted_at标记 + 回收站);批量剪枝与手动羽化需二次确认对话框。 - 事件溯源:
events表追加式(UPDATE/DELETE 由 DB trigger 拒绝),sequence单调递增;P0-P2 事件落库,P4 仅内存。 - 统一 API:前缀
/api/v1;响应格式{ok:true,data,meta:{page,pageSize,total}}或{ok:false,error:{code,message,detail}};错误码枚举ERR_VALIDATION/ERR_NOT_FOUND/ERR_CONFLICT/ERR_LLM_TIMEOUT/ERR_LLM_QUOTA/ERR_INTERNAL/ERR_ETHICS_BLOCKED。 - 伦理审计不可变性:
ethics_audit_log由 trigger 拒绝 UPDATE/DELETE,prev_log_id追加链。 - 设计令牌:组件禁止硬编码色值,一律 CSS 变量(
var(--mg-*));交互参数禁止魔法数字,一律interactionSpecs.ts。 - 深度滑块(0-100):
uiStore.depthLevel;<30右侧面板折叠为细线、底部面板隐藏;Ctrl+滚轮微调;悬浮显示层级名(静默/低唤醒/高唤醒/沉浸)。 - 测试:vitest(server 测试默认 node 环境;组件测试文件首行
// @vitest-environment jsdom);server API 测试用:memory:SQLite + supertest;每个任务以 commit 结尾(仓库先git init)。 - 迁移兼容:现有
server.ts路由与src/App.tsx组件不得破坏(M1 只加不改其行为);LLM 编排 API 保持可用。
File Structure
| 路径 | 职责 |
|---|---|
src/lib/id.ts | UUID v7 + 语义前缀(前后端共用,server 直接 import) |
src/types.ts | 修改:追加 27 实体类型 + 枚举 + ID_PREFIX 常量(现有类型保留) |
server/db/schema.sql | 22 表全量 DDL + 触发器 + 索引(幂等) |
server/db/db.ts | better-sqlite3 连接单例(WAL/FK/初始化/:memory: 支持) |
server/db/repositories/*.ts | 4 个 repo:projects / graph(nodes·hyphae·knots) / history(operations·events·snapshots) / meta;行↔实体 JSON 映射 |
server/lib/errors.ts | ApiError + 错误码 + HTTP 映射 |
server/lib/respond.ts | sendOk/sendErr/parsePagination |
server/lib/taskManager.ts | SSE 任务内存管理(create/append/subscribe/cancel) |
server/lib/legacyBridge.ts | 遗留 API 埋点:记录 operations + events |
server/routes/v1/*.ts | projects.ts/graph.ts/history.ts/llm.ts/index.ts |
server.ts | 修改:openDb() 启动、挂载 /api/v1、导出 callUniversalLLM、5 处埋点 |
src/store/*.ts | 5 个 Zustand store + 命令注册表 + 命令实现(node/hypha/knot/project) |
src/api/client.ts | fetch 封装:统一响应解包/错误映射 |
src/api/llmStream.ts | SSE 客户端(EventSource + 轮询降级) |
src/design/tokens.ts + tokens.css | 七大色彩域令牌 → CSS 变量 |
src/config/interactionSpecs.ts | 全部交互参数(含深度映射/动效总表/震颤阈值) |
src/app/main.tsx | router 入口(/ 工作台、/project/:id 实验室、/project/:id/monograph) |
src/app/WorkspaceLayout.tsx | 四区布局(TopBar/LeftToolbar/中央/RightPanel/BottomPanel) |
src/components/layout/*.tsx | TopBar(含 DepthSlider)/LeftToolbar/RightPanel/BottomPanel |
src/components/shared/OperationPanel.tsx、ConfirmDialog.tsx、DepthSlider.tsx | 面板外壳/二次确认/深度滑块 |
src/components/workspace/*.tsx | ProjectGallery + ProjectWizard(三步向导) |
src/lib/viewProjection.ts | hypergraphStore → 现有 HypergraphCanvas 数据投影 |
src/lib/autoSnapshot.ts | 每 50 操作或 5 分钟自动快照 |
src/lib/legacyImport.ts | localStorage 旧项目导入 SQLite |
src/hooks/useShortcuts.ts | 快捷键注册中心 |
vitest.config.ts | 测试配置 |
data/(运行时生成) | hypermind.db |
Task 1: 测试脚手架与版本控制初始化
Files:
- Create:
vitest.config.ts - Create:
server/lib/__tests__/smoke.test.ts - Create:
src/store/__tests__/smoke.test.ts - Create:
.gitignore - Modify:
package.json(scripts + devDependencies)
Interfaces:
-
Consumes: 无(项目已存在)
-
Produces: 可运行的
npm test;git 初始提交;后续所有任务依赖的目录server/lib/、server/db/、src/store/、src/api/、src/design/、src/config/、src/app/、src/hooks/骨架(Task 各步骤创建其自有文件)。 -
Step 1: 初始化 git 仓库与 .gitignore
项目根(F:\AI_Sci\AI_Scientist_Lab\mindgraph-lab(V1))下执行:
git init
Create .gitignore:
node_modules/
dist/
data/
.env
.env.local
*.log
- Step 2: 安装新依赖
npm i zustand react-router-dom better-sqlite3
npm i -D vitest supertest @types/better-sqlite3 @types/supertest @testing-library/react @testing-library/jest-dom jsdom
Expected: 安装成功,package.json dependencies 新增 zustand/react-router-dom/better-sqlite3。
- Step 3: 添加测试脚本
Edit package.json scripts:
"test": "vitest run",
"test:watch": "vitest",
"smoke": "tsx scripts/smoke.ts"
- Step 4: 创建 vitest 配置
Create vitest.config.ts:
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
include: ['**/*.test.{ts,tsx}'],
// server 测试默认 node 环境;组件测试文件用首行注释 // @vitest-environment jsdom
},
});
- Step 5: 编写 smoke 测试并验证环境
Create server/lib/__tests__/smoke.test.ts:
import { describe, expect, it } from 'vitest';
describe('server smoke', () => {
it('runs in node environment', () => {
expect(typeof process).toBe('object');
expect(1 + 1).toBe(2);
});
});
Create src/store/__tests__/smoke.test.ts:
// @vitest-environment jsdom
import { describe, expect, it } from 'vitest';
describe('client smoke', () => {
it('runs in jsdom environment', () => {
expect(typeof document).toBe('object');
expect('a' + 'b').toBe('ab');
});
});
- Step 6: 运行测试验证
npm test
Expected: 2 个测试文件 PASS(1 server + 1 client)。
- Step 7: Commit
git add -A
git commit -m "chore: init test scaffold, git, deps (zustand, router, better-sqlite3)"
Task 2: 共享类型系统全量对齐(types.ts)
Files:
- Modify:
src/types.ts(追加,不删现有类型) - Test:
src/__tests__/types.test.ts(新建src/__tests__/)
Interfaces:
-
Consumes: 无
-
Produces: 全部实体类型与枚举,供 server repos 与前端 stores 共用;
ID_PREFIX常量(20 前缀);EntityType字符串联合;CommandType字符串联合(‘node.create’ | ‘node.update’ | ‘node.soft_delete’ | ‘node.restore’ | ‘hypha.create’ | ‘hypha.soft_delete’ | ‘knot.confirm’ | ‘knot.dissolve’ | …)。 -
Step 1: 写失败测试
Create src/__tests__/types.test.ts:
import { describe, expect, it } from 'vitest';
import { ID_PREFIX, CognitiveType, EggStatus, type ConceptNode, type ProjectRecord } from '../types';
describe('types alignment', () => {
it('has all 20 semantic ID prefixes', () => {
const prefixes = Object.values(ID_PREFIX);
expect(prefixes).toHaveLength(20);
expect(prefixes).toContain('node_');
expect(prefixes).toContain('evnt_');
expect(prefixes).toContain('trrn_');
});
it('ConceptNode carries paradigm-revolution fields', () => {
const node = {} as ConceptNode;
expect(node.cognitiveType).toBeUndefined(); // 类型必须声明该字段
const cn: keyof ConceptNode = 'paradigmGeneration';
expect(cn).toBe('paradigmGeneration');
});
it('cognitive type enum covers 5 forms', () => {
expect(Object.values(CognitiveType)).toHaveLength(5);
expect(CognitiveType).toHaveProperty('negativeSpace');
});
it('egg status covers lifecycle', () => {
expect(Object.values(EggStatus)).toContain('hatched');
expect(Object.values(EggStatus)).toContain('dismissed');
});
it('project record has validation mode + value weights', () => {
const p = {} as ProjectRecord;
expect(p.validationMode).toBeUndefined();
expect(p.valueWeights).toBeUndefined();
});
});
- Step 2: 运行确认失败
Run: npx vitest run src/__tests__/types.test.ts
Expected: FAIL(ID_PREFIX/ConceptNode 等不存在)。
- Step 3: 追加类型定义
Append 到 src/types.ts 末尾(现有类型全部保留):
/* ================= M1 全量对齐(数据要素最终版) ================= */
// ---- ID 前缀 ----
export const ID_PREFIX = {
node: 'node_', hypha: 'hyph_', knot: 'knot_', project: 'proj_',
egg: 'egg_', wormhole: 'wmhl_', ethics: 'ethc_', aesthetic: 'aest_',
elegance: 'elgm_', resonance: 'rsnc_', publicKnowledge: 'pks_',
federation: 'fedc_', consensus: 'cnss_', highDim: 'hdrc_',
gravityWell: 'grav_', repulsionWall: 'rpwl_', terrain: 'trrn_',
operation: 'oper_', event: 'evnt_', valueSnapshot: 'vals_',
} as const;
export type IdPrefix = (typeof ID_PREFIX)[keyof typeof ID_PREFIX];
export type EntityType = 'node' | 'hypha' | 'knot' | 'project' | 'egg' | 'wormhole' |
'ethics_assessment' | 'ethics_audit' | 'aesthetic_event' | 'federation_record' |
'space_object' | 'high_dim_reconstruction' | 'operation' | 'event' | 'value_snapshot' |
'snapshot' | 'artifact' | 'experiment' | 'episode' | 'sprite_interaction';
// ---- 枚举 ----
export enum CognitiveType { normal = 'normal', counterfactual = 'counterfactual', paradox = 'paradox', paradigmShift = 'paradigm_shift', negativeSpace = 'negative_space' }
export enum InnovationDepth { L1 = 'L1', L2 = 'L2', L3 = 'L3', L4 = 'L4' }
export enum NodeSource { user = 'user', fission = 'fission', fusion = 'fusion', expansion = 'expansion', external = 'external' }
export enum NodeType { seed = 'seed', derived = 'derived', merged = 'merged', external = 'external' }
export enum HyphaType { semanticAuto = 'semantic_auto', userManual = 'user_manual', causal = 'causal', analogical = 'analogical', contradiction = 'contradiction', hierarchical = 'hierarchical', sequential = 'sequential', complementary = 'complementary' }
export enum HyphaEmergence { system = 'system', user = 'user', exploration = 'exploration', federation = 'federation' }
export enum KnotStatus { emerging = 'emerging', confirmed = 'confirmed', dissolving = 'dissolving', dissolved = 'dissolved' }
export enum KnotClusterType { normal = 'normal', newParadigm = 'new_paradigm' }
export enum ValidationMode { deductive = 'deductive', simulation = 'simulation', hybrid = 'hybrid', experimental = 'experimental' }
export enum ValidationStatus { unverified = 'unverified', verified = 'verified', falsified = 'falsified', partial = 'partial', pending = 'pending' }
export enum EvidenceResult { support = 'support', refute = 'refute', inconclusive = 'inconclusive' }
export enum EggStatus { detected = 'detected', incubating = 'incubating', hatched = 'hatched', failed = 'failed', dismissed = 'dismissed' }
export enum AnomalySource { isolationForest = 'isolation_forest', surpriseAccumulation = 'surprise_accumulation', semanticBlackhole = 'semantic_blackhole', topologicalCavity = 'topological_cavity' }
export enum TransformationType { redefined = 'redefined', preserved = 'preserved', newlyEmerg = 'newly_emerged', discarded = 'discarded' }
export enum HumanReviewStatus { pending = 'pending', approved = 'approved', conditional = 'conditional', rejected = 'rejected' }
export enum RedTeamStatus { running = 'running', completed = 'completed', failed = 'failed' }
export enum EthicsActionType { inputScreening = 'input_screening', dualUseAssessment = 'dual_use_assessment', redTeamSimulation = 'red_team_simulation', humanReview = 'human_review', humanOverride = 'human_override', ethicsWaiver = 'ethics_waiver' }
export enum EthicsResult { passed = 'passed', flagged = 'flagged', blocked = 'blocked', conditionallyApproved = 'conditionally_approved' }
export enum TremorTriggerType { longGaze = 'long_gaze', repeatedZoom = 'repeated_zoom', rapidScreenshot = 'rapid_screenshot', prolongedHighlight = 'prolonged_highlight' }
export enum ReconstructionStatus { processing = 'processing', completed = 'completed', failed = 'failed' }
export enum TerrainType { highland = 'highland', lowland = 'lowland' }
export enum SpaceObjectType { gravityWell = 'gravity_well', repulsionWall = 'repulsion_wall', terrain = 'terrain' }
export enum FederationRecordType { summary = 'summary', connection = 'connection', consensus = 'consensus' }
export enum SnapshotType { manual = 'manual', auto = 'auto', milestone = 'milestone' }
export enum EventPriority { P0 = 0, P1 = 1, P2 = 2, P3 = 3, P4 = 4, P5 = 5 }
export enum OperationStatus { running = 'running', done = 'done', failed = 'failed', cancelled = 'cancelled' }
// ---- 嵌套值对象 ----
export interface Proposition { id: string; text: string; confidence: number; verified: boolean; verificationNote?: string }
export interface Reference { title: string; url?: string; doi?: string; note?: string }
export interface ValueScores { survival: number; cognitive: number; aesthetic: number; ethical: number; transcendent: number }
export interface ValidationRecord { mode: ValidationMode; status: ValidationStatus; evidence: EvidenceRecord[] }
export interface EvidenceRecord { evidenceId: string; type: string; summary: string; result: EvidenceResult; timestamp: string; dataReference?: string }
export interface DualUseRisk { score: number; assessedAt?: string; assessedBy?: string; riskCategories: string[]; riskSummary?: string; mitigationSuggestions: string[]; requiresHumanReview: boolean; humanReviewStatus?: HumanReviewStatus | null; humanReviewerId?: string; humanReviewedAt?: string }
export interface IncubationState { cocoonId: string; role: 'core' | 'peripheral'; stability: number; joinedAt: string }
export interface Spatial3 { position: [number, number, number]; radius: number; color: string }
// ---- 核心实体(对齐数据要素最终版字段) ----
export interface ConceptNode {
id: string; // node_{uuidV7}
projectId: string;
type: NodeType;
title: string;
summary: string;
content: { fullText?: string; propositions: Proposition[]; tags: string[]; domainLabels: string[]; references: Reference[] };
domain?: string;
level?: number;
dimension?: string; // DimensionType 兼容
cognitiveType: CognitiveType; // 默认 normal
embedding?: number[]; embeddingModel?: string; embeddingUpdatedAt?: string;
cognitivePotential?: number; // E'_i = E_i - ζ·V_i
valueScores: ValueScores;
validation: ValidationRecord;
dualUseRisk?: DualUseRisk | null;
aestheticTremors: Array<{ tremorId: string; detectedAt: string; triggerType: TremorTriggerType; gazeDurationMs?: number; zoomCount?: number; intensity: number; sessionId?: string }>;
incubationState?: IncubationState | null;
innovationDepth?: InnovationDepth;
paradigmGeneration: number; // 默认 0
paretoRank?: number;
spatial: Spatial3;
metadata: { createdAt: string; updatedAt: string; lastActivatedAt?: string; activationCount: number; source: NodeSource; parentOperationId?: string; createdBy?: string; version: number; versionHistory: Array<{ versionId: string; timestamp: string; changeSummary: string; operator?: string }> };
parentId?: string;
depth: number;
deletedAt?: string | null; // 软删除(30s 回收站)
}
export interface HyphaConnection {
id: string; // hyph_{uuidV7}
projectId: string;
sourceNodeId: string;
targetNodeId: string;
type: HyphaType;
strength: number; // 0-1
semanticSimilarity: number;
coactivationCount: number;
manualReinforcementCount: number;
growthHistory: { emergedAt: string; emergedBy: HyphaEmergence; confirmedAt?: string; lastReinforcedAt?: string; decayStartAt?: string };
valueFlow?: { direction: 'bidirectional' | 'source_to_target' | 'target_to_source'; intensity: number; flowTypes: Array<'cognitive' | 'aesthetic' | 'methodological'> };
visualProperties: { thickness: number; opacity: number; color: string; dashPattern: 'solid' | 'dashed' | 'dotted'; flowParticleDensity: number };
path?: Array<[number, number, number]>;
isWormhole: boolean;
wormholeProperties?: { sourceParadigmId: string; targetParadigmId: string; transformedConcepts: Array<{ conceptName: string; oldDefinition: string; newDefinition: string; transformationType: TransformationType }>; wormholeEstablishedAt: string; wormholeStability: number } | null;
federationProperties?: { sourceInstanceId: string; targetInstanceId: string; sharedAbstractHash: string; similarityScore: number; establishedAt: string } | null;
createdAt: string;
deletedAt?: string | null;
}
export interface HyphalKnot {
id: string; // knot_{uuidV7}
projectId: string;
memberNodeIds: string[]; // min 3
internalHyphaeIds: string[];
density: number;
status: KnotStatus;
emergenceConfidence: number;
autoGeneratedSummary?: string;
suggestedName: string[];
userGivenName?: string;
confirmedAt?: string;
dismissedAt?: string;
contour: { center: [number, number, number]; boundingRadius: number; controlPoints: Array<[number, number, number]> };
clusterType: KnotClusterType; // 默认 normal
paradigmProperties?: { paradigmGeneration: number; coreParadigmNodeIds: string[]; oldParadigmKnotId: string; wormholeHyphaeIds: string[]; incubationHistory: { eggId: string; cocoonFormedAt: string; cocoonBrokeAt?: string; growthStages: Array<{ stage: string; timestamp: string; skeletonStability: number; externalExplanatoryGain: number; internalNodeCount: number }> } } | null;
topologicalProperties?: { modularityScore: number; h1CyclesCount: number; h2CavitiesCount: number; averagePathLength: number; clusteringCoefficient: number; computedAt: string };
createdAt: string;
deletedAt?: string | null;
}
export interface ProjectRecord {
id: string; // proj_{uuidV7}
name: string;
description?: string;
rootSeedIdea: string;
validationMode: ValidationMode;
valueWeights: ValueScores; // 总和=1
privacyMode: boolean;
paradigmGeneration: number;
status: 'active' | 'archived';
createdAt: string;
updatedAt: string;
deletedAt?: string | null;
}
// ---- 事件溯源 ----
export interface EventRecord {
id: string; // evnt_{uuidV7}
projectId: string;
sequence: number;
eventType: string; // 'node.created' | 'command.execute' | ...
entityType: EntityType;
entityId?: string;
payload: unknown; // 命令级:{ commandType, payload } 用于撤销栈重建
priority: EventPriority;
occurredAt: string;
}
export interface OperationRecord {
id: string; // oper_{uuidV7}
projectId: string;
operationType: string; // 'fission' | 'fusion' | 'metabolism.tick' | ...
operatorId?: string;
inputIds: string[];
outputIds: string[];
params: Record<string, unknown>;
durationMs?: number;
status: OperationStatus;
error?: string;
createdAt: string;
}
// ---- 其余实体(M1 建表不建 repo,类型先落位供 DDL 对齐) ----
export interface CognitiveEgg { id: string; projectId: string; status: EggStatus; anomalySource: AnomalySource; anomalyNodes: string[]; anomalyMetrics: { avgSurprise: number; semanticIsolation: number; topologicalHoleSize: number; anomalyScore: number; computedAt: string }; incubation?: { startedAt: string; startedBy?: string; cocoonedNodeIds: string[]; coreNodeIds: string[]; internalHyphaeDensity: number; skeletonStability: number; externalExplanatoryGain: number; fusionIterations: number; expansionRounds: number; growthLog: Array<{ timestamp: string; event: string; details?: Record<string, unknown> }> } | null; hatchResult?: { newParadigmKnotId: string; hatchConfidence: number; hatchedAt: string; paradigmShiftSummary: string; transformedConceptsCount: number; newlyEmergedConceptsCount: number } | null; detectedAt: string; updatedAt: string; lastUserActionAt?: string }
export interface WormholeRecord { id: string; projectId: string; hyphaId: string; sourceKnotId: string; targetKnotId: string; paradigmGenerationGap: number; transformedConceptsDetail: Array<{ conceptName: string; oldParadigmDefinition: string; newParadigmDefinition: string; transformationType: TransformationType; semanticShiftMagnitude: number }>; establishedAt: string; lastTraversedAt?: string; traversalCount: number }
export interface EthicsAssessment { id: string; projectId: string; entityType: 'dual_use' | 'red_team'; targetNodeId?: string; targetAchievementId?: string; score?: number; riskCategories: string[]; assessmentDetail?: string; status?: RedTeamStatus; abusePathsFound?: Array<{ pathId: string; description: string; feasibilityScore: number; requiredResources: string; potentialImpact: string; mitigationSuggestions: string[] }>; assessedBy?: string; assessedAt: string }
export interface EthicsAuditEntry { id: string; projectId: string; timestamp: string; actionType: EthicsActionType; targetNodeId?: string; result: EthicsResult; detail?: Record<string, unknown>; reviewerId?: string; digitalSignature?: string; prevLogId?: string | null }
export interface AestheticTremor { id: string; projectId: string; entityType: 'tremor' | 'resonance'; nodeId: string; sessionId?: string; triggerType?: TremorTriggerType; intensity?: number; context?: { viewportState?: unknown; activeMode?: string; timeOfDay?: string }; structuralSimilarity?: number; matchedAt?: string; userNotified?: boolean; userEngaged?: boolean }
export interface FederationRecord { id: string; projectId: string; entityType: FederationRecordType; anonymousId?: string; simhash?: string; abstractSummary?: string; structuralFingerprint?: { simhash: string; topologicalSignature: string; domainLabels: string[] }; peerInstanceId?: string; connectionStatus?: 'established' | 'active' | 'suspended'; consensusStrength?: number; accessCount?: number }
export interface SpaceObject { id: string; projectId: string; objectType: SpaceObjectType; position?: [number, number, number]; radius?: number; strength: number; active: boolean; points?: Array<[number, number, number]>; boundaryPoints?: Array<[number, number, number]>; terrainType?: TerrainType; createdBy?: string; createdAt: string }
export interface HighDimReconstruction { id: string; projectId: string; sourcePerspectiveNodeIds: string[]; prismNodeId?: string; reconstructionStatus: ReconstructionStatus; transformationRules: Array<{ perspectiveA: string; perspectiveB: string; mappingDescription: string }>; emergentProperties: string[]; independentPredictions: Array<{ prediction: string; verificationMethod: string; verificationStatus: 'pending' | 'verified' | 'falsified' }>; reconstructedAt?: string }
export interface ValueSnapshot { id: string; projectId: string; entityType: 'node' | 'project' | 'knot'; entityId: string; values: ValueScores; paretoRank?: number; createdAt: string }
export interface Snapshot { id: string; projectId: string; snapshotType: SnapshotType; data: unknown; reason?: string; createdAt: string }
export interface Artifact { id: string; projectId: string; artifactType: string; title: string; content: unknown; relatedKnotIds: string[]; status: string; exportedAt?: string }
export interface Experiment { id: string; projectId: string; experimentType: string; config: unknown; result?: unknown; status: string; logs?: unknown }
export interface Episode { id: string; projectId: string; episodeType: string; summary: string; nodeIds: string[]; hyphaeIds: string[]; context?: unknown; archivedAt?: string }
export interface SpriteInteraction { id: string; projectId: string; spriteId: string; interactionType: string; context?: unknown; response?: unknown; satisfactionRating?: number }
// ---- 命令类型 ----
export type CommandType =
| 'node.create' | 'node.update' | 'node.soft_delete' | 'node.restore'
| 'hypha.create' | 'hypha.update' | 'hypha.soft_delete' | 'hypha.restore'
| 'knot.confirm' | 'knot.dissolve' | 'knot.update'
| 'project.create' | 'project.update' | 'project.soft_delete';
// ---- 视图投影 DTO(现有画布兼容) ----
export interface ViewProjection {
nodes: Array<{ id: string; title: string; summary: string; dimension?: string; domain?: string; level?: number; x: number; y: number; parentId?: string; depth: number; cognitiveType: CognitiveType; valueScores: ValueScores }>;
edges: Array<{ id: string; sourceNodeId: string; targetNodeId: string; relationType: string; strength: number }>;
rootSeedIdea: string;
createdTimestamp: string;
}
- Step 4: 运行测试验证
Run: npx vitest run src/__tests__/types.test.ts
Expected: PASS(4 个断言全过)。
- Step 5: 确认现有代码兼容(tsc)
Run: npm run lint
Expected: 无新增错误(现有类型保留,新增为纯追加)。
- Step 6: Commit
git add src/types.ts src/__tests__/types.test.ts
git commit -m "feat(types): full alignment with data-element spec (27 entities, enums, ID prefixes)"
Task 3: 服务端基础工具(id / errors / respond)
Files:
- Create:
src/lib/id.ts(前后端共用) - Create:
server/lib/errors.ts - Create:
server/lib/respond.ts - Test:
server/lib/__tests__/id.test.ts、server/lib/__tests__/respond.test.ts
Interfaces:
-
Consumes: 无
-
Produces:
uuidV7(): string、prefixedId(prefix: IdPrefix | string): stringApiError extends Error { code: string; status: number; detail?: unknown }、apiError(code, message, detail?)、toHttpStatus(code): numbersendOk(res, data, meta?)、sendErr(res, err: unknown)、parsePagination(query): { page: number; pageSize: number; offset: number }
-
Step 1: 写失败测试
Create server/lib/__tests__/id.test.ts:
import { describe, expect, it } from 'vitest';
import { uuidV7, prefixedId } from '../../../src/lib/id';
describe('id utils', () => {
it('uuidV7 has version 7 and variant bits', () => {
const id = uuidV7();
expect(id[14]).toBe('7'); // version
expect(id[19]).toBe('8'); // variant 10xx
});
it('uuidV7 embeds a recent timestamp', () => {
const a = uuidV7();
const b = uuidV7();
expect(a < b).toBe(true); // 前缀时间戳单调(同毫秒内概率极小)
});
it('prefixedId applies semantic prefix', () => {
expect(prefixedId('node_')).toMatch(/^node_[0-9a-f-]{36}$/);
expect(prefixedId('evnt_')).toMatch(/^evnt_/);
});
});
Create server/lib/__tests__/respond.test.ts:
import { describe, expect, it } from 'vitest';
import { apiError, toHttpStatus } from '../errors';
import { parsePagination } from '../respond';
describe('errors', () => {
it('maps error codes to HTTP status', () => {
expect(toHttpStatus('ERR_NOT_FOUND')).toBe(404);
expect(toHttpStatus('ERR_VALIDATION')).toBe(400);
expect(toHttpStatus('ERR_CONFLICT')).toBe(409);
expect(toHttpStatus('ERR_INTERNAL')).toBe(500);
});
it('apiError builds ApiError', () => {
const e = apiError('ERR_VALIDATION', 'bad input', { field: 'title' });
expect(e.status).toBe(400);
expect(e.detail).toEqual({ field: 'title' });
});
});
describe('respond', () => {
it('parses pagination with defaults and clamps', () => {
expect(parsePagination({})).toEqual({ page: 1, pageSize: 50, offset: 0 });
expect(parsePagination({ page: '3', pageSize: '20' })).toEqual({ page: 3, pageSize: 20, offset: 40 });
expect(parsePagination({ page: '0', pageSize: '999' })).toEqual({ page: 1, pageSize: 100, offset: 0 });
});
});
- Step 2: 运行确认失败
Run: npx vitest run server/lib/__tests__/id.test.ts server/lib/__tests__/respond.test.ts
Expected: FAIL(模块不存在)。
- Step 3: 实现
Create src/lib/id.ts:
/** UUID v7(时间前缀 + 随机),Node 18+/浏览器均可用 */
export function uuidV7(): string {
const bytes = new Uint8Array(16);
crypto.getRandomValues(bytes);
const now = Date.now();
bytes[0] = (now >>> 24) & 0xff;
bytes[1] = (now >>> 16) & 0xff;
bytes[2] = (now >>> 8) & 0xff;
bytes[3] = now & 0xff;
bytes[4] = (bytes[4] & 0x0f) | 0x70; // version 7
bytes[8] = (bytes[8] & 0x3f) | 0x80; // variant 10xx
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
}
export function prefixedId(prefix: string): string {
return `${prefix}${uuidV7()}`;
}
/** ISO 时间戳(UTC) */
export function nowIso(): string {
return new Date().toISOString();
}
Create server/lib/errors.ts:
export type ErrorCode =
| 'ERR_VALIDATION' | 'ERR_NOT_FOUND' | 'ERR_CONFLICT' | 'ERR_LLM_TIMEOUT'
| 'ERR_LLM_QUOTA' | 'ERR_INTERNAL' | 'ERR_ETHICS_BLOCKED';
const HTTP_BY_CODE: Record<ErrorCode, number> = {
ERR_VALIDATION: 400,
ERR_NOT_FOUND: 404,
ERR_CONFLICT: 409,
ERR_LLM_TIMEOUT: 504,
ERR_LLM_QUOTA: 429,
ERR_INTERNAL: 500,
ERR_ETHICS_BLOCKED: 451,
};
export class ApiError extends Error {
readonly code: ErrorCode;
readonly status: number;
readonly detail?: unknown;
constructor(code: ErrorCode, message: string, detail?: unknown) {
super(message);
this.name = 'ApiError';
this.code = code;
this.status = HTTP_BY_CODE[code];
this.detail = detail;
}
}
export function apiError(code: ErrorCode, message: string, detail?: unknown): ApiError {
return new ApiError(code, message, detail);
}
export function toHttpStatus(code: string): number {
return HTTP_BY_CODE[code as ErrorCode] ?? 500;
}
export function isApiError(err: unknown): err is ApiError {
return err instanceof ApiError;
}
Create server/lib/respond.ts:
import type { Response, Request } from 'express';
import { ApiError, apiError, isApiError } from './errors';
const DEFAULT_PAGE = 1;
const DEFAULT_PAGE_SIZE = 50;
const MAX_PAGE_SIZE = 100;
export function parsePagination(query: Record<string, unknown>): { page: number; pageSize: number; offset: number } {
const rawPage = Number(query.page ?? DEFAULT_PAGE);
const rawSize = Number(query.pageSize ?? DEFAULT_PAGE_SIZE);
const page = Number.isInteger(rawPage) && rawPage >= 1 ? rawPage : DEFAULT_PAGE;
const pageSize = Number.isInteger(rawSize) && rawSize >= 1 ? Math.min(rawSize, MAX_PAGE_SIZE) : DEFAULT_PAGE_SIZE;
return { page, pageSize, offset: (page - 1) * pageSize };
}
export function sendOk(res: Response, data: unknown, meta?: { page: number; pageSize: number; total: number }): void {
res.json({ ok: true, data, meta });
}
export function sendErr(res: Response, err: unknown): void {
if (isApiError(err)) {
res.status(err.status).json({ ok: false, error: { code: err.code, message: err.message, detail: err.detail } });
return;
}
const fallback = apiError('ERR_INTERNAL', err instanceof Error ? err.message : 'internal error');
console.error('[api] unhandled error:', err);
res.status(500).json({ ok: false, error: { code: fallback.code, message: fallback.message } });
}
- Step 4: 运行测试验证
Run: npx vitest run server/lib/__tests__/id.test.ts server/lib/__tests__/respond.test.ts
Expected: PASS。
- Step 5: Commit
git add src/lib/id.ts server/lib/errors.ts server/lib/respond.ts server/lib/__tests__/
git commit -m "feat(server): id/errors/respond utilities"
34




被折叠的 条评论
为什么被折叠?



