rag-memory-epf-mcp 3.6.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -6
- package/dist/index.d.ts +74 -5
- package/dist/index.js +909 -243
- package/dist/src/backup/preflight.d.ts +5 -0
- package/dist/src/backup/preflight.js +185 -0
- package/dist/src/chunkerC.d.ts +18 -0
- package/dist/src/chunkerC.js +210 -0
- package/dist/src/migrations/migrations.d.ts +2 -0
- package/dist/src/migrations/migrations.js +139 -0
- package/dist/src/observations/history.d.ts +8 -0
- package/dist/src/observations/history.js +64 -0
- package/dist/src/observations/lifecycle.d.ts +45 -0
- package/dist/src/observations/lifecycle.js +101 -0
- package/dist/src/observations/projection.d.ts +3 -0
- package/dist/src/observations/projection.js +31 -0
- package/dist/src/observations/schema.d.ts +1 -0
- package/dist/src/observations/schema.js +115 -0
- package/dist/src/tools/graph-query-tools.js +29 -5
- package/dist/src/tools/knowledge-graph-tools.d.ts +14 -0
- package/dist/src/tools/knowledge-graph-tools.js +244 -5
- package/dist/src/tools/migration-tools.js +1 -1
- package/dist/src/tools/rag-tools.js +10 -10
- package/dist/src/tools/tool-registry.d.ts +7 -0
- package/dist/src/tools/tool-registry.js +43 -3
- package/dist/src/tools/types.d.ts +1 -0
- package/docs/UPDATING.md +100 -0
- package/package.json +3 -3
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type Database from 'better-sqlite3';
|
|
2
|
+
export type ObsStatus = 'active' | 'superseded' | 'retracted' | 'provisional';
|
|
3
|
+
export type ObsEvent = 'add' | 'correct' | 'retract' | 'restore' | 'approve' | 'decline' | 'import';
|
|
4
|
+
export type SourceInput = {
|
|
5
|
+
source_kind: 'document' | 'conversation' | 'decision' | 'import';
|
|
6
|
+
source_ref: string;
|
|
7
|
+
source_hash?: string | null;
|
|
8
|
+
};
|
|
9
|
+
export declare function nextProjectionOrder(db: Database.Database, entityId: string): number;
|
|
10
|
+
export declare function recordEvent(db: Database.Database, a: {
|
|
11
|
+
rootId: string;
|
|
12
|
+
event: ObsEvent;
|
|
13
|
+
fromId?: string | null;
|
|
14
|
+
toId?: string | null;
|
|
15
|
+
changeKind?: 'correction' | 'world_change' | 'retraction' | null;
|
|
16
|
+
reason?: string | null;
|
|
17
|
+
actor?: string | null;
|
|
18
|
+
batchId?: string | null;
|
|
19
|
+
ts: string;
|
|
20
|
+
}): void;
|
|
21
|
+
export declare function linkSources(db: Database.Database, observationId: string, sources: SourceInput[], ts: string): number;
|
|
22
|
+
export declare function addRevision(db: Database.Database, a: {
|
|
23
|
+
entityId: string;
|
|
24
|
+
content: string;
|
|
25
|
+
status: Extract<ObsStatus, 'active' | 'provisional'>;
|
|
26
|
+
sources?: SourceInput[];
|
|
27
|
+
actor?: string | null;
|
|
28
|
+
ts: string;
|
|
29
|
+
event?: Extract<ObsEvent, 'add' | 'import'>;
|
|
30
|
+
}): string;
|
|
31
|
+
export declare function correctRevision(db: Database.Database, a: {
|
|
32
|
+
observationId: string;
|
|
33
|
+
content: string;
|
|
34
|
+
changeKind: 'correction' | 'world_change';
|
|
35
|
+
reason?: string | null;
|
|
36
|
+
actor?: string | null;
|
|
37
|
+
ts: string;
|
|
38
|
+
}): string;
|
|
39
|
+
export declare function transitionStatus(db: Database.Database, a: {
|
|
40
|
+
observationId: string;
|
|
41
|
+
event: 'retract' | 'restore' | 'approve' | 'decline';
|
|
42
|
+
reason?: string | null;
|
|
43
|
+
actor?: string | null;
|
|
44
|
+
ts: string;
|
|
45
|
+
}): void;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
// 순번 정본은 observation_roots 다. entity_observations 로 세면 revision 이
|
|
3
|
+
// purge 된 root 의 순번을 재사용해 이후 restore/approve 가 UNIQUE 로 실패한다.
|
|
4
|
+
export function nextProjectionOrder(db, entityId) {
|
|
5
|
+
const r = db.prepare(`SELECT COALESCE(MAX(projection_order), -1) + 1 AS n FROM observation_roots WHERE entity_id = ?`).get(entityId);
|
|
6
|
+
return r.n;
|
|
7
|
+
}
|
|
8
|
+
export function recordEvent(db, a) {
|
|
9
|
+
db.prepare(`INSERT INTO observation_events
|
|
10
|
+
(event_id, root_id, from_id, to_id, event, change_kind, reason, actor, batch_id, recorded_at)
|
|
11
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
12
|
+
.run(randomUUID(), a.rootId, a.fromId ?? null, a.toId ?? null, a.event, a.changeKind ?? null, a.reason ?? null, a.actor ?? null, a.batchId ?? null, a.ts);
|
|
13
|
+
}
|
|
14
|
+
// 한 관찰에 evidence 를 더한다. 같은 사실이 다른 출처에서 다시 오면
|
|
15
|
+
// 새 revision 이 아니라 source link 가 늘어난다(spec §8.3 T13).
|
|
16
|
+
export function linkSources(db, observationId, sources, ts) {
|
|
17
|
+
let n = 0;
|
|
18
|
+
for (const s of sources) {
|
|
19
|
+
n += db.prepare(`INSERT OR IGNORE INTO observation_sources
|
|
20
|
+
(observation_id, source_kind, source_ref, source_hash, recorded_at)
|
|
21
|
+
VALUES (?, ?, ?, ?, ?)`)
|
|
22
|
+
.run(observationId, s.source_kind, s.source_ref, s.source_hash ?? null, ts).changes;
|
|
23
|
+
}
|
|
24
|
+
return n;
|
|
25
|
+
}
|
|
26
|
+
// 새 논리 관찰 = root 1 + rev1 1 + sources N + event 1.
|
|
27
|
+
// event 는 기본 'add' 지만 legacy import 경로는 'import' 를 남겨야 한다(spec §6.4):
|
|
28
|
+
// 그 관찰은 사람이 방금 추가한 것이 아니라 옛 형식 dump 에서 승격된 것이고,
|
|
29
|
+
// 'add' 로 적으면 history 가 유입 경로를 잘못 말한다(advisor beta 발견 1).
|
|
30
|
+
export function addRevision(db, a) {
|
|
31
|
+
const rootId = randomUUID();
|
|
32
|
+
const obsId = randomUUID();
|
|
33
|
+
const order = nextProjectionOrder(db, a.entityId);
|
|
34
|
+
db.prepare(`INSERT INTO observation_roots (root_id, entity_id, projection_order, created_at)
|
|
35
|
+
VALUES (?, ?, ?, ?)`).run(rootId, a.entityId, order, a.ts);
|
|
36
|
+
db.prepare(`INSERT INTO entity_observations
|
|
37
|
+
(observation_id, root_id, entity_id, revision_no, projection_order,
|
|
38
|
+
content, status, supersedes_id, recorded_at, superseded_at)
|
|
39
|
+
VALUES (?, ?, ?, 1, ?, ?, ?, NULL, ?, NULL)`)
|
|
40
|
+
.run(obsId, rootId, a.entityId, order, a.content, a.status, a.ts);
|
|
41
|
+
// 출처를 모르면 source 행을 만들지 않는다.
|
|
42
|
+
// source_ref 가 NOT NULL 인 것은 "행을 반드시 만들라"는 뜻이 아니다.
|
|
43
|
+
// sources 가 생략됐는데 conversation/unspecified 를 넣으면 "대화에서 왔다"는
|
|
44
|
+
// 사실을 발명하는 것이고, provenance 의 목적과 정반대다
|
|
45
|
+
// (advisor 구현리뷰 r1 발견 5). unknown 은 0행으로 표현한다.
|
|
46
|
+
if (a.sources?.length)
|
|
47
|
+
linkSources(db, obsId, a.sources, a.ts);
|
|
48
|
+
recordEvent(db, { rootId, event: a.event ?? 'add', toId: obsId,
|
|
49
|
+
actor: a.actor ?? null, ts: a.ts });
|
|
50
|
+
return obsId;
|
|
51
|
+
}
|
|
52
|
+
// §4.4 복합 전이: ①구 active -> superseded ②신규 revision -> active ③correct event.
|
|
53
|
+
// 셋이 한 트랜잭션 안에서 일어나야 한다 — 호출자가 mutateEntityAndInvalidate 로 감싼다.
|
|
54
|
+
export function correctRevision(db, a) {
|
|
55
|
+
const cur = db.prepare(`SELECT observation_id, root_id, entity_id, revision_no, projection_order, status
|
|
56
|
+
FROM entity_observations WHERE observation_id = ?`).get(a.observationId);
|
|
57
|
+
if (!cur)
|
|
58
|
+
throw new Error(`observation ${a.observationId} not found`);
|
|
59
|
+
if (cur.status !== 'active')
|
|
60
|
+
throw new Error(`cannot correct an observation in status '${cur.status}' — only 'active' (spec §4.4)`);
|
|
61
|
+
const newId = randomUUID();
|
|
62
|
+
// ① 구 행을 먼저 superseded 로. active-per-root 부분 UNIQUE 때문에 순서가 계약이다.
|
|
63
|
+
db.prepare(`UPDATE entity_observations SET status='superseded', superseded_at=? WHERE observation_id=?`)
|
|
64
|
+
.run(a.ts, cur.observation_id);
|
|
65
|
+
// ② 신규 revision. projection_order 는 전임자 상속 = 배열 위치가 움직이지 않는다.
|
|
66
|
+
db.prepare(`INSERT INTO entity_observations
|
|
67
|
+
(observation_id, root_id, entity_id, revision_no, projection_order,
|
|
68
|
+
content, status, supersedes_id, recorded_at, superseded_at)
|
|
69
|
+
VALUES (?, ?, ?, ?, ?, ?, 'active', ?, ?, NULL)`)
|
|
70
|
+
.run(newId, cur.root_id, cur.entity_id, cur.revision_no + 1, cur.projection_order, a.content, cur.observation_id, a.ts);
|
|
71
|
+
// ③ event
|
|
72
|
+
recordEvent(db, { rootId: cur.root_id, event: 'correct', fromId: cur.observation_id,
|
|
73
|
+
toId: newId, changeKind: a.changeKind, reason: a.reason ?? null,
|
|
74
|
+
actor: a.actor ?? null, ts: a.ts });
|
|
75
|
+
return newId;
|
|
76
|
+
}
|
|
77
|
+
// §4.4 전이 표를 코드로. 표에 없는 조합은 거부한다 — 'superseded' 는 종착이다.
|
|
78
|
+
const ALLOWED = {
|
|
79
|
+
active: [{ to: 'retracted', event: 'retract' }],
|
|
80
|
+
retracted: [{ to: 'active', event: 'restore' }],
|
|
81
|
+
provisional: [{ to: 'active', event: 'approve' },
|
|
82
|
+
{ to: 'retracted', event: 'decline' }],
|
|
83
|
+
superseded: [],
|
|
84
|
+
};
|
|
85
|
+
export function transitionStatus(db, a) {
|
|
86
|
+
const cur = db.prepare(`SELECT observation_id, root_id, entity_id, status FROM entity_observations WHERE observation_id = ?`).get(a.observationId);
|
|
87
|
+
if (!cur)
|
|
88
|
+
throw new Error(`observation ${a.observationId} not found`);
|
|
89
|
+
const allowed = (ALLOWED[cur.status] ?? []).find(x => x.event === a.event);
|
|
90
|
+
if (!allowed) {
|
|
91
|
+
throw new Error(`illegal transition: '${a.event}' from status '${cur.status}' is not in the §4.4 table` +
|
|
92
|
+
(cur.status === 'superseded'
|
|
93
|
+
? " — 'superseded' is terminal; create a new revision instead"
|
|
94
|
+
: ''));
|
|
95
|
+
}
|
|
96
|
+
db.prepare(`UPDATE entity_observations SET status = ? WHERE observation_id = ?`)
|
|
97
|
+
.run(allowed.to, cur.observation_id);
|
|
98
|
+
recordEvent(db, { rootId: cur.root_id, event: a.event, fromId: cur.observation_id,
|
|
99
|
+
toId: cur.observation_id, reason: a.reason ?? null,
|
|
100
|
+
actor: a.actor ?? null, ts: a.ts });
|
|
101
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// active 행을 projection_order 순으로 모아 entities.observations 를 재작성한다.
|
|
2
|
+
// 단계 1 에서는 이 배열이 여전히 FTS/벡터의 입력이다 — 그래서 projection 갱신만으로
|
|
3
|
+
// 기존 entity FTS 트리거와 벡터 무효화가 따라온다(spec §4.5).
|
|
4
|
+
export function rebuildProjection(db, entityId) {
|
|
5
|
+
const rows = db.prepare(`SELECT content FROM entity_observations
|
|
6
|
+
WHERE entity_id = ? AND status = 'active'
|
|
7
|
+
ORDER BY projection_order`).all(entityId);
|
|
8
|
+
db.prepare(`UPDATE entities SET observations = ? WHERE id = ?`)
|
|
9
|
+
.run(JSON.stringify(rows.map(r => r.content)), entityId);
|
|
10
|
+
}
|
|
11
|
+
// D4: observation 이 바뀌면 그 entity 를 가리키는 KG chunk 는 stale 이다.
|
|
12
|
+
// 단계 1 에서는 재생성하지 않고 fail-closed 로 제거한다 — 낡은 텍스트가
|
|
13
|
+
// hybridSearch 에 남아 있는 것이 없는 것보다 나쁘다.
|
|
14
|
+
//
|
|
15
|
+
// KG chunk 의 식별자는 document_id 가 아니라 (chunk_type, entity_id) 다:
|
|
16
|
+
// generateKnowledgeGraphChunks() 는 document_id 를 넣지 않는다.
|
|
17
|
+
//
|
|
18
|
+
// 이 경로는 현재 dormant 다 — generateKnowledgeGraphChunks/embedKnowledgeGraphChunks
|
|
19
|
+
// 는 MCP 도구로 노출되지 않고 내부 호출 지점도 없으며, 실사용 DB 의 chunk 는 전부
|
|
20
|
+
// chunk_type='document' 였다. 그래서 이것은 미래·타 배포 대비 방어층이고,
|
|
21
|
+
// 테스트는 KG chunk 를 직접 심어서 검증한다(자연 발생하지 않는다).
|
|
22
|
+
export function deleteStaleKgChunks(db, entityId) {
|
|
23
|
+
const chunks = db.prepare(`SELECT rowid FROM chunk_metadata WHERE chunk_type = 'entity' AND entity_id = ?`).all(entityId);
|
|
24
|
+
let n = 0;
|
|
25
|
+
for (const c of chunks) {
|
|
26
|
+
db.exec(`DELETE FROM chunks WHERE rowid = ${Number(c.rowid)}`);
|
|
27
|
+
db.prepare(`DELETE FROM chunk_metadata WHERE rowid = ?`).run(c.rowid);
|
|
28
|
+
n++;
|
|
29
|
+
}
|
|
30
|
+
return n;
|
|
31
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const OBSERVATION_SCHEMA_SQL = "\nCREATE TABLE IF NOT EXISTS observation_roots (\n root_id TEXT PRIMARY KEY NOT NULL,\n entity_id TEXT NOT NULL REFERENCES entities(id) ON DELETE CASCADE,\n projection_order INTEGER NOT NULL\n CHECK (typeof(projection_order) = 'integer' AND projection_order >= 0),\n created_at DATETIME NOT NULL,\n UNIQUE (entity_id, projection_order)\n);\n\nCREATE TABLE IF NOT EXISTS entity_observations (\n observation_id TEXT PRIMARY KEY NOT NULL,\n root_id TEXT NOT NULL REFERENCES observation_roots(root_id) ON DELETE CASCADE,\n entity_id TEXT NOT NULL REFERENCES entities(id) ON DELETE CASCADE,\n revision_no INTEGER NOT NULL\n CHECK (typeof(revision_no) = 'integer' AND revision_no >= 1),\n projection_order INTEGER NOT NULL\n CHECK (typeof(projection_order) = 'integer' AND projection_order >= 0),\n content TEXT NOT NULL,\n status TEXT NOT NULL CHECK (status IN\n ('active','superseded','retracted','provisional')),\n supersedes_id TEXT REFERENCES entity_observations(observation_id),\n recorded_at DATETIME NOT NULL,\n superseded_at DATETIME,\n UNIQUE (root_id, revision_no)\n);\n\nCREATE TABLE IF NOT EXISTS observation_sources (\n observation_id TEXT NOT NULL REFERENCES entity_observations(observation_id) ON DELETE CASCADE,\n source_kind TEXT NOT NULL CHECK (source_kind IN\n ('document','conversation','decision','import')),\n source_ref TEXT NOT NULL,\n source_hash TEXT,\n recorded_at DATETIME NOT NULL,\n PRIMARY KEY (observation_id, source_kind, source_ref)\n);\n\nCREATE TABLE IF NOT EXISTS observation_events (\n event_id TEXT PRIMARY KEY NOT NULL,\n root_id TEXT NOT NULL REFERENCES observation_roots(root_id) ON DELETE CASCADE,\n from_id TEXT,\n to_id TEXT,\n event TEXT NOT NULL CHECK (event IN\n ('add','correct','retract','restore','approve','decline','import')),\n change_kind TEXT CHECK (change_kind IN ('correction','world_change','retraction')),\n reason TEXT,\n actor TEXT,\n batch_id TEXT,\n recorded_at DATETIME NOT NULL\n);\n\nCREATE UNIQUE INDEX IF NOT EXISTS idx_obs_active_per_root\n ON entity_observations(root_id) WHERE status = 'active';\n\nCREATE UNIQUE INDEX IF NOT EXISTS idx_obs_active_order\n ON entity_observations(entity_id, projection_order) WHERE status = 'active';\n\nCREATE INDEX IF NOT EXISTS idx_obs_entity ON entity_observations(entity_id);\nCREATE INDEX IF NOT EXISTS idx_obs_root ON entity_observations(root_id);\nCREATE INDEX IF NOT EXISTS idx_obs_events_root ON observation_events(root_id);\n\nCREATE TRIGGER IF NOT EXISTS trg_roots_immutable\nBEFORE UPDATE ON observation_roots\nBEGIN\n SELECT RAISE(ABORT, 'observation_roots is immutable');\nEND;\n\nCREATE TRIGGER IF NOT EXISTS trg_obs_content_immutable\nBEFORE UPDATE OF content ON entity_observations\nBEGIN\n SELECT RAISE(ABORT, 'observation content is immutable; create a new revision');\nEND;\n\nCREATE TRIGGER IF NOT EXISTS trg_obs_identity_immutable\nBEFORE UPDATE OF observation_id, root_id, entity_id, revision_no, supersedes_id,\n projection_order, recorded_at\nON entity_observations\nBEGIN\n SELECT RAISE(ABORT, 'identity/order fields are immutable after insert');\nEND;\n\nCREATE TRIGGER IF NOT EXISTS trg_obs_matches_root\nBEFORE INSERT ON entity_observations\nBEGIN\n SELECT RAISE(ABORT, 'root_id must exist and (entity_id, projection_order) must match it')\n WHERE NOT EXISTS (\n SELECT 1 FROM observation_roots r\n WHERE r.root_id = NEW.root_id\n AND r.entity_id = NEW.entity_id\n AND r.projection_order = NEW.projection_order);\nEND;\n\nCREATE TRIGGER IF NOT EXISTS trg_obs_chain_wellformed\nBEFORE INSERT ON entity_observations\nBEGIN\n SELECT RAISE(ABORT, 'revision_no must be >= 1') WHERE NEW.revision_no < 1;\n SELECT RAISE(ABORT, 'first revision must have NULL supersedes_id')\n WHERE NEW.revision_no = 1 AND NEW.supersedes_id IS NOT NULL;\n SELECT RAISE(ABORT, 'non-first revision must have a predecessor')\n WHERE NEW.revision_no > 1 AND NEW.supersedes_id IS NULL;\n SELECT RAISE(ABORT, 'supersedes must be the immediately preceding revision of the same root')\n WHERE NEW.supersedes_id IS NOT NULL AND NOT EXISTS (\n SELECT 1 FROM entity_observations p\n WHERE p.observation_id = NEW.supersedes_id\n AND p.root_id = NEW.root_id\n AND p.revision_no = NEW.revision_no - 1);\nEND;\n";
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// v13 observation lifecycle schema (spec 2026-07-30 §4.1~4.3).
|
|
2
|
+
// 마이그레이션과 테스트가 이 상수를 공유한다 — 두 곳에 DDL 을 복제하면 갈라진다.
|
|
3
|
+
//
|
|
4
|
+
// SQLite 함정 3가지가 이 DDL 의 형태를 결정했다 (전부 실측):
|
|
5
|
+
// 1. INTEGER NOT NULL 은 타입 강제가 아니다 -> CHECK (typeof(x)='integer')
|
|
6
|
+
// 2. TEXT PRIMARY KEY 는 NULL 을 허용한다 -> NOT NULL 명시
|
|
7
|
+
// 3. BEFORE 트리거가 CHECK 보다 먼저 실행된다 -> 오류 문자열 순서가 정해진다
|
|
8
|
+
export const OBSERVATION_SCHEMA_SQL = `
|
|
9
|
+
CREATE TABLE IF NOT EXISTS observation_roots (
|
|
10
|
+
root_id TEXT PRIMARY KEY NOT NULL,
|
|
11
|
+
entity_id TEXT NOT NULL REFERENCES entities(id) ON DELETE CASCADE,
|
|
12
|
+
projection_order INTEGER NOT NULL
|
|
13
|
+
CHECK (typeof(projection_order) = 'integer' AND projection_order >= 0),
|
|
14
|
+
created_at DATETIME NOT NULL,
|
|
15
|
+
UNIQUE (entity_id, projection_order)
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
CREATE TABLE IF NOT EXISTS entity_observations (
|
|
19
|
+
observation_id TEXT PRIMARY KEY NOT NULL,
|
|
20
|
+
root_id TEXT NOT NULL REFERENCES observation_roots(root_id) ON DELETE CASCADE,
|
|
21
|
+
entity_id TEXT NOT NULL REFERENCES entities(id) ON DELETE CASCADE,
|
|
22
|
+
revision_no INTEGER NOT NULL
|
|
23
|
+
CHECK (typeof(revision_no) = 'integer' AND revision_no >= 1),
|
|
24
|
+
projection_order INTEGER NOT NULL
|
|
25
|
+
CHECK (typeof(projection_order) = 'integer' AND projection_order >= 0),
|
|
26
|
+
content TEXT NOT NULL,
|
|
27
|
+
status TEXT NOT NULL CHECK (status IN
|
|
28
|
+
('active','superseded','retracted','provisional')),
|
|
29
|
+
supersedes_id TEXT REFERENCES entity_observations(observation_id),
|
|
30
|
+
recorded_at DATETIME NOT NULL,
|
|
31
|
+
superseded_at DATETIME,
|
|
32
|
+
UNIQUE (root_id, revision_no)
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
CREATE TABLE IF NOT EXISTS observation_sources (
|
|
36
|
+
observation_id TEXT NOT NULL REFERENCES entity_observations(observation_id) ON DELETE CASCADE,
|
|
37
|
+
source_kind TEXT NOT NULL CHECK (source_kind IN
|
|
38
|
+
('document','conversation','decision','import')),
|
|
39
|
+
source_ref TEXT NOT NULL,
|
|
40
|
+
source_hash TEXT,
|
|
41
|
+
recorded_at DATETIME NOT NULL,
|
|
42
|
+
PRIMARY KEY (observation_id, source_kind, source_ref)
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
CREATE TABLE IF NOT EXISTS observation_events (
|
|
46
|
+
event_id TEXT PRIMARY KEY NOT NULL,
|
|
47
|
+
root_id TEXT NOT NULL REFERENCES observation_roots(root_id) ON DELETE CASCADE,
|
|
48
|
+
from_id TEXT,
|
|
49
|
+
to_id TEXT,
|
|
50
|
+
event TEXT NOT NULL CHECK (event IN
|
|
51
|
+
('add','correct','retract','restore','approve','decline','import')),
|
|
52
|
+
change_kind TEXT CHECK (change_kind IN ('correction','world_change','retraction')),
|
|
53
|
+
reason TEXT,
|
|
54
|
+
actor TEXT,
|
|
55
|
+
batch_id TEXT,
|
|
56
|
+
recorded_at DATETIME NOT NULL
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_obs_active_per_root
|
|
60
|
+
ON entity_observations(root_id) WHERE status = 'active';
|
|
61
|
+
|
|
62
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_obs_active_order
|
|
63
|
+
ON entity_observations(entity_id, projection_order) WHERE status = 'active';
|
|
64
|
+
|
|
65
|
+
CREATE INDEX IF NOT EXISTS idx_obs_entity ON entity_observations(entity_id);
|
|
66
|
+
CREATE INDEX IF NOT EXISTS idx_obs_root ON entity_observations(root_id);
|
|
67
|
+
CREATE INDEX IF NOT EXISTS idx_obs_events_root ON observation_events(root_id);
|
|
68
|
+
|
|
69
|
+
CREATE TRIGGER IF NOT EXISTS trg_roots_immutable
|
|
70
|
+
BEFORE UPDATE ON observation_roots
|
|
71
|
+
BEGIN
|
|
72
|
+
SELECT RAISE(ABORT, 'observation_roots is immutable');
|
|
73
|
+
END;
|
|
74
|
+
|
|
75
|
+
CREATE TRIGGER IF NOT EXISTS trg_obs_content_immutable
|
|
76
|
+
BEFORE UPDATE OF content ON entity_observations
|
|
77
|
+
BEGIN
|
|
78
|
+
SELECT RAISE(ABORT, 'observation content is immutable; create a new revision');
|
|
79
|
+
END;
|
|
80
|
+
|
|
81
|
+
CREATE TRIGGER IF NOT EXISTS trg_obs_identity_immutable
|
|
82
|
+
BEFORE UPDATE OF observation_id, root_id, entity_id, revision_no, supersedes_id,
|
|
83
|
+
projection_order, recorded_at
|
|
84
|
+
ON entity_observations
|
|
85
|
+
BEGIN
|
|
86
|
+
SELECT RAISE(ABORT, 'identity/order fields are immutable after insert');
|
|
87
|
+
END;
|
|
88
|
+
|
|
89
|
+
CREATE TRIGGER IF NOT EXISTS trg_obs_matches_root
|
|
90
|
+
BEFORE INSERT ON entity_observations
|
|
91
|
+
BEGIN
|
|
92
|
+
SELECT RAISE(ABORT, 'root_id must exist and (entity_id, projection_order) must match it')
|
|
93
|
+
WHERE NOT EXISTS (
|
|
94
|
+
SELECT 1 FROM observation_roots r
|
|
95
|
+
WHERE r.root_id = NEW.root_id
|
|
96
|
+
AND r.entity_id = NEW.entity_id
|
|
97
|
+
AND r.projection_order = NEW.projection_order);
|
|
98
|
+
END;
|
|
99
|
+
|
|
100
|
+
CREATE TRIGGER IF NOT EXISTS trg_obs_chain_wellformed
|
|
101
|
+
BEFORE INSERT ON entity_observations
|
|
102
|
+
BEGIN
|
|
103
|
+
SELECT RAISE(ABORT, 'revision_no must be >= 1') WHERE NEW.revision_no < 1;
|
|
104
|
+
SELECT RAISE(ABORT, 'first revision must have NULL supersedes_id')
|
|
105
|
+
WHERE NEW.revision_no = 1 AND NEW.supersedes_id IS NOT NULL;
|
|
106
|
+
SELECT RAISE(ABORT, 'non-first revision must have a predecessor')
|
|
107
|
+
WHERE NEW.revision_no > 1 AND NEW.supersedes_id IS NULL;
|
|
108
|
+
SELECT RAISE(ABORT, 'supersedes must be the immediately preceding revision of the same root')
|
|
109
|
+
WHERE NEW.supersedes_id IS NOT NULL AND NOT EXISTS (
|
|
110
|
+
SELECT 1 FROM entity_observations p
|
|
111
|
+
WHERE p.observation_id = NEW.supersedes_id
|
|
112
|
+
AND p.root_id = NEW.root_id
|
|
113
|
+
AND p.revision_no = NEW.revision_no - 1);
|
|
114
|
+
END;
|
|
115
|
+
`;
|
|
@@ -563,7 +563,8 @@ Supports both merge (additive) and replace (clear + import) modes.
|
|
|
563
563
|
- Replace mode provides clean import without conflicts
|
|
564
564
|
- Handles partial imports (entities only, relations only, etc.)
|
|
565
565
|
- Reports imported and skipped counts for verification
|
|
566
|
-
- Supports full
|
|
566
|
+
- Supports the full exportGraph format, including the v13 observation lifecycle tables
|
|
567
|
+
(revision history, provenance and events survive a round-trip)
|
|
567
568
|
</features>
|
|
568
569
|
|
|
569
570
|
<bestPractices>
|
|
@@ -587,12 +588,35 @@ Supports both merge (additive) and replace (clear + import) modes.
|
|
|
587
588
|
- Full replace: {"data": {"entities": [...], "relations": [...], "documents": [...]}, "merge": false}
|
|
588
589
|
- Entities only: {"data": {"entities": [{"id": "abc", "name": "Test", "entityType": "CONCEPT", "observations": ["fact1"]}]}}
|
|
589
590
|
</examples>`;
|
|
591
|
+
// dump 의 행은 객체다. z.any() 로 두면 광고 스키마에 type:'string' fallback 으로 나가
|
|
592
|
+
// listTools 계약이 실제 입력과 어긋난다. z.record 는 "문자열 키를 가진 객체"로 광고되고
|
|
593
|
+
// 비객체 입력을 거부한다(advisor beta r3 남은 P2).
|
|
594
|
+
const dumpRow = () => z.record(z.any());
|
|
590
595
|
const importGraphSchema = {
|
|
591
596
|
data: z.object({
|
|
592
|
-
entities: z.array(
|
|
593
|
-
relations: z.array(
|
|
594
|
-
documents: z.array(
|
|
595
|
-
|
|
597
|
+
entities: z.array(dumpRow()).optional().describe('Array of entity objects to import'),
|
|
598
|
+
relations: z.array(dumpRow()).optional().describe('Array of relation objects to import'),
|
|
599
|
+
documents: z.array(dumpRow()).optional().describe('Array of document objects to import'),
|
|
600
|
+
// v13: 이 네 배열이 스키마에 없으면 z.object().parse() 가 조용히 버린다. 그러면
|
|
601
|
+
// exportGraph 가 낸 완전한 dump 를 MCP 로 되돌릴 때 revision history·provenance·
|
|
602
|
+
// event log 가 전부 사라지고 legacy 관찰로 재생성된다 — 백업/복원이 조용히
|
|
603
|
+
// 손실 연산이 된다(advisor beta 발견 1, MCP 왕복으로 실측).
|
|
604
|
+
observation_roots: z.array(dumpRow()).optional()
|
|
605
|
+
.describe('Array of observation root rows (v13 lifecycle). Required to preserve history on restore'),
|
|
606
|
+
entity_observations: z.array(dumpRow()).optional()
|
|
607
|
+
.describe('Array of observation revision rows (v13 lifecycle)'),
|
|
608
|
+
observation_sources: z.array(dumpRow()).optional()
|
|
609
|
+
.describe('Array of observation provenance rows (v13 lifecycle)'),
|
|
610
|
+
observation_events: z.array(dumpRow()).optional()
|
|
611
|
+
.describe('Array of observation event rows (v13 lifecycle)'),
|
|
612
|
+
metadata: z.record(z.any()).optional()
|
|
613
|
+
.describe('Export metadata (exportedAt, version, counts). Ignored on import'),
|
|
614
|
+
// strict: dump 의 모든 정상 키를 위에 열거했으므로, 모르는 키는 **거부**한다.
|
|
615
|
+
// 기본 strip 은 모르는 테이블을 조용히 버리고(그게 lifecycle 4배열에서 실제로
|
|
616
|
+
// 일어난 일이다), passthrough 는 그것을 manager 까지 흘려보내 거기서 다시 조용히
|
|
617
|
+
// 무시된다 — 둘 다 같은 미래 데이터 손실 경로다. 지원하지 않는 dump 는 크게 실패해야
|
|
618
|
+
// 운영자가 엔진을 올린다(advisor beta r3).
|
|
619
|
+
}).strict().describe('A graph dump as produced by exportGraph, including the v13 observation lifecycle tables'),
|
|
596
620
|
merge: z.boolean().optional().default(true).describe('If true (default), merge with existing data. If false, clear existing data first.'),
|
|
597
621
|
};
|
|
598
622
|
export const importGraphTool = {
|
|
@@ -6,6 +6,13 @@ export declare const hybridSearchTool: ToolDefinition;
|
|
|
6
6
|
export declare const embedAllEntitiesTool: ToolDefinition;
|
|
7
7
|
export declare const getDetailedContextTool: ToolDefinition;
|
|
8
8
|
export declare const updateRelationsTool: ToolDefinition;
|
|
9
|
+
export declare const retractObservationTool: ToolDefinition;
|
|
10
|
+
export declare const restoreObservationTool: ToolDefinition;
|
|
11
|
+
export declare const approveObservationTool: ToolDefinition;
|
|
12
|
+
export declare const declineObservationTool: ToolDefinition;
|
|
13
|
+
export declare const correctObservationTool: ToolDefinition;
|
|
14
|
+
export declare const purgeObservationTool: ToolDefinition;
|
|
15
|
+
export declare const getObservationHistoryTool: ToolDefinition;
|
|
9
16
|
export declare const knowledgeGraphTools: {
|
|
10
17
|
createEntities: ToolDefinition;
|
|
11
18
|
createRelations: ToolDefinition;
|
|
@@ -14,4 +21,11 @@ export declare const knowledgeGraphTools: {
|
|
|
14
21
|
hybridSearch: ToolDefinition;
|
|
15
22
|
embedAllEntities: ToolDefinition;
|
|
16
23
|
getDetailedContext: ToolDefinition;
|
|
24
|
+
correctObservation: ToolDefinition;
|
|
25
|
+
retractObservation: ToolDefinition;
|
|
26
|
+
restoreObservation: ToolDefinition;
|
|
27
|
+
approveObservation: ToolDefinition;
|
|
28
|
+
declineObservation: ToolDefinition;
|
|
29
|
+
purgeObservation: ToolDefinition;
|
|
30
|
+
getObservationHistory: ToolDefinition;
|
|
17
31
|
};
|