@porast1/mcp-cognitive 1.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.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +142 -0
  3. package/dist/adapters/sqlite.adapter.d.ts +29 -0
  4. package/dist/adapters/sqlite.adapter.d.ts.map +1 -0
  5. package/dist/adapters/sqlite.adapter.js +450 -0
  6. package/dist/adapters/sqlite.adapter.js.map +1 -0
  7. package/dist/adapters/weaviate.adapter.d.ts +43 -0
  8. package/dist/adapters/weaviate.adapter.d.ts.map +1 -0
  9. package/dist/adapters/weaviate.adapter.js +678 -0
  10. package/dist/adapters/weaviate.adapter.js.map +1 -0
  11. package/dist/cli/audit.d.ts +2 -0
  12. package/dist/cli/audit.d.ts.map +1 -0
  13. package/dist/cli/audit.js +50 -0
  14. package/dist/cli/audit.js.map +1 -0
  15. package/dist/cli/migrate-to-weaviate.d.ts +2 -0
  16. package/dist/cli/migrate-to-weaviate.d.ts.map +1 -0
  17. package/dist/cli/migrate-to-weaviate.js +65 -0
  18. package/dist/cli/migrate-to-weaviate.js.map +1 -0
  19. package/dist/cli/stale.d.ts +2 -0
  20. package/dist/cli/stale.d.ts.map +1 -0
  21. package/dist/cli/stale.js +27 -0
  22. package/dist/cli/stale.js.map +1 -0
  23. package/dist/cli/sync-ddd-docs.d.ts +2 -0
  24. package/dist/cli/sync-ddd-docs.d.ts.map +1 -0
  25. package/dist/cli/sync-ddd-docs.js +88 -0
  26. package/dist/cli/sync-ddd-docs.js.map +1 -0
  27. package/dist/cli/verify.d.ts +2 -0
  28. package/dist/cli/verify.d.ts.map +1 -0
  29. package/dist/cli/verify.js +36 -0
  30. package/dist/cli/verify.js.map +1 -0
  31. package/dist/hooks/post-commit.d.ts +13 -0
  32. package/dist/hooks/post-commit.d.ts.map +1 -0
  33. package/dist/hooks/post-commit.js +197 -0
  34. package/dist/hooks/post-commit.js.map +1 -0
  35. package/dist/ports/cognitive-store.port.d.ts +34 -0
  36. package/dist/ports/cognitive-store.port.d.ts.map +1 -0
  37. package/dist/ports/cognitive-store.port.js +2 -0
  38. package/dist/ports/cognitive-store.port.js.map +1 -0
  39. package/dist/profiles/agent-profiles.d.ts +20 -0
  40. package/dist/profiles/agent-profiles.d.ts.map +1 -0
  41. package/dist/profiles/agent-profiles.js +74 -0
  42. package/dist/profiles/agent-profiles.js.map +1 -0
  43. package/dist/server.d.ts +2 -0
  44. package/dist/server.d.ts.map +1 -0
  45. package/dist/server.js +59 -0
  46. package/dist/server.js.map +1 -0
  47. package/dist/tools/audit.tool.d.ts +8 -0
  48. package/dist/tools/audit.tool.d.ts.map +1 -0
  49. package/dist/tools/audit.tool.js +71 -0
  50. package/dist/tools/audit.tool.js.map +1 -0
  51. package/dist/tools/recall.tool.d.ts +30 -0
  52. package/dist/tools/recall.tool.d.ts.map +1 -0
  53. package/dist/tools/recall.tool.js +43 -0
  54. package/dist/tools/recall.tool.js.map +1 -0
  55. package/dist/tools/store.tool.d.ts +34 -0
  56. package/dist/tools/store.tool.d.ts.map +1 -0
  57. package/dist/tools/store.tool.js +51 -0
  58. package/dist/tools/store.tool.js.map +1 -0
  59. package/dist/tools/verify.tool.d.ts +10 -0
  60. package/dist/tools/verify.tool.d.ts.map +1 -0
  61. package/dist/tools/verify.tool.js +56 -0
  62. package/dist/tools/verify.tool.js.map +1 -0
  63. package/dist/types.d.ts +85 -0
  64. package/dist/types.d.ts.map +1 -0
  65. package/dist/types.js +3 -0
  66. package/dist/types.js.map +1 -0
  67. package/dist/utils/citation-checker.d.ts +21 -0
  68. package/dist/utils/citation-checker.d.ts.map +1 -0
  69. package/dist/utils/citation-checker.js +84 -0
  70. package/dist/utils/citation-checker.js.map +1 -0
  71. package/dist/utils/decay.d.ts +16 -0
  72. package/dist/utils/decay.d.ts.map +1 -0
  73. package/dist/utils/decay.js +62 -0
  74. package/dist/utils/decay.js.map +1 -0
  75. package/package.json +57 -0
@@ -0,0 +1,678 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import weaviate from 'weaviate-ts-client';
3
+ import { getAgentProfile } from '../profiles/agent-profiles.js';
4
+ import { checkCitations, overallCitationStatus } from '../utils/citation-checker.js';
5
+ import { checkDecayBatch } from '../utils/decay.js';
6
+ const FACT_CLASS = 'CognitiveFact';
7
+ const DOC_CLASS = 'DddDocument';
8
+ const FACT_TYPES = [
9
+ 'invariant',
10
+ 'policy',
11
+ 'convention',
12
+ 'observation',
13
+ 'ephemeral',
14
+ ];
15
+ const MODULES = [
16
+ 'identity',
17
+ 'organization',
18
+ 'platform',
19
+ 'infrastructure',
20
+ 'tooling',
21
+ 'testing',
22
+ 'general',
23
+ ];
24
+ const FACT_FIELDS = [
25
+ 'factText',
26
+ 'factType',
27
+ 'module',
28
+ 'confidence',
29
+ 'tags',
30
+ 'citationsJson',
31
+ 'epoch',
32
+ 'status',
33
+ 'createdAt',
34
+ 'updatedAt',
35
+ 'lastRecalled',
36
+ 'recallCount',
37
+ 'supersedes',
38
+ '_additional { id score }',
39
+ ].join(' ');
40
+ const DOC_FIELDS = [
41
+ 'filePath',
42
+ 'module',
43
+ 'content',
44
+ 'checksum',
45
+ 'lastSynced',
46
+ '_additional { id }',
47
+ ].join(' ');
48
+ function parseCitations(raw) {
49
+ if (!raw)
50
+ return [];
51
+ try {
52
+ return JSON.parse(raw);
53
+ }
54
+ catch {
55
+ return [];
56
+ }
57
+ }
58
+ function toFact(record) {
59
+ return {
60
+ id: record._additional?.id ?? '',
61
+ fact: record.factText,
62
+ type: record.factType,
63
+ module: record.module,
64
+ confidence: record.confidence,
65
+ citations: parseCitations(record.citationsJson),
66
+ tags: record.tags ?? [],
67
+ epoch: record.epoch,
68
+ createdAt: record.createdAt,
69
+ updatedAt: record.updatedAt,
70
+ lastRecalled: record.lastRecalled ?? null,
71
+ recallCount: record.recallCount ?? 0,
72
+ supersedes: record.supersedes ?? null,
73
+ status: record.status,
74
+ };
75
+ }
76
+ function toDocument(record) {
77
+ return {
78
+ id: record._additional?.id ?? '',
79
+ filePath: record.filePath,
80
+ module: record.module,
81
+ content: record.content,
82
+ checksum: record.checksum,
83
+ lastSynced: record.lastSynced,
84
+ };
85
+ }
86
+ function validateInput(input) {
87
+ if (!input.fact || !input.type || !input.module) {
88
+ throw new Error('Fact input missing required fields');
89
+ }
90
+ if (!FACT_TYPES.includes(input.type)) {
91
+ throw new Error(`Invalid fact type: ${input.type}`);
92
+ }
93
+ if (!MODULES.includes(input.module)) {
94
+ throw new Error(`Invalid module: ${input.module}`);
95
+ }
96
+ }
97
+ export class WeaviateStore {
98
+ client;
99
+ workspaceRoot;
100
+ constructor(client, workspaceRoot) {
101
+ this.client = client;
102
+ this.workspaceRoot = workspaceRoot;
103
+ }
104
+ static async create(url, workspaceRoot) {
105
+ const scheme = url.startsWith('https://') ? 'https' : 'http';
106
+ const host = url.replace(/^https?:\/\//, '');
107
+ const client = weaviate.client({ scheme, host });
108
+ const store = new WeaviateStore(client, workspaceRoot);
109
+ await store.ensureSchema();
110
+ return store;
111
+ }
112
+ async ensureSchema() {
113
+ let schema = null;
114
+ try {
115
+ const raw = await this.client.schema.getter().do();
116
+ schema = raw;
117
+ }
118
+ catch (error) {
119
+ const message = error instanceof Error ? error.message : String(error);
120
+ throw new Error(`Weaviate connection failed: ${message}`);
121
+ }
122
+ const existing = new Set(schema?.classes?.map((cls) => cls.class) ?? []);
123
+ if (!existing.has(FACT_CLASS)) {
124
+ await this.client.schema
125
+ .classCreator()
126
+ .withClass({
127
+ class: FACT_CLASS,
128
+ vectorizer: 'text2vec-transformers',
129
+ moduleConfig: {
130
+ 'text2vec-transformers': {
131
+ vectorizeClassName: false,
132
+ vectorizePropertyName: false,
133
+ },
134
+ },
135
+ properties: [
136
+ { name: 'factText', dataType: ['text'] },
137
+ { name: 'factType', dataType: ['text'] },
138
+ { name: 'module', dataType: ['text'] },
139
+ { name: 'confidence', dataType: ['number'] },
140
+ { name: 'tags', dataType: ['text'], cardinality: 'many' },
141
+ { name: 'citationsJson', dataType: ['text'] },
142
+ { name: 'epoch', dataType: ['int'] },
143
+ { name: 'status', dataType: ['text'] },
144
+ { name: 'createdAt', dataType: ['date'] },
145
+ { name: 'updatedAt', dataType: ['date'] },
146
+ { name: 'lastRecalled', dataType: ['date'] },
147
+ { name: 'recallCount', dataType: ['int'] },
148
+ { name: 'supersedes', dataType: ['text'] },
149
+ { name: 'documentedIn', dataType: [DOC_CLASS], cardinality: 'many' },
150
+ ],
151
+ })
152
+ .do();
153
+ }
154
+ if (!existing.has(DOC_CLASS)) {
155
+ await this.client.schema
156
+ .classCreator()
157
+ .withClass({
158
+ class: DOC_CLASS,
159
+ vectorizer: 'text2vec-transformers',
160
+ moduleConfig: {
161
+ 'text2vec-transformers': {
162
+ vectorizeClassName: false,
163
+ },
164
+ },
165
+ properties: [
166
+ { name: 'filePath', dataType: ['text'] },
167
+ { name: 'module', dataType: ['text'] },
168
+ { name: 'content', dataType: ['text'] },
169
+ { name: 'checksum', dataType: ['text'] },
170
+ { name: 'lastSynced', dataType: ['date'] },
171
+ ],
172
+ })
173
+ .do();
174
+ }
175
+ }
176
+ // ─── CRUD ─────────────────────────────────────────────────────────────
177
+ async store(input) {
178
+ validateInput(input);
179
+ const now = new Date().toISOString();
180
+ const id = randomUUID();
181
+ await this.client.data
182
+ .creator()
183
+ .withClassName(FACT_CLASS)
184
+ .withId(id)
185
+ .withProperties({
186
+ factText: input.fact,
187
+ factType: input.type,
188
+ module: input.module,
189
+ confidence: input.confidence ?? 0.8,
190
+ tags: input.tags ?? [],
191
+ citationsJson: JSON.stringify(input.citations ?? []),
192
+ epoch: input.epoch ?? 3,
193
+ status: 'active',
194
+ createdAt: now,
195
+ updatedAt: now,
196
+ lastRecalled: null,
197
+ recallCount: 0,
198
+ supersedes: input.supersedes ?? null,
199
+ })
200
+ .do();
201
+ if (input.supersedes) {
202
+ await this.archive(input.supersedes, `Superseded by ${id}`);
203
+ }
204
+ const stored = await this.getById(id);
205
+ if (!stored)
206
+ throw new Error('Failed to store fact');
207
+ return stored;
208
+ }
209
+ async update(id, patch) {
210
+ const existing = await this.getById(id);
211
+ if (!existing)
212
+ throw new Error(`Fact not found: ${id}`);
213
+ const now = new Date().toISOString();
214
+ const updates = { updatedAt: now };
215
+ if (patch.fact !== undefined)
216
+ updates['factText'] = patch.fact;
217
+ if (patch.type !== undefined)
218
+ updates['factType'] = patch.type;
219
+ if (patch.module !== undefined)
220
+ updates['module'] = patch.module;
221
+ if (patch.confidence !== undefined)
222
+ updates['confidence'] = patch.confidence;
223
+ if (patch.citations !== undefined)
224
+ updates['citationsJson'] = JSON.stringify(patch.citations);
225
+ if (patch.tags !== undefined)
226
+ updates['tags'] = patch.tags;
227
+ if (patch.epoch !== undefined)
228
+ updates['epoch'] = patch.epoch;
229
+ if (patch.supersedes !== undefined)
230
+ updates['supersedes'] = patch.supersedes;
231
+ await this.client.data
232
+ .merger()
233
+ .withClassName(FACT_CLASS)
234
+ .withId(id)
235
+ .withProperties(updates)
236
+ .do();
237
+ const updated = await this.getById(id);
238
+ if (!updated)
239
+ throw new Error('Failed to update fact');
240
+ return updated;
241
+ }
242
+ async archive(id, _reason) {
243
+ const existing = await this.getById(id);
244
+ if (!existing)
245
+ throw new Error(`Fact not found: ${id}`);
246
+ await this.client.data
247
+ .merger()
248
+ .withClassName(FACT_CLASS)
249
+ .withId(id)
250
+ .withProperties({
251
+ status: 'archived',
252
+ updatedAt: new Date().toISOString(),
253
+ })
254
+ .do();
255
+ }
256
+ // ─── Search ────────────────────────────────────────────────────────────
257
+ async recall(query) {
258
+ const started = Date.now();
259
+ const profile = query.agent ? getAgentProfile(query.agent) : null;
260
+ const types = query.types ?? profile?.priorityTypes;
261
+ const module = query.module ?? profile?.defaultModule;
262
+ const whereOperands = [
263
+ { path: ['status'], operator: 'Equal', valueText: 'active' },
264
+ ];
265
+ if (module) {
266
+ whereOperands.push({ path: ['module'], operator: 'Equal', valueText: module });
267
+ }
268
+ if (types && types.length > 0) {
269
+ whereOperands.push({
270
+ operator: 'Or',
271
+ operands: types.map((type) => ({
272
+ path: ['factType'],
273
+ operator: 'Equal',
274
+ valueText: type,
275
+ })),
276
+ });
277
+ }
278
+ if (query.tags && query.tags.length > 0) {
279
+ whereOperands.push({
280
+ path: ['tags'],
281
+ operator: 'ContainsAll',
282
+ valueTextArray: query.tags,
283
+ });
284
+ }
285
+ if (query.minConfidence !== undefined) {
286
+ whereOperands.push({
287
+ path: ['confidence'],
288
+ operator: 'GreaterThanEqual',
289
+ valueNumber: query.minConfidence,
290
+ });
291
+ }
292
+ const where = whereOperands.length === 1
293
+ ? whereOperands[0]
294
+ : { operator: 'And', operands: whereOperands };
295
+ const limit = query.limit ?? profile?.maxRecall ?? 10;
296
+ const trimmedQuery = query.query.trim();
297
+ let result;
298
+ if (trimmedQuery.length === 0) {
299
+ const query = this.client.graphql
300
+ .get()
301
+ .withClassName(FACT_CLASS)
302
+ .withLimit(limit)
303
+ .withSort([
304
+ { path: ['confidence'], order: 'desc' },
305
+ { path: ['updatedAt'], order: 'desc' },
306
+ ])
307
+ .withFields(FACT_FIELDS);
308
+ if (where)
309
+ query.withWhere(where);
310
+ result = await query.do();
311
+ }
312
+ else {
313
+ const query = this.client.graphql
314
+ .get()
315
+ .withClassName(FACT_CLASS)
316
+ .withHybrid({ query: trimmedQuery, alpha: 0.5 })
317
+ .withLimit(limit)
318
+ .withFields(FACT_FIELDS);
319
+ if (where)
320
+ query.withWhere(where);
321
+ result = await query.do();
322
+ }
323
+ const records = result.data?.Get?.[FACT_CLASS] ?? [];
324
+ const enriched = records.map((record) => ({
325
+ fact: toFact(record),
326
+ baseScore: record._additional?.score ?? 0,
327
+ }));
328
+ const suppressed = profile && profile.suppressTags.length > 0
329
+ ? enriched.filter(({ fact }) => !fact.tags.some((tag) => profile.suppressTags.includes(tag)))
330
+ : enriched;
331
+ const boosted = suppressed
332
+ .map(({ fact, baseScore }) => {
333
+ const boostCount = profile
334
+ ? fact.tags.filter((tag) => profile.boostTags.includes(tag)).length
335
+ : 0;
336
+ return { fact, score: baseScore + boostCount * 0.05 };
337
+ })
338
+ .sort((a, b) => {
339
+ if (b.score !== a.score)
340
+ return b.score - a.score;
341
+ if (b.fact.confidence !== a.fact.confidence) {
342
+ return b.fact.confidence - a.fact.confidence;
343
+ }
344
+ return b.fact.updatedAt.localeCompare(a.fact.updatedAt);
345
+ })
346
+ .map((entry) => entry.fact);
347
+ await this.updateRecallMetadata(boosted);
348
+ return {
349
+ facts: boosted,
350
+ totalMatches: boosted.length,
351
+ queryTimeMs: Date.now() - started,
352
+ };
353
+ }
354
+ // ─── Maintenance ───────────────────────────────────────────────────────
355
+ async verify(factId) {
356
+ const fact = await this.getById(factId);
357
+ if (!fact)
358
+ throw new Error(`Fact not found: ${factId}`);
359
+ const results = checkCitations(fact.citations, this.workspaceRoot);
360
+ const overall = overallCitationStatus(results);
361
+ return {
362
+ factId: fact.id,
363
+ factSnippet: fact.fact.substring(0, 100),
364
+ citations: results.map((r) => ({
365
+ citation: r.citation,
366
+ status: r.status,
367
+ ...(r.detail !== undefined ? { detail: r.detail } : {}),
368
+ })),
369
+ overallStatus: overall,
370
+ };
371
+ }
372
+ async verifyAll() {
373
+ const allFacts = await this.exportAll();
374
+ const results = [];
375
+ for (const fact of allFacts.filter((f) => f.status === 'active')) {
376
+ if (fact.citations.length === 0)
377
+ continue;
378
+ const citResults = checkCitations(fact.citations, this.workspaceRoot);
379
+ const overall = overallCitationStatus(citResults);
380
+ results.push({
381
+ factId: fact.id,
382
+ factSnippet: fact.fact.substring(0, 100),
383
+ citations: citResults.map((r) => ({
384
+ citation: r.citation,
385
+ status: r.status,
386
+ ...(r.detail !== undefined ? { detail: r.detail } : {}),
387
+ })),
388
+ overallStatus: overall,
389
+ });
390
+ }
391
+ return results;
392
+ }
393
+ async audit() {
394
+ const allFacts = await this.exportAll();
395
+ const activeFacts = allFacts.filter((f) => f.status === 'active');
396
+ const byStatus = { active: 0, stale: 0, archived: 0 };
397
+ for (const fact of allFacts) {
398
+ byStatus[fact.status]++;
399
+ }
400
+ const byType = {
401
+ invariant: 0,
402
+ policy: 0,
403
+ convention: 0,
404
+ observation: 0,
405
+ ephemeral: 0,
406
+ };
407
+ for (const fact of allFacts) {
408
+ byType[fact.type]++;
409
+ }
410
+ const byModule = {};
411
+ for (const fact of allFacts) {
412
+ byModule[fact.module] = (byModule[fact.module] ?? 0) + 1;
413
+ }
414
+ const decayActions = checkDecayBatch(activeFacts);
415
+ const staleFacts = decayActions
416
+ .filter((action) => action.action === 'mark-stale')
417
+ .map((action) => action.fact);
418
+ const verifications = await this.verifyAll();
419
+ const brokenCitations = verifications.filter((v) => v.overallStatus === 'broken');
420
+ const duplicateCandidates = this.findDuplicateCandidates(activeFacts);
421
+ return {
422
+ totalFacts: allFacts.length,
423
+ byStatus,
424
+ byType,
425
+ byModule,
426
+ staleFacts,
427
+ brokenCitations,
428
+ duplicateCandidates,
429
+ generatedAt: new Date().toISOString(),
430
+ };
431
+ }
432
+ // ─── Lifecycle ──────────────────────────────────────────────────────────
433
+ async decayCheck() {
434
+ const allFacts = await this.exportAll();
435
+ const candidates = allFacts.filter((fact) => fact.status === 'active' || fact.status === 'stale');
436
+ const actions = checkDecayBatch(candidates);
437
+ const now = new Date().toISOString();
438
+ for (const action of actions) {
439
+ const status = action.action === 'mark-stale' ? 'stale' : 'archived';
440
+ await this.client.data
441
+ .merger()
442
+ .withClassName(FACT_CLASS)
443
+ .withId(action.fact.id)
444
+ .withProperties({ status, updatedAt: now })
445
+ .do();
446
+ }
447
+ return actions.map((action) => action.fact);
448
+ }
449
+ async migrate(facts) {
450
+ const batchSize = 50;
451
+ const now = new Date().toISOString();
452
+ for (let i = 0; i < facts.length; i += batchSize) {
453
+ const batch = facts.slice(i, i + batchSize);
454
+ const batcher = this.client.batch.objectsBatcher();
455
+ for (const input of batch) {
456
+ validateInput(input);
457
+ batcher.withObject({
458
+ class: FACT_CLASS,
459
+ id: randomUUID(),
460
+ properties: {
461
+ factText: input.fact,
462
+ factType: input.type,
463
+ module: input.module,
464
+ confidence: input.confidence ?? 0.8,
465
+ tags: input.tags ?? [],
466
+ citationsJson: JSON.stringify(input.citations ?? []),
467
+ epoch: input.epoch ?? 3,
468
+ status: 'active',
469
+ createdAt: now,
470
+ updatedAt: now,
471
+ lastRecalled: null,
472
+ recallCount: 0,
473
+ supersedes: input.supersedes ?? null,
474
+ },
475
+ });
476
+ }
477
+ await batcher.do();
478
+ }
479
+ }
480
+ // ─── Export ────────────────────────────────────────────────────────────
481
+ async exportAll() {
482
+ const limit = 100;
483
+ let offset = 0;
484
+ const facts = [];
485
+ while (true) {
486
+ const result = await this.client.graphql
487
+ .get()
488
+ .withClassName(FACT_CLASS)
489
+ .withLimit(limit)
490
+ .withOffset(offset)
491
+ .withFields(FACT_FIELDS)
492
+ .do();
493
+ const batch = result.data?.Get?.[FACT_CLASS] ?? [];
494
+ if (batch.length === 0)
495
+ break;
496
+ facts.push(...batch.map(toFact));
497
+ if (batch.length < limit)
498
+ break;
499
+ offset += limit;
500
+ }
501
+ return facts;
502
+ }
503
+ async count() {
504
+ try {
505
+ const result = await this.client.graphql
506
+ .aggregate()
507
+ .withClassName(FACT_CLASS)
508
+ .withFields('meta { count }')
509
+ .do();
510
+ const count = result.data?.Aggregate?.[FACT_CLASS]?.[0]?.meta?.count;
511
+ if (typeof count === 'number')
512
+ return count;
513
+ }
514
+ catch {
515
+ // fallback to exportAll
516
+ }
517
+ const allFacts = await this.exportAll();
518
+ return allFacts.length;
519
+ }
520
+ // ─── Weaviate-specific helpers ─────────────────────────────────────────
521
+ async upsertDocument(input) {
522
+ const existing = await this.findDocumentByPath(input.filePath);
523
+ if (existing) {
524
+ await this.client.data
525
+ .merger()
526
+ .withClassName(DOC_CLASS)
527
+ .withId(existing.id)
528
+ .withProperties({
529
+ module: input.module,
530
+ content: input.content,
531
+ checksum: input.checksum,
532
+ lastSynced: input.lastSynced,
533
+ })
534
+ .do();
535
+ return existing.id;
536
+ }
537
+ const id = randomUUID();
538
+ await this.client.data
539
+ .creator()
540
+ .withClassName(DOC_CLASS)
541
+ .withId(id)
542
+ .withProperties({
543
+ filePath: input.filePath,
544
+ module: input.module,
545
+ content: input.content,
546
+ checksum: input.checksum,
547
+ lastSynced: input.lastSynced,
548
+ })
549
+ .do();
550
+ return id;
551
+ }
552
+ async linkToDocument(factId, docPath) {
553
+ const fact = await this.getById(factId);
554
+ if (!fact)
555
+ throw new Error(`Fact not found: ${factId}`);
556
+ const document = await this.findDocumentByPath(docPath);
557
+ if (!document) {
558
+ throw new Error(`Document not found for citation: ${docPath}`);
559
+ }
560
+ await this.client.data
561
+ .referenceCreator()
562
+ .withClassName(FACT_CLASS)
563
+ .withId(factId)
564
+ .withReferenceProperty('documentedIn')
565
+ .withReference({
566
+ beacon: `weaviate://localhost/${DOC_CLASS}/${document.id}`,
567
+ })
568
+ .do();
569
+ }
570
+ async getLinkedDocuments(factId) {
571
+ const result = await this.client.graphql
572
+ .get()
573
+ .withClassName(FACT_CLASS)
574
+ .withWhere({ path: ['id'], operator: 'Equal', valueText: factId })
575
+ .withFields(`documentedIn { ${DOC_FIELDS} }`)
576
+ .withLimit(1)
577
+ .do();
578
+ const facts = result.data?.Get?.[FACT_CLASS] ?? [];
579
+ const docs = facts[0]?.documentedIn ?? [];
580
+ return docs.map((doc) => toDocument(doc));
581
+ }
582
+ async getFactsByDocument(docPath) {
583
+ const result = await this.client.graphql
584
+ .get()
585
+ .withClassName(FACT_CLASS)
586
+ .withWhere({
587
+ operator: 'Equal',
588
+ path: ['documentedIn', DOC_CLASS, 'filePath'],
589
+ valueText: docPath,
590
+ })
591
+ .withFields(FACT_FIELDS)
592
+ .do();
593
+ const facts = result.data?.Get?.[FACT_CLASS] ?? [];
594
+ return facts.map(toFact);
595
+ }
596
+ async getDocumentByPath(docPath) {
597
+ return this.findDocumentByPath(docPath);
598
+ }
599
+ // ─── Internal helpers ──────────────────────────────────────────────────
600
+ async getById(id) {
601
+ try {
602
+ const result = await this.client.data
603
+ .getterById()
604
+ .withClassName(FACT_CLASS)
605
+ .withId(id)
606
+ .do();
607
+ const record = result?.properties;
608
+ if (!record)
609
+ return null;
610
+ return toFact({ ...record, _additional: { id } });
611
+ }
612
+ catch {
613
+ return null;
614
+ }
615
+ }
616
+ async findDocumentByPath(filePath) {
617
+ const result = await this.client.graphql
618
+ .get()
619
+ .withClassName(DOC_CLASS)
620
+ .withWhere({ path: ['filePath'], operator: 'Equal', valueText: filePath })
621
+ .withFields(DOC_FIELDS)
622
+ .withLimit(1)
623
+ .do();
624
+ const docs = result.data?.Get?.[DOC_CLASS] ?? [];
625
+ if (docs.length === 0)
626
+ return null;
627
+ return toDocument(docs[0]);
628
+ }
629
+ async updateRecallMetadata(facts) {
630
+ if (facts.length === 0)
631
+ return;
632
+ const now = new Date().toISOString();
633
+ for (const fact of facts) {
634
+ await this.client.data
635
+ .merger()
636
+ .withClassName(FACT_CLASS)
637
+ .withId(fact.id)
638
+ .withProperties({
639
+ lastRecalled: now,
640
+ recallCount: (fact.recallCount ?? 0) + 1,
641
+ })
642
+ .do();
643
+ }
644
+ }
645
+ findDuplicateCandidates(facts) {
646
+ const candidates = [];
647
+ for (let i = 0; i < facts.length; i++) {
648
+ for (let j = i + 1; j < facts.length; j++) {
649
+ const first = facts[i];
650
+ const second = facts[j];
651
+ if (first.module !== second.module)
652
+ continue;
653
+ const similarity = this.wordOverlap(first.fact, second.fact);
654
+ if (similarity >= 0.6) {
655
+ candidates.push({
656
+ a: first.id,
657
+ b: second.id,
658
+ similarity: `${Math.round(similarity * 100)}%`,
659
+ });
660
+ }
661
+ }
662
+ }
663
+ return candidates;
664
+ }
665
+ wordOverlap(textA, textB) {
666
+ const wordsA = new Set(textA.toLowerCase().split(/\W+/).filter((w) => w.length > 2));
667
+ const wordsB = new Set(textB.toLowerCase().split(/\W+/).filter((w) => w.length > 2));
668
+ if (wordsA.size === 0 || wordsB.size === 0)
669
+ return 0;
670
+ let overlap = 0;
671
+ for (const word of wordsA) {
672
+ if (wordsB.has(word))
673
+ overlap++;
674
+ }
675
+ return overlap / Math.max(wordsA.size, wordsB.size);
676
+ }
677
+ }
678
+ //# sourceMappingURL=weaviate.adapter.js.map