@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.
- package/LICENSE +21 -0
- package/README.md +142 -0
- package/dist/adapters/sqlite.adapter.d.ts +29 -0
- package/dist/adapters/sqlite.adapter.d.ts.map +1 -0
- package/dist/adapters/sqlite.adapter.js +450 -0
- package/dist/adapters/sqlite.adapter.js.map +1 -0
- package/dist/adapters/weaviate.adapter.d.ts +43 -0
- package/dist/adapters/weaviate.adapter.d.ts.map +1 -0
- package/dist/adapters/weaviate.adapter.js +678 -0
- package/dist/adapters/weaviate.adapter.js.map +1 -0
- package/dist/cli/audit.d.ts +2 -0
- package/dist/cli/audit.d.ts.map +1 -0
- package/dist/cli/audit.js +50 -0
- package/dist/cli/audit.js.map +1 -0
- package/dist/cli/migrate-to-weaviate.d.ts +2 -0
- package/dist/cli/migrate-to-weaviate.d.ts.map +1 -0
- package/dist/cli/migrate-to-weaviate.js +65 -0
- package/dist/cli/migrate-to-weaviate.js.map +1 -0
- package/dist/cli/stale.d.ts +2 -0
- package/dist/cli/stale.d.ts.map +1 -0
- package/dist/cli/stale.js +27 -0
- package/dist/cli/stale.js.map +1 -0
- package/dist/cli/sync-ddd-docs.d.ts +2 -0
- package/dist/cli/sync-ddd-docs.d.ts.map +1 -0
- package/dist/cli/sync-ddd-docs.js +88 -0
- package/dist/cli/sync-ddd-docs.js.map +1 -0
- package/dist/cli/verify.d.ts +2 -0
- package/dist/cli/verify.d.ts.map +1 -0
- package/dist/cli/verify.js +36 -0
- package/dist/cli/verify.js.map +1 -0
- package/dist/hooks/post-commit.d.ts +13 -0
- package/dist/hooks/post-commit.d.ts.map +1 -0
- package/dist/hooks/post-commit.js +197 -0
- package/dist/hooks/post-commit.js.map +1 -0
- package/dist/ports/cognitive-store.port.d.ts +34 -0
- package/dist/ports/cognitive-store.port.d.ts.map +1 -0
- package/dist/ports/cognitive-store.port.js +2 -0
- package/dist/ports/cognitive-store.port.js.map +1 -0
- package/dist/profiles/agent-profiles.d.ts +20 -0
- package/dist/profiles/agent-profiles.d.ts.map +1 -0
- package/dist/profiles/agent-profiles.js +74 -0
- package/dist/profiles/agent-profiles.js.map +1 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +59 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/audit.tool.d.ts +8 -0
- package/dist/tools/audit.tool.d.ts.map +1 -0
- package/dist/tools/audit.tool.js +71 -0
- package/dist/tools/audit.tool.js.map +1 -0
- package/dist/tools/recall.tool.d.ts +30 -0
- package/dist/tools/recall.tool.d.ts.map +1 -0
- package/dist/tools/recall.tool.js +43 -0
- package/dist/tools/recall.tool.js.map +1 -0
- package/dist/tools/store.tool.d.ts +34 -0
- package/dist/tools/store.tool.d.ts.map +1 -0
- package/dist/tools/store.tool.js +51 -0
- package/dist/tools/store.tool.js.map +1 -0
- package/dist/tools/verify.tool.d.ts +10 -0
- package/dist/tools/verify.tool.d.ts.map +1 -0
- package/dist/tools/verify.tool.js +56 -0
- package/dist/tools/verify.tool.js.map +1 -0
- package/dist/types.d.ts +85 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/citation-checker.d.ts +21 -0
- package/dist/utils/citation-checker.d.ts.map +1 -0
- package/dist/utils/citation-checker.js +84 -0
- package/dist/utils/citation-checker.js.map +1 -0
- package/dist/utils/decay.d.ts +16 -0
- package/dist/utils/decay.d.ts.map +1 -0
- package/dist/utils/decay.js +62 -0
- package/dist/utils/decay.js.map +1 -0
- 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
|