@chainlesschain/personal-data-hub 0.1.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.
Files changed (50) hide show
  1. package/README.md +241 -0
  2. package/__tests__/adapter-spec.test.js +78 -0
  3. package/__tests__/adapters/email-adapter.test.js +605 -0
  4. package/__tests__/adapters/email-imap-session.test.js +334 -0
  5. package/__tests__/adapters/email-parser.test.js +244 -0
  6. package/__tests__/adapters/email-providers.test.js +84 -0
  7. package/__tests__/analysis.test.js +302 -0
  8. package/__tests__/batch.test.js +133 -0
  9. package/__tests__/bridges-cc-kg.test.js +231 -0
  10. package/__tests__/bridges-cc-llm.test.js +191 -0
  11. package/__tests__/bridges-cc-rag.test.js +162 -0
  12. package/__tests__/ids.test.js +45 -0
  13. package/__tests__/key-providers.test.js +126 -0
  14. package/__tests__/kg-derive.test.js +219 -0
  15. package/__tests__/llm-client.test.js +122 -0
  16. package/__tests__/mock-adapter.test.js +93 -0
  17. package/__tests__/prompt-builder.test.js +204 -0
  18. package/__tests__/query-parser.test.js +150 -0
  19. package/__tests__/rag-derive.test.js +169 -0
  20. package/__tests__/registry.test.js +304 -0
  21. package/__tests__/schemas.test.js +331 -0
  22. package/__tests__/vault.test.js +506 -0
  23. package/lib/adapter-spec.js +155 -0
  24. package/lib/adapters/email-imap/email-adapter.js +398 -0
  25. package/lib/adapters/email-imap/email-parser.js +177 -0
  26. package/lib/adapters/email-imap/imap-session.js +294 -0
  27. package/lib/adapters/email-imap/index.js +26 -0
  28. package/lib/adapters/email-imap/providers.js +111 -0
  29. package/lib/analysis.js +226 -0
  30. package/lib/batch.js +123 -0
  31. package/lib/bridges/cc-kg-sink.js +264 -0
  32. package/lib/bridges/cc-llm-adapter.js +169 -0
  33. package/lib/bridges/cc-rag-sink.js +118 -0
  34. package/lib/bridges/index.js +44 -0
  35. package/lib/constants.js +92 -0
  36. package/lib/ids.js +103 -0
  37. package/lib/index.js +141 -0
  38. package/lib/key-providers.js +146 -0
  39. package/lib/kg-derive.js +214 -0
  40. package/lib/llm-client.js +171 -0
  41. package/lib/migrations.js +246 -0
  42. package/lib/mock-adapter.js +199 -0
  43. package/lib/prompt-builder.js +205 -0
  44. package/lib/query-parser.js +250 -0
  45. package/lib/rag-derive.js +186 -0
  46. package/lib/registry.js +398 -0
  47. package/lib/schemas.js +379 -0
  48. package/lib/vault.js +883 -0
  49. package/package.json +63 -0
  50. package/vitest.config.js +10 -0
@@ -0,0 +1,331 @@
1
+ "use strict";
2
+
3
+ import { describe, it, expect } from "vitest";
4
+
5
+ const {
6
+ validate,
7
+ validatePerson,
8
+ validateEvent,
9
+ validatePlace,
10
+ validateItem,
11
+ validateTopic,
12
+ } = require("../lib/schemas");
13
+ const { newId } = require("../lib/ids");
14
+
15
+ // ─── Fixtures ─────────────────────────────────────────────────────────────
16
+
17
+ const sourceOk = () => ({
18
+ adapter: "email-imap",
19
+ adapterVersion: "0.1.0",
20
+ capturedAt: Date.now(),
21
+ capturedBy: "api",
22
+ });
23
+
24
+ const personOk = (overrides = {}) => ({
25
+ id: newId(),
26
+ type: "person",
27
+ subtype: "contact",
28
+ names: ["妈妈", "陈某某"],
29
+ ingestedAt: Date.now(),
30
+ source: sourceOk(),
31
+ ...overrides,
32
+ });
33
+
34
+ const eventOk = (overrides = {}) => ({
35
+ id: newId(),
36
+ type: "event",
37
+ subtype: "message",
38
+ occurredAt: Date.now() - 1000,
39
+ ingestedAt: Date.now(),
40
+ content: { text: "Hello" },
41
+ source: sourceOk(),
42
+ ...overrides,
43
+ });
44
+
45
+ const placeOk = (overrides = {}) => ({
46
+ id: newId(),
47
+ type: "place",
48
+ name: "家",
49
+ aliases: ["home"],
50
+ ingestedAt: Date.now(),
51
+ source: sourceOk(),
52
+ ...overrides,
53
+ });
54
+
55
+ const itemOk = (overrides = {}) => ({
56
+ id: newId(),
57
+ type: "item",
58
+ subtype: "product",
59
+ name: "蛋白粉",
60
+ ingestedAt: Date.now(),
61
+ source: sourceOk(),
62
+ ...overrides,
63
+ });
64
+
65
+ const topicOk = (overrides = {}) => ({
66
+ id: newId(),
67
+ type: "topic",
68
+ name: "母亲健康",
69
+ ingestedAt: Date.now(),
70
+ source: sourceOk(),
71
+ ...overrides,
72
+ });
73
+
74
+ // ─── Person ───────────────────────────────────────────────────────────────
75
+
76
+ describe("validatePerson", () => {
77
+ it("accepts a minimal valid contact", () => {
78
+ const r = validatePerson(personOk());
79
+ expect(r.valid).toBe(true);
80
+ expect(r.errors).toEqual([]);
81
+ });
82
+
83
+ it("accepts ai-agent subtype (for AI vendors)", () => {
84
+ const r = validatePerson(
85
+ personOk({ subtype: "ai-agent", names: ["DeepSeek"], identifiers: { vendor: "deepseek" } })
86
+ );
87
+ expect(r.valid).toBe(true);
88
+ });
89
+
90
+ it("rejects empty names array", () => {
91
+ const r = validatePerson(personOk({ names: [] }));
92
+ expect(r.valid).toBe(false);
93
+ expect(r.errors.some((e) => e.includes("names"))).toBe(true);
94
+ });
95
+
96
+ it("rejects unknown subtype", () => {
97
+ const r = validatePerson(personOk({ subtype: "alien" }));
98
+ expect(r.valid).toBe(false);
99
+ expect(r.errors.some((e) => e.includes("subtype"))).toBe(true);
100
+ });
101
+
102
+ it("rejects wrong type field", () => {
103
+ const r = validatePerson(personOk({ type: "event" }));
104
+ expect(r.valid).toBe(false);
105
+ });
106
+
107
+ it("accepts identifiers with string or string[] values", () => {
108
+ const r = validatePerson(
109
+ personOk({
110
+ identifiers: { phone: ["138-0000-1111"], email: "mom@example.com", wechatId: "wxid_xyz" },
111
+ })
112
+ );
113
+ expect(r.valid).toBe(true);
114
+ });
115
+
116
+ it("rejects identifiers value of wrong type", () => {
117
+ const r = validatePerson(personOk({ identifiers: { phone: 13800001111 } }));
118
+ expect(r.valid).toBe(false);
119
+ expect(r.errors.some((e) => e.includes("identifiers.phone"))).toBe(true);
120
+ });
121
+ });
122
+
123
+ // ─── Event ────────────────────────────────────────────────────────────────
124
+
125
+ describe("validateEvent", () => {
126
+ it("accepts a minimal valid message event", () => {
127
+ const r = validateEvent(eventOk());
128
+ expect(r.valid).toBe(true);
129
+ });
130
+
131
+ it("accepts an order event with amount", () => {
132
+ const r = validateEvent(
133
+ eventOk({
134
+ subtype: "order",
135
+ content: {
136
+ title: "蛋白粉",
137
+ amount: { value: 288.5, currency: "CNY", direction: "out" },
138
+ },
139
+ })
140
+ );
141
+ expect(r.valid).toBe(true);
142
+ });
143
+
144
+ it("rejects amount missing direction", () => {
145
+ const r = validateEvent(
146
+ eventOk({ content: { amount: { value: 100, currency: "CNY" } } })
147
+ );
148
+ expect(r.valid).toBe(false);
149
+ expect(r.errors.some((e) => e.includes("direction"))).toBe(true);
150
+ });
151
+
152
+ it("rejects amount with non-number value", () => {
153
+ const r = validateEvent(
154
+ eventOk({ content: { amount: { value: "100", currency: "CNY", direction: "out" } } })
155
+ );
156
+ expect(r.valid).toBe(false);
157
+ });
158
+
159
+ it("rejects negative occurredAt", () => {
160
+ const r = validateEvent(eventOk({ occurredAt: -1 }));
161
+ expect(r.valid).toBe(false);
162
+ });
163
+
164
+ it("rejects unknown subtype", () => {
165
+ const r = validateEvent(eventOk({ subtype: "frobnicate" }));
166
+ expect(r.valid).toBe(false);
167
+ });
168
+
169
+ it("accepts ai-message subtype with vendor extra", () => {
170
+ const r = validateEvent(
171
+ eventOk({
172
+ subtype: "ai-message",
173
+ extra: { vendor: "deepseek", role: "assistant", modelName: "deepseek-r1" },
174
+ })
175
+ );
176
+ expect(r.valid).toBe(true);
177
+ });
178
+
179
+ it("accepts ai-image-generation subtype (Dreamina)", () => {
180
+ const r = validateEvent(eventOk({ subtype: "ai-image-generation" }));
181
+ expect(r.valid).toBe(true);
182
+ });
183
+
184
+ it("rejects participants of wrong type", () => {
185
+ const r = validateEvent(eventOk({ participants: ["valid-id", 42] }));
186
+ expect(r.valid).toBe(false);
187
+ });
188
+
189
+ it("accepts media event with mediaRefs", () => {
190
+ const r = validateEvent(
191
+ eventOk({ subtype: "media", content: { mediaRefs: ["/var/data/img1.jpg"] } })
192
+ );
193
+ expect(r.valid).toBe(true);
194
+ });
195
+ });
196
+
197
+ // ─── Place ────────────────────────────────────────────────────────────────
198
+
199
+ describe("validatePlace", () => {
200
+ it("accepts a minimal place", () => {
201
+ const r = validatePlace(placeOk());
202
+ expect(r.valid).toBe(true);
203
+ });
204
+
205
+ it("accepts place with coordinates", () => {
206
+ const r = validatePlace(placeOk({ coordinates: { lat: 24.4798, lng: 118.0894 } }));
207
+ expect(r.valid).toBe(true);
208
+ });
209
+
210
+ it("rejects coordinates out of range", () => {
211
+ const r = validatePlace(placeOk({ coordinates: { lat: 91, lng: 0 } }));
212
+ expect(r.valid).toBe(false);
213
+ expect(r.errors.some((e) => e.includes("lat"))).toBe(true);
214
+ });
215
+
216
+ it("rejects place missing aliases (required, possibly empty array)", () => {
217
+ const r = validatePlace(placeOk({ aliases: undefined }));
218
+ expect(r.valid).toBe(false);
219
+ });
220
+
221
+ it("accepts place with empty aliases array", () => {
222
+ const r = validatePlace(placeOk({ aliases: [] }));
223
+ expect(r.valid).toBe(true);
224
+ });
225
+ });
226
+
227
+ // ─── Item ─────────────────────────────────────────────────────────────────
228
+
229
+ describe("validateItem", () => {
230
+ it("accepts a minimal product item", () => {
231
+ const r = validateItem(itemOk());
232
+ expect(r.valid).toBe(true);
233
+ });
234
+
235
+ it("accepts item with price", () => {
236
+ const r = validateItem(itemOk({ price: { value: 99.9, currency: "CNY" } }));
237
+ expect(r.valid).toBe(true);
238
+ });
239
+
240
+ it("rejects price with missing currency", () => {
241
+ const r = validateItem(itemOk({ price: { value: 99 } }));
242
+ expect(r.valid).toBe(false);
243
+ });
244
+
245
+ it("rejects empty name", () => {
246
+ const r = validateItem(itemOk({ name: "" }));
247
+ expect(r.valid).toBe(false);
248
+ });
249
+
250
+ it("accepts link subtype", () => {
251
+ const r = validateItem(
252
+ itemOk({ subtype: "link", name: "Some article", externalUrl: "https://example.com/x" })
253
+ );
254
+ expect(r.valid).toBe(true);
255
+ });
256
+ });
257
+
258
+ // ─── Topic ────────────────────────────────────────────────────────────────
259
+
260
+ describe("validateTopic", () => {
261
+ it("accepts a minimal topic", () => {
262
+ const r = validateTopic(topicOk());
263
+ expect(r.valid).toBe(true);
264
+ });
265
+
266
+ it("accepts topic with derivedFromEvents", () => {
267
+ const r = validateTopic(
268
+ topicOk({ derivedFromEvents: [newId(), newId(), newId()] })
269
+ );
270
+ expect(r.valid).toBe(true);
271
+ });
272
+
273
+ it("rejects derivedFromEvents of wrong type", () => {
274
+ const r = validateTopic(topicOk({ derivedFromEvents: "not-array" }));
275
+ expect(r.valid).toBe(false);
276
+ });
277
+ });
278
+
279
+ // ─── Base / source / generic dispatch ─────────────────────────────────────
280
+
281
+ describe("validateBase + generic validate()", () => {
282
+ it("rejects entity missing source", () => {
283
+ const e = eventOk();
284
+ delete e.source;
285
+ const r = validateEvent(e);
286
+ expect(r.valid).toBe(false);
287
+ expect(r.errors.some((x) => x.includes("source"))).toBe(true);
288
+ });
289
+
290
+ it("rejects source.capturedBy outside enum", () => {
291
+ const e = eventOk({ source: { ...sourceOk(), capturedBy: "telepathy" } });
292
+ const r = validateEvent(e);
293
+ expect(r.valid).toBe(false);
294
+ });
295
+
296
+ it("rejects negative ingestedAt", () => {
297
+ const r = validatePerson(personOk({ ingestedAt: -5 }));
298
+ expect(r.valid).toBe(false);
299
+ });
300
+
301
+ it("accepts confidence in [0,1]", () => {
302
+ const r = validateEvent(eventOk({ confidence: 0.5 }));
303
+ expect(r.valid).toBe(true);
304
+ });
305
+
306
+ it("rejects confidence outside [0,1]", () => {
307
+ expect(validateEvent(eventOk({ confidence: 1.5 })).valid).toBe(false);
308
+ expect(validateEvent(eventOk({ confidence: -0.1 })).valid).toBe(false);
309
+ });
310
+
311
+ it("generic validate() dispatches by type", () => {
312
+ expect(validate(personOk()).valid).toBe(true);
313
+ expect(validate(eventOk()).valid).toBe(true);
314
+ expect(validate(placeOk()).valid).toBe(true);
315
+ expect(validate(itemOk()).valid).toBe(true);
316
+ expect(validate(topicOk()).valid).toBe(true);
317
+ });
318
+
319
+ it("generic validate() rejects unknown type", () => {
320
+ const r = validate({ ...personOk(), type: "unknown-type" });
321
+ expect(r.valid).toBe(false);
322
+ expect(r.errors[0]).toMatch(/unknown entity type/);
323
+ });
324
+
325
+ it("rejects null / non-object input", () => {
326
+ expect(validate(null).valid).toBe(false);
327
+ expect(validate(undefined).valid).toBe(false);
328
+ expect(validate("string").valid).toBe(false);
329
+ expect(validate([]).valid).toBe(false);
330
+ });
331
+ });