@zabaca/lattice 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 (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +398 -0
  3. package/dist/cli.js +2712 -0
  4. package/package.json +61 -0
package/dist/cli.js ADDED
@@ -0,0 +1,2712 @@
1
+ #!/usr/bin/env node
2
+ // @bun
3
+ var __legacyDecorateClassTS = function(decorators, target, key, desc) {
4
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
5
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function")
6
+ r = Reflect.decorate(decorators, target, key, desc);
7
+ else
8
+ for (var i = decorators.length - 1;i >= 0; i--)
9
+ if (d = decorators[i])
10
+ r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
11
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
12
+ };
13
+ var __legacyMetadataTS = (k, v) => {
14
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function")
15
+ return Reflect.metadata(k, v);
16
+ };
17
+
18
+ // src/main.ts
19
+ import { program } from "commander";
20
+
21
+ // src/commands/sync.command.ts
22
+ import { NestFactory } from "@nestjs/core";
23
+ import { watch } from "fs";
24
+ import { join } from "path";
25
+
26
+ // src/app.module.ts
27
+ import { Module as Module5 } from "@nestjs/common";
28
+ import { ConfigModule as ConfigModule2 } from "@nestjs/config";
29
+
30
+ // src/graph/graph.module.ts
31
+ import { Module } from "@nestjs/common";
32
+
33
+ // src/graph/graph.service.ts
34
+ import { Injectable, Logger } from "@nestjs/common";
35
+ import { ConfigService } from "@nestjs/config";
36
+ import Redis from "ioredis";
37
+ class GraphService {
38
+ configService;
39
+ logger = new Logger(GraphService.name);
40
+ redis = null;
41
+ config;
42
+ connecting = null;
43
+ constructor(configService) {
44
+ this.configService = configService;
45
+ this.config = {
46
+ host: this.configService.get("FALKORDB_HOST", "localhost"),
47
+ port: this.configService.get("FALKORDB_PORT", 6379),
48
+ graphName: this.configService.get("GRAPH_NAME", "research_knowledge")
49
+ };
50
+ }
51
+ async onModuleDestroy() {
52
+ await this.disconnect();
53
+ }
54
+ async ensureConnected() {
55
+ if (this.redis) {
56
+ return this.redis;
57
+ }
58
+ if (this.connecting) {
59
+ await this.connecting;
60
+ return this.redis;
61
+ }
62
+ this.connecting = this.connect();
63
+ await this.connecting;
64
+ this.connecting = null;
65
+ return this.redis;
66
+ }
67
+ async connect() {
68
+ try {
69
+ this.redis = new Redis({
70
+ host: this.config.host,
71
+ port: this.config.port,
72
+ maxRetriesPerRequest: 3,
73
+ retryStrategy: (times) => {
74
+ if (times > 3) {
75
+ return null;
76
+ }
77
+ const delay = Math.min(times * 50, 2000);
78
+ return delay;
79
+ },
80
+ lazyConnect: true
81
+ });
82
+ this.redis.on("error", (err) => {
83
+ this.logger.debug(`Redis connection error: ${err.message}`);
84
+ });
85
+ await this.redis.ping();
86
+ this.logger.log(`Connected to FalkorDB at ${this.config.host}:${this.config.port}`);
87
+ } catch (error) {
88
+ this.redis = null;
89
+ this.logger.error(`Failed to connect to FalkorDB: ${error instanceof Error ? error.message : String(error)}`);
90
+ throw error;
91
+ }
92
+ }
93
+ async disconnect() {
94
+ if (this.redis) {
95
+ await this.redis.quit();
96
+ this.logger.log("Disconnected from FalkorDB");
97
+ }
98
+ }
99
+ async query(cypher, _params) {
100
+ try {
101
+ const redis = await this.ensureConnected();
102
+ const result = await redis.call("GRAPH.QUERY", this.config.graphName, cypher);
103
+ const resultArray = Array.isArray(result) ? result : [];
104
+ return {
105
+ resultSet: Array.isArray(resultArray[1]) ? resultArray[1] : [],
106
+ stats: this.parseStats(result)
107
+ };
108
+ } catch (error) {
109
+ this.logger.error(`Cypher query failed: ${error instanceof Error ? error.message : String(error)}`);
110
+ throw error;
111
+ }
112
+ }
113
+ async getStats() {
114
+ try {
115
+ const labels = await this.getLabels();
116
+ const relationshipTypes = await this.getRelationshipTypes();
117
+ const nodeCountResult = await this.query("MATCH (n) RETURN count(n) as count");
118
+ const nodeCount = nodeCountResult.resultSet?.[0]?.[0] || 0;
119
+ const edgeCountResult = await this.query("MATCH ()-[r]-() RETURN count(r) as count");
120
+ const edgeCount = edgeCountResult.resultSet?.[0]?.[0] || 0;
121
+ const entityCounts = {};
122
+ for (const label of labels) {
123
+ const result = await this.query(`MATCH (n:\`${label}\`) RETURN count(n) as count`);
124
+ entityCounts[label] = result.resultSet?.[0]?.[0] || 0;
125
+ }
126
+ const relationshipCounts = {};
127
+ for (const relType of relationshipTypes) {
128
+ const result = await this.query(`MATCH ()-[r:\`${relType}\`]-() RETURN count(r) as count`);
129
+ relationshipCounts[relType] = result.resultSet?.[0]?.[0] || 0;
130
+ }
131
+ return {
132
+ nodeCount,
133
+ edgeCount,
134
+ labels,
135
+ relationshipTypes,
136
+ entityCounts,
137
+ relationshipCounts
138
+ };
139
+ } catch (error) {
140
+ this.logger.error(`Failed to get stats: ${error instanceof Error ? error.message : String(error)}`);
141
+ throw error;
142
+ }
143
+ }
144
+ async getLabels() {
145
+ try {
146
+ const result = await this.query("CALL db.labels() YIELD label RETURN label");
147
+ return (result.resultSet || []).map((row) => row[0]);
148
+ } catch (error) {
149
+ this.logger.error(`Failed to get labels: ${error instanceof Error ? error.message : String(error)}`);
150
+ return [];
151
+ }
152
+ }
153
+ async getRelationshipTypes() {
154
+ try {
155
+ const result = await this.query("CALL db.relationshipTypes() YIELD relationshipType RETURN relationshipType");
156
+ return (result.resultSet || []).map((row) => row[0]);
157
+ } catch (error) {
158
+ this.logger.error(`Failed to get relationship types: ${error instanceof Error ? error.message : String(error)}`);
159
+ return [];
160
+ }
161
+ }
162
+ async upsertNode(label, properties) {
163
+ try {
164
+ const { name, ...otherProps } = properties;
165
+ if (!name) {
166
+ throw new Error("Node must have a 'name' property");
167
+ }
168
+ const escapedName = this.escapeCypher(String(name));
169
+ const escapedLabel = this.escapeCypher(label);
170
+ const propAssignments = Object.entries({
171
+ name,
172
+ ...otherProps
173
+ }).map(([key, value]) => {
174
+ const escapedKey = this.escapeCypher(key);
175
+ const escapedValue = this.escapeCypherValue(value);
176
+ return `n.\`${escapedKey}\` = ${escapedValue}`;
177
+ }).join(", ");
178
+ const cypher = `MERGE (n:\`${escapedLabel}\` { name: '${escapedName}' }) ` + `SET ${propAssignments}`;
179
+ await this.query(cypher);
180
+ } catch (error) {
181
+ this.logger.error(`Failed to upsert node: ${error instanceof Error ? error.message : String(error)}`);
182
+ throw error;
183
+ }
184
+ }
185
+ async upsertRelationship(sourceLabel, sourceName, relation, targetLabel, targetName, properties) {
186
+ try {
187
+ const escapedSourceLabel = this.escapeCypher(sourceLabel);
188
+ const escapedSourceName = this.escapeCypher(sourceName);
189
+ const escapedRelation = this.escapeCypher(relation);
190
+ const escapedTargetLabel = this.escapeCypher(targetLabel);
191
+ const escapedTargetName = this.escapeCypher(targetName);
192
+ let relPropAssignments = "";
193
+ if (properties && Object.keys(properties).length > 0) {
194
+ relPropAssignments = ` SET ` + Object.entries(properties).map(([key, value]) => {
195
+ const escapedKey = this.escapeCypher(key);
196
+ const escapedValue = this.escapeCypherValue(value);
197
+ return `r.\`${escapedKey}\` = ${escapedValue}`;
198
+ }).join(", ");
199
+ }
200
+ const cypher = `MERGE (source:\`${escapedSourceLabel}\` { name: '${escapedSourceName}' }) ` + `MERGE (target:\`${escapedTargetLabel}\` { name: '${escapedTargetName}' }) ` + `MERGE (source)-[r:\`${escapedRelation}\`]->(target)` + relPropAssignments;
201
+ await this.query(cypher);
202
+ } catch (error) {
203
+ this.logger.error(`Failed to upsert relationship: ${error instanceof Error ? error.message : String(error)}`);
204
+ throw error;
205
+ }
206
+ }
207
+ async deleteNode(label, name) {
208
+ try {
209
+ const escapedLabel = this.escapeCypher(label);
210
+ const escapedName = this.escapeCypher(name);
211
+ const cypher = `MATCH (n:\`${escapedLabel}\` { name: '${escapedName}' }) ` + `DETACH DELETE n`;
212
+ await this.query(cypher);
213
+ } catch (error) {
214
+ this.logger.error(`Failed to delete node: ${error instanceof Error ? error.message : String(error)}`);
215
+ throw error;
216
+ }
217
+ }
218
+ async deleteDocumentRelationships(documentPath) {
219
+ try {
220
+ const escapedPath = this.escapeCypher(documentPath);
221
+ const cypher = `MATCH ()-[r { documentPath: '${escapedPath}' }]-() ` + `DELETE r`;
222
+ await this.query(cypher);
223
+ } catch (error) {
224
+ this.logger.error(`Failed to delete document relationships: ${error instanceof Error ? error.message : String(error)}`);
225
+ throw error;
226
+ }
227
+ }
228
+ async findNodesByLabel(label, limit) {
229
+ try {
230
+ const escapedLabel = this.escapeCypher(label);
231
+ const limitClause = limit ? ` LIMIT ${limit}` : "";
232
+ const cypher = `MATCH (n:\`${escapedLabel}\`) RETURN n${limitClause}`;
233
+ const result = await this.query(cypher);
234
+ return (result.resultSet || []).map((row) => row[0]);
235
+ } catch (error) {
236
+ this.logger.error(`Failed to find nodes by label: ${error instanceof Error ? error.message : String(error)}`);
237
+ return [];
238
+ }
239
+ }
240
+ async findRelationships(nodeName) {
241
+ try {
242
+ const escapedName = this.escapeCypher(nodeName);
243
+ const cypher = `MATCH (n { name: '${escapedName}' })-[r]-(m) ` + `RETURN type(r), m.name`;
244
+ const result = await this.query(cypher);
245
+ return result.resultSet || [];
246
+ } catch (error) {
247
+ this.logger.error(`Failed to find relationships: ${error instanceof Error ? error.message : String(error)}`);
248
+ return [];
249
+ }
250
+ }
251
+ async createVectorIndex(label, property, dimensions) {
252
+ try {
253
+ const escapedLabel = this.escapeCypher(label);
254
+ const escapedProperty = this.escapeCypher(property);
255
+ const cypher = `CREATE VECTOR INDEX FOR (n:\`${escapedLabel}\`) ON (n.\`${escapedProperty}\`) OPTIONS { dimension: ${dimensions}, similarityFunction: 'cosine' }`;
256
+ await this.query(cypher);
257
+ this.logger.log(`Created vector index on ${label}.${property} with ${dimensions} dimensions`);
258
+ } catch (error) {
259
+ const errorMessage = error instanceof Error ? error.message : String(error);
260
+ if (!errorMessage.includes("already exists")) {
261
+ this.logger.error(`Failed to create vector index: ${errorMessage}`);
262
+ throw error;
263
+ }
264
+ this.logger.debug(`Vector index on ${label}.${property} already exists`);
265
+ }
266
+ }
267
+ async updateNodeEmbedding(label, name, embedding) {
268
+ try {
269
+ const escapedLabel = this.escapeCypher(label);
270
+ const escapedName = this.escapeCypher(name);
271
+ const vectorStr = `[${embedding.join(", ")}]`;
272
+ const cypher = `MATCH (n:\`${escapedLabel}\` { name: '${escapedName}' }) ` + `SET n.embedding = vecf32(${vectorStr})`;
273
+ await this.query(cypher);
274
+ } catch (error) {
275
+ this.logger.error(`Failed to update node embedding: ${error instanceof Error ? error.message : String(error)}`);
276
+ throw error;
277
+ }
278
+ }
279
+ async vectorSearch(label, queryVector, k = 10) {
280
+ try {
281
+ const escapedLabel = this.escapeCypher(label);
282
+ const vectorStr = `[${queryVector.join(", ")}]`;
283
+ const cypher = `CALL db.idx.vector.queryNodes('${escapedLabel}', 'embedding', ${k}, vecf32(${vectorStr})) ` + `YIELD node, score ` + `RETURN node.name AS name, node.title AS title, (2 - score) / 2 AS similarity ` + `ORDER BY similarity DESC`;
284
+ const result = await this.query(cypher);
285
+ return (result.resultSet || []).map((row) => ({
286
+ name: row[0],
287
+ title: row[1],
288
+ score: row[2]
289
+ }));
290
+ } catch (error) {
291
+ this.logger.error(`Vector search failed: ${error instanceof Error ? error.message : String(error)}`);
292
+ throw error;
293
+ }
294
+ }
295
+ async vectorSearchAll(queryVector, k = 10) {
296
+ const allLabels = ["Document", "Concept", "Process", "Tool", "Technology", "Organization", "Topic", "Person"];
297
+ const allResults = [];
298
+ for (const label of allLabels) {
299
+ try {
300
+ const escapedLabel = this.escapeCypher(label);
301
+ const vectorStr = `[${queryVector.join(", ")}]`;
302
+ const cypher = `CALL db.idx.vector.queryNodes('${escapedLabel}', 'embedding', ${k}, vecf32(${vectorStr})) ` + `YIELD node, score ` + `RETURN node.name AS name, node.title AS title, node.description AS description, (2 - score) / 2 AS similarity ` + `ORDER BY similarity DESC`;
303
+ const result = await this.query(cypher);
304
+ const labelResults = (result.resultSet || []).map((row) => ({
305
+ name: row[0],
306
+ label,
307
+ title: row[1],
308
+ description: row[2],
309
+ score: row[3]
310
+ }));
311
+ allResults.push(...labelResults);
312
+ } catch (error) {
313
+ this.logger.debug(`Vector search for ${label} skipped: ${error instanceof Error ? error.message : String(error)}`);
314
+ }
315
+ }
316
+ return allResults.sort((a, b) => b.score - a.score).slice(0, k);
317
+ }
318
+ escapeCypher(value) {
319
+ return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/"/g, "\\\"");
320
+ }
321
+ escapeCypherValue(value) {
322
+ if (value === null || value === undefined) {
323
+ return "null";
324
+ }
325
+ if (typeof value === "string") {
326
+ const escaped = this.escapeCypher(value);
327
+ return `'${escaped}'`;
328
+ }
329
+ if (typeof value === "number" || typeof value === "boolean") {
330
+ return String(value);
331
+ }
332
+ if (Array.isArray(value)) {
333
+ return `[${value.map((v) => this.escapeCypherValue(v)).join(", ")}]`;
334
+ }
335
+ if (typeof value === "object") {
336
+ const pairs = Object.entries(value).map(([k, v]) => `${k}: ${this.escapeCypherValue(v)}`).join(", ");
337
+ return `{${pairs}}`;
338
+ }
339
+ return String(value);
340
+ }
341
+ parseStats(result) {
342
+ if (!Array.isArray(result) || result.length < 2) {
343
+ return;
344
+ }
345
+ const statsStr = result[1];
346
+ if (!statsStr || typeof statsStr !== "string") {
347
+ return;
348
+ }
349
+ const stats = {
350
+ nodesCreated: 0,
351
+ nodesDeleted: 0,
352
+ relationshipsCreated: 0,
353
+ relationshipsDeleted: 0,
354
+ propertiesSet: 0
355
+ };
356
+ const nodeCreatedMatch = statsStr.match(/Nodes created: (\d+)/);
357
+ if (nodeCreatedMatch) {
358
+ stats.nodesCreated = parseInt(nodeCreatedMatch[1], 10);
359
+ }
360
+ const nodeDeletedMatch = statsStr.match(/Nodes deleted: (\d+)/);
361
+ if (nodeDeletedMatch) {
362
+ stats.nodesDeleted = parseInt(nodeDeletedMatch[1], 10);
363
+ }
364
+ const relCreatedMatch = statsStr.match(/Relationships created: (\d+)/);
365
+ if (relCreatedMatch) {
366
+ stats.relationshipsCreated = parseInt(relCreatedMatch[1], 10);
367
+ }
368
+ const relDeletedMatch = statsStr.match(/Relationships deleted: (\d+)/);
369
+ if (relDeletedMatch) {
370
+ stats.relationshipsDeleted = parseInt(relDeletedMatch[1], 10);
371
+ }
372
+ const propSetMatch = statsStr.match(/Properties set: (\d+)/);
373
+ if (propSetMatch) {
374
+ stats.propertiesSet = parseInt(propSetMatch[1], 10);
375
+ }
376
+ return stats;
377
+ }
378
+ }
379
+ GraphService = __legacyDecorateClassTS([
380
+ Injectable(),
381
+ __legacyMetadataTS("design:paramtypes", [
382
+ typeof ConfigService === "undefined" ? Object : ConfigService
383
+ ])
384
+ ], GraphService);
385
+
386
+ // src/graph/graph.module.ts
387
+ class GraphModule {
388
+ }
389
+ GraphModule = __legacyDecorateClassTS([
390
+ Module({
391
+ providers: [GraphService],
392
+ exports: [GraphService]
393
+ })
394
+ ], GraphModule);
395
+
396
+ // src/sync/sync.module.ts
397
+ import { Module as Module3 } from "@nestjs/common";
398
+
399
+ // src/sync/sync.service.ts
400
+ import { Injectable as Injectable7, Logger as Logger6 } from "@nestjs/common";
401
+
402
+ // src/sync/manifest.service.ts
403
+ import { Injectable as Injectable2 } from "@nestjs/common";
404
+ import { createHash } from "crypto";
405
+ import { readFile, writeFile } from "fs/promises";
406
+ import { existsSync } from "fs";
407
+ import { resolve } from "path";
408
+ function getProjectRoot() {
409
+ if (process.env.PROJECT_ROOT) {
410
+ return process.env.PROJECT_ROOT;
411
+ }
412
+ return process.cwd();
413
+ }
414
+
415
+ class ManifestService {
416
+ manifestPath;
417
+ manifest = null;
418
+ constructor() {
419
+ const docsPath = process.env.DOCS_PATH || "docs";
420
+ this.manifestPath = resolve(getProjectRoot(), docsPath, ".sync-manifest.json");
421
+ }
422
+ async load() {
423
+ try {
424
+ if (existsSync(this.manifestPath)) {
425
+ const content = await readFile(this.manifestPath, "utf-8");
426
+ this.manifest = JSON.parse(content);
427
+ } else {
428
+ this.manifest = this.createEmptyManifest();
429
+ }
430
+ } catch (error) {
431
+ this.manifest = this.createEmptyManifest();
432
+ }
433
+ return this.manifest;
434
+ }
435
+ async save() {
436
+ if (!this.manifest) {
437
+ throw new Error("Manifest not loaded. Call load() first.");
438
+ }
439
+ this.manifest.lastSync = new Date().toISOString();
440
+ const content = JSON.stringify(this.manifest, null, 2);
441
+ await writeFile(this.manifestPath, content, "utf-8");
442
+ }
443
+ getContentHash(content) {
444
+ return createHash("sha256").update(content).digest("hex");
445
+ }
446
+ detectChange(path, contentHash, frontmatterHash) {
447
+ if (!this.manifest) {
448
+ throw new Error("Manifest not loaded. Call load() first.");
449
+ }
450
+ const existing = this.manifest.documents[path];
451
+ if (!existing) {
452
+ return "new";
453
+ }
454
+ if (existing.contentHash === contentHash && existing.frontmatterHash === frontmatterHash) {
455
+ return "unchanged";
456
+ }
457
+ return "updated";
458
+ }
459
+ updateEntry(path, contentHash, frontmatterHash, entityCount, relationshipCount) {
460
+ if (!this.manifest) {
461
+ throw new Error("Manifest not loaded. Call load() first.");
462
+ }
463
+ this.manifest.documents[path] = {
464
+ contentHash,
465
+ frontmatterHash,
466
+ lastSynced: new Date().toISOString(),
467
+ entityCount,
468
+ relationshipCount
469
+ };
470
+ }
471
+ removeEntry(path) {
472
+ if (!this.manifest) {
473
+ throw new Error("Manifest not loaded. Call load() first.");
474
+ }
475
+ delete this.manifest.documents[path];
476
+ }
477
+ getTrackedPaths() {
478
+ if (!this.manifest) {
479
+ throw new Error("Manifest not loaded. Call load() first.");
480
+ }
481
+ return Object.keys(this.manifest.documents);
482
+ }
483
+ createEmptyManifest() {
484
+ return {
485
+ version: "1.0",
486
+ lastSync: new Date().toISOString(),
487
+ documents: {}
488
+ };
489
+ }
490
+ }
491
+ ManifestService = __legacyDecorateClassTS([
492
+ Injectable2(),
493
+ __legacyMetadataTS("design:paramtypes", [])
494
+ ], ManifestService);
495
+
496
+ // src/sync/document-parser.service.ts
497
+ import { Injectable as Injectable3, Logger as Logger2 } from "@nestjs/common";
498
+ import { glob } from "glob";
499
+ import { readFile as readFile2 } from "fs/promises";
500
+ import { createHash as createHash2 } from "crypto";
501
+ import { resolve as resolve2 } from "path";
502
+
503
+ // src/utils/frontmatter.ts
504
+ import { z } from "zod";
505
+ import matter from "gray-matter";
506
+ var EntityTypeSchema = z.enum([
507
+ "Topic",
508
+ "Technology",
509
+ "Concept",
510
+ "Tool",
511
+ "Process",
512
+ "Person",
513
+ "Organization",
514
+ "Document"
515
+ ]);
516
+ var RelationTypeSchema = z.enum([
517
+ "REFERENCES"
518
+ ]);
519
+ var EntitySchema = z.object({
520
+ name: z.string().min(1),
521
+ type: EntityTypeSchema,
522
+ description: z.string().optional()
523
+ });
524
+ var RelationshipSchema = z.object({
525
+ source: z.string().min(1),
526
+ relation: RelationTypeSchema,
527
+ target: z.string().min(1)
528
+ });
529
+ var GraphMetadataSchema = z.object({
530
+ importance: z.enum(["high", "medium", "low"]).optional(),
531
+ domain: z.string().optional()
532
+ });
533
+ var validateDateFormat = (dateStr) => {
534
+ const match = dateStr.match(/^(\d{4})-(\d{2})-(\d{2})$/);
535
+ if (!match)
536
+ return false;
537
+ const [, yearStr, monthStr, dayStr] = match;
538
+ const year = parseInt(yearStr, 10);
539
+ const month = parseInt(monthStr, 10);
540
+ const day = parseInt(dayStr, 10);
541
+ if (month < 1 || month > 12)
542
+ return false;
543
+ if (day < 1 || day > 31)
544
+ return false;
545
+ const daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
546
+ if (year % 4 === 0 && year % 100 !== 0 || year % 400 === 0) {
547
+ daysInMonth[1] = 29;
548
+ }
549
+ return day <= daysInMonth[month - 1];
550
+ };
551
+ var FrontmatterSchema = z.object({
552
+ created: z.string().refine(validateDateFormat, "Date must be in YYYY-MM-DD format"),
553
+ updated: z.string().refine(validateDateFormat, "Date must be in YYYY-MM-DD format"),
554
+ status: z.enum(["draft", "ongoing", "complete"]).optional(),
555
+ topic: z.string().optional(),
556
+ tags: z.array(z.string()).optional(),
557
+ summary: z.string().optional(),
558
+ entities: z.array(EntitySchema).optional(),
559
+ relationships: z.array(RelationshipSchema).optional(),
560
+ graph: GraphMetadataSchema.optional()
561
+ }).passthrough();
562
+ function parseFrontmatter(content) {
563
+ try {
564
+ const { data, content: markdown } = matter(content);
565
+ if (Object.keys(data).length === 0) {
566
+ return {
567
+ frontmatter: null,
568
+ content: markdown.trim(),
569
+ raw: content
570
+ };
571
+ }
572
+ const normalizedData = normalizeData(data);
573
+ const validated = FrontmatterSchema.safeParse(normalizedData);
574
+ return {
575
+ frontmatter: validated.success ? validated.data : normalizedData,
576
+ content: markdown.trim(),
577
+ raw: content
578
+ };
579
+ } catch (error) {
580
+ const errorMessage = error instanceof Error ? error.message : String(error);
581
+ throw new Error(`YAML parsing error: ${errorMessage}`);
582
+ }
583
+ }
584
+ function normalizeData(data) {
585
+ if (data instanceof Date) {
586
+ return data.toISOString().split("T")[0];
587
+ }
588
+ if (Array.isArray(data)) {
589
+ return data.map(normalizeData);
590
+ }
591
+ if (data !== null && typeof data === "object") {
592
+ const normalized = {};
593
+ for (const [key, value] of Object.entries(data)) {
594
+ normalized[key] = normalizeData(value);
595
+ }
596
+ return normalized;
597
+ }
598
+ return data;
599
+ }
600
+
601
+ // src/sync/document-parser.service.ts
602
+ function getProjectRoot2() {
603
+ if (process.env.PROJECT_ROOT) {
604
+ return process.env.PROJECT_ROOT;
605
+ }
606
+ return process.cwd();
607
+ }
608
+
609
+ class DocumentParserService {
610
+ logger = new Logger2(DocumentParserService.name);
611
+ docsPath;
612
+ constructor() {
613
+ const docsPathEnv = process.env.DOCS_PATH || "docs";
614
+ if (docsPathEnv.startsWith("/")) {
615
+ this.docsPath = docsPathEnv;
616
+ } else {
617
+ this.docsPath = resolve2(getProjectRoot2(), docsPathEnv);
618
+ }
619
+ }
620
+ getDocsPath() {
621
+ return this.docsPath;
622
+ }
623
+ async discoverDocuments() {
624
+ const pattern = `${this.docsPath}/**/*.md`;
625
+ const files = await glob(pattern, {
626
+ ignore: ["**/node_modules/**", "**/.git/**"]
627
+ });
628
+ return files.sort();
629
+ }
630
+ async parseDocument(filePath) {
631
+ const content = await readFile2(filePath, "utf-8");
632
+ const parsed = parseFrontmatter(content);
633
+ const title = this.extractTitle(content, filePath);
634
+ const contentHash = this.computeHash(content);
635
+ const frontmatterHash = this.computeHash(JSON.stringify(parsed.frontmatter || {}));
636
+ const entities = this.extractEntities(parsed.frontmatter, filePath);
637
+ const relationships = this.extractRelationships(parsed.frontmatter, filePath);
638
+ const graphMetadata = this.extractGraphMetadata(parsed.frontmatter);
639
+ return {
640
+ path: filePath,
641
+ title,
642
+ content: parsed.content,
643
+ contentHash,
644
+ frontmatterHash,
645
+ summary: parsed.frontmatter?.summary,
646
+ topic: parsed.frontmatter?.topic,
647
+ entities,
648
+ relationships,
649
+ graphMetadata,
650
+ tags: parsed.frontmatter?.tags || [],
651
+ created: parsed.frontmatter?.created,
652
+ updated: parsed.frontmatter?.updated,
653
+ status: parsed.frontmatter?.status
654
+ };
655
+ }
656
+ async parseAllDocuments() {
657
+ const { docs } = await this.parseAllDocumentsWithErrors();
658
+ return docs;
659
+ }
660
+ async parseAllDocumentsWithErrors() {
661
+ const files = await this.discoverDocuments();
662
+ const docs = [];
663
+ const errors = [];
664
+ for (const file of files) {
665
+ try {
666
+ const parsed = await this.parseDocument(file);
667
+ docs.push(parsed);
668
+ } catch (error) {
669
+ const errorMsg = error instanceof Error ? error.message : String(error);
670
+ errors.push({ path: file, error: errorMsg });
671
+ this.logger.warn(`Failed to parse ${file}: ${error}`);
672
+ }
673
+ }
674
+ return { docs, errors };
675
+ }
676
+ extractTitle(content, filePath) {
677
+ const h1Match = content.match(/^#\s+(.+)$/m);
678
+ if (h1Match) {
679
+ return h1Match[1];
680
+ }
681
+ const parts = filePath.split("/");
682
+ return parts[parts.length - 1].replace(".md", "");
683
+ }
684
+ extractEntities(frontmatter, docPath) {
685
+ if (!frontmatter?.entities || !Array.isArray(frontmatter.entities)) {
686
+ return [];
687
+ }
688
+ const validEntities = [];
689
+ const errors = [];
690
+ for (let i = 0;i < frontmatter.entities.length; i++) {
691
+ const e = frontmatter.entities[i];
692
+ const result = EntitySchema.safeParse(e);
693
+ if (result.success) {
694
+ validEntities.push(result.data);
695
+ } else {
696
+ const entityPreview = typeof e === "string" ? `"${e}"` : JSON.stringify(e);
697
+ errors.push(`Entity[${i}]: ${entityPreview} - Expected object with {name, type}, got ${typeof e}`);
698
+ }
699
+ }
700
+ if (errors.length > 0) {
701
+ const errorMsg = `Invalid entity schema in ${docPath}:
702
+ ${errors.join(`
703
+ `)}`;
704
+ throw new Error(errorMsg);
705
+ }
706
+ return validEntities;
707
+ }
708
+ extractRelationships(frontmatter, docPath) {
709
+ if (!frontmatter?.relationships || !Array.isArray(frontmatter.relationships)) {
710
+ return [];
711
+ }
712
+ const validRelationships = [];
713
+ const errors = [];
714
+ const validRelationTypes = RelationTypeSchema.options;
715
+ for (let i = 0;i < frontmatter.relationships.length; i++) {
716
+ const r = frontmatter.relationships[i];
717
+ const result = RelationshipSchema.safeParse(r);
718
+ if (result.success) {
719
+ const rel = result.data;
720
+ if (rel.source === "this") {
721
+ rel.source = docPath;
722
+ }
723
+ if (rel.target === "this") {
724
+ rel.target = docPath;
725
+ }
726
+ validRelationships.push(rel);
727
+ } else {
728
+ if (typeof r === "string") {
729
+ errors.push(`Relationship[${i}]: "${r}" - Expected object with {source, relation, target}, got string`);
730
+ } else if (typeof r === "object" && r !== null) {
731
+ const issues = [];
732
+ if (!r.source)
733
+ issues.push("missing source");
734
+ if (!r.target)
735
+ issues.push("missing target");
736
+ if (!r.relation) {
737
+ issues.push("missing relation");
738
+ } else if (!validRelationTypes.includes(r.relation)) {
739
+ issues.push(`invalid relation "${r.relation}" (allowed: ${validRelationTypes.join(", ")})`);
740
+ }
741
+ errors.push(`Relationship[${i}]: ${issues.join(", ")}`);
742
+ } else {
743
+ errors.push(`Relationship[${i}]: Expected object, got ${typeof r}`);
744
+ }
745
+ }
746
+ }
747
+ if (errors.length > 0) {
748
+ const errorMsg = `Invalid relationship schema in ${docPath}:
749
+ ${errors.join(`
750
+ `)}`;
751
+ throw new Error(errorMsg);
752
+ }
753
+ return validRelationships;
754
+ }
755
+ extractGraphMetadata(frontmatter) {
756
+ if (!frontmatter?.graph) {
757
+ return;
758
+ }
759
+ const result = GraphMetadataSchema.safeParse(frontmatter.graph);
760
+ return result.success ? result.data : undefined;
761
+ }
762
+ computeHash(content) {
763
+ return createHash2("sha256").update(content).digest("hex");
764
+ }
765
+ }
766
+ DocumentParserService = __legacyDecorateClassTS([
767
+ Injectable3(),
768
+ __legacyMetadataTS("design:paramtypes", [])
769
+ ], DocumentParserService);
770
+
771
+ // src/sync/cascade.service.ts
772
+ import { Injectable as Injectable4, Logger as Logger3 } from "@nestjs/common";
773
+ class CascadeService {
774
+ graph;
775
+ _parser;
776
+ logger = new Logger3(CascadeService.name);
777
+ constructor(graph, _parser) {
778
+ this.graph = graph;
779
+ this._parser = _parser;
780
+ }
781
+ async analyzeEntityChange(change) {
782
+ let affectedDocuments = [];
783
+ let summary;
784
+ switch (change.trigger) {
785
+ case "entity_renamed":
786
+ affectedDocuments = await this.findAffectedByRename(change.entityName, change.newValue || "");
787
+ summary = `Entity "${change.oldValue}" was renamed to "${change.newValue}"`;
788
+ break;
789
+ case "entity_deleted":
790
+ affectedDocuments = await this.findAffectedByDeletion(change.entityName);
791
+ summary = `Entity "${change.entityName}" was deleted`;
792
+ break;
793
+ case "entity_type_changed":
794
+ affectedDocuments = await this.findAffectedByTypeChange(change.entityName, change.oldValue || "", change.newValue || "");
795
+ summary = `Entity "${change.entityName}" type changed from "${change.oldValue}" to "${change.newValue}"`;
796
+ break;
797
+ case "relationship_changed":
798
+ affectedDocuments = await this.findAffectedByRelationshipChange(change.entityName);
799
+ summary = `Relationship involving "${change.entityName}" was changed`;
800
+ break;
801
+ case "document_deleted":
802
+ affectedDocuments = await this.findAffectedByDocumentDeletion(change.documentPath);
803
+ summary = `Document "${change.documentPath}" was deleted`;
804
+ break;
805
+ default:
806
+ summary = `Unknown change type for entity "${change.entityName}"`;
807
+ }
808
+ affectedDocuments = affectedDocuments.filter((doc) => doc.path !== change.documentPath);
809
+ return {
810
+ trigger: change.trigger,
811
+ sourceDocument: change.documentPath,
812
+ affectedDocuments,
813
+ summary
814
+ };
815
+ }
816
+ detectEntityRenames(oldDoc, newDoc) {
817
+ const changes = [];
818
+ const oldNames = new Set(oldDoc.entities.map((e) => e.name));
819
+ const newNames = new Set(newDoc.entities.map((e) => e.name));
820
+ const removedEntities = oldDoc.entities.filter((e) => !newNames.has(e.name));
821
+ const addedEntities = newDoc.entities.filter((e) => !oldNames.has(e.name));
822
+ const removedByType = new Map;
823
+ for (const entity of removedEntities) {
824
+ const existing = removedByType.get(entity.type) || [];
825
+ existing.push(entity);
826
+ removedByType.set(entity.type, existing);
827
+ }
828
+ const addedByType = new Map;
829
+ for (const entity of addedEntities) {
830
+ const existing = addedByType.get(entity.type) || [];
831
+ existing.push(entity);
832
+ addedByType.set(entity.type, existing);
833
+ }
834
+ for (const [type, removed] of removedByType) {
835
+ const added = addedByType.get(type) || [];
836
+ const pairCount = Math.min(removed.length, added.length);
837
+ for (let i = 0;i < pairCount; i++) {
838
+ changes.push({
839
+ trigger: "entity_renamed",
840
+ entityName: removed[i].name,
841
+ oldValue: removed[i].name,
842
+ newValue: added[i].name,
843
+ documentPath: oldDoc.path
844
+ });
845
+ }
846
+ }
847
+ return changes;
848
+ }
849
+ detectEntityDeletions(oldDoc, newDoc) {
850
+ const changes = [];
851
+ const newNames = new Set(newDoc.entities.map((e) => e.name));
852
+ const renames = this.detectEntityRenames(oldDoc, newDoc);
853
+ const renamedNames = new Set(renames.map((r) => r.oldValue));
854
+ for (const entity of oldDoc.entities) {
855
+ if (!newNames.has(entity.name) && !renamedNames.has(entity.name)) {
856
+ changes.push({
857
+ trigger: "entity_deleted",
858
+ entityName: entity.name,
859
+ documentPath: oldDoc.path
860
+ });
861
+ }
862
+ }
863
+ return changes;
864
+ }
865
+ detectEntityTypeChanges(oldDoc, newDoc) {
866
+ const changes = [];
867
+ const newEntityMap = new Map(newDoc.entities.map((e) => [e.name, e]));
868
+ for (const oldEntity of oldDoc.entities) {
869
+ const newEntity = newEntityMap.get(oldEntity.name);
870
+ if (newEntity && newEntity.type !== oldEntity.type) {
871
+ changes.push({
872
+ trigger: "entity_type_changed",
873
+ entityName: oldEntity.name,
874
+ oldValue: oldEntity.type,
875
+ newValue: newEntity.type,
876
+ documentPath: oldDoc.path
877
+ });
878
+ }
879
+ }
880
+ return changes;
881
+ }
882
+ async findAffectedByRename(entityName, _newName) {
883
+ try {
884
+ const escapedName = this.escapeForCypher(entityName);
885
+ const query = `
886
+ MATCH (e {name: '${escapedName}'})-[:APPEARS_IN]->(d:Document)
887
+ RETURN d.name, d.title
888
+ `.trim();
889
+ const result = await this.graph.query(query);
890
+ return (result.resultSet || []).map((row) => ({
891
+ path: row[0],
892
+ reason: `References "${entityName}" in entities`,
893
+ suggestedAction: "update_reference",
894
+ confidence: "high",
895
+ affectedEntities: [entityName]
896
+ }));
897
+ } catch (error) {
898
+ this.logger.warn(`Failed to find documents affected by rename: ${error instanceof Error ? error.message : String(error)}`);
899
+ return [];
900
+ }
901
+ }
902
+ async findAffectedByDeletion(entityName) {
903
+ try {
904
+ const escapedName = this.escapeForCypher(entityName);
905
+ const query = `
906
+ MATCH (e {name: '${escapedName}'})-[:APPEARS_IN]->(d:Document)
907
+ RETURN d.name, d.title
908
+ `.trim();
909
+ const result = await this.graph.query(query);
910
+ return (result.resultSet || []).map((row) => ({
911
+ path: row[0],
912
+ reason: `References deleted entity "${entityName}"`,
913
+ suggestedAction: "review_content",
914
+ confidence: "high",
915
+ affectedEntities: [entityName]
916
+ }));
917
+ } catch (error) {
918
+ this.logger.warn(`Failed to find documents affected by deletion: ${error instanceof Error ? error.message : String(error)}`);
919
+ return [];
920
+ }
921
+ }
922
+ async findAffectedByTypeChange(entityName, oldType, newType) {
923
+ try {
924
+ const escapedName = this.escapeForCypher(entityName);
925
+ const query = `
926
+ MATCH (e {name: '${escapedName}'})-[:APPEARS_IN]->(d:Document)
927
+ RETURN d.name, d.title
928
+ `.trim();
929
+ const result = await this.graph.query(query);
930
+ return (result.resultSet || []).map((row) => ({
931
+ path: row[0],
932
+ reason: `References "${entityName}" with type "${oldType}" (now "${newType}")`,
933
+ suggestedAction: "review_content",
934
+ confidence: "medium",
935
+ affectedEntities: [entityName]
936
+ }));
937
+ } catch (error) {
938
+ this.logger.warn(`Failed to find documents affected by type change: ${error instanceof Error ? error.message : String(error)}`);
939
+ return [];
940
+ }
941
+ }
942
+ async findAffectedByRelationshipChange(entityName) {
943
+ try {
944
+ const escapedName = this.escapeForCypher(entityName);
945
+ const query = `
946
+ MATCH (e {name: '${escapedName}'})-[r]->(d:Document)
947
+ RETURN d.name, d.title, type(r) as relType
948
+ `.trim();
949
+ const result = await this.graph.query(query);
950
+ return (result.resultSet || []).map((row) => ({
951
+ path: row[0],
952
+ reason: `Has relationship with "${entityName}"`,
953
+ suggestedAction: "review_content",
954
+ confidence: "medium",
955
+ affectedEntities: [entityName]
956
+ }));
957
+ } catch (error) {
958
+ this.logger.warn(`Failed to find documents affected by relationship change: ${error instanceof Error ? error.message : String(error)}`);
959
+ return [];
960
+ }
961
+ }
962
+ async findAffectedByDocumentDeletion(documentPath) {
963
+ try {
964
+ const escapedPath = this.escapeForCypher(documentPath);
965
+ const query = `
966
+ MATCH (d:Document)-[r]->(deleted:Document {name: '${escapedPath}'})
967
+ RETURN d.name, type(r) as relType
968
+ `.trim();
969
+ const result = await this.graph.query(query);
970
+ return (result.resultSet || []).map((row) => ({
971
+ path: row[0],
972
+ reason: `Links to deleted document "${documentPath}"`,
973
+ suggestedAction: "remove_reference",
974
+ confidence: "high",
975
+ affectedEntities: []
976
+ }));
977
+ } catch (error) {
978
+ this.logger.warn(`Failed to find documents affected by document deletion: ${error instanceof Error ? error.message : String(error)}`);
979
+ return [];
980
+ }
981
+ }
982
+ async analyzeDocumentChange(oldDoc, newDoc) {
983
+ if (!oldDoc) {
984
+ return [];
985
+ }
986
+ const analyses = [];
987
+ const renames = this.detectEntityRenames(oldDoc, newDoc);
988
+ const deletions = this.detectEntityDeletions(oldDoc, newDoc);
989
+ const typeChanges = this.detectEntityTypeChanges(oldDoc, newDoc);
990
+ for (const change of renames) {
991
+ const analysis = await this.analyzeEntityChange(change);
992
+ if (analysis.affectedDocuments.length > 0) {
993
+ analyses.push(analysis);
994
+ }
995
+ }
996
+ for (const change of deletions) {
997
+ const analysis = await this.analyzeEntityChange(change);
998
+ if (analysis.affectedDocuments.length > 0) {
999
+ analyses.push(analysis);
1000
+ }
1001
+ }
1002
+ for (const change of typeChanges) {
1003
+ const analysis = await this.analyzeEntityChange(change);
1004
+ if (analysis.affectedDocuments.length > 0) {
1005
+ analyses.push(analysis);
1006
+ }
1007
+ }
1008
+ return analyses;
1009
+ }
1010
+ formatWarnings(analyses) {
1011
+ if (analyses.length === 0) {
1012
+ return "";
1013
+ }
1014
+ const lines = [];
1015
+ lines.push(`
1016
+ === Cascade Impact Detected ===
1017
+ `);
1018
+ for (const analysis of analyses) {
1019
+ lines.push(analysis.summary);
1020
+ lines.push(` Source: ${analysis.sourceDocument}`);
1021
+ lines.push("");
1022
+ if (analysis.affectedDocuments.length > 0) {
1023
+ lines.push(` Affected documents (${analysis.affectedDocuments.length}):`);
1024
+ for (const doc of analysis.affectedDocuments) {
1025
+ lines.push(` [${doc.confidence}] ${doc.path}`);
1026
+ lines.push(` ${doc.reason}`);
1027
+ lines.push(` -> Suggested: ${this.formatSuggestedAction(doc.suggestedAction, analysis)}`);
1028
+ }
1029
+ }
1030
+ lines.push("");
1031
+ }
1032
+ return lines.join(`
1033
+ `);
1034
+ }
1035
+ formatSuggestedAction(action, analysis) {
1036
+ switch (action) {
1037
+ case "update_reference":
1038
+ if (analysis.trigger === "entity_renamed") {
1039
+ const match = analysis.summary.match(/renamed to "([^"]+)"/);
1040
+ const newName = match ? match[1] : "new name";
1041
+ return `Update reference to "${newName}"`;
1042
+ }
1043
+ return "Update reference";
1044
+ case "remove_reference":
1045
+ return "Remove broken reference";
1046
+ case "review_content":
1047
+ return "Review content for consistency";
1048
+ case "add_entity":
1049
+ return "Consider adding entity definition";
1050
+ default:
1051
+ return action;
1052
+ }
1053
+ }
1054
+ escapeForCypher(value) {
1055
+ return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/"/g, "\\\"");
1056
+ }
1057
+ }
1058
+ CascadeService = __legacyDecorateClassTS([
1059
+ Injectable4(),
1060
+ __legacyMetadataTS("design:paramtypes", [
1061
+ typeof GraphService === "undefined" ? Object : GraphService,
1062
+ typeof DocumentParserService === "undefined" ? Object : DocumentParserService
1063
+ ])
1064
+ ], CascadeService);
1065
+
1066
+ // src/sync/path-resolver.service.ts
1067
+ import { Injectable as Injectable5, Logger as Logger4 } from "@nestjs/common";
1068
+ import { resolve as resolve3, isAbsolute } from "path";
1069
+ import { existsSync as existsSync2 } from "fs";
1070
+ class PathResolverService {
1071
+ parser;
1072
+ logger = new Logger4(PathResolverService.name);
1073
+ constructor(parser) {
1074
+ this.parser = parser;
1075
+ }
1076
+ getDocsPath() {
1077
+ return this.parser.getDocsPath();
1078
+ }
1079
+ resolveDocPath(userPath, options = {}) {
1080
+ const { requireExists = true, requireInDocs = true } = options;
1081
+ let resolvedPath;
1082
+ if (isAbsolute(userPath)) {
1083
+ resolvedPath = userPath;
1084
+ } else {
1085
+ const fromCwd = resolve3(process.cwd(), userPath);
1086
+ const fromDocs = resolve3(this.getDocsPath(), userPath);
1087
+ const docsPrefix = "docs/";
1088
+ const strippedFromDocs = userPath.startsWith(docsPrefix) ? resolve3(this.getDocsPath(), userPath.slice(docsPrefix.length)) : null;
1089
+ if (this.isUnderDocs(fromCwd) && existsSync2(fromCwd)) {
1090
+ resolvedPath = fromCwd;
1091
+ } else if (strippedFromDocs && existsSync2(strippedFromDocs)) {
1092
+ resolvedPath = strippedFromDocs;
1093
+ } else if (existsSync2(fromDocs)) {
1094
+ resolvedPath = fromDocs;
1095
+ } else if (this.isUnderDocs(fromCwd)) {
1096
+ resolvedPath = fromCwd;
1097
+ } else if (strippedFromDocs) {
1098
+ resolvedPath = strippedFromDocs;
1099
+ } else {
1100
+ resolvedPath = fromDocs;
1101
+ }
1102
+ }
1103
+ if (requireInDocs && !this.isUnderDocs(resolvedPath)) {
1104
+ throw new Error(`Path "${userPath}" resolves to "${resolvedPath}" which is outside the docs directory (${this.getDocsPath()})`);
1105
+ }
1106
+ if (requireExists && !existsSync2(resolvedPath)) {
1107
+ throw new Error(`Path "${userPath}" does not exist (resolved to: ${resolvedPath})`);
1108
+ }
1109
+ return resolvedPath;
1110
+ }
1111
+ resolveDocPaths(userPaths, options = {}) {
1112
+ return userPaths.map((p) => this.resolveDocPath(p, options));
1113
+ }
1114
+ isUnderDocs(absolutePath) {
1115
+ const docsPath = this.getDocsPath();
1116
+ const normalizedPath = absolutePath.replace(/\/$/, "");
1117
+ const normalizedDocs = docsPath.replace(/\/$/, "");
1118
+ return normalizedPath.startsWith(normalizedDocs + "/") || normalizedPath === normalizedDocs;
1119
+ }
1120
+ getRelativePath(absolutePath) {
1121
+ const docsPath = this.getDocsPath();
1122
+ if (absolutePath.startsWith(docsPath)) {
1123
+ return absolutePath.slice(docsPath.length + 1);
1124
+ }
1125
+ return absolutePath;
1126
+ }
1127
+ }
1128
+ PathResolverService = __legacyDecorateClassTS([
1129
+ Injectable5(),
1130
+ __legacyMetadataTS("design:paramtypes", [
1131
+ typeof DocumentParserService === "undefined" ? Object : DocumentParserService
1132
+ ])
1133
+ ], PathResolverService);
1134
+
1135
+ // src/embedding/embedding.service.ts
1136
+ import { Injectable as Injectable6, Logger as Logger5 } from "@nestjs/common";
1137
+ import { ConfigService as ConfigService2 } from "@nestjs/config";
1138
+
1139
+ // src/embedding/embedding.types.ts
1140
+ var DEFAULT_EMBEDDING_CONFIG = {
1141
+ provider: "voyage",
1142
+ model: "voyage-3.5-lite",
1143
+ dimensions: 512
1144
+ };
1145
+
1146
+ // src/embedding/providers/openai.provider.ts
1147
+ class OpenAIEmbeddingProvider {
1148
+ name = "openai";
1149
+ dimensions;
1150
+ model;
1151
+ apiKey;
1152
+ baseUrl = "https://api.openai.com/v1";
1153
+ constructor(config) {
1154
+ const apiKey = config?.apiKey || process.env.OPENAI_API_KEY;
1155
+ if (!apiKey) {
1156
+ throw new Error("OpenAI API key is required. Set OPENAI_API_KEY environment variable or pass apiKey in config.");
1157
+ }
1158
+ this.apiKey = apiKey;
1159
+ this.model = config?.model || "text-embedding-3-small";
1160
+ this.dimensions = config?.dimensions || 1536;
1161
+ }
1162
+ async generateEmbedding(text) {
1163
+ const embeddings = await this.generateEmbeddings([text]);
1164
+ return embeddings[0];
1165
+ }
1166
+ async generateEmbeddings(texts) {
1167
+ if (!texts || texts.length === 0) {
1168
+ return [];
1169
+ }
1170
+ try {
1171
+ const response = await fetch(`${this.baseUrl}/embeddings`, {
1172
+ method: "POST",
1173
+ headers: {
1174
+ "Content-Type": "application/json",
1175
+ Authorization: `Bearer ${this.apiKey}`
1176
+ },
1177
+ body: JSON.stringify({
1178
+ model: this.model,
1179
+ input: texts,
1180
+ dimensions: this.dimensions
1181
+ })
1182
+ });
1183
+ if (!response.ok) {
1184
+ const error = await response.json().catch(() => ({}));
1185
+ throw new Error(`OpenAI API error: ${response.status} ${JSON.stringify(error)}`);
1186
+ }
1187
+ const data = await response.json();
1188
+ const sortedData = data.data.sort((a, b) => a.index - b.index);
1189
+ return sortedData.map((item) => item.embedding);
1190
+ } catch (error) {
1191
+ if (error instanceof Error) {
1192
+ throw new Error(`Failed to generate embeddings: ${error.message}`);
1193
+ }
1194
+ throw error;
1195
+ }
1196
+ }
1197
+ }
1198
+
1199
+ // src/embedding/providers/voyage.provider.ts
1200
+ class VoyageEmbeddingProvider {
1201
+ name = "voyage";
1202
+ dimensions;
1203
+ model;
1204
+ apiKey;
1205
+ inputType;
1206
+ baseUrl = "https://api.voyageai.com/v1";
1207
+ constructor(config) {
1208
+ const apiKey = config?.apiKey || process.env.VOYAGE_API_KEY;
1209
+ if (!apiKey) {
1210
+ throw new Error("Voyage API key is required. Set VOYAGE_API_KEY environment variable or pass apiKey in config.");
1211
+ }
1212
+ this.apiKey = apiKey;
1213
+ this.model = config?.model || "voyage-3.5-lite";
1214
+ this.dimensions = config?.dimensions || 512;
1215
+ this.inputType = config?.inputType || "document";
1216
+ }
1217
+ async generateEmbedding(text) {
1218
+ const embeddings = await this.generateEmbeddings([text]);
1219
+ return embeddings[0];
1220
+ }
1221
+ async generateEmbeddings(texts) {
1222
+ if (!texts || texts.length === 0) {
1223
+ return [];
1224
+ }
1225
+ try {
1226
+ const response = await fetch(`${this.baseUrl}/embeddings`, {
1227
+ method: "POST",
1228
+ headers: {
1229
+ "Content-Type": "application/json",
1230
+ Authorization: `Bearer ${this.apiKey}`
1231
+ },
1232
+ body: JSON.stringify({
1233
+ model: this.model,
1234
+ input: texts,
1235
+ output_dimension: this.dimensions,
1236
+ input_type: this.inputType
1237
+ })
1238
+ });
1239
+ if (!response.ok) {
1240
+ const error = await response.json().catch(() => ({}));
1241
+ throw new Error(`Voyage API error: ${response.status} ${JSON.stringify(error)}`);
1242
+ }
1243
+ const data = await response.json();
1244
+ const sortedData = data.data.sort((a, b) => a.index - b.index);
1245
+ return sortedData.map((item) => item.embedding);
1246
+ } catch (error) {
1247
+ if (error instanceof Error) {
1248
+ throw new Error(`Failed to generate embeddings: ${error.message}`);
1249
+ }
1250
+ throw error;
1251
+ }
1252
+ }
1253
+ }
1254
+
1255
+ // src/embedding/providers/mock.provider.ts
1256
+ class MockEmbeddingProvider {
1257
+ name = "mock";
1258
+ dimensions;
1259
+ constructor(dimensions = 1536) {
1260
+ this.dimensions = dimensions;
1261
+ }
1262
+ async generateEmbedding(text) {
1263
+ return this.generateDeterministicEmbedding(text);
1264
+ }
1265
+ async generateEmbeddings(texts) {
1266
+ return Promise.all(texts.map((text) => this.generateEmbedding(text)));
1267
+ }
1268
+ generateDeterministicEmbedding(text) {
1269
+ const embedding = new Array(this.dimensions);
1270
+ let hash = 0;
1271
+ for (let i = 0;i < text.length; i++) {
1272
+ hash = (hash << 5) - hash + text.charCodeAt(i);
1273
+ hash = hash & hash;
1274
+ }
1275
+ for (let i = 0;i < this.dimensions; i++) {
1276
+ const seed = hash * (i + 1) ^ i * 31;
1277
+ embedding[i] = (seed % 2000 - 1000) / 1000;
1278
+ }
1279
+ const magnitude = Math.sqrt(embedding.reduce((sum, val) => sum + val * val, 0));
1280
+ return embedding.map((val) => val / magnitude);
1281
+ }
1282
+ }
1283
+
1284
+ // src/embedding/embedding.service.ts
1285
+ class EmbeddingService {
1286
+ configService;
1287
+ logger = new Logger5(EmbeddingService.name);
1288
+ provider;
1289
+ config;
1290
+ constructor(configService) {
1291
+ this.configService = configService;
1292
+ this.config = this.loadConfig();
1293
+ this.provider = this.createProvider();
1294
+ this.logger.log(`Initialized embedding service with provider: ${this.provider.name}`);
1295
+ }
1296
+ loadConfig() {
1297
+ const providerEnv = this.configService.get("EMBEDDING_PROVIDER");
1298
+ const modelEnv = this.configService.get("EMBEDDING_MODEL");
1299
+ const dimensionsEnv = this.configService.get("EMBEDDING_DIMENSIONS");
1300
+ const provider = providerEnv ?? DEFAULT_EMBEDDING_CONFIG.provider;
1301
+ let apiKey;
1302
+ if (provider === "voyage") {
1303
+ apiKey = this.configService.get("VOYAGE_API_KEY");
1304
+ } else if (provider === "openai") {
1305
+ apiKey = this.configService.get("OPENAI_API_KEY");
1306
+ }
1307
+ return {
1308
+ provider,
1309
+ apiKey,
1310
+ model: modelEnv ?? DEFAULT_EMBEDDING_CONFIG.model,
1311
+ dimensions: dimensionsEnv ? Number(dimensionsEnv) : DEFAULT_EMBEDDING_CONFIG.dimensions
1312
+ };
1313
+ }
1314
+ createProvider() {
1315
+ switch (this.config.provider) {
1316
+ case "openai":
1317
+ if (!this.config.apiKey) {
1318
+ throw new Error("OPENAI_API_KEY environment variable is required for embeddings. " + "Set it in packages/graph/.env or use --no-embeddings to skip embedding generation.");
1319
+ }
1320
+ return new OpenAIEmbeddingProvider({
1321
+ apiKey: this.config.apiKey,
1322
+ model: this.config.model,
1323
+ dimensions: this.config.dimensions
1324
+ });
1325
+ case "mock":
1326
+ return new MockEmbeddingProvider(this.config.dimensions);
1327
+ case "voyage":
1328
+ if (!this.config.apiKey) {
1329
+ throw new Error("VOYAGE_API_KEY environment variable is required for embeddings. " + "Set it in packages/graph/.env or use --no-embeddings to skip embedding generation.");
1330
+ }
1331
+ return new VoyageEmbeddingProvider({
1332
+ apiKey: this.config.apiKey,
1333
+ model: this.config.model,
1334
+ dimensions: this.config.dimensions
1335
+ });
1336
+ case "nomic":
1337
+ throw new Error(`Provider ${this.config.provider} not yet implemented. Use 'voyage', 'openai', or 'mock'.`);
1338
+ default:
1339
+ throw new Error(`Unknown embedding provider: ${this.config.provider}. Use 'voyage', 'openai', or 'mock'.`);
1340
+ }
1341
+ }
1342
+ getProviderName() {
1343
+ return this.provider.name;
1344
+ }
1345
+ getDimensions() {
1346
+ return this.provider.dimensions;
1347
+ }
1348
+ async generateEmbedding(text) {
1349
+ if (!text || text.trim().length === 0) {
1350
+ throw new Error("Cannot generate embedding for empty text");
1351
+ }
1352
+ return this.provider.generateEmbedding(text);
1353
+ }
1354
+ async generateEmbeddings(texts) {
1355
+ const validTexts = texts.filter((t) => t && t.trim().length > 0);
1356
+ if (validTexts.length === 0) {
1357
+ return [];
1358
+ }
1359
+ return this.provider.generateEmbeddings(validTexts);
1360
+ }
1361
+ isRealProvider() {
1362
+ return this.provider.name !== "mock";
1363
+ }
1364
+ }
1365
+ EmbeddingService = __legacyDecorateClassTS([
1366
+ Injectable6(),
1367
+ __legacyMetadataTS("design:paramtypes", [
1368
+ typeof ConfigService2 === "undefined" ? Object : ConfigService2
1369
+ ])
1370
+ ], EmbeddingService);
1371
+
1372
+ // src/sync/sync.service.ts
1373
+ var ENTITY_TYPES = [
1374
+ "Topic",
1375
+ "Technology",
1376
+ "Concept",
1377
+ "Tool",
1378
+ "Process",
1379
+ "Person",
1380
+ "Organization"
1381
+ ];
1382
+ function validateDocuments(docs) {
1383
+ const errors = [];
1384
+ const entityIndex = new Map;
1385
+ for (const doc of docs) {
1386
+ for (const entity of doc.entities) {
1387
+ if (!entityIndex.has(entity.name)) {
1388
+ entityIndex.set(entity.name, new Set);
1389
+ }
1390
+ entityIndex.get(entity.name).add(doc.path);
1391
+ }
1392
+ }
1393
+ for (const doc of docs) {
1394
+ for (const rel of doc.relationships) {
1395
+ if (rel.source !== doc.path && !entityIndex.has(rel.source)) {
1396
+ errors.push({
1397
+ path: doc.path,
1398
+ error: `Relationship source "${rel.source}" not found in any document`
1399
+ });
1400
+ }
1401
+ const isDocPath = rel.target.endsWith(".md");
1402
+ const isKnownEntity = entityIndex.has(rel.target);
1403
+ const isSelfReference = rel.target === doc.path;
1404
+ if (!isDocPath && !isKnownEntity && !isSelfReference) {
1405
+ errors.push({
1406
+ path: doc.path,
1407
+ error: `Relationship target "${rel.target}" not found as entity`
1408
+ });
1409
+ }
1410
+ }
1411
+ }
1412
+ return errors;
1413
+ }
1414
+
1415
+ class SyncService {
1416
+ manifest;
1417
+ parser;
1418
+ graph;
1419
+ cascade;
1420
+ pathResolver;
1421
+ embeddingService;
1422
+ logger = new Logger6(SyncService.name);
1423
+ constructor(manifest, parser, graph, cascade, pathResolver, embeddingService) {
1424
+ this.manifest = manifest;
1425
+ this.parser = parser;
1426
+ this.graph = graph;
1427
+ this.cascade = cascade;
1428
+ this.pathResolver = pathResolver;
1429
+ this.embeddingService = embeddingService;
1430
+ }
1431
+ async sync(options = {}) {
1432
+ const startTime = Date.now();
1433
+ const result = {
1434
+ added: 0,
1435
+ updated: 0,
1436
+ deleted: 0,
1437
+ unchanged: 0,
1438
+ errors: [],
1439
+ duration: 0,
1440
+ changes: [],
1441
+ cascadeWarnings: [],
1442
+ embeddingsGenerated: 0,
1443
+ entityEmbeddingsGenerated: 0
1444
+ };
1445
+ try {
1446
+ await this.manifest.load();
1447
+ if (options.force) {
1448
+ if (options.paths && options.paths.length > 0) {
1449
+ if (options.verbose) {
1450
+ this.logger.log(`Force mode: marking ${options.paths.length} document(s) for re-sync`);
1451
+ }
1452
+ await this.clearManifestEntries(options.paths);
1453
+ } else {
1454
+ if (options.verbose) {
1455
+ this.logger.log("Force mode: clearing manifest to force full re-sync");
1456
+ }
1457
+ await this.clearManifest();
1458
+ }
1459
+ }
1460
+ const changes = await this.detectChanges(options.paths);
1461
+ result.changes = changes;
1462
+ const docsToSync = [];
1463
+ const docsByPath = new Map;
1464
+ for (const change of changes) {
1465
+ if (change.changeType === "new" || change.changeType === "updated") {
1466
+ try {
1467
+ const doc = await this.parser.parseDocument(change.path);
1468
+ docsToSync.push(doc);
1469
+ docsByPath.set(change.path, doc);
1470
+ } catch (error) {
1471
+ const errorMessage = error instanceof Error ? error.message : String(error);
1472
+ result.errors.push({ path: change.path, error: errorMessage });
1473
+ this.logger.warn(`Failed to parse ${change.path}: ${errorMessage}`);
1474
+ }
1475
+ }
1476
+ }
1477
+ const validationErrors = validateDocuments(docsToSync);
1478
+ if (validationErrors.length > 0) {
1479
+ for (const err of validationErrors) {
1480
+ result.errors.push(err);
1481
+ this.logger.error(`Validation error in ${err.path}: ${err.error}`);
1482
+ }
1483
+ this.logger.error(`Sync aborted: ${validationErrors.length} validation error(s) found. Fix the errors and try again.`);
1484
+ result.duration = Date.now() - startTime;
1485
+ return result;
1486
+ }
1487
+ const uniqueEntities = this.collectUniqueEntities(docsToSync);
1488
+ if (options.verbose) {
1489
+ this.logger.log(`Collected ${uniqueEntities.size} unique entities from ${docsToSync.length} documents`);
1490
+ }
1491
+ if (options.embeddings && !options.skipEmbeddings && this.embeddingService) {
1492
+ try {
1493
+ const dimensions = this.embeddingService.getDimensions();
1494
+ await this.graph.createVectorIndex("Document", "embedding", dimensions);
1495
+ } catch (error) {
1496
+ this.logger.debug(`Vector index setup for Document: ${error instanceof Error ? error.message : String(error)}`);
1497
+ }
1498
+ await this.createEntityVectorIndices();
1499
+ }
1500
+ if (!options.dryRun) {
1501
+ result.entityEmbeddingsGenerated = await this.syncEntities(uniqueEntities, options);
1502
+ if (options.verbose) {
1503
+ this.logger.log(`Synced ${uniqueEntities.size} entities, generated ${result.entityEmbeddingsGenerated} embeddings`);
1504
+ }
1505
+ }
1506
+ for (const change of changes) {
1507
+ try {
1508
+ const doc = docsByPath.get(change.path);
1509
+ const cascadeWarnings = await this.processChange(change, options, doc);
1510
+ result.cascadeWarnings.push(...cascadeWarnings);
1511
+ switch (change.changeType) {
1512
+ case "new":
1513
+ result.added++;
1514
+ break;
1515
+ case "updated":
1516
+ result.updated++;
1517
+ break;
1518
+ case "deleted":
1519
+ result.deleted++;
1520
+ break;
1521
+ case "unchanged":
1522
+ result.unchanged++;
1523
+ break;
1524
+ }
1525
+ if (change.embeddingGenerated) {
1526
+ result.embeddingsGenerated++;
1527
+ }
1528
+ } catch (error) {
1529
+ const errorMessage = error instanceof Error ? error.message : String(error);
1530
+ result.errors.push({ path: change.path, error: errorMessage });
1531
+ this.logger.warn(`Error processing ${change.path}: ${errorMessage}`);
1532
+ }
1533
+ }
1534
+ if (!options.dryRun) {
1535
+ await this.manifest.save();
1536
+ }
1537
+ } catch (error) {
1538
+ const errorMessage = error instanceof Error ? error.message : String(error);
1539
+ this.logger.error(`Sync failed: ${errorMessage}`);
1540
+ result.errors.push({ path: "sync", error: errorMessage });
1541
+ }
1542
+ result.duration = Date.now() - startTime;
1543
+ return result;
1544
+ }
1545
+ async detectChanges(paths) {
1546
+ const changes = [];
1547
+ let allDocPaths = await this.parser.discoverDocuments();
1548
+ if (paths && paths.length > 0) {
1549
+ const normalizedPaths = this.pathResolver.resolveDocPaths(paths, {
1550
+ requireExists: true,
1551
+ requireInDocs: true
1552
+ });
1553
+ const pathSet = new Set(normalizedPaths);
1554
+ allDocPaths = allDocPaths.filter((p) => pathSet.has(p));
1555
+ }
1556
+ const trackedPaths = new Set(this.manifest.getTrackedPaths());
1557
+ for (const docPath of allDocPaths) {
1558
+ try {
1559
+ const doc = await this.parser.parseDocument(docPath);
1560
+ const changeType = this.manifest.detectChange(docPath, doc.contentHash, doc.frontmatterHash);
1561
+ changes.push({
1562
+ path: docPath,
1563
+ changeType,
1564
+ reason: this.getChangeReason(changeType)
1565
+ });
1566
+ trackedPaths.delete(docPath);
1567
+ } catch (error) {
1568
+ const errorMessage = error instanceof Error ? error.message : String(error);
1569
+ this.logger.warn(`Failed to parse ${docPath}: ${errorMessage}`);
1570
+ changes.push({
1571
+ path: docPath,
1572
+ changeType: "new",
1573
+ reason: `Parse error: ${errorMessage}`
1574
+ });
1575
+ trackedPaths.delete(docPath);
1576
+ }
1577
+ }
1578
+ if (!paths || paths.length === 0) {
1579
+ for (const deletedPath of trackedPaths) {
1580
+ changes.push({
1581
+ path: deletedPath,
1582
+ changeType: "deleted",
1583
+ reason: "File no longer exists"
1584
+ });
1585
+ }
1586
+ }
1587
+ return changes;
1588
+ }
1589
+ async syncDocument(doc, options = {}, skipEntityCreation = false) {
1590
+ await this.graph.deleteDocumentRelationships(doc.path);
1591
+ const documentProps = {
1592
+ name: doc.path,
1593
+ title: doc.title,
1594
+ contentHash: doc.contentHash,
1595
+ tags: doc.tags
1596
+ };
1597
+ if (doc.summary)
1598
+ documentProps.summary = doc.summary;
1599
+ if (doc.created)
1600
+ documentProps.created = doc.created;
1601
+ if (doc.updated)
1602
+ documentProps.updated = doc.updated;
1603
+ if (doc.status)
1604
+ documentProps.status = doc.status;
1605
+ if (doc.graphMetadata) {
1606
+ if (doc.graphMetadata.importance) {
1607
+ documentProps.importance = doc.graphMetadata.importance;
1608
+ }
1609
+ if (doc.graphMetadata.domain) {
1610
+ documentProps.domain = doc.graphMetadata.domain;
1611
+ }
1612
+ }
1613
+ await this.graph.upsertNode("Document", documentProps);
1614
+ let embeddingGenerated = false;
1615
+ if (options.embeddings && !options.skipEmbeddings && this.embeddingService) {
1616
+ try {
1617
+ const textForEmbedding = this.composeEmbeddingText(doc);
1618
+ if (textForEmbedding.trim()) {
1619
+ const embedding = await this.embeddingService.generateEmbedding(textForEmbedding);
1620
+ await this.graph.updateNodeEmbedding("Document", doc.path, embedding);
1621
+ embeddingGenerated = true;
1622
+ this.logger.debug(`Generated embedding for ${doc.path}`);
1623
+ }
1624
+ } catch (error) {
1625
+ const errorMessage = error instanceof Error ? error.message : String(error);
1626
+ this.logger.warn(`Failed to generate embedding for ${doc.path}: ${errorMessage}`);
1627
+ }
1628
+ }
1629
+ const entityTypeMap = new Map;
1630
+ entityTypeMap.set(doc.path, "Document");
1631
+ this.logger.debug(`syncDocument: ${doc.path} has ${doc.entities.length} entities, skipEntityCreation=${skipEntityCreation}`);
1632
+ for (const entity of doc.entities) {
1633
+ entityTypeMap.set(entity.name, entity.type);
1634
+ if (!skipEntityCreation) {
1635
+ const entityProps = {
1636
+ name: entity.name
1637
+ };
1638
+ if (entity.description) {
1639
+ entityProps.description = entity.description;
1640
+ }
1641
+ await this.graph.upsertNode(entity.type, entityProps);
1642
+ }
1643
+ this.logger.debug(`Creating APPEARS_IN: ${entity.type}:${entity.name} -> Document:${doc.path}`);
1644
+ await this.graph.upsertRelationship(entity.type, entity.name, "APPEARS_IN", "Document", doc.path, { documentPath: doc.path });
1645
+ }
1646
+ for (const rel of doc.relationships) {
1647
+ if (rel.source === "this") {
1648
+ const targetType2 = entityTypeMap.get(rel.target);
1649
+ if (!targetType2) {
1650
+ this.logger.warn(`Unknown target entity "${rel.target}" in relationship, document: ${doc.path}`);
1651
+ continue;
1652
+ }
1653
+ await this.graph.upsertRelationship("Document", doc.path, rel.relation, targetType2, rel.target, { documentPath: doc.path });
1654
+ continue;
1655
+ }
1656
+ if (rel.target.endsWith(".md")) {
1657
+ const sourceType2 = entityTypeMap.get(rel.source);
1658
+ if (!sourceType2) {
1659
+ this.logger.warn(`Unknown source entity "${rel.source}" in relationship, document: ${doc.path}`);
1660
+ continue;
1661
+ }
1662
+ await this.graph.upsertRelationship(sourceType2, rel.source, rel.relation, "Document", rel.target, { documentPath: doc.path });
1663
+ continue;
1664
+ }
1665
+ const sourceType = entityTypeMap.get(rel.source);
1666
+ const targetType = entityTypeMap.get(rel.target);
1667
+ if (!sourceType) {
1668
+ this.logger.warn(`Unknown source entity "${rel.source}" in relationship, document: ${doc.path}`);
1669
+ continue;
1670
+ }
1671
+ if (!targetType) {
1672
+ this.logger.warn(`Unknown target entity "${rel.target}" in relationship, document: ${doc.path}`);
1673
+ continue;
1674
+ }
1675
+ await this.graph.upsertRelationship(sourceType, rel.source, rel.relation, targetType, rel.target, { documentPath: doc.path });
1676
+ }
1677
+ return embeddingGenerated;
1678
+ }
1679
+ async removeDocument(path) {
1680
+ await this.graph.deleteDocumentRelationships(path);
1681
+ await this.graph.deleteNode("Document", path);
1682
+ }
1683
+ async processChange(change, options, preloadedDoc) {
1684
+ const cascadeWarnings = [];
1685
+ if (options.verbose) {
1686
+ this.logger.log(`Processing ${change.changeType}: ${change.path}`);
1687
+ }
1688
+ if (options.dryRun) {
1689
+ this.logger.log(`[DRY-RUN] Would ${change.changeType}: ${change.path}`);
1690
+ return cascadeWarnings;
1691
+ }
1692
+ switch (change.changeType) {
1693
+ case "new":
1694
+ case "updated": {
1695
+ const doc = preloadedDoc || await this.parser.parseDocument(change.path);
1696
+ if (change.changeType === "updated" && !options.skipCascade) {
1697
+ try {
1698
+ const oldDoc = await this.getOldDocumentFromManifest(change.path);
1699
+ if (oldDoc) {
1700
+ const cascadeAnalyses = await this.cascade.analyzeDocumentChange(oldDoc, doc);
1701
+ if (cascadeAnalyses.length > 0) {
1702
+ cascadeWarnings.push(...cascadeAnalyses);
1703
+ cascadeAnalyses.forEach((cascade) => {
1704
+ this.logger.debug(`Cascade detected: ${cascade.trigger} in ${cascade.sourceDocument}`);
1705
+ });
1706
+ }
1707
+ }
1708
+ } catch (error) {
1709
+ const errorMessage = error instanceof Error ? error.message : String(error);
1710
+ this.logger.warn(`Failed to analyze cascade impacts for ${change.path}: ${errorMessage}`);
1711
+ }
1712
+ }
1713
+ const embeddingGenerated = await this.syncDocument(doc, options, preloadedDoc !== undefined);
1714
+ change.embeddingGenerated = embeddingGenerated;
1715
+ this.manifest.updateEntry(doc.path, doc.contentHash, doc.frontmatterHash, doc.entities.length, doc.relationships.length);
1716
+ break;
1717
+ }
1718
+ case "deleted": {
1719
+ await this.removeDocument(change.path);
1720
+ this.manifest.removeEntry(change.path);
1721
+ break;
1722
+ }
1723
+ case "unchanged":
1724
+ break;
1725
+ }
1726
+ return cascadeWarnings;
1727
+ }
1728
+ async clearGraph() {
1729
+ await this.graph.query("MATCH (n) DETACH DELETE n");
1730
+ this.logger.log("Graph cleared");
1731
+ }
1732
+ async clearManifest() {
1733
+ const manifest = await this.manifest.load();
1734
+ for (const path of Object.keys(manifest.documents)) {
1735
+ this.manifest.removeEntry(path);
1736
+ }
1737
+ this.logger.log("Manifest cleared");
1738
+ }
1739
+ async clearManifestEntries(paths) {
1740
+ const normalizedPaths = this.pathResolver.resolveDocPaths(paths, {
1741
+ requireExists: true,
1742
+ requireInDocs: true
1743
+ });
1744
+ for (const docPath of normalizedPaths) {
1745
+ this.manifest.removeEntry(docPath);
1746
+ this.logger.debug(`Cleared manifest entry: ${docPath}`);
1747
+ }
1748
+ this.logger.log(`Marked ${normalizedPaths.length} document(s) for re-sync`);
1749
+ }
1750
+ composeEmbeddingText(doc) {
1751
+ const parts = [];
1752
+ if (doc.title) {
1753
+ parts.push(`Title: ${doc.title}`);
1754
+ }
1755
+ if (doc.topic) {
1756
+ parts.push(`Topic: ${doc.topic}`);
1757
+ }
1758
+ if (doc.tags && doc.tags.length > 0) {
1759
+ parts.push(`Tags: ${doc.tags.join(", ")}`);
1760
+ }
1761
+ if (doc.entities && doc.entities.length > 0) {
1762
+ const entityNames = doc.entities.map((e) => e.name).join(", ");
1763
+ parts.push(`Entities: ${entityNames}`);
1764
+ }
1765
+ if (doc.summary) {
1766
+ parts.push(doc.summary);
1767
+ } else {
1768
+ parts.push(doc.content.slice(0, 500));
1769
+ }
1770
+ return parts.join(" | ");
1771
+ }
1772
+ getChangeReason(changeType) {
1773
+ switch (changeType) {
1774
+ case "new":
1775
+ return "New document";
1776
+ case "updated":
1777
+ return "Content or frontmatter changed";
1778
+ case "deleted":
1779
+ return "File no longer exists";
1780
+ case "unchanged":
1781
+ return "No changes detected";
1782
+ }
1783
+ }
1784
+ async getOldDocumentFromManifest(path) {
1785
+ try {
1786
+ const manifest = await this.manifest.load();
1787
+ const entry = manifest.documents[path];
1788
+ if (!entry) {
1789
+ return null;
1790
+ }
1791
+ try {
1792
+ return await this.parser.parseDocument(path);
1793
+ } catch {
1794
+ return null;
1795
+ }
1796
+ } catch (error) {
1797
+ this.logger.warn(`Failed to retrieve old document for ${path}: ${error instanceof Error ? error.message : String(error)}`);
1798
+ return null;
1799
+ }
1800
+ }
1801
+ collectUniqueEntities(docs) {
1802
+ const entities = new Map;
1803
+ for (const doc of docs) {
1804
+ for (const entity of doc.entities) {
1805
+ const key = `${entity.type}:${entity.name}`;
1806
+ if (!entities.has(key)) {
1807
+ entities.set(key, {
1808
+ type: entity.type,
1809
+ name: entity.name,
1810
+ description: entity.description,
1811
+ documentPaths: [doc.path]
1812
+ });
1813
+ } else {
1814
+ const existing = entities.get(key);
1815
+ existing.documentPaths.push(doc.path);
1816
+ if (entity.description && (!existing.description || entity.description.length > existing.description.length)) {
1817
+ existing.description = entity.description;
1818
+ }
1819
+ }
1820
+ }
1821
+ }
1822
+ return entities;
1823
+ }
1824
+ async syncEntities(entities, options) {
1825
+ let embeddingsGenerated = 0;
1826
+ for (const [_key, entity] of entities) {
1827
+ const entityProps = {
1828
+ name: entity.name
1829
+ };
1830
+ if (entity.description) {
1831
+ entityProps.description = entity.description;
1832
+ }
1833
+ await this.graph.upsertNode(entity.type, entityProps);
1834
+ if (options.embeddings && !options.skipEmbeddings && this.embeddingService) {
1835
+ try {
1836
+ const text = this.composeEntityEmbeddingText(entity);
1837
+ const embedding = await this.embeddingService.generateEmbedding(text);
1838
+ await this.graph.updateNodeEmbedding(entity.type, entity.name, embedding);
1839
+ embeddingsGenerated++;
1840
+ this.logger.debug(`Generated embedding for ${entity.type}:${entity.name}`);
1841
+ } catch (error) {
1842
+ const errorMessage = error instanceof Error ? error.message : String(error);
1843
+ this.logger.warn(`Failed to generate embedding for ${entity.type}:${entity.name}: ${errorMessage}`);
1844
+ }
1845
+ }
1846
+ }
1847
+ return embeddingsGenerated;
1848
+ }
1849
+ composeEntityEmbeddingText(entity) {
1850
+ const parts = [`${entity.type}: ${entity.name}`];
1851
+ if (entity.description) {
1852
+ parts.push(entity.description);
1853
+ }
1854
+ return parts.join(". ");
1855
+ }
1856
+ async createEntityVectorIndices() {
1857
+ if (!this.embeddingService)
1858
+ return;
1859
+ const dimensions = this.embeddingService.getDimensions();
1860
+ for (const entityType of ENTITY_TYPES) {
1861
+ try {
1862
+ await this.graph.createVectorIndex(entityType, "embedding", dimensions);
1863
+ } catch (error) {
1864
+ this.logger.debug(`Vector index setup for ${entityType}: ${error instanceof Error ? error.message : String(error)}`);
1865
+ }
1866
+ }
1867
+ }
1868
+ }
1869
+ SyncService = __legacyDecorateClassTS([
1870
+ Injectable7(),
1871
+ __legacyMetadataTS("design:paramtypes", [
1872
+ typeof ManifestService === "undefined" ? Object : ManifestService,
1873
+ typeof DocumentParserService === "undefined" ? Object : DocumentParserService,
1874
+ typeof GraphService === "undefined" ? Object : GraphService,
1875
+ typeof CascadeService === "undefined" ? Object : CascadeService,
1876
+ typeof PathResolverService === "undefined" ? Object : PathResolverService,
1877
+ typeof EmbeddingService === "undefined" ? Object : EmbeddingService
1878
+ ])
1879
+ ], SyncService);
1880
+
1881
+ // src/sync/ontology.service.ts
1882
+ import { Injectable as Injectable8 } from "@nestjs/common";
1883
+ class OntologyService {
1884
+ parser;
1885
+ constructor(parser) {
1886
+ this.parser = parser;
1887
+ }
1888
+ async deriveOntology() {
1889
+ const docs = await this.parser.parseAllDocuments();
1890
+ return this.deriveFromDocuments(docs);
1891
+ }
1892
+ deriveFromDocuments(docs) {
1893
+ const entityTypeSet = new Set;
1894
+ const relationshipTypeSet = new Set;
1895
+ const entityCounts = {};
1896
+ const relationshipCounts = {};
1897
+ const entityExamples = {};
1898
+ let documentsWithEntities = 0;
1899
+ let documentsWithoutEntities = 0;
1900
+ let totalRelationships = 0;
1901
+ for (const doc of docs) {
1902
+ if (doc.entities.length > 0) {
1903
+ documentsWithEntities++;
1904
+ } else {
1905
+ documentsWithoutEntities++;
1906
+ }
1907
+ for (const entity of doc.entities) {
1908
+ entityTypeSet.add(entity.type);
1909
+ entityCounts[entity.type] = (entityCounts[entity.type] || 0) + 1;
1910
+ if (!entityExamples[entity.name]) {
1911
+ entityExamples[entity.name] = { type: entity.type, documents: [] };
1912
+ }
1913
+ if (!entityExamples[entity.name].documents.includes(doc.path)) {
1914
+ entityExamples[entity.name].documents.push(doc.path);
1915
+ }
1916
+ }
1917
+ for (const rel of doc.relationships) {
1918
+ relationshipTypeSet.add(rel.relation);
1919
+ relationshipCounts[rel.relation] = (relationshipCounts[rel.relation] || 0) + 1;
1920
+ totalRelationships++;
1921
+ }
1922
+ }
1923
+ return {
1924
+ entityTypes: Array.from(entityTypeSet).sort(),
1925
+ relationshipTypes: Array.from(relationshipTypeSet).sort(),
1926
+ entityCounts,
1927
+ relationshipCounts,
1928
+ totalEntities: Object.keys(entityExamples).length,
1929
+ totalRelationships,
1930
+ documentsWithEntities,
1931
+ documentsWithoutEntities,
1932
+ entityExamples
1933
+ };
1934
+ }
1935
+ printSummary(ontology) {
1936
+ console.log(`
1937
+ Derived Ontology Summary
1938
+ `);
1939
+ console.log(`Documents: ${ontology.documentsWithEntities} with entities, ${ontology.documentsWithoutEntities} without`);
1940
+ console.log(`Unique Entities: ${ontology.totalEntities}`);
1941
+ console.log(`Total Relationships: ${ontology.totalRelationships}`);
1942
+ console.log(`
1943
+ Entity Types:`);
1944
+ for (const type of ontology.entityTypes) {
1945
+ console.log(` ${type}: ${ontology.entityCounts[type]} instances`);
1946
+ }
1947
+ console.log(`
1948
+ Relationship Types:`);
1949
+ for (const type of ontology.relationshipTypes) {
1950
+ console.log(` ${type}: ${ontology.relationshipCounts[type]} instances`);
1951
+ }
1952
+ console.log(`
1953
+ Top Entities (by document count):`);
1954
+ const sorted = Object.entries(ontology.entityExamples).sort((a, b) => b[1].documents.length - a[1].documents.length).slice(0, 10);
1955
+ for (const [name, info] of sorted) {
1956
+ console.log(` ${name} (${info.type}): ${info.documents.length} docs`);
1957
+ }
1958
+ }
1959
+ }
1960
+ OntologyService = __legacyDecorateClassTS([
1961
+ Injectable8(),
1962
+ __legacyMetadataTS("design:paramtypes", [
1963
+ typeof DocumentParserService === "undefined" ? Object : DocumentParserService
1964
+ ])
1965
+ ], OntologyService);
1966
+
1967
+ // src/embedding/embedding.module.ts
1968
+ import { Module as Module2 } from "@nestjs/common";
1969
+ import { ConfigModule } from "@nestjs/config";
1970
+ class EmbeddingModule {
1971
+ }
1972
+ EmbeddingModule = __legacyDecorateClassTS([
1973
+ Module2({
1974
+ imports: [ConfigModule],
1975
+ providers: [EmbeddingService],
1976
+ exports: [EmbeddingService]
1977
+ })
1978
+ ], EmbeddingModule);
1979
+
1980
+ // src/sync/sync.module.ts
1981
+ class SyncModule {
1982
+ }
1983
+ SyncModule = __legacyDecorateClassTS([
1984
+ Module3({
1985
+ imports: [GraphModule, EmbeddingModule],
1986
+ providers: [
1987
+ SyncService,
1988
+ ManifestService,
1989
+ DocumentParserService,
1990
+ OntologyService,
1991
+ CascadeService,
1992
+ PathResolverService
1993
+ ],
1994
+ exports: [
1995
+ SyncService,
1996
+ ManifestService,
1997
+ DocumentParserService,
1998
+ OntologyService,
1999
+ CascadeService,
2000
+ PathResolverService
2001
+ ]
2002
+ })
2003
+ ], SyncModule);
2004
+
2005
+ // src/query/query.module.ts
2006
+ import { Module as Module4 } from "@nestjs/common";
2007
+
2008
+ // src/query/query.service.ts
2009
+ import { Injectable as Injectable9, Logger as Logger7 } from "@nestjs/common";
2010
+ class QueryService {
2011
+ graphService;
2012
+ logger = new Logger7(QueryService.name);
2013
+ constructor(graphService) {
2014
+ this.graphService = graphService;
2015
+ }
2016
+ async query(cypher) {
2017
+ this.logger.debug(`Executing query: ${cypher}`);
2018
+ return await this.graphService.query(cypher);
2019
+ }
2020
+ }
2021
+ QueryService = __legacyDecorateClassTS([
2022
+ Injectable9(),
2023
+ __legacyMetadataTS("design:paramtypes", [
2024
+ typeof GraphService === "undefined" ? Object : GraphService
2025
+ ])
2026
+ ], QueryService);
2027
+
2028
+ // src/query/query.module.ts
2029
+ class QueryModule {
2030
+ }
2031
+ QueryModule = __legacyDecorateClassTS([
2032
+ Module4({
2033
+ imports: [GraphModule, EmbeddingModule],
2034
+ providers: [QueryService, GraphService],
2035
+ exports: [QueryService, GraphService]
2036
+ })
2037
+ ], QueryModule);
2038
+
2039
+ // src/app.module.ts
2040
+ class AppModule {
2041
+ }
2042
+ AppModule = __legacyDecorateClassTS([
2043
+ Module5({
2044
+ imports: [
2045
+ ConfigModule2.forRoot({
2046
+ isGlobal: true
2047
+ }),
2048
+ GraphModule,
2049
+ SyncModule,
2050
+ EmbeddingModule,
2051
+ QueryModule
2052
+ ]
2053
+ })
2054
+ ], AppModule);
2055
+
2056
+ // src/commands/sync.command.ts
2057
+ function registerSyncCommand(program) {
2058
+ program.command("sync [paths...]").description("Synchronize documents to the knowledge graph").option("-f, --force", "Force re-sync: with paths, clears only those docs; without paths, rebuilds entire graph").option("-d, --dry-run", "Show what would change without applying").option("-v, --verbose", "Show detailed output").option("-w, --watch", "Watch for file changes and sync automatically").option("--diff", "Show only changed documents (alias for --dry-run)").option("--skip-cascade", "Skip cascade analysis (faster for large repos)").option("--no-embeddings", "Disable embedding generation during sync").action(async (paths, options) => {
2059
+ let app;
2060
+ let watcher = null;
2061
+ let isShuttingDown = false;
2062
+ const printSyncResults = (result, isWatchMode = false) => {
2063
+ console.log(`
2064
+ \uD83D\uDCCA Sync Results:
2065
+ `);
2066
+ console.log(` \u2705 Added: ${result.added}`);
2067
+ console.log(` \uD83D\uDD04 Updated: ${result.updated}`);
2068
+ console.log(` \uD83D\uDDD1\uFE0F Deleted: ${result.deleted}`);
2069
+ console.log(` \u23ED\uFE0F Unchanged: ${result.unchanged}`);
2070
+ if (result.embeddingsGenerated > 0) {
2071
+ console.log(` \uD83E\uDDE0 Embeddings: ${result.embeddingsGenerated}`);
2072
+ }
2073
+ console.log(` \u23F1\uFE0F Duration: ${result.duration}ms`);
2074
+ if (result.errors.length > 0) {
2075
+ console.log(`
2076
+ \u274C Errors (${result.errors.length}):
2077
+ `);
2078
+ result.errors.forEach((e) => {
2079
+ console.log(` ${e.path}: ${e.error}`);
2080
+ });
2081
+ }
2082
+ if (options.verbose && result.changes.length > 0) {
2083
+ console.log(`
2084
+ \uD83D\uDCDD Changes:
2085
+ `);
2086
+ result.changes.forEach((c) => {
2087
+ const icon = {
2088
+ new: "\u2795",
2089
+ updated: "\uD83D\uDD04",
2090
+ deleted: "\uD83D\uDDD1\uFE0F",
2091
+ unchanged: "\u23ED\uFE0F"
2092
+ }[c.changeType];
2093
+ console.log(` ${icon} ${c.changeType}: ${c.path}`);
2094
+ if (c.reason) {
2095
+ console.log(` ${c.reason}`);
2096
+ }
2097
+ });
2098
+ }
2099
+ if (result.cascadeWarnings && result.cascadeWarnings.length > 0) {
2100
+ console.log(`
2101
+ \u26A0\uFE0F Cascade Impacts Detected:
2102
+ `);
2103
+ const warningsByTrigger = new Map;
2104
+ for (const warning of result.cascadeWarnings) {
2105
+ const existing = warningsByTrigger.get(warning.trigger) || [];
2106
+ existing.push(warning);
2107
+ warningsByTrigger.set(warning.trigger, existing);
2108
+ }
2109
+ for (const [trigger, warnings] of warningsByTrigger) {
2110
+ const triggerLabel = trigger.split("_").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
2111
+ console.log(` \uD83D\uDCCC ${triggerLabel}
2112
+ `);
2113
+ for (const analysis of warnings) {
2114
+ console.log(` ${analysis.summary}`);
2115
+ console.log(` Source: ${analysis.sourceDocument}
2116
+ `);
2117
+ if (analysis.affectedDocuments.length > 0) {
2118
+ for (const affected of analysis.affectedDocuments) {
2119
+ const icon = affected.confidence === "high" ? "\uD83D\uDD34" : affected.confidence === "medium" ? "\uD83D\uDFE1" : "\uD83D\uDFE2";
2120
+ console.log(` ${icon} [${affected.confidence.toUpperCase()}] ${affected.path}`);
2121
+ console.log(` ${affected.reason}`);
2122
+ const suggestedAction = affected.suggestedAction.split("_").join(" ").replace(/\b\w/g, (char) => char.toUpperCase());
2123
+ console.log(` \u2192 ${suggestedAction}`);
2124
+ }
2125
+ } else {
2126
+ console.log(` \u2139\uFE0F No directly affected documents detected`);
2127
+ }
2128
+ console.log();
2129
+ }
2130
+ }
2131
+ console.log(` \uD83D\uDCA1 Run /update-related to apply suggested changes
2132
+ `);
2133
+ }
2134
+ if (isWatchMode) {
2135
+ console.log(`
2136
+ \u23F3 Watching for changes... (Ctrl+C to stop)
2137
+ `);
2138
+ }
2139
+ };
2140
+ const shutdown = async () => {
2141
+ if (isShuttingDown)
2142
+ return;
2143
+ isShuttingDown = true;
2144
+ console.log(`
2145
+
2146
+ \uD83D\uDC4B Stopping watch mode...`);
2147
+ if (watcher) {
2148
+ watcher.close();
2149
+ }
2150
+ if (app) {
2151
+ await app.close();
2152
+ }
2153
+ process.exit(0);
2154
+ };
2155
+ try {
2156
+ app = await NestFactory.createApplicationContext(AppModule, {
2157
+ logger: options.verbose ? ["log", "error", "warn"] : false
2158
+ });
2159
+ const sync = app.get(SyncService);
2160
+ if (options.watch && options.dryRun) {
2161
+ console.log(`
2162
+ \u26A0\uFE0F Watch mode is not compatible with --dry-run mode
2163
+ `);
2164
+ await app.close();
2165
+ process.exit(1);
2166
+ }
2167
+ if (options.watch && options.force) {
2168
+ console.log(`
2169
+ \u26A0\uFE0F Watch mode is not compatible with --force mode (for safety)
2170
+ `);
2171
+ await app.close();
2172
+ process.exit(1);
2173
+ }
2174
+ const syncOptions = {
2175
+ force: options.force,
2176
+ dryRun: options.dryRun || options.diff,
2177
+ verbose: options.verbose,
2178
+ paths: paths.length > 0 ? paths : undefined,
2179
+ skipCascade: options.skipCascade,
2180
+ embeddings: options.embeddings !== false
2181
+ };
2182
+ console.log(`
2183
+ \uD83D\uDD04 Graph Sync
2184
+ `);
2185
+ if (syncOptions.force) {
2186
+ if (syncOptions.paths && syncOptions.paths.length > 0) {
2187
+ console.log(`\u26A0\uFE0F Force mode: ${syncOptions.paths.length} document(s) will be cleared and re-synced
2188
+ `);
2189
+ } else {
2190
+ console.log(`\u26A0\uFE0F Force mode: Entire graph will be cleared and rebuilt
2191
+ `);
2192
+ }
2193
+ }
2194
+ if (syncOptions.dryRun) {
2195
+ console.log(`\uD83D\uDCCB Dry run mode: No changes will be applied
2196
+ `);
2197
+ }
2198
+ if (syncOptions.skipCascade) {
2199
+ console.log(`\u26A1 Cascade analysis skipped
2200
+ `);
2201
+ }
2202
+ if (!syncOptions.embeddings) {
2203
+ console.log(`\uD83D\uDEAB Embedding generation disabled
2204
+ `);
2205
+ }
2206
+ if (syncOptions.paths) {
2207
+ console.log(`\uD83D\uDCC1 Syncing specific paths: ${syncOptions.paths.join(", ")}
2208
+ `);
2209
+ }
2210
+ const initialResult = await sync.sync(syncOptions);
2211
+ printSyncResults(initialResult, options.watch);
2212
+ if (options.dryRun) {
2213
+ console.log("\uD83D\uDCA1 Run without --dry-run to apply changes");
2214
+ }
2215
+ if (options.watch) {
2216
+ const docsPath = process.env.DOCS_PATH || "docs";
2217
+ let debounceTimeout = null;
2218
+ const trackedFiles = new Set;
2219
+ const debouncedSync = () => {
2220
+ if (debounceTimeout) {
2221
+ clearTimeout(debounceTimeout);
2222
+ }
2223
+ debounceTimeout = setTimeout(async () => {
2224
+ if (isShuttingDown)
2225
+ return;
2226
+ if (trackedFiles.size === 0)
2227
+ return;
2228
+ try {
2229
+ const changedPaths = Array.from(trackedFiles);
2230
+ trackedFiles.clear();
2231
+ console.log(`
2232
+ \uD83D\uDCDD Changes detected (${changedPaths.length} file${changedPaths.length !== 1 ? "s" : ""})`);
2233
+ const watchResult = await sync.sync({
2234
+ ...syncOptions,
2235
+ paths: changedPaths,
2236
+ dryRun: false
2237
+ });
2238
+ const hasChanges = watchResult.added > 0 || watchResult.updated > 0 || watchResult.deleted > 0;
2239
+ if (hasChanges) {
2240
+ console.log(` \u2705 Synced: +${watchResult.added} ~${watchResult.updated} -${watchResult.deleted}`);
2241
+ if (watchResult.errors.length > 0) {
2242
+ console.log(` \u274C Errors: ${watchResult.errors.map((e) => e.path).join(", ")}`);
2243
+ }
2244
+ if (watchResult.cascadeWarnings && watchResult.cascadeWarnings.length > 0) {
2245
+ console.log(` \u26A0\uFE0F Cascade impacts detected: ${watchResult.cascadeWarnings.length} warning(s)`);
2246
+ }
2247
+ } else {
2248
+ console.log(" \u23ED\uFE0F No changes detected");
2249
+ }
2250
+ console.log(`\u23F3 Watching for changes...
2251
+ `);
2252
+ } catch (error) {
2253
+ console.error(` \u274C Sync failed: ${error instanceof Error ? error.message : String(error)}`);
2254
+ console.log(`\u23F3 Watching for changes...
2255
+ `);
2256
+ }
2257
+ }, 500);
2258
+ };
2259
+ console.log(`
2260
+ \uD83D\uDC41\uFE0F Watch mode enabled
2261
+ `);
2262
+ watcher = watch(docsPath, { recursive: true }, (event, filename) => {
2263
+ if (filename && filename.endsWith(".md")) {
2264
+ const fullPath = join(docsPath, filename);
2265
+ trackedFiles.add(fullPath);
2266
+ debouncedSync();
2267
+ }
2268
+ });
2269
+ process.on("SIGINT", shutdown);
2270
+ await new Promise(() => {});
2271
+ } else {
2272
+ await app.close();
2273
+ process.exit(initialResult.errors.length > 0 ? 1 : 0);
2274
+ }
2275
+ } catch (error) {
2276
+ console.error(`
2277
+ \u274C Sync failed:`, error instanceof Error ? error.message : String(error));
2278
+ if (app)
2279
+ await app.close();
2280
+ process.exit(1);
2281
+ }
2282
+ });
2283
+ }
2284
+ // src/commands/status.command.ts
2285
+ import { NestFactory as NestFactory2 } from "@nestjs/core";
2286
+ function registerStatusCommand(program) {
2287
+ program.command("status").description("Show documents that need syncing (new or updated)").option("-v, --verbose", "Show all documents including unchanged").action(async (options) => {
2288
+ let app;
2289
+ try {
2290
+ app = await NestFactory2.createApplicationContext(AppModule, {
2291
+ logger: false
2292
+ });
2293
+ const sync = app.get(SyncService);
2294
+ const manifest = app.get(ManifestService);
2295
+ await manifest.load();
2296
+ const changes = await sync.detectChanges();
2297
+ const newDocs = changes.filter((c) => c.changeType === "new");
2298
+ const updatedDocs = changes.filter((c) => c.changeType === "updated");
2299
+ const deletedDocs = changes.filter((c) => c.changeType === "deleted");
2300
+ const unchangedDocs = changes.filter((c) => c.changeType === "unchanged");
2301
+ const pendingCount = newDocs.length + updatedDocs.length + deletedDocs.length;
2302
+ console.log(`
2303
+ \uD83D\uDCCA Graph Status
2304
+ `);
2305
+ if (newDocs.length > 0) {
2306
+ console.log(`New (${newDocs.length}):`);
2307
+ newDocs.forEach((doc) => {
2308
+ console.log(` + ${doc.path}`);
2309
+ });
2310
+ console.log();
2311
+ }
2312
+ if (updatedDocs.length > 0) {
2313
+ console.log(`Updated (${updatedDocs.length}):`);
2314
+ updatedDocs.forEach((doc) => {
2315
+ console.log(` ~ ${doc.path}`);
2316
+ });
2317
+ console.log();
2318
+ }
2319
+ if (deletedDocs.length > 0) {
2320
+ console.log(`Deleted (${deletedDocs.length}):`);
2321
+ deletedDocs.forEach((doc) => {
2322
+ console.log(` - ${doc.path}`);
2323
+ });
2324
+ console.log();
2325
+ }
2326
+ if (options.verbose && unchangedDocs.length > 0) {
2327
+ console.log(`Unchanged (${unchangedDocs.length}):`);
2328
+ unchangedDocs.forEach((doc) => {
2329
+ console.log(` \xB7 ${doc.path}`);
2330
+ });
2331
+ console.log();
2332
+ }
2333
+ if (pendingCount === 0) {
2334
+ console.log(`\u2705 All documents are in sync
2335
+ `);
2336
+ } else {
2337
+ console.log(`Total: ${pendingCount} document(s) need syncing`);
2338
+ console.log("\uD83D\uDCA1 Run `bun graph sync` to apply changes\n");
2339
+ }
2340
+ await app.close();
2341
+ process.exit(0);
2342
+ } catch (error) {
2343
+ console.error("Error:", error instanceof Error ? error.message : String(error));
2344
+ if (app)
2345
+ await app.close();
2346
+ process.exit(1);
2347
+ }
2348
+ });
2349
+ }
2350
+ // src/commands/query.command.ts
2351
+ import { NestFactory as NestFactory3 } from "@nestjs/core";
2352
+ function registerQueryCommands(program) {
2353
+ program.command("stats").description("Show graph statistics").action(async () => {
2354
+ let app;
2355
+ try {
2356
+ app = await NestFactory3.createApplicationContext(AppModule, {
2357
+ logger: false
2358
+ });
2359
+ const graph = app.get(GraphService);
2360
+ const stats = await graph.getStats();
2361
+ console.log(`
2362
+ === Graph Statistics ===
2363
+ `);
2364
+ console.log(`Total Nodes: ${stats.nodeCount}`);
2365
+ console.log(`Total Relationships: ${stats.edgeCount}
2366
+ `);
2367
+ console.log(`Node Labels (${stats.labels.length}):`);
2368
+ stats.labels.forEach((label) => {
2369
+ const count = stats.entityCounts[label] || 0;
2370
+ console.log(` - ${label}: ${count}`);
2371
+ });
2372
+ console.log(`
2373
+ Relationship Types (${stats.relationshipTypes.length}):`);
2374
+ stats.relationshipTypes.forEach((relType) => {
2375
+ const count = stats.relationshipCounts[relType] || 0;
2376
+ console.log(` - ${relType}: ${count}`);
2377
+ });
2378
+ console.log();
2379
+ await app.close();
2380
+ process.exit(0);
2381
+ } catch (error) {
2382
+ console.error("Error:", error instanceof Error ? error.message : String(error));
2383
+ if (app)
2384
+ await app.close();
2385
+ process.exit(1);
2386
+ }
2387
+ });
2388
+ program.command("search").description("Search for nodes in the graph or perform semantic search").option("-l, --label <label>", "Filter by node label").option("-n, --name <name>", "Filter by name (substring match)").option("-s, --semantic <query>", "Perform semantic/vector search on documents").option("--limit <n>", "Limit results", "20").action(async (options) => {
2389
+ let app;
2390
+ try {
2391
+ app = await NestFactory3.createApplicationContext(AppModule, {
2392
+ logger: false
2393
+ });
2394
+ const graph = app.get(GraphService);
2395
+ if (options.semantic) {
2396
+ const embedding = app.get(EmbeddingService);
2397
+ const limit2 = Math.min(parseInt(options.limit, 10), 100);
2398
+ try {
2399
+ const queryEmbedding = await embedding.generateEmbedding(options.semantic);
2400
+ const results2 = await graph.vectorSearchAll(queryEmbedding, limit2);
2401
+ console.log(`
2402
+ === Semantic Search Results for "${options.semantic}" ===
2403
+ `);
2404
+ if (results2.length === 0) {
2405
+ console.log(`No results found with semantic search.
2406
+ `);
2407
+ await app.close();
2408
+ process.exit(0);
2409
+ }
2410
+ results2.forEach((result2, idx) => {
2411
+ console.log(`${idx + 1}. [${result2.label}] ${result2.name}`);
2412
+ if (result2.title) {
2413
+ console.log(` Title: ${result2.title}`);
2414
+ }
2415
+ if (result2.description && result2.label !== "Document") {
2416
+ const desc = result2.description.length > 80 ? result2.description.slice(0, 80) + "..." : result2.description;
2417
+ console.log(` ${desc}`);
2418
+ }
2419
+ console.log(` Similarity: ${(result2.score * 100).toFixed(2)}%`);
2420
+ });
2421
+ console.log();
2422
+ await app.close();
2423
+ process.exit(0);
2424
+ } catch (semanticError) {
2425
+ const errorMsg = semanticError instanceof Error ? semanticError.message : String(semanticError);
2426
+ console.error("Semantic search error:", errorMsg);
2427
+ if (errorMsg.includes("no embeddings") || errorMsg.includes("vector")) {
2428
+ console.log(`
2429
+ Note: Semantic search requires embeddings to be generated first.`);
2430
+ console.log(`Run 'bun graph sync' to generate embeddings for documents.
2431
+ `);
2432
+ }
2433
+ await app.close();
2434
+ process.exit(1);
2435
+ }
2436
+ }
2437
+ let cypher;
2438
+ const limit = Math.min(parseInt(options.limit, 10), 100);
2439
+ if (options.label && options.name) {
2440
+ const escapedLabel = options.label.replace(/`/g, "\\`");
2441
+ const escapedName = options.name.replace(/'/g, "\\'");
2442
+ cypher = `MATCH (n:\`${escapedLabel}\`) WHERE n.name CONTAINS '${escapedName}' RETURN n LIMIT ${limit}`;
2443
+ } else if (options.label) {
2444
+ const escapedLabel = options.label.replace(/`/g, "\\`");
2445
+ cypher = `MATCH (n:\`${escapedLabel}\`) RETURN n LIMIT ${limit}`;
2446
+ } else if (options.name) {
2447
+ const escapedName = options.name.replace(/'/g, "\\'");
2448
+ cypher = `MATCH (n) WHERE n.name CONTAINS '${escapedName}' RETURN n LIMIT ${limit}`;
2449
+ } else {
2450
+ cypher = `MATCH (n) RETURN n LIMIT ${limit}`;
2451
+ }
2452
+ const result = await graph.query(cypher);
2453
+ const results = result.resultSet || [];
2454
+ console.log(`
2455
+ === Search Results (${results.length} nodes) ===
2456
+ `);
2457
+ if (results.length === 0) {
2458
+ console.log(`No nodes found matching criteria.
2459
+ `);
2460
+ await app.close();
2461
+ process.exit(0);
2462
+ }
2463
+ results.forEach((row) => {
2464
+ const node = row[0];
2465
+ const labels = (node.labels || []).join(", ");
2466
+ const name = node.properties?.name || "unnamed";
2467
+ console.log(`[${labels}] ${name}`);
2468
+ if (node.properties?.description) {
2469
+ console.log(` Description: ${node.properties.description}`);
2470
+ }
2471
+ if (node.properties?.importance) {
2472
+ console.log(` Importance: ${node.properties.importance}`);
2473
+ }
2474
+ });
2475
+ console.log();
2476
+ await app.close();
2477
+ process.exit(0);
2478
+ } catch (error) {
2479
+ console.error("Error:", error instanceof Error ? error.message : String(error));
2480
+ if (app)
2481
+ await app.close();
2482
+ process.exit(1);
2483
+ }
2484
+ });
2485
+ program.command("rels <name>").description("Show relationships for a node").action(async (name) => {
2486
+ let app;
2487
+ try {
2488
+ app = await NestFactory3.createApplicationContext(AppModule, {
2489
+ logger: false
2490
+ });
2491
+ const graph = app.get(GraphService);
2492
+ const escapedName = name.replace(/'/g, "\\'");
2493
+ const cypher = `MATCH (a { name: '${escapedName}' })-[r]-(b) RETURN a, r, b`;
2494
+ const result = await graph.query(cypher);
2495
+ const results = result.resultSet || [];
2496
+ console.log(`
2497
+ === Relationships for "${name}" ===
2498
+ `);
2499
+ if (results.length === 0) {
2500
+ console.log(`No relationships found.
2501
+ `);
2502
+ await app.close();
2503
+ process.exit(0);
2504
+ }
2505
+ const incoming = [];
2506
+ const outgoing = [];
2507
+ results.forEach((row) => {
2508
+ const [source, rel, target] = row;
2509
+ const sourceName = source.properties?.name || "unknown";
2510
+ const targetName = target.properties?.name || "unknown";
2511
+ const relType = rel.type || "UNKNOWN";
2512
+ if (sourceName === name) {
2513
+ outgoing.push(` -[${relType}]-> ${targetName}`);
2514
+ } else {
2515
+ incoming.push(` <-[${relType}]- ${sourceName}`);
2516
+ }
2517
+ });
2518
+ if (outgoing.length > 0) {
2519
+ console.log("Outgoing:");
2520
+ outgoing.forEach((r) => console.log(r));
2521
+ }
2522
+ if (incoming.length > 0) {
2523
+ if (outgoing.length > 0)
2524
+ console.log();
2525
+ console.log("Incoming:");
2526
+ incoming.forEach((r) => console.log(r));
2527
+ }
2528
+ console.log();
2529
+ await app.close();
2530
+ process.exit(0);
2531
+ } catch (error) {
2532
+ console.error("Error:", error instanceof Error ? error.message : String(error));
2533
+ if (app)
2534
+ await app.close();
2535
+ process.exit(1);
2536
+ }
2537
+ });
2538
+ program.command("cypher <query>").description("Execute raw Cypher query").action(async (query) => {
2539
+ let app;
2540
+ try {
2541
+ app = await NestFactory3.createApplicationContext(AppModule, {
2542
+ logger: false
2543
+ });
2544
+ const graph = app.get(GraphService);
2545
+ const result = await graph.query(query);
2546
+ console.log(`
2547
+ === Cypher Query Results ===
2548
+ `);
2549
+ console.log(JSON.stringify(result, null, 2));
2550
+ console.log();
2551
+ await app.close();
2552
+ process.exit(0);
2553
+ } catch (error) {
2554
+ console.error("Error:", error instanceof Error ? error.message : String(error));
2555
+ if (app)
2556
+ await app.close();
2557
+ process.exit(1);
2558
+ }
2559
+ });
2560
+ program.command("related <path>").description("Find documents related to the given document").option("--limit <n>", "Limit results", "10").action(async (path, options) => {
2561
+ let app;
2562
+ try {
2563
+ app = await NestFactory3.createApplicationContext(AppModule, {
2564
+ logger: false
2565
+ });
2566
+ const graph = app.get(GraphService);
2567
+ const pathResolver = app.get(PathResolverService);
2568
+ const absolutePath = pathResolver.resolveDocPath(path, {
2569
+ requireExists: true,
2570
+ requireInDocs: true
2571
+ });
2572
+ const limit = Math.min(parseInt(options.limit, 10), 50);
2573
+ const escapedPath = absolutePath.replace(/'/g, "\\'");
2574
+ const cypher = `
2575
+ MATCH (d:Document { name: '${escapedPath}' })<-[:APPEARS_IN]-(e)-[:APPEARS_IN]->(other:Document)
2576
+ WHERE other.name <> '${escapedPath}'
2577
+ RETURN DISTINCT other.name as path, other.title as title, count(e) as shared
2578
+ ORDER BY shared DESC
2579
+ LIMIT ${limit}
2580
+ `;
2581
+ const result = await graph.query(cypher);
2582
+ const results = result.resultSet || [];
2583
+ console.log(`
2584
+ === Documents Related to "${path}" ===
2585
+ `);
2586
+ if (results.length === 0) {
2587
+ console.log(`No related documents found.
2588
+ `);
2589
+ await app.close();
2590
+ process.exit(0);
2591
+ }
2592
+ results.forEach((row) => {
2593
+ const docPath = row[0];
2594
+ const title = row[1];
2595
+ const shared = row[2];
2596
+ console.log(`[${shared} shared entities] ${docPath}`);
2597
+ if (title) {
2598
+ console.log(` Title: ${title}`);
2599
+ }
2600
+ });
2601
+ console.log();
2602
+ await app.close();
2603
+ process.exit(0);
2604
+ } catch (error) {
2605
+ console.error("Error:", error instanceof Error ? error.message : String(error));
2606
+ if (app)
2607
+ await app.close();
2608
+ process.exit(1);
2609
+ }
2610
+ });
2611
+ }
2612
+ // src/commands/validate.command.ts
2613
+ import { NestFactory as NestFactory4 } from "@nestjs/core";
2614
+ function registerValidateCommand(program) {
2615
+ program.command("validate").description("Validate entity references and relationships across documents").option("--fix", "Show suggestions for common issues").action(async (options) => {
2616
+ let app;
2617
+ try {
2618
+ app = await NestFactory4.createApplicationContext(AppModule, {
2619
+ logger: false
2620
+ });
2621
+ const parser = app.get(DocumentParserService);
2622
+ console.log(`Validating entities and relationships...
2623
+ `);
2624
+ const { docs, errors: schemaErrors } = await parser.parseAllDocumentsWithErrors();
2625
+ const issues = [];
2626
+ for (const schemaError of schemaErrors) {
2627
+ issues.push({
2628
+ type: "error",
2629
+ path: schemaError.path,
2630
+ message: schemaError.error
2631
+ });
2632
+ }
2633
+ const entityIndex = new Map;
2634
+ for (const doc of docs) {
2635
+ for (const entity of doc.entities) {
2636
+ if (!entityIndex.has(entity.name)) {
2637
+ entityIndex.set(entity.name, new Set);
2638
+ }
2639
+ entityIndex.get(entity.name).add(doc.path);
2640
+ }
2641
+ }
2642
+ const validationErrors = validateDocuments(docs);
2643
+ for (const err of validationErrors) {
2644
+ issues.push({
2645
+ type: "error",
2646
+ path: err.path,
2647
+ message: err.error,
2648
+ suggestion: "Add entity definition or fix the reference"
2649
+ });
2650
+ }
2651
+ console.log(`Scanned ${docs.length} documents`);
2652
+ console.log(`Found ${entityIndex.size} unique entities
2653
+ `);
2654
+ if (issues.length > 0) {
2655
+ console.log(`Errors (${issues.length}):
2656
+ `);
2657
+ issues.forEach((i) => {
2658
+ console.log(` ${i.path}`);
2659
+ console.log(` Error: ${i.message}`);
2660
+ if (options.fix && i.suggestion) {
2661
+ console.log(` Suggestion: ${i.suggestion}`);
2662
+ }
2663
+ console.log("");
2664
+ });
2665
+ }
2666
+ if (issues.length === 0) {
2667
+ console.log("All validations passed!");
2668
+ }
2669
+ await app.close();
2670
+ process.exit(issues.length > 0 ? 1 : 0);
2671
+ } catch (error) {
2672
+ console.error("Validation failed:", error instanceof Error ? error.message : String(error));
2673
+ if (app)
2674
+ await app.close();
2675
+ process.exit(1);
2676
+ }
2677
+ });
2678
+ }
2679
+ // src/commands/ontology.command.ts
2680
+ import { NestFactory as NestFactory5 } from "@nestjs/core";
2681
+ function registerOntologyCommand(program) {
2682
+ program.command("ontology").description("Derive and display ontology from all documents").action(async () => {
2683
+ let app;
2684
+ try {
2685
+ app = await NestFactory5.createApplicationContext(AppModule, {
2686
+ logger: false
2687
+ });
2688
+ const ontologyService = app.get(OntologyService);
2689
+ const ontology = await ontologyService.deriveOntology();
2690
+ ontologyService.printSummary(ontology);
2691
+ await app.close();
2692
+ process.exit(0);
2693
+ } catch (error) {
2694
+ console.error(`
2695
+ \u274C Ontology derivation failed:`, error instanceof Error ? error.message : String(error));
2696
+ if (app)
2697
+ await app.close();
2698
+ process.exit(1);
2699
+ }
2700
+ });
2701
+ }
2702
+ // src/main.ts
2703
+ program.name("lattice").description("Human-initiated, AI-powered knowledge graph for markdown documentation").version("0.1.0");
2704
+ registerSyncCommand(program);
2705
+ registerStatusCommand(program);
2706
+ registerQueryCommands(program);
2707
+ registerValidateCommand(program);
2708
+ registerOntologyCommand(program);
2709
+ program.parse(process.argv);
2710
+ if (!process.argv.slice(2).length) {
2711
+ program.outputHelp();
2712
+ }