@zabaca/lattice 0.3.3 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +68 -64
  2. package/dist/main.js +1675 -1585
  3. package/package.json +63 -59
package/dist/main.js CHANGED
@@ -23,86 +23,890 @@ import { CommandFactory } from "nest-commander";
23
23
  import { Module as Module5 } from "@nestjs/common";
24
24
  import { ConfigModule as ConfigModule2 } from "@nestjs/config";
25
25
 
26
- // src/graph/graph.module.ts
27
- import { Module } from "@nestjs/common";
26
+ // src/commands/init.command.ts
27
+ import * as fs from "fs/promises";
28
+ import { homedir } from "os";
29
+ import * as path from "path";
30
+ import { fileURLToPath } from "url";
31
+ import { Injectable } from "@nestjs/common";
32
+ import { Command, CommandRunner, Option } from "nest-commander";
33
+ var __filename2 = fileURLToPath(import.meta.url);
34
+ var __dirname2 = path.dirname(__filename2);
35
+ var COMMANDS = ["research.md", "graph-sync.md", "entity-extract.md"];
36
+
37
+ class InitCommand extends CommandRunner {
38
+ async run(_inputs, options) {
39
+ try {
40
+ const targetDir = options.global ? path.join(homedir(), ".claude", "commands") : path.join(process.cwd(), ".claude", "commands");
41
+ let commandsSourceDir = path.resolve(__dirname2, "..", "commands");
42
+ try {
43
+ await fs.access(commandsSourceDir);
44
+ } catch {
45
+ commandsSourceDir = path.resolve(__dirname2, "..", "..", "commands");
46
+ }
47
+ try {
48
+ await fs.access(commandsSourceDir);
49
+ } catch {
50
+ console.error("Error: Commands source directory not found at", commandsSourceDir);
51
+ console.error("This may indicate a corrupted installation. Try reinstalling @zabaca/lattice.");
52
+ process.exit(1);
53
+ }
54
+ await fs.mkdir(targetDir, { recursive: true });
55
+ let copied = 0;
56
+ let skipped = 0;
57
+ const installed = [];
58
+ for (const file of COMMANDS) {
59
+ const sourcePath = path.join(commandsSourceDir, file);
60
+ const targetPath = path.join(targetDir, file);
61
+ try {
62
+ await fs.access(sourcePath);
63
+ try {
64
+ await fs.access(targetPath);
65
+ const sourceContent = await fs.readFile(sourcePath, "utf-8");
66
+ const targetContent = await fs.readFile(targetPath, "utf-8");
67
+ if (sourceContent === targetContent) {
68
+ skipped++;
69
+ continue;
70
+ }
71
+ } catch {}
72
+ await fs.copyFile(sourcePath, targetPath);
73
+ installed.push(file);
74
+ copied++;
75
+ } catch (err) {
76
+ console.error(`Warning: Could not copy ${file}:`, err instanceof Error ? err.message : String(err));
77
+ }
78
+ }
79
+ console.log();
80
+ console.log(`\u2705 Lattice commands installed to ${targetDir}`);
81
+ console.log();
82
+ if (copied > 0) {
83
+ console.log(`Installed ${copied} command(s):`);
84
+ installed.forEach((f) => {
85
+ const name = f.replace(".md", "");
86
+ console.log(` - /${name}`);
87
+ });
88
+ }
89
+ if (skipped > 0) {
90
+ console.log(`Skipped ${skipped} unchanged command(s)`);
91
+ }
92
+ console.log();
93
+ console.log("Available commands in Claude Code:");
94
+ console.log(" /research <topic> - AI-assisted research workflow");
95
+ console.log(" /graph-sync - Extract entities and sync to graph");
96
+ console.log(" /entity-extract - Extract entities from a single document");
97
+ console.log();
98
+ if (!options.global) {
99
+ console.log("\uD83D\uDCA1 Tip: Use 'lattice init --global' to install for all projects");
100
+ }
101
+ process.exit(0);
102
+ } catch (error) {
103
+ console.error("Error:", error instanceof Error ? error.message : String(error));
104
+ process.exit(1);
105
+ }
106
+ }
107
+ parseGlobal() {
108
+ return true;
109
+ }
110
+ }
111
+ __legacyDecorateClassTS([
112
+ Option({
113
+ flags: "-g, --global",
114
+ description: "Install to ~/.claude/commands/ (available in all projects)"
115
+ }),
116
+ __legacyMetadataTS("design:type", Function),
117
+ __legacyMetadataTS("design:paramtypes", []),
118
+ __legacyMetadataTS("design:returntype", Boolean)
119
+ ], InitCommand.prototype, "parseGlobal", null);
120
+ InitCommand = __legacyDecorateClassTS([
121
+ Injectable(),
122
+ Command({
123
+ name: "init",
124
+ description: "Install Claude Code slash commands for Lattice"
125
+ })
126
+ ], InitCommand);
127
+ // src/commands/ontology.command.ts
128
+ import { Injectable as Injectable4 } from "@nestjs/common";
129
+ import { Command as Command2, CommandRunner as CommandRunner2 } from "nest-commander";
130
+
131
+ // src/sync/ontology.service.ts
132
+ import { Injectable as Injectable3 } from "@nestjs/common";
133
+
134
+ // src/sync/document-parser.service.ts
135
+ import { createHash } from "crypto";
136
+ import { readFile as readFile2 } from "fs/promises";
137
+ import { resolve as resolve2 } from "path";
138
+ import { Injectable as Injectable2, Logger } from "@nestjs/common";
139
+ import { glob } from "glob";
140
+
141
+ // src/schemas/config.schemas.ts
142
+ import { z } from "zod";
143
+ var DuckDBConfigSchema = z.object({
144
+ dbPath: z.string().optional(),
145
+ embeddingDimensions: z.coerce.number().int().positive().default(512)
146
+ });
147
+ var EmbeddingConfigSchema = z.object({
148
+ provider: z.enum(["openai", "voyage", "nomic", "mock"]).default("voyage"),
149
+ apiKey: z.string().optional(),
150
+ model: z.string().min(1).default("voyage-3.5-lite"),
151
+ dimensions: z.coerce.number().int().positive().default(512)
152
+ });
153
+ var DocsConfigSchema = z.object({
154
+ projectRoot: z.string().default(process.cwd()),
155
+ docsPath: z.string().default("docs")
156
+ });
157
+
158
+ // src/utils/frontmatter.ts
159
+ import matter from "gray-matter";
160
+ import { z as z2 } from "zod";
161
+ var EntityTypeSchema = z2.enum([
162
+ "Topic",
163
+ "Technology",
164
+ "Concept",
165
+ "Tool",
166
+ "Process",
167
+ "Person",
168
+ "Organization",
169
+ "Document"
170
+ ]);
171
+ var RelationTypeSchema = z2.enum(["REFERENCES"]);
172
+ var EntitySchema = z2.object({
173
+ name: z2.string().min(1),
174
+ type: EntityTypeSchema,
175
+ description: z2.string().optional()
176
+ });
177
+ var RelationshipSchema = z2.object({
178
+ source: z2.string().min(1),
179
+ relation: RelationTypeSchema,
180
+ target: z2.string().min(1)
181
+ });
182
+ var GraphMetadataSchema = z2.object({
183
+ importance: z2.enum(["high", "medium", "low"]).optional(),
184
+ domain: z2.string().optional()
185
+ });
186
+ var validateDateFormat = (dateStr) => {
187
+ const match = dateStr.match(/^(\d{4})-(\d{2})-(\d{2})$/);
188
+ if (!match)
189
+ return false;
190
+ const [, yearStr, monthStr, dayStr] = match;
191
+ const year = parseInt(yearStr, 10);
192
+ const month = parseInt(monthStr, 10);
193
+ const day = parseInt(dayStr, 10);
194
+ if (month < 1 || month > 12)
195
+ return false;
196
+ if (day < 1 || day > 31)
197
+ return false;
198
+ const daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
199
+ if (year % 4 === 0 && year % 100 !== 0 || year % 400 === 0) {
200
+ daysInMonth[1] = 29;
201
+ }
202
+ return day <= daysInMonth[month - 1];
203
+ };
204
+ var FrontmatterSchema = z2.object({
205
+ created: z2.string().refine(validateDateFormat, "Date must be in YYYY-MM-DD format"),
206
+ updated: z2.string().refine(validateDateFormat, "Date must be in YYYY-MM-DD format"),
207
+ status: z2.enum(["draft", "ongoing", "complete"]).optional(),
208
+ topic: z2.string().optional(),
209
+ tags: z2.array(z2.string()).optional(),
210
+ summary: z2.string().optional(),
211
+ entities: z2.array(EntitySchema).optional(),
212
+ relationships: z2.array(RelationshipSchema).optional(),
213
+ graph: GraphMetadataSchema.optional()
214
+ }).passthrough();
215
+ function parseFrontmatter(content) {
216
+ try {
217
+ const { data, content: markdown } = matter(content);
218
+ if (Object.keys(data).length === 0) {
219
+ return {
220
+ frontmatter: null,
221
+ content: markdown.trim(),
222
+ raw: content
223
+ };
224
+ }
225
+ const normalizedData = normalizeData(data);
226
+ const validated = FrontmatterSchema.safeParse(normalizedData);
227
+ return {
228
+ frontmatter: validated.success ? validated.data : normalizedData,
229
+ content: markdown.trim(),
230
+ raw: content
231
+ };
232
+ } catch (error) {
233
+ const errorMessage = error instanceof Error ? error.message : String(error);
234
+ throw new Error(`YAML parsing error: ${errorMessage}`);
235
+ }
236
+ }
237
+ function normalizeData(data) {
238
+ if (data instanceof Date) {
239
+ return data.toISOString().split("T")[0];
240
+ }
241
+ if (Array.isArray(data)) {
242
+ return data.map(normalizeData);
243
+ }
244
+ if (data !== null && typeof data === "object") {
245
+ const normalized = {};
246
+ for (const [key, value] of Object.entries(data)) {
247
+ normalized[key] = normalizeData(value);
248
+ }
249
+ return normalized;
250
+ }
251
+ return data;
252
+ }
253
+
254
+ // src/sync/document-parser.service.ts
255
+ class DocumentParserService {
256
+ logger = new Logger(DocumentParserService.name);
257
+ docsPath;
258
+ constructor() {
259
+ const config = DocsConfigSchema.parse({
260
+ projectRoot: process.env.PROJECT_ROOT,
261
+ docsPath: process.env.DOCS_PATH
262
+ });
263
+ if (config.docsPath.startsWith("/")) {
264
+ this.docsPath = config.docsPath;
265
+ } else {
266
+ this.docsPath = resolve2(config.projectRoot, config.docsPath);
267
+ }
268
+ }
269
+ getDocsPath() {
270
+ return this.docsPath;
271
+ }
272
+ async discoverDocuments() {
273
+ const pattern = `${this.docsPath}/**/*.md`;
274
+ const files = await glob(pattern, {
275
+ ignore: ["**/node_modules/**", "**/.git/**"]
276
+ });
277
+ return files.sort();
278
+ }
279
+ async parseDocument(filePath) {
280
+ const content = await readFile2(filePath, "utf-8");
281
+ const parsed = parseFrontmatter(content);
282
+ const title = this.extractTitle(content, filePath);
283
+ const contentHash = this.computeHash(content);
284
+ const frontmatterHash = this.computeHash(JSON.stringify(parsed.frontmatter || {}));
285
+ const entities = this.extractEntities(parsed.frontmatter, filePath);
286
+ const relationships = this.extractRelationships(parsed.frontmatter, filePath);
287
+ const graphMetadata = this.extractGraphMetadata(parsed.frontmatter);
288
+ return {
289
+ path: filePath,
290
+ title,
291
+ content: parsed.content,
292
+ contentHash,
293
+ frontmatterHash,
294
+ summary: parsed.frontmatter?.summary,
295
+ topic: parsed.frontmatter?.topic,
296
+ entities,
297
+ relationships,
298
+ graphMetadata,
299
+ tags: parsed.frontmatter?.tags || [],
300
+ created: parsed.frontmatter?.created,
301
+ updated: parsed.frontmatter?.updated,
302
+ status: parsed.frontmatter?.status
303
+ };
304
+ }
305
+ async parseAllDocuments() {
306
+ const { docs } = await this.parseAllDocumentsWithErrors();
307
+ return docs;
308
+ }
309
+ async parseAllDocumentsWithErrors() {
310
+ const files = await this.discoverDocuments();
311
+ const docs = [];
312
+ const errors = [];
313
+ for (const file of files) {
314
+ try {
315
+ const parsed = await this.parseDocument(file);
316
+ docs.push(parsed);
317
+ } catch (error) {
318
+ const errorMsg = error instanceof Error ? error.message : String(error);
319
+ errors.push({ path: file, error: errorMsg });
320
+ this.logger.warn(`Failed to parse ${file}: ${error}`);
321
+ }
322
+ }
323
+ return { docs, errors };
324
+ }
325
+ extractTitle(content, filePath) {
326
+ const h1Match = content.match(/^#\s+(.+)$/m);
327
+ if (h1Match) {
328
+ return h1Match[1];
329
+ }
330
+ const parts = filePath.split("/");
331
+ return parts[parts.length - 1].replace(".md", "");
332
+ }
333
+ extractEntities(frontmatter, docPath) {
334
+ const fm = frontmatter;
335
+ if (!fm?.entities || !Array.isArray(fm.entities)) {
336
+ return [];
337
+ }
338
+ const validEntities = [];
339
+ const errors = [];
340
+ for (let i = 0;i < fm.entities.length; i++) {
341
+ const e = fm.entities[i];
342
+ const result = EntitySchema.safeParse(e);
343
+ if (result.success) {
344
+ validEntities.push(result.data);
345
+ } else {
346
+ const entityPreview = typeof e === "string" ? `"${e}"` : JSON.stringify(e);
347
+ errors.push(`Entity[${i}]: ${entityPreview} - Expected object with {name, type}, got ${typeof e}`);
348
+ }
349
+ }
350
+ if (errors.length > 0) {
351
+ const errorMsg = `Invalid entity schema in ${docPath}:
352
+ ${errors.join(`
353
+ `)}`;
354
+ throw new Error(errorMsg);
355
+ }
356
+ return validEntities;
357
+ }
358
+ extractRelationships(frontmatter, docPath) {
359
+ const fm = frontmatter;
360
+ if (!fm?.relationships || !Array.isArray(fm.relationships)) {
361
+ return [];
362
+ }
363
+ const validRelationships = [];
364
+ const errors = [];
365
+ const validRelationTypes = RelationTypeSchema.options;
366
+ for (let i = 0;i < fm.relationships.length; i++) {
367
+ const r = fm.relationships[i];
368
+ const result = RelationshipSchema.safeParse(r);
369
+ if (result.success) {
370
+ const rel = result.data;
371
+ if (rel.source === "this") {
372
+ rel.source = docPath;
373
+ }
374
+ if (rel.target === "this") {
375
+ rel.target = docPath;
376
+ }
377
+ validRelationships.push(rel);
378
+ } else {
379
+ if (typeof r === "string") {
380
+ errors.push(`Relationship[${i}]: "${r}" - Expected object with {source, relation, target}, got string`);
381
+ } else if (typeof r === "object" && r !== null) {
382
+ const issues = [];
383
+ if (!r.source)
384
+ issues.push("missing source");
385
+ if (!r.target)
386
+ issues.push("missing target");
387
+ if (!r.relation) {
388
+ issues.push("missing relation");
389
+ } else if (!validRelationTypes.includes(r.relation)) {
390
+ issues.push(`invalid relation "${r.relation}" (allowed: ${validRelationTypes.join(", ")})`);
391
+ }
392
+ errors.push(`Relationship[${i}]: ${issues.join(", ")}`);
393
+ } else {
394
+ errors.push(`Relationship[${i}]: Expected object, got ${typeof r}`);
395
+ }
396
+ }
397
+ }
398
+ if (errors.length > 0) {
399
+ const errorMsg = `Invalid relationship schema in ${docPath}:
400
+ ${errors.join(`
401
+ `)}`;
402
+ throw new Error(errorMsg);
403
+ }
404
+ return validRelationships;
405
+ }
406
+ extractGraphMetadata(frontmatter) {
407
+ const fm = frontmatter;
408
+ if (!fm?.graph) {
409
+ return;
410
+ }
411
+ const result = GraphMetadataSchema.safeParse(fm.graph);
412
+ return result.success ? result.data : undefined;
413
+ }
414
+ computeHash(content) {
415
+ return createHash("sha256").update(content).digest("hex");
416
+ }
417
+ }
418
+ DocumentParserService = __legacyDecorateClassTS([
419
+ Injectable2(),
420
+ __legacyMetadataTS("design:paramtypes", [])
421
+ ], DocumentParserService);
422
+
423
+ // src/sync/ontology.service.ts
424
+ class OntologyService {
425
+ parser;
426
+ constructor(parser) {
427
+ this.parser = parser;
428
+ }
429
+ async deriveOntology() {
430
+ const docs = await this.parser.parseAllDocuments();
431
+ return this.deriveFromDocuments(docs);
432
+ }
433
+ deriveFromDocuments(docs) {
434
+ const entityTypeSet = new Set;
435
+ const relationshipTypeSet = new Set;
436
+ const entityCounts = {};
437
+ const relationshipCounts = {};
438
+ const entityExamples = {};
439
+ let documentsWithEntities = 0;
440
+ let documentsWithoutEntities = 0;
441
+ let totalRelationships = 0;
442
+ for (const doc of docs) {
443
+ if (doc.entities.length > 0) {
444
+ documentsWithEntities++;
445
+ } else {
446
+ documentsWithoutEntities++;
447
+ }
448
+ for (const entity of doc.entities) {
449
+ entityTypeSet.add(entity.type);
450
+ entityCounts[entity.type] = (entityCounts[entity.type] || 0) + 1;
451
+ if (!entityExamples[entity.name]) {
452
+ entityExamples[entity.name] = { type: entity.type, documents: [] };
453
+ }
454
+ if (!entityExamples[entity.name].documents.includes(doc.path)) {
455
+ entityExamples[entity.name].documents.push(doc.path);
456
+ }
457
+ }
458
+ for (const rel of doc.relationships) {
459
+ relationshipTypeSet.add(rel.relation);
460
+ relationshipCounts[rel.relation] = (relationshipCounts[rel.relation] || 0) + 1;
461
+ totalRelationships++;
462
+ }
463
+ }
464
+ return {
465
+ entityTypes: Array.from(entityTypeSet).sort(),
466
+ relationshipTypes: Array.from(relationshipTypeSet).sort(),
467
+ entityCounts,
468
+ relationshipCounts,
469
+ totalEntities: Object.keys(entityExamples).length,
470
+ totalRelationships,
471
+ documentsWithEntities,
472
+ documentsWithoutEntities,
473
+ entityExamples
474
+ };
475
+ }
476
+ printSummary(ontology) {
477
+ console.log(`
478
+ Derived Ontology Summary
479
+ `);
480
+ console.log(`Documents: ${ontology.documentsWithEntities} with entities, ${ontology.documentsWithoutEntities} without`);
481
+ console.log(`Unique Entities: ${ontology.totalEntities}`);
482
+ console.log(`Total Relationships: ${ontology.totalRelationships}`);
483
+ console.log(`
484
+ Entity Types:`);
485
+ for (const type of ontology.entityTypes) {
486
+ console.log(` ${type}: ${ontology.entityCounts[type]} instances`);
487
+ }
488
+ console.log(`
489
+ Relationship Types:`);
490
+ for (const type of ontology.relationshipTypes) {
491
+ console.log(` ${type}: ${ontology.relationshipCounts[type]} instances`);
492
+ }
493
+ console.log(`
494
+ Top Entities (by document count):`);
495
+ const sorted = Object.entries(ontology.entityExamples).sort((a, b) => b[1].documents.length - a[1].documents.length).slice(0, 10);
496
+ for (const [name, info] of sorted) {
497
+ console.log(` ${name} (${info.type}): ${info.documents.length} docs`);
498
+ }
499
+ }
500
+ }
501
+ OntologyService = __legacyDecorateClassTS([
502
+ Injectable3(),
503
+ __legacyMetadataTS("design:paramtypes", [
504
+ typeof DocumentParserService === "undefined" ? Object : DocumentParserService
505
+ ])
506
+ ], OntologyService);
507
+
508
+ // src/commands/ontology.command.ts
509
+ class OntologyCommand extends CommandRunner2 {
510
+ ontologyService;
511
+ constructor(ontologyService) {
512
+ super();
513
+ this.ontologyService = ontologyService;
514
+ }
515
+ async run() {
516
+ try {
517
+ const ontology = await this.ontologyService.deriveOntology();
518
+ this.ontologyService.printSummary(ontology);
519
+ process.exit(0);
520
+ } catch (error) {
521
+ console.error(`
522
+ \u274C Ontology derivation failed:`, error instanceof Error ? error.message : String(error));
523
+ process.exit(1);
524
+ }
525
+ }
526
+ }
527
+ OntologyCommand = __legacyDecorateClassTS([
528
+ Injectable4(),
529
+ Command2({
530
+ name: "ontology",
531
+ description: "Derive and display ontology from all documents"
532
+ }),
533
+ __legacyMetadataTS("design:paramtypes", [
534
+ typeof OntologyService === "undefined" ? Object : OntologyService
535
+ ])
536
+ ], OntologyCommand);
537
+ // src/commands/query.command.ts
538
+ import { Injectable as Injectable7 } from "@nestjs/common";
539
+ import { Command as Command3, CommandRunner as CommandRunner3, Option as Option2 } from "nest-commander";
540
+
541
+ // src/embedding/embedding.service.ts
542
+ import { Injectable as Injectable5, Logger as Logger2 } from "@nestjs/common";
543
+ import { ConfigService } from "@nestjs/config";
544
+
545
+ // src/embedding/embedding.types.ts
546
+ var DEFAULT_EMBEDDING_CONFIG = {
547
+ provider: "voyage",
548
+ model: "voyage-3.5-lite",
549
+ dimensions: 512
550
+ };
551
+
552
+ // src/embedding/providers/mock.provider.ts
553
+ class MockEmbeddingProvider {
554
+ name = "mock";
555
+ dimensions;
556
+ constructor(dimensions = 1536) {
557
+ this.dimensions = dimensions;
558
+ }
559
+ async generateEmbedding(text) {
560
+ return this.generateDeterministicEmbedding(text);
561
+ }
562
+ async generateEmbeddings(texts) {
563
+ return Promise.all(texts.map((text) => this.generateEmbedding(text)));
564
+ }
565
+ generateDeterministicEmbedding(text) {
566
+ const embedding = new Array(this.dimensions);
567
+ let hash = 0;
568
+ for (let i = 0;i < text.length; i++) {
569
+ hash = (hash << 5) - hash + text.charCodeAt(i);
570
+ hash = hash & hash;
571
+ }
572
+ for (let i = 0;i < this.dimensions; i++) {
573
+ const seed = hash * (i + 1) ^ i * 31;
574
+ embedding[i] = (seed % 2000 - 1000) / 1000;
575
+ }
576
+ const magnitude = Math.sqrt(embedding.reduce((sum, val) => sum + val * val, 0));
577
+ return embedding.map((val) => val / magnitude);
578
+ }
579
+ }
580
+
581
+ // src/embedding/providers/openai.provider.ts
582
+ class OpenAIEmbeddingProvider {
583
+ name = "openai";
584
+ dimensions;
585
+ model;
586
+ apiKey;
587
+ baseUrl = "https://api.openai.com/v1";
588
+ constructor(config) {
589
+ const apiKey = config?.apiKey || process.env.OPENAI_API_KEY;
590
+ if (!apiKey) {
591
+ throw new Error("OpenAI API key is required. Set OPENAI_API_KEY environment variable or pass apiKey in config.");
592
+ }
593
+ this.apiKey = apiKey;
594
+ this.model = config?.model || "text-embedding-3-small";
595
+ this.dimensions = config?.dimensions || 1536;
596
+ }
597
+ async generateEmbedding(text) {
598
+ const embeddings = await this.generateEmbeddings([text]);
599
+ return embeddings[0];
600
+ }
601
+ async generateEmbeddings(texts) {
602
+ if (!texts || texts.length === 0) {
603
+ return [];
604
+ }
605
+ try {
606
+ const response = await fetch(`${this.baseUrl}/embeddings`, {
607
+ method: "POST",
608
+ headers: {
609
+ "Content-Type": "application/json",
610
+ Authorization: `Bearer ${this.apiKey}`
611
+ },
612
+ body: JSON.stringify({
613
+ model: this.model,
614
+ input: texts,
615
+ dimensions: this.dimensions
616
+ })
617
+ });
618
+ if (!response.ok) {
619
+ const error = await response.json().catch(() => ({}));
620
+ throw new Error(`OpenAI API error: ${response.status} ${JSON.stringify(error)}`);
621
+ }
622
+ const data = await response.json();
623
+ const sortedData = data.data.sort((a, b) => a.index - b.index);
624
+ return sortedData.map((item) => item.embedding);
625
+ } catch (error) {
626
+ if (error instanceof Error) {
627
+ throw new Error(`Failed to generate embeddings: ${error.message}`);
628
+ }
629
+ throw error;
630
+ }
631
+ }
632
+ }
633
+
634
+ // src/schemas/embedding.schemas.ts
635
+ import { z as z3 } from "zod";
636
+ var VoyageEmbeddingResponseSchema = z3.object({
637
+ object: z3.string(),
638
+ data: z3.array(z3.object({
639
+ object: z3.string(),
640
+ embedding: z3.array(z3.number()),
641
+ index: z3.number().int().nonnegative()
642
+ })),
643
+ model: z3.string(),
644
+ usage: z3.object({
645
+ total_tokens: z3.number().int().positive()
646
+ })
647
+ });
648
+
649
+ // src/embedding/providers/voyage.provider.ts
650
+ class VoyageEmbeddingProvider {
651
+ name = "voyage";
652
+ dimensions;
653
+ model;
654
+ apiKey;
655
+ inputType;
656
+ baseUrl = "https://api.voyageai.com/v1";
657
+ constructor(config) {
658
+ const apiKey = config?.apiKey || process.env.VOYAGE_API_KEY;
659
+ if (!apiKey) {
660
+ throw new Error("Voyage API key is required. Set VOYAGE_API_KEY environment variable or pass apiKey in config.");
661
+ }
662
+ this.apiKey = apiKey;
663
+ this.model = config?.model || "voyage-3.5-lite";
664
+ this.dimensions = config?.dimensions || 512;
665
+ this.inputType = config?.inputType || "document";
666
+ }
667
+ async generateEmbedding(text) {
668
+ const embeddings = await this.generateEmbeddings([text]);
669
+ return embeddings[0];
670
+ }
671
+ async generateEmbeddings(texts) {
672
+ if (!texts || texts.length === 0) {
673
+ return [];
674
+ }
675
+ try {
676
+ const response = await fetch(`${this.baseUrl}/embeddings`, {
677
+ method: "POST",
678
+ headers: {
679
+ "Content-Type": "application/json",
680
+ Authorization: `Bearer ${this.apiKey}`
681
+ },
682
+ body: JSON.stringify({
683
+ model: this.model,
684
+ input: texts,
685
+ output_dimension: this.dimensions,
686
+ input_type: this.inputType
687
+ })
688
+ });
689
+ if (!response.ok) {
690
+ const error = await response.json().catch(() => ({}));
691
+ throw new Error(`Voyage API error: ${response.status} ${JSON.stringify(error)}`);
692
+ }
693
+ const data = VoyageEmbeddingResponseSchema.parse(await response.json());
694
+ const sortedData = data.data.sort((a, b) => a.index - b.index);
695
+ return sortedData.map((item) => item.embedding);
696
+ } catch (error) {
697
+ if (error instanceof Error) {
698
+ throw new Error(`Failed to generate embeddings: ${error.message}`);
699
+ }
700
+ throw error;
701
+ }
702
+ }
703
+ }
704
+
705
+ // src/embedding/embedding.service.ts
706
+ class EmbeddingService {
707
+ configService;
708
+ logger = new Logger2(EmbeddingService.name);
709
+ provider;
710
+ config;
711
+ constructor(configService) {
712
+ this.configService = configService;
713
+ this.config = this.loadConfig();
714
+ this.provider = this.createProvider();
715
+ this.logger.log(`Initialized embedding service with provider: ${this.provider.name}`);
716
+ }
717
+ loadConfig() {
718
+ const providerEnv = this.configService.get("EMBEDDING_PROVIDER");
719
+ const provider = providerEnv ?? DEFAULT_EMBEDDING_CONFIG.provider;
720
+ let apiKey;
721
+ if (provider === "voyage") {
722
+ apiKey = this.configService.get("VOYAGE_API_KEY");
723
+ } else if (provider === "openai") {
724
+ apiKey = this.configService.get("OPENAI_API_KEY");
725
+ }
726
+ return EmbeddingConfigSchema.parse({
727
+ provider: providerEnv,
728
+ apiKey,
729
+ model: this.configService.get("EMBEDDING_MODEL"),
730
+ dimensions: this.configService.get("EMBEDDING_DIMENSIONS")
731
+ });
732
+ }
733
+ createProvider() {
734
+ switch (this.config.provider) {
735
+ case "openai":
736
+ if (!this.config.apiKey) {
737
+ throw new Error("OPENAI_API_KEY environment variable is required for embeddings. " + "Set it in .env or use --no-embeddings to skip embedding generation.");
738
+ }
739
+ return new OpenAIEmbeddingProvider({
740
+ apiKey: this.config.apiKey,
741
+ model: this.config.model,
742
+ dimensions: this.config.dimensions
743
+ });
744
+ case "mock":
745
+ return new MockEmbeddingProvider(this.config.dimensions);
746
+ case "voyage":
747
+ if (!this.config.apiKey) {
748
+ throw new Error("VOYAGE_API_KEY environment variable is required for embeddings. " + "Set it in .env or use --no-embeddings to skip embedding generation.");
749
+ }
750
+ return new VoyageEmbeddingProvider({
751
+ apiKey: this.config.apiKey,
752
+ model: this.config.model,
753
+ dimensions: this.config.dimensions
754
+ });
755
+ case "nomic":
756
+ throw new Error(`Provider ${this.config.provider} not yet implemented. Use 'voyage', 'openai', or 'mock'.`);
757
+ default:
758
+ throw new Error(`Unknown embedding provider: ${this.config.provider}. Use 'voyage', 'openai', or 'mock'.`);
759
+ }
760
+ }
761
+ getProviderName() {
762
+ return this.provider.name;
763
+ }
764
+ getDimensions() {
765
+ return this.provider.dimensions;
766
+ }
767
+ async generateEmbedding(text) {
768
+ if (!text || text.trim().length === 0) {
769
+ throw new Error("Cannot generate embedding for empty text");
770
+ }
771
+ return this.provider.generateEmbedding(text);
772
+ }
773
+ async generateEmbeddings(texts) {
774
+ const validTexts = texts.filter((t) => t && t.trim().length > 0);
775
+ if (validTexts.length === 0) {
776
+ return [];
777
+ }
778
+ return this.provider.generateEmbeddings(validTexts);
779
+ }
780
+ isRealProvider() {
781
+ return this.provider.name !== "mock";
782
+ }
783
+ }
784
+ EmbeddingService = __legacyDecorateClassTS([
785
+ Injectable5(),
786
+ __legacyMetadataTS("design:paramtypes", [
787
+ typeof ConfigService === "undefined" ? Object : ConfigService
788
+ ])
789
+ ], EmbeddingService);
28
790
 
29
791
  // src/graph/graph.service.ts
30
- import { Injectable, Logger } from "@nestjs/common";
31
- import { ConfigService } from "@nestjs/config";
32
- import Redis from "ioredis";
792
+ import { DuckDBInstance } from "@duckdb/node-api";
793
+ import { Injectable as Injectable6, Logger as Logger3 } from "@nestjs/common";
794
+ import { ConfigService as ConfigService2 } from "@nestjs/config";
33
795
  class GraphService {
34
796
  configService;
35
- logger = new Logger(GraphService.name);
36
- redis = null;
37
- config;
797
+ logger = new Logger3(GraphService.name);
798
+ instance = null;
799
+ connection = null;
800
+ dbPath;
38
801
  connecting = null;
802
+ vectorIndexes = new Set;
803
+ embeddingDimensions;
39
804
  constructor(configService) {
40
805
  this.configService = configService;
41
- this.config = {
42
- host: this.configService.get("FALKORDB_HOST", "localhost"),
43
- port: this.configService.get("FALKORDB_PORT", 6379),
44
- graphName: this.configService.get("GRAPH_NAME", "research_knowledge")
45
- };
806
+ this.dbPath = this.configService.get("DUCKDB_PATH") || "./.lattice.duckdb";
807
+ this.embeddingDimensions = this.configService.get("EMBEDDING_DIMENSIONS") || 512;
46
808
  }
47
809
  async onModuleDestroy() {
48
810
  await this.disconnect();
49
811
  }
50
812
  async ensureConnected() {
51
- if (this.redis) {
52
- return this.redis;
813
+ if (this.connection) {
814
+ return this.connection;
53
815
  }
54
816
  if (this.connecting) {
55
817
  await this.connecting;
56
- return this.redis;
818
+ if (!this.connection) {
819
+ throw new Error("Connection failed to establish");
820
+ }
821
+ return this.connection;
57
822
  }
58
823
  this.connecting = this.connect();
59
824
  await this.connecting;
60
825
  this.connecting = null;
61
- return this.redis;
826
+ if (!this.connection) {
827
+ throw new Error("Connection failed to establish");
828
+ }
829
+ return this.connection;
62
830
  }
63
831
  async connect() {
64
832
  try {
65
- this.redis = new Redis({
66
- host: this.config.host,
67
- port: this.config.port,
68
- maxRetriesPerRequest: 3,
69
- retryStrategy: (times) => {
70
- if (times > 3) {
71
- return null;
72
- }
73
- const delay = Math.min(times * 50, 2000);
74
- return delay;
75
- },
76
- lazyConnect: true
77
- });
78
- this.redis.on("error", (err) => {
79
- this.logger.debug(`Redis connection error: ${err.message}`);
833
+ this.instance = await DuckDBInstance.create(this.dbPath, {
834
+ allow_unsigned_extensions: "true"
80
835
  });
81
- await this.redis.ping();
82
- this.logger.log(`Connected to FalkorDB at ${this.config.host}:${this.config.port}`);
836
+ this.connection = await this.instance.connect();
837
+ await this.connection.run("INSTALL vss; LOAD vss;");
838
+ await this.connection.run("SET hnsw_enable_experimental_persistence = true;");
839
+ try {
840
+ await this.connection.run("SET custom_extension_repository = 'http://duckpgq.s3.eu-north-1.amazonaws.com';");
841
+ await this.connection.run("FORCE INSTALL 'duckpgq';");
842
+ await this.connection.run("LOAD 'duckpgq';");
843
+ this.logger.log("DuckPGQ extension loaded successfully");
844
+ } catch (e) {
845
+ this.logger.warn(`DuckPGQ extension not available: ${e instanceof Error ? e.message : String(e)}`);
846
+ }
847
+ await this.initializeSchema();
848
+ this.logger.log(`Connected to DuckDB at ${this.dbPath}`);
83
849
  } catch (error) {
84
- this.redis = null;
85
- this.logger.error(`Failed to connect to FalkorDB: ${error instanceof Error ? error.message : String(error)}`);
850
+ this.connection = null;
851
+ this.instance = null;
852
+ this.logger.error(`Failed to connect to DuckDB: ${error instanceof Error ? error.message : String(error)}`);
86
853
  throw error;
87
854
  }
88
855
  }
89
856
  async disconnect() {
90
- if (this.redis) {
91
- await this.redis.quit();
92
- this.logger.log("Disconnected from FalkorDB");
93
- }
94
- }
95
- async query(cypher, _params) {
857
+ if (this.connection) {
858
+ this.connection.closeSync();
859
+ this.connection = null;
860
+ this.logger.log("Disconnected from DuckDB");
861
+ }
862
+ if (this.instance) {
863
+ this.instance = null;
864
+ }
865
+ }
866
+ async initializeSchema() {
867
+ if (!this.connection) {
868
+ throw new Error("Cannot initialize schema: not connected");
869
+ }
870
+ const conn = this.connection;
871
+ await conn.run(`
872
+ CREATE TABLE IF NOT EXISTS nodes (
873
+ label VARCHAR NOT NULL,
874
+ name VARCHAR NOT NULL,
875
+ properties JSON,
876
+ embedding FLOAT[${this.embeddingDimensions}],
877
+ created_at TIMESTAMP DEFAULT NOW(),
878
+ updated_at TIMESTAMP DEFAULT NOW(),
879
+ PRIMARY KEY(label, name)
880
+ )
881
+ `);
882
+ await conn.run(`
883
+ CREATE TABLE IF NOT EXISTS relationships (
884
+ source_label VARCHAR NOT NULL,
885
+ source_name VARCHAR NOT NULL,
886
+ relation_type VARCHAR NOT NULL,
887
+ target_label VARCHAR NOT NULL,
888
+ target_name VARCHAR NOT NULL,
889
+ properties JSON,
890
+ created_at TIMESTAMP DEFAULT NOW(),
891
+ PRIMARY KEY(source_label, source_name, relation_type, target_label, target_name)
892
+ )
893
+ `);
894
+ await conn.run("CREATE INDEX IF NOT EXISTS idx_nodes_label ON nodes(label)");
895
+ await conn.run("CREATE INDEX IF NOT EXISTS idx_nodes_label_name ON nodes(label, name)");
896
+ await conn.run("CREATE INDEX IF NOT EXISTS idx_rels_source ON relationships(source_label, source_name)");
897
+ await conn.run("CREATE INDEX IF NOT EXISTS idx_rels_target ON relationships(target_label, target_name)");
898
+ }
899
+ async query(sql, _params) {
96
900
  try {
97
- const redis = await this.ensureConnected();
98
- const result = await redis.call("GRAPH.QUERY", this.config.graphName, cypher);
99
- const resultArray = Array.isArray(result) ? result : [];
901
+ const conn = await this.ensureConnected();
902
+ const reader = await conn.runAndReadAll(sql);
903
+ const rows = reader.getRows();
100
904
  return {
101
- resultSet: Array.isArray(resultArray[1]) ? resultArray[1] : [],
102
- stats: this.parseStats(result)
905
+ resultSet: rows,
906
+ stats: undefined
103
907
  };
104
908
  } catch (error) {
105
- this.logger.error(`Cypher query failed: ${error instanceof Error ? error.message : String(error)}`);
909
+ this.logger.error(`Query failed: ${error instanceof Error ? error.message : String(error)}`);
106
910
  throw error;
107
911
  }
108
912
  }
@@ -112,18 +916,15 @@ class GraphService {
112
916
  if (!name) {
113
917
  throw new Error("Node must have a 'name' property");
114
918
  }
115
- const escapedName = this.escapeCypher(String(name));
116
- const escapedLabel = this.escapeCypher(label);
117
- const propAssignments = Object.entries({
118
- name,
119
- ...otherProps
120
- }).map(([key, value]) => {
121
- const escapedKey = this.escapeCypher(key);
122
- const escapedValue = this.escapeCypherValue(value);
123
- return `n.\`${escapedKey}\` = ${escapedValue}`;
124
- }).join(", ");
125
- const cypher = `MERGE (n:\`${escapedLabel}\` { name: '${escapedName}' }) ` + `SET ${propAssignments}`;
126
- await this.query(cypher);
919
+ const conn = await this.ensureConnected();
920
+ const propsJson = JSON.stringify(otherProps);
921
+ await conn.run(`
922
+ INSERT INTO nodes (label, name, properties)
923
+ VALUES ('${this.escape(String(label))}', '${this.escape(String(name))}', '${this.escape(propsJson)}')
924
+ ON CONFLICT (label, name) DO UPDATE SET
925
+ properties = EXCLUDED.properties,
926
+ updated_at = NOW()
927
+ `);
127
928
  } catch (error) {
128
929
  this.logger.error(`Failed to upsert node: ${error instanceof Error ? error.message : String(error)}`);
129
930
  throw error;
@@ -131,21 +932,31 @@ class GraphService {
131
932
  }
132
933
  async upsertRelationship(sourceLabel, sourceName, relation, targetLabel, targetName, properties) {
133
934
  try {
134
- const escapedSourceLabel = this.escapeCypher(sourceLabel);
135
- const escapedSourceName = this.escapeCypher(sourceName);
136
- const escapedRelation = this.escapeCypher(relation);
137
- const escapedTargetLabel = this.escapeCypher(targetLabel);
138
- const escapedTargetName = this.escapeCypher(targetName);
139
- let relPropAssignments = "";
140
- if (properties && Object.keys(properties).length > 0) {
141
- relPropAssignments = ` SET ` + Object.entries(properties).map(([key, value]) => {
142
- const escapedKey = this.escapeCypher(key);
143
- const escapedValue = this.escapeCypherValue(value);
144
- return `r.\`${escapedKey}\` = ${escapedValue}`;
145
- }).join(", ");
146
- }
147
- const cypher = `MERGE (source:\`${escapedSourceLabel}\` { name: '${escapedSourceName}' }) ` + `MERGE (target:\`${escapedTargetLabel}\` { name: '${escapedTargetName}' }) ` + `MERGE (source)-[r:\`${escapedRelation}\`]->(target)` + relPropAssignments;
148
- await this.query(cypher);
935
+ const conn = await this.ensureConnected();
936
+ await conn.run(`
937
+ INSERT INTO nodes (label, name, properties)
938
+ VALUES ('${this.escape(sourceLabel)}', '${this.escape(sourceName)}', '{}')
939
+ ON CONFLICT (label, name) DO NOTHING
940
+ `);
941
+ await conn.run(`
942
+ INSERT INTO nodes (label, name, properties)
943
+ VALUES ('${this.escape(targetLabel)}', '${this.escape(targetName)}', '{}')
944
+ ON CONFLICT (label, name) DO NOTHING
945
+ `);
946
+ const propsJson = properties ? JSON.stringify(properties) : "{}";
947
+ await conn.run(`
948
+ INSERT INTO relationships (source_label, source_name, relation_type, target_label, target_name, properties)
949
+ VALUES (
950
+ '${this.escape(sourceLabel)}',
951
+ '${this.escape(sourceName)}',
952
+ '${this.escape(relation)}',
953
+ '${this.escape(targetLabel)}',
954
+ '${this.escape(targetName)}',
955
+ '${this.escape(propsJson)}'
956
+ )
957
+ ON CONFLICT (source_label, source_name, relation_type, target_label, target_name) DO UPDATE SET
958
+ properties = EXCLUDED.properties
959
+ `);
149
960
  } catch (error) {
150
961
  this.logger.error(`Failed to upsert relationship: ${error instanceof Error ? error.message : String(error)}`);
151
962
  throw error;
@@ -153,10 +964,16 @@ class GraphService {
153
964
  }
154
965
  async deleteNode(label, name) {
155
966
  try {
156
- const escapedLabel = this.escapeCypher(label);
157
- const escapedName = this.escapeCypher(name);
158
- const cypher = `MATCH (n:\`${escapedLabel}\` { name: '${escapedName}' }) ` + `DETACH DELETE n`;
159
- await this.query(cypher);
967
+ const conn = await this.ensureConnected();
968
+ await conn.run(`
969
+ DELETE FROM relationships
970
+ WHERE (source_label = '${this.escape(label)}' AND source_name = '${this.escape(name)}')
971
+ OR (target_label = '${this.escape(label)}' AND target_name = '${this.escape(name)}')
972
+ `);
973
+ await conn.run(`
974
+ DELETE FROM nodes
975
+ WHERE label = '${this.escape(label)}' AND name = '${this.escape(name)}'
976
+ `);
160
977
  } catch (error) {
161
978
  this.logger.error(`Failed to delete node: ${error instanceof Error ? error.message : String(error)}`);
162
979
  throw error;
@@ -164,9 +981,11 @@ class GraphService {
164
981
  }
165
982
  async deleteDocumentRelationships(documentPath) {
166
983
  try {
167
- const escapedPath = this.escapeCypher(documentPath);
168
- const cypher = `MATCH ()-[r { documentPath: '${escapedPath}' }]-() ` + `DELETE r`;
169
- await this.query(cypher);
984
+ const conn = await this.ensureConnected();
985
+ await conn.run(`
986
+ DELETE FROM relationships
987
+ WHERE properties->>'documentPath' = '${this.escape(documentPath)}'
988
+ `);
170
989
  } catch (error) {
171
990
  this.logger.error(`Failed to delete document relationships: ${error instanceof Error ? error.message : String(error)}`);
172
991
  throw error;
@@ -174,11 +993,18 @@ class GraphService {
174
993
  }
175
994
  async findNodesByLabel(label, limit) {
176
995
  try {
177
- const escapedLabel = this.escapeCypher(label);
996
+ const conn = await this.ensureConnected();
178
997
  const limitClause = limit ? ` LIMIT ${limit}` : "";
179
- const cypher = `MATCH (n:\`${escapedLabel}\`) RETURN n${limitClause}`;
180
- const result = await this.query(cypher);
181
- return (result.resultSet || []).map((row) => row[0]);
998
+ const reader = await conn.runAndReadAll(`
999
+ SELECT name, properties
1000
+ FROM nodes
1001
+ WHERE label = '${this.escape(label)}'${limitClause}
1002
+ `);
1003
+ return reader.getRows().map((row) => {
1004
+ const [name, properties] = row;
1005
+ const props = properties ? JSON.parse(properties) : {};
1006
+ return { name, ...props };
1007
+ });
182
1008
  } catch (error) {
183
1009
  this.logger.error(`Failed to find nodes by label: ${error instanceof Error ? error.message : String(error)}`);
184
1010
  return [];
@@ -186,10 +1012,17 @@ class GraphService {
186
1012
  }
187
1013
  async findRelationships(nodeName) {
188
1014
  try {
189
- const escapedName = this.escapeCypher(nodeName);
190
- const cypher = `MATCH (n { name: '${escapedName}' })-[r]-(m) ` + `RETURN type(r), m.name`;
191
- const result = await this.query(cypher);
192
- return result.resultSet || [];
1015
+ const conn = await this.ensureConnected();
1016
+ const reader = await conn.runAndReadAll(`
1017
+ SELECT relation_type, target_name, source_name
1018
+ FROM relationships
1019
+ WHERE source_name = '${this.escape(nodeName)}'
1020
+ OR target_name = '${this.escape(nodeName)}'
1021
+ `);
1022
+ return reader.getRows().map((row) => {
1023
+ const [relType, targetName, sourceName] = row;
1024
+ return [relType, sourceName === nodeName ? targetName : sourceName];
1025
+ });
193
1026
  } catch (error) {
194
1027
  this.logger.error(`Failed to find relationships: ${error instanceof Error ? error.message : String(error)}`);
195
1028
  return [];
@@ -197,27 +1030,39 @@ class GraphService {
197
1030
  }
198
1031
  async createVectorIndex(label, property, dimensions) {
199
1032
  try {
200
- const escapedLabel = this.escapeCypher(label);
201
- const escapedProperty = this.escapeCypher(property);
202
- const cypher = `CREATE VECTOR INDEX FOR (n:\`${escapedLabel}\`) ON (n.\`${escapedProperty}\`) OPTIONS { dimension: ${dimensions}, similarityFunction: 'cosine' }`;
203
- await this.query(cypher);
1033
+ const indexKey = `${label}_${property}`;
1034
+ if (this.vectorIndexes.has(indexKey)) {
1035
+ return;
1036
+ }
1037
+ const conn = await this.ensureConnected();
1038
+ try {
1039
+ await conn.run(`
1040
+ CREATE INDEX idx_embedding_${this.escape(label)}
1041
+ ON nodes USING HNSW (embedding)
1042
+ WITH (metric = 'cosine')
1043
+ `);
1044
+ } catch {
1045
+ this.logger.debug(`Vector index on ${label}.${property} already exists`);
1046
+ }
1047
+ this.vectorIndexes.add(indexKey);
204
1048
  this.logger.log(`Created vector index on ${label}.${property} with ${dimensions} dimensions`);
205
1049
  } catch (error) {
206
1050
  const errorMessage = error instanceof Error ? error.message : String(error);
207
- if (!errorMessage.includes("already indexed")) {
1051
+ if (!errorMessage.includes("already exists")) {
208
1052
  this.logger.error(`Failed to create vector index: ${errorMessage}`);
209
1053
  throw error;
210
1054
  }
211
- this.logger.debug(`Vector index on ${label}.${property} already exists`);
212
1055
  }
213
1056
  }
214
1057
  async updateNodeEmbedding(label, name, embedding) {
215
1058
  try {
216
- const escapedLabel = this.escapeCypher(label);
217
- const escapedName = this.escapeCypher(name);
1059
+ const conn = await this.ensureConnected();
218
1060
  const vectorStr = `[${embedding.join(", ")}]`;
219
- const cypher = `MATCH (n:\`${escapedLabel}\` { name: '${escapedName}' }) ` + `SET n.embedding = vecf32(${vectorStr})`;
220
- await this.query(cypher);
1061
+ await conn.run(`
1062
+ UPDATE nodes
1063
+ SET embedding = ${vectorStr}::FLOAT[${this.embeddingDimensions}]
1064
+ WHERE label = '${this.escape(label)}' AND name = '${this.escape(name)}'
1065
+ `);
221
1066
  } catch (error) {
222
1067
  this.logger.error(`Failed to update node embedding: ${error instanceof Error ? error.message : String(error)}`);
223
1068
  throw error;
@@ -225,133 +1070,286 @@ class GraphService {
225
1070
  }
226
1071
  async vectorSearch(label, queryVector, k = 10) {
227
1072
  try {
228
- const escapedLabel = this.escapeCypher(label);
1073
+ const conn = await this.ensureConnected();
229
1074
  const vectorStr = `[${queryVector.join(", ")}]`;
230
- 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`;
231
- const result = await this.query(cypher);
232
- return (result.resultSet || []).map((row) => ({
233
- name: row[0],
234
- title: row[1],
235
- score: row[2]
236
- }));
1075
+ const reader = await conn.runAndReadAll(`
1076
+ SELECT
1077
+ name,
1078
+ properties->>'title' as title,
1079
+ array_cosine_similarity(embedding, ${vectorStr}::FLOAT[${this.embeddingDimensions}]) as similarity
1080
+ FROM nodes
1081
+ WHERE label = '${this.escape(label)}'
1082
+ AND embedding IS NOT NULL
1083
+ ORDER BY similarity DESC
1084
+ LIMIT ${k}
1085
+ `);
1086
+ return reader.getRows().map((row) => {
1087
+ const [name, title, similarity] = row;
1088
+ return {
1089
+ name,
1090
+ title: title || undefined,
1091
+ score: similarity
1092
+ };
1093
+ });
237
1094
  } catch (error) {
238
1095
  this.logger.error(`Vector search failed: ${error instanceof Error ? error.message : String(error)}`);
239
1096
  throw error;
240
1097
  }
241
1098
  }
242
1099
  async vectorSearchAll(queryVector, k = 10) {
243
- const allLabels = ["Document", "Concept", "Process", "Tool", "Technology", "Organization", "Topic", "Person"];
244
1100
  const allResults = [];
245
- for (const label of allLabels) {
246
- try {
247
- const escapedLabel = this.escapeCypher(label);
248
- const vectorStr = `[${queryVector.join(", ")}]`;
249
- 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`;
250
- const result = await this.query(cypher);
251
- const labelResults = (result.resultSet || []).map((row) => ({
252
- name: row[0],
1101
+ const conn = await this.ensureConnected();
1102
+ const vectorStr = `[${queryVector.join(", ")}]`;
1103
+ try {
1104
+ const reader = await conn.runAndReadAll(`
1105
+ SELECT
1106
+ name,
1107
+ label,
1108
+ properties->>'title' as title,
1109
+ properties->>'description' as description,
1110
+ array_cosine_similarity(embedding, ${vectorStr}::FLOAT[${this.embeddingDimensions}]) as similarity
1111
+ FROM nodes
1112
+ WHERE embedding IS NOT NULL
1113
+ ORDER BY similarity DESC
1114
+ LIMIT ${k}
1115
+ `);
1116
+ for (const row of reader.getRows()) {
1117
+ const [name, label, title, description, similarity] = row;
1118
+ allResults.push({
1119
+ name,
253
1120
  label,
254
- title: row[1],
255
- description: row[2],
256
- score: row[3]
257
- }));
258
- allResults.push(...labelResults);
259
- } catch (error) {
260
- this.logger.debug(`Vector search for ${label} skipped: ${error instanceof Error ? error.message : String(error)}`);
1121
+ title: title || undefined,
1122
+ description: description || undefined,
1123
+ score: similarity
1124
+ });
261
1125
  }
1126
+ } catch (error) {
1127
+ this.logger.debug(`Vector search failed: ${error instanceof Error ? error.message : String(error)}`);
262
1128
  }
263
1129
  return allResults.sort((a, b) => b.score - a.score).slice(0, k);
264
1130
  }
265
- escapeCypher(value) {
266
- return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/"/g, "\\\"");
1131
+ escape(value) {
1132
+ return value.replace(/'/g, "''");
267
1133
  }
268
- escapeCypherValue(value) {
269
- if (value === null || value === undefined) {
270
- return "null";
271
- }
272
- if (typeof value === "string") {
273
- const escaped = this.escapeCypher(value);
274
- return `'${escaped}'`;
275
- }
276
- if (typeof value === "number" || typeof value === "boolean") {
277
- return String(value);
278
- }
279
- if (Array.isArray(value)) {
280
- return `[${value.map((v) => this.escapeCypherValue(v)).join(", ")}]`;
281
- }
282
- if (typeof value === "object") {
283
- const pairs = Object.entries(value).map(([k, v]) => `${k}: ${this.escapeCypherValue(v)}`).join(", ");
284
- return `{${pairs}}`;
285
- }
286
- return String(value);
1134
+ }
1135
+ GraphService = __legacyDecorateClassTS([
1136
+ Injectable6(),
1137
+ __legacyMetadataTS("design:paramtypes", [
1138
+ typeof ConfigService2 === "undefined" ? Object : ConfigService2
1139
+ ])
1140
+ ], GraphService);
1141
+
1142
+ // src/commands/query.command.ts
1143
+ class SearchCommand extends CommandRunner3 {
1144
+ graphService;
1145
+ embeddingService;
1146
+ constructor(graphService, embeddingService) {
1147
+ super();
1148
+ this.graphService = graphService;
1149
+ this.embeddingService = embeddingService;
287
1150
  }
288
- parseStats(result) {
289
- if (!Array.isArray(result) || result.length < 3) {
290
- return;
291
- }
292
- const statsStr = result[2];
293
- if (!statsStr || typeof statsStr !== "string") {
294
- return;
295
- }
296
- const stats = {
297
- nodesCreated: 0,
298
- nodesDeleted: 0,
299
- relationshipsCreated: 0,
300
- relationshipsDeleted: 0,
301
- propertiesSet: 0
302
- };
303
- const nodeCreatedMatch = statsStr.match(/Nodes created: (\d+)/);
304
- if (nodeCreatedMatch) {
305
- stats.nodesCreated = parseInt(nodeCreatedMatch[1], 10);
306
- }
307
- const nodeDeletedMatch = statsStr.match(/Nodes deleted: (\d+)/);
308
- if (nodeDeletedMatch) {
309
- stats.nodesDeleted = parseInt(nodeDeletedMatch[1], 10);
310
- }
311
- const relCreatedMatch = statsStr.match(/Relationships created: (\d+)/);
312
- if (relCreatedMatch) {
313
- stats.relationshipsCreated = parseInt(relCreatedMatch[1], 10);
314
- }
315
- const relDeletedMatch = statsStr.match(/Relationships deleted: (\d+)/);
316
- if (relDeletedMatch) {
317
- stats.relationshipsDeleted = parseInt(relDeletedMatch[1], 10);
1151
+ async run(inputs, options) {
1152
+ const query = inputs[0];
1153
+ const limit = Math.min(parseInt(options.limit || "20", 10), 100);
1154
+ try {
1155
+ const queryEmbedding = await this.embeddingService.generateEmbedding(query);
1156
+ let results;
1157
+ if (options.label) {
1158
+ const labelResults = await this.graphService.vectorSearch(options.label, queryEmbedding, limit);
1159
+ results = labelResults.map((r) => ({
1160
+ name: r.name,
1161
+ label: options.label,
1162
+ title: r.title,
1163
+ score: r.score
1164
+ }));
1165
+ } else {
1166
+ results = await this.graphService.vectorSearchAll(queryEmbedding, limit);
1167
+ }
1168
+ const labelSuffix = options.label ? ` (${options.label})` : "";
1169
+ console.log(`
1170
+ === Semantic Search Results for "${query}"${labelSuffix} ===
1171
+ `);
1172
+ if (results.length === 0) {
1173
+ console.log(`No results found.
1174
+ `);
1175
+ if (options.label) {
1176
+ console.log(`Tip: Try without --label to search all entity types.
1177
+ `);
1178
+ }
1179
+ process.exit(0);
1180
+ }
1181
+ results.forEach((result, idx) => {
1182
+ console.log(`${idx + 1}. [${result.label}] ${result.name}`);
1183
+ if (result.title) {
1184
+ console.log(` Title: ${result.title}`);
1185
+ }
1186
+ if (result.description && result.label !== "Document") {
1187
+ const desc = result.description.length > 80 ? `${result.description.slice(0, 80)}...` : result.description;
1188
+ console.log(` ${desc}`);
1189
+ }
1190
+ console.log(` Similarity: ${(result.score * 100).toFixed(2)}%`);
1191
+ });
1192
+ console.log();
1193
+ process.exit(0);
1194
+ } catch (error) {
1195
+ const errorMsg = error instanceof Error ? error.message : String(error);
1196
+ console.error("Error:", errorMsg);
1197
+ if (errorMsg.includes("no embeddings") || errorMsg.includes("vector")) {
1198
+ console.log(`
1199
+ Note: Semantic search requires embeddings to be generated first.`);
1200
+ console.log(`Run 'lattice sync' to generate embeddings for documents.
1201
+ `);
1202
+ }
1203
+ process.exit(1);
318
1204
  }
319
- const propSetMatch = statsStr.match(/Properties set: (\d+)/);
320
- if (propSetMatch) {
321
- stats.propertiesSet = parseInt(propSetMatch[1], 10);
1205
+ }
1206
+ parseLabel(value) {
1207
+ return value;
1208
+ }
1209
+ parseLimit(value) {
1210
+ return value;
1211
+ }
1212
+ }
1213
+ __legacyDecorateClassTS([
1214
+ Option2({
1215
+ flags: "-l, --label <label>",
1216
+ description: "Filter by entity label (e.g., Technology, Concept, Document)"
1217
+ }),
1218
+ __legacyMetadataTS("design:type", Function),
1219
+ __legacyMetadataTS("design:paramtypes", [
1220
+ String
1221
+ ]),
1222
+ __legacyMetadataTS("design:returntype", String)
1223
+ ], SearchCommand.prototype, "parseLabel", null);
1224
+ __legacyDecorateClassTS([
1225
+ Option2({
1226
+ flags: "--limit <n>",
1227
+ description: "Limit results",
1228
+ defaultValue: "20"
1229
+ }),
1230
+ __legacyMetadataTS("design:type", Function),
1231
+ __legacyMetadataTS("design:paramtypes", [
1232
+ String
1233
+ ]),
1234
+ __legacyMetadataTS("design:returntype", String)
1235
+ ], SearchCommand.prototype, "parseLimit", null);
1236
+ SearchCommand = __legacyDecorateClassTS([
1237
+ Injectable7(),
1238
+ Command3({
1239
+ name: "search",
1240
+ arguments: "<query>",
1241
+ description: "Semantic search across the knowledge graph"
1242
+ }),
1243
+ __legacyMetadataTS("design:paramtypes", [
1244
+ typeof GraphService === "undefined" ? Object : GraphService,
1245
+ typeof EmbeddingService === "undefined" ? Object : EmbeddingService
1246
+ ])
1247
+ ], SearchCommand);
1248
+
1249
+ class RelsCommand extends CommandRunner3 {
1250
+ graphService;
1251
+ constructor(graphService) {
1252
+ super();
1253
+ this.graphService = graphService;
1254
+ }
1255
+ async run(inputs) {
1256
+ const name = inputs[0];
1257
+ try {
1258
+ const relationships = await this.graphService.findRelationships(name);
1259
+ console.log(`
1260
+ === Relationships for "${name}" ===
1261
+ `);
1262
+ if (relationships.length === 0) {
1263
+ console.log(`No relationships found.
1264
+ `);
1265
+ process.exit(0);
1266
+ }
1267
+ console.log("Relationships:");
1268
+ for (const rel of relationships) {
1269
+ const [relType, targetName] = rel;
1270
+ console.log(` -[${relType}]-> ${targetName}`);
1271
+ }
1272
+ console.log();
1273
+ process.exit(0);
1274
+ } catch (error) {
1275
+ console.error("Error:", error instanceof Error ? error.message : String(error));
1276
+ process.exit(1);
322
1277
  }
323
- return stats;
324
1278
  }
325
1279
  }
326
- GraphService = __legacyDecorateClassTS([
327
- Injectable(),
1280
+ RelsCommand = __legacyDecorateClassTS([
1281
+ Injectable7(),
1282
+ Command3({
1283
+ name: "rels",
1284
+ arguments: "<name>",
1285
+ description: "Show relationships for a node"
1286
+ }),
1287
+ __legacyMetadataTS("design:paramtypes", [
1288
+ typeof GraphService === "undefined" ? Object : GraphService
1289
+ ])
1290
+ ], RelsCommand);
1291
+
1292
+ class SqlCommand extends CommandRunner3 {
1293
+ graphService;
1294
+ constructor(graphService) {
1295
+ super();
1296
+ this.graphService = graphService;
1297
+ }
1298
+ async run(inputs) {
1299
+ const query = inputs[0];
1300
+ try {
1301
+ const result = await this.graphService.query(query);
1302
+ console.log(`
1303
+ === SQL Query Results ===
1304
+ `);
1305
+ const replacer = (_key, value) => typeof value === "bigint" ? Number(value) : value;
1306
+ console.log(JSON.stringify(result, replacer, 2));
1307
+ console.log();
1308
+ process.exit(0);
1309
+ } catch (error) {
1310
+ console.error("Error:", error instanceof Error ? error.message : String(error));
1311
+ process.exit(1);
1312
+ }
1313
+ }
1314
+ }
1315
+ SqlCommand = __legacyDecorateClassTS([
1316
+ Injectable7(),
1317
+ Command3({
1318
+ name: "sql",
1319
+ arguments: "<query>",
1320
+ description: "Execute raw SQL query against DuckDB"
1321
+ }),
328
1322
  __legacyMetadataTS("design:paramtypes", [
329
- typeof ConfigService === "undefined" ? Object : ConfigService
1323
+ typeof GraphService === "undefined" ? Object : GraphService
330
1324
  ])
331
- ], GraphService);
332
-
333
- // src/graph/graph.module.ts
334
- class GraphModule {
335
- }
336
- GraphModule = __legacyDecorateClassTS([
337
- Module({
338
- providers: [GraphService],
339
- exports: [GraphService]
340
- })
341
- ], GraphModule);
1325
+ ], SqlCommand);
1326
+ // src/commands/status.command.ts
1327
+ import { Injectable as Injectable12 } from "@nestjs/common";
1328
+ import { Command as Command4, CommandRunner as CommandRunner4, Option as Option3 } from "nest-commander";
342
1329
 
343
- // src/sync/sync.module.ts
344
- import { Module as Module3 } from "@nestjs/common";
1330
+ // src/sync/manifest.service.ts
1331
+ import { createHash as createHash2 } from "crypto";
1332
+ import { existsSync } from "fs";
1333
+ import { readFile as readFile3, writeFile } from "fs/promises";
1334
+ import { resolve as resolve3 } from "path";
1335
+ import { Injectable as Injectable8 } from "@nestjs/common";
345
1336
 
346
- // src/sync/sync.service.ts
347
- import { Injectable as Injectable7, Logger as Logger6 } from "@nestjs/common";
1337
+ // src/schemas/manifest.schemas.ts
1338
+ import { z as z4 } from "zod";
1339
+ var ManifestEntrySchema = z4.object({
1340
+ contentHash: z4.string(),
1341
+ frontmatterHash: z4.string(),
1342
+ lastSynced: z4.string(),
1343
+ entityCount: z4.number().int().nonnegative(),
1344
+ relationshipCount: z4.number().int().nonnegative()
1345
+ });
1346
+ var SyncManifestSchema = z4.object({
1347
+ version: z4.string(),
1348
+ lastSync: z4.string(),
1349
+ documents: z4.record(z4.string(), ManifestEntrySchema)
1350
+ });
348
1351
 
349
1352
  // src/sync/manifest.service.ts
350
- import { Injectable as Injectable2 } from "@nestjs/common";
351
- import { createHash } from "crypto";
352
- import { readFile, writeFile } from "fs/promises";
353
- import { existsSync } from "fs";
354
- import { resolve } from "path";
355
1353
  function getProjectRoot() {
356
1354
  if (process.env.PROJECT_ROOT) {
357
1355
  return process.env.PROJECT_ROOT;
@@ -364,17 +1362,17 @@ class ManifestService {
364
1362
  manifest = null;
365
1363
  constructor() {
366
1364
  const docsPath = process.env.DOCS_PATH || "docs";
367
- this.manifestPath = resolve(getProjectRoot(), docsPath, ".sync-manifest.json");
1365
+ this.manifestPath = resolve3(getProjectRoot(), docsPath, ".sync-manifest.json");
368
1366
  }
369
1367
  async load() {
370
1368
  try {
371
1369
  if (existsSync(this.manifestPath)) {
372
- const content = await readFile(this.manifestPath, "utf-8");
373
- this.manifest = JSON.parse(content);
1370
+ const content = await readFile3(this.manifestPath, "utf-8");
1371
+ this.manifest = SyncManifestSchema.parse(JSON.parse(content));
374
1372
  } else {
375
1373
  this.manifest = this.createEmptyManifest();
376
1374
  }
377
- } catch (error) {
1375
+ } catch (_error) {
378
1376
  this.manifest = this.createEmptyManifest();
379
1377
  }
380
1378
  return this.manifest;
@@ -388,13 +1386,13 @@ class ManifestService {
388
1386
  await writeFile(this.manifestPath, content, "utf-8");
389
1387
  }
390
1388
  getContentHash(content) {
391
- return createHash("sha256").update(content).digest("hex");
1389
+ return createHash2("sha256").update(content).digest("hex");
392
1390
  }
393
- detectChange(path, contentHash, frontmatterHash) {
1391
+ detectChange(path2, contentHash, frontmatterHash) {
394
1392
  if (!this.manifest) {
395
1393
  throw new Error("Manifest not loaded. Call load() first.");
396
1394
  }
397
- const existing = this.manifest.documents[path];
1395
+ const existing = this.manifest.documents[path2];
398
1396
  if (!existing) {
399
1397
  return "new";
400
1398
  }
@@ -403,11 +1401,11 @@ class ManifestService {
403
1401
  }
404
1402
  return "updated";
405
1403
  }
406
- updateEntry(path, contentHash, frontmatterHash, entityCount, relationshipCount) {
1404
+ updateEntry(path2, contentHash, frontmatterHash, entityCount, relationshipCount) {
407
1405
  if (!this.manifest) {
408
1406
  throw new Error("Manifest not loaded. Call load() first.");
409
1407
  }
410
- this.manifest.documents[path] = {
1408
+ this.manifest.documents[path2] = {
411
1409
  contentHash,
412
1410
  frontmatterHash,
413
1411
  lastSynced: new Date().toISOString(),
@@ -415,11 +1413,11 @@ class ManifestService {
415
1413
  relationshipCount
416
1414
  };
417
1415
  }
418
- removeEntry(path) {
1416
+ removeEntry(path2) {
419
1417
  if (!this.manifest) {
420
1418
  throw new Error("Manifest not loaded. Call load() first.");
421
1419
  }
422
- delete this.manifest.documents[path];
1420
+ delete this.manifest.documents[path2];
423
1421
  }
424
1422
  getTrackedPaths() {
425
1423
  if (!this.manifest) {
@@ -436,291 +1434,119 @@ class ManifestService {
436
1434
  }
437
1435
  }
438
1436
  ManifestService = __legacyDecorateClassTS([
439
- Injectable2(),
1437
+ Injectable8(),
440
1438
  __legacyMetadataTS("design:paramtypes", [])
441
1439
  ], ManifestService);
442
1440
 
443
- // src/sync/document-parser.service.ts
444
- import { Injectable as Injectable3, Logger as Logger2 } from "@nestjs/common";
445
- import { glob } from "glob";
446
- import { readFile as readFile2 } from "fs/promises";
447
- import { createHash as createHash2 } from "crypto";
448
- import { resolve as resolve2 } from "path";
1441
+ // src/sync/sync.service.ts
1442
+ import { Injectable as Injectable11, Logger as Logger5 } from "@nestjs/common";
449
1443
 
450
- // src/utils/frontmatter.ts
451
- import { z } from "zod";
452
- import matter from "gray-matter";
453
- var EntityTypeSchema = z.enum([
454
- "Topic",
455
- "Technology",
456
- "Concept",
457
- "Tool",
458
- "Process",
459
- "Person",
460
- "Organization",
461
- "Document"
462
- ]);
463
- var RelationTypeSchema = z.enum([
464
- "REFERENCES"
465
- ]);
466
- var EntitySchema = z.object({
467
- name: z.string().min(1),
468
- type: EntityTypeSchema,
469
- description: z.string().optional()
470
- });
471
- var RelationshipSchema = z.object({
472
- source: z.string().min(1),
473
- relation: RelationTypeSchema,
474
- target: z.string().min(1)
475
- });
476
- var GraphMetadataSchema = z.object({
477
- importance: z.enum(["high", "medium", "low"]).optional(),
478
- domain: z.string().optional()
479
- });
480
- var validateDateFormat = (dateStr) => {
481
- const match = dateStr.match(/^(\d{4})-(\d{2})-(\d{2})$/);
482
- if (!match)
483
- return false;
484
- const [, yearStr, monthStr, dayStr] = match;
485
- const year = parseInt(yearStr, 10);
486
- const month = parseInt(monthStr, 10);
487
- const day = parseInt(dayStr, 10);
488
- if (month < 1 || month > 12)
489
- return false;
490
- if (day < 1 || day > 31)
491
- return false;
492
- const daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
493
- if (year % 4 === 0 && year % 100 !== 0 || year % 400 === 0) {
494
- daysInMonth[1] = 29;
1444
+ // src/pure/embedding-text.ts
1445
+ function composeDocumentEmbeddingText(doc) {
1446
+ const parts = [];
1447
+ if (doc.title) {
1448
+ parts.push(`Title: ${doc.title}`);
495
1449
  }
496
- return day <= daysInMonth[month - 1];
497
- };
498
- var FrontmatterSchema = z.object({
499
- created: z.string().refine(validateDateFormat, "Date must be in YYYY-MM-DD format"),
500
- updated: z.string().refine(validateDateFormat, "Date must be in YYYY-MM-DD format"),
501
- status: z.enum(["draft", "ongoing", "complete"]).optional(),
502
- topic: z.string().optional(),
503
- tags: z.array(z.string()).optional(),
504
- summary: z.string().optional(),
505
- entities: z.array(EntitySchema).optional(),
506
- relationships: z.array(RelationshipSchema).optional(),
507
- graph: GraphMetadataSchema.optional()
508
- }).passthrough();
509
- function parseFrontmatter(content) {
510
- try {
511
- const { data, content: markdown } = matter(content);
512
- if (Object.keys(data).length === 0) {
513
- return {
514
- frontmatter: null,
515
- content: markdown.trim(),
516
- raw: content
517
- };
518
- }
519
- const normalizedData = normalizeData(data);
520
- const validated = FrontmatterSchema.safeParse(normalizedData);
521
- return {
522
- frontmatter: validated.success ? validated.data : normalizedData,
523
- content: markdown.trim(),
524
- raw: content
525
- };
526
- } catch (error) {
527
- const errorMessage = error instanceof Error ? error.message : String(error);
528
- throw new Error(`YAML parsing error: ${errorMessage}`);
1450
+ if (doc.topic) {
1451
+ parts.push(`Topic: ${doc.topic}`);
529
1452
  }
530
- }
531
- function normalizeData(data) {
532
- if (data instanceof Date) {
533
- return data.toISOString().split("T")[0];
1453
+ if (doc.tags && doc.tags.length > 0) {
1454
+ parts.push(`Tags: ${doc.tags.join(", ")}`);
534
1455
  }
535
- if (Array.isArray(data)) {
536
- return data.map(normalizeData);
1456
+ if (doc.entities && doc.entities.length > 0) {
1457
+ const entityNames = doc.entities.map((e) => e.name).join(", ");
1458
+ parts.push(`Entities: ${entityNames}`);
537
1459
  }
538
- if (data !== null && typeof data === "object") {
539
- const normalized = {};
540
- for (const [key, value] of Object.entries(data)) {
541
- normalized[key] = normalizeData(value);
542
- }
543
- return normalized;
1460
+ if (doc.summary) {
1461
+ parts.push(doc.summary);
1462
+ } else {
1463
+ parts.push(doc.content.slice(0, 500));
544
1464
  }
545
- return data;
1465
+ return parts.join(" | ");
546
1466
  }
547
-
548
- // src/sync/document-parser.service.ts
549
- function getProjectRoot2() {
550
- if (process.env.PROJECT_ROOT) {
551
- return process.env.PROJECT_ROOT;
1467
+ function composeEntityEmbeddingText(entity) {
1468
+ const parts = [`${entity.type}: ${entity.name}`];
1469
+ if (entity.description) {
1470
+ parts.push(entity.description);
552
1471
  }
553
- return process.cwd();
1472
+ return parts.join(". ");
554
1473
  }
555
-
556
- class DocumentParserService {
557
- logger = new Logger2(DocumentParserService.name);
558
- docsPath;
559
- constructor() {
560
- const docsPathEnv = process.env.DOCS_PATH || "docs";
561
- if (docsPathEnv.startsWith("/")) {
562
- this.docsPath = docsPathEnv;
563
- } else {
564
- this.docsPath = resolve2(getProjectRoot2(), docsPathEnv);
565
- }
566
- }
567
- getDocsPath() {
568
- return this.docsPath;
569
- }
570
- async discoverDocuments() {
571
- const pattern = `${this.docsPath}/**/*.md`;
572
- const files = await glob(pattern, {
573
- ignore: ["**/node_modules/**", "**/.git/**"]
574
- });
575
- return files.sort();
576
- }
577
- async parseDocument(filePath) {
578
- const content = await readFile2(filePath, "utf-8");
579
- const parsed = parseFrontmatter(content);
580
- const title = this.extractTitle(content, filePath);
581
- const contentHash = this.computeHash(content);
582
- const frontmatterHash = this.computeHash(JSON.stringify(parsed.frontmatter || {}));
583
- const entities = this.extractEntities(parsed.frontmatter, filePath);
584
- const relationships = this.extractRelationships(parsed.frontmatter, filePath);
585
- const graphMetadata = this.extractGraphMetadata(parsed.frontmatter);
586
- return {
587
- path: filePath,
588
- title,
589
- content: parsed.content,
590
- contentHash,
591
- frontmatterHash,
592
- summary: parsed.frontmatter?.summary,
593
- topic: parsed.frontmatter?.topic,
594
- entities,
595
- relationships,
596
- graphMetadata,
597
- tags: parsed.frontmatter?.tags || [],
598
- created: parsed.frontmatter?.created,
599
- updated: parsed.frontmatter?.updated,
600
- status: parsed.frontmatter?.status
601
- };
602
- }
603
- async parseAllDocuments() {
604
- const { docs } = await this.parseAllDocumentsWithErrors();
605
- return docs;
606
- }
607
- async parseAllDocumentsWithErrors() {
608
- const files = await this.discoverDocuments();
609
- const docs = [];
610
- const errors = [];
611
- for (const file of files) {
612
- try {
613
- const parsed = await this.parseDocument(file);
614
- docs.push(parsed);
615
- } catch (error) {
616
- const errorMsg = error instanceof Error ? error.message : String(error);
617
- errors.push({ path: file, error: errorMsg });
618
- this.logger.warn(`Failed to parse ${file}: ${error}`);
619
- }
620
- }
621
- return { docs, errors };
622
- }
623
- extractTitle(content, filePath) {
624
- const h1Match = content.match(/^#\s+(.+)$/m);
625
- if (h1Match) {
626
- return h1Match[1];
627
- }
628
- const parts = filePath.split("/");
629
- return parts[parts.length - 1].replace(".md", "");
630
- }
631
- extractEntities(frontmatter, docPath) {
632
- if (!frontmatter?.entities || !Array.isArray(frontmatter.entities)) {
633
- return [];
634
- }
635
- const validEntities = [];
636
- const errors = [];
637
- for (let i = 0;i < frontmatter.entities.length; i++) {
638
- const e = frontmatter.entities[i];
639
- const result = EntitySchema.safeParse(e);
640
- if (result.success) {
641
- validEntities.push(result.data);
1474
+ function collectUniqueEntities(docs) {
1475
+ const entities = new Map;
1476
+ for (const doc of docs) {
1477
+ for (const entity of doc.entities) {
1478
+ const key = `${entity.type}:${entity.name}`;
1479
+ const existing = entities.get(key);
1480
+ if (!existing) {
1481
+ entities.set(key, {
1482
+ type: entity.type,
1483
+ name: entity.name,
1484
+ description: entity.description,
1485
+ documentPaths: [doc.path]
1486
+ });
642
1487
  } else {
643
- const entityPreview = typeof e === "string" ? `"${e}"` : JSON.stringify(e);
644
- errors.push(`Entity[${i}]: ${entityPreview} - Expected object with {name, type}, got ${typeof e}`);
1488
+ existing.documentPaths.push(doc.path);
1489
+ if (entity.description && (!existing.description || entity.description.length > existing.description.length)) {
1490
+ existing.description = entity.description;
1491
+ }
645
1492
  }
646
1493
  }
647
- if (errors.length > 0) {
648
- const errorMsg = `Invalid entity schema in ${docPath}:
649
- ${errors.join(`
650
- `)}`;
651
- throw new Error(errorMsg);
652
- }
653
- return validEntities;
654
1494
  }
655
- extractRelationships(frontmatter, docPath) {
656
- if (!frontmatter?.relationships || !Array.isArray(frontmatter.relationships)) {
657
- return [];
658
- }
659
- const validRelationships = [];
660
- const errors = [];
661
- const validRelationTypes = RelationTypeSchema.options;
662
- for (let i = 0;i < frontmatter.relationships.length; i++) {
663
- const r = frontmatter.relationships[i];
664
- const result = RelationshipSchema.safeParse(r);
665
- if (result.success) {
666
- const rel = result.data;
667
- if (rel.source === "this") {
668
- rel.source = docPath;
669
- }
670
- if (rel.target === "this") {
671
- rel.target = docPath;
672
- }
673
- validRelationships.push(rel);
674
- } else {
675
- if (typeof r === "string") {
676
- errors.push(`Relationship[${i}]: "${r}" - Expected object with {source, relation, target}, got string`);
677
- } else if (typeof r === "object" && r !== null) {
678
- const issues = [];
679
- if (!r.source)
680
- issues.push("missing source");
681
- if (!r.target)
682
- issues.push("missing target");
683
- if (!r.relation) {
684
- issues.push("missing relation");
685
- } else if (!validRelationTypes.includes(r.relation)) {
686
- issues.push(`invalid relation "${r.relation}" (allowed: ${validRelationTypes.join(", ")})`);
687
- }
688
- errors.push(`Relationship[${i}]: ${issues.join(", ")}`);
689
- } else {
690
- errors.push(`Relationship[${i}]: Expected object, got ${typeof r}`);
691
- }
1495
+ return entities;
1496
+ }
1497
+ // src/pure/validation.ts
1498
+ function validateDocuments(docs) {
1499
+ const errors = [];
1500
+ const entityIndex = new Map;
1501
+ for (const doc of docs) {
1502
+ for (const entity of doc.entities) {
1503
+ let docSet = entityIndex.get(entity.name);
1504
+ if (!docSet) {
1505
+ docSet = new Set;
1506
+ entityIndex.set(entity.name, docSet);
692
1507
  }
1508
+ docSet.add(doc.path);
693
1509
  }
694
- if (errors.length > 0) {
695
- const errorMsg = `Invalid relationship schema in ${docPath}:
696
- ${errors.join(`
697
- `)}`;
698
- throw new Error(errorMsg);
699
- }
700
- return validRelationships;
701
1510
  }
702
- extractGraphMetadata(frontmatter) {
703
- if (!frontmatter?.graph) {
704
- return;
1511
+ for (const doc of docs) {
1512
+ for (const rel of doc.relationships) {
1513
+ if (rel.source !== doc.path && !entityIndex.has(rel.source)) {
1514
+ errors.push({
1515
+ path: doc.path,
1516
+ error: `Relationship source "${rel.source}" not found in any document`
1517
+ });
1518
+ }
1519
+ const isDocPath = rel.target.endsWith(".md");
1520
+ const isKnownEntity = entityIndex.has(rel.target);
1521
+ const isSelfReference = rel.target === doc.path;
1522
+ if (!isDocPath && !isKnownEntity && !isSelfReference) {
1523
+ errors.push({
1524
+ path: doc.path,
1525
+ error: `Relationship target "${rel.target}" not found as entity`
1526
+ });
1527
+ }
705
1528
  }
706
- const result = GraphMetadataSchema.safeParse(frontmatter.graph);
707
- return result.success ? result.data : undefined;
708
1529
  }
709
- computeHash(content) {
710
- return createHash2("sha256").update(content).digest("hex");
1530
+ return errors;
1531
+ }
1532
+ function getChangeReason(changeType) {
1533
+ switch (changeType) {
1534
+ case "new":
1535
+ return "New document";
1536
+ case "updated":
1537
+ return "Content or frontmatter changed";
1538
+ case "deleted":
1539
+ return "File no longer exists";
1540
+ case "unchanged":
1541
+ return "No changes detected";
711
1542
  }
712
1543
  }
713
- DocumentParserService = __legacyDecorateClassTS([
714
- Injectable3(),
715
- __legacyMetadataTS("design:paramtypes", [])
716
- ], DocumentParserService);
717
-
718
1544
  // src/sync/cascade.service.ts
719
- import { Injectable as Injectable4, Logger as Logger3 } from "@nestjs/common";
1545
+ import { Injectable as Injectable9, Logger as Logger4 } from "@nestjs/common";
720
1546
  class CascadeService {
721
1547
  graph;
722
1548
  _parser;
723
- logger = new Logger3(CascadeService.name);
1549
+ logger = new Logger4(CascadeService.name);
724
1550
  constructor(graph, _parser) {
725
1551
  this.graph = graph;
726
1552
  this._parser = _parser;
@@ -828,10 +1654,13 @@ class CascadeService {
828
1654
  }
829
1655
  async findAffectedByRename(entityName, _newName) {
830
1656
  try {
831
- const escapedName = this.escapeForCypher(entityName);
1657
+ const escapedName = this.escapeForSql(entityName);
832
1658
  const query = `
833
- MATCH (e {name: '${escapedName}'})-[:APPEARS_IN]->(d:Document)
834
- RETURN d.name, d.title
1659
+ SELECT DISTINCT n.name, n.properties->>'title' as title
1660
+ FROM nodes n
1661
+ INNER JOIN relationships r ON r.target_label = n.label AND r.target_name = n.name
1662
+ WHERE r.source_name = '${escapedName}'
1663
+ AND n.label = 'Document'
835
1664
  `.trim();
836
1665
  const result = await this.graph.query(query);
837
1666
  return (result.resultSet || []).map((row) => ({
@@ -848,10 +1677,13 @@ class CascadeService {
848
1677
  }
849
1678
  async findAffectedByDeletion(entityName) {
850
1679
  try {
851
- const escapedName = this.escapeForCypher(entityName);
1680
+ const escapedName = this.escapeForSql(entityName);
852
1681
  const query = `
853
- MATCH (e {name: '${escapedName}'})-[:APPEARS_IN]->(d:Document)
854
- RETURN d.name, d.title
1682
+ SELECT DISTINCT n.name, n.properties->>'title' as title
1683
+ FROM nodes n
1684
+ INNER JOIN relationships r ON r.target_label = n.label AND r.target_name = n.name
1685
+ WHERE r.source_name = '${escapedName}'
1686
+ AND n.label = 'Document'
855
1687
  `.trim();
856
1688
  const result = await this.graph.query(query);
857
1689
  return (result.resultSet || []).map((row) => ({
@@ -868,10 +1700,13 @@ class CascadeService {
868
1700
  }
869
1701
  async findAffectedByTypeChange(entityName, oldType, newType) {
870
1702
  try {
871
- const escapedName = this.escapeForCypher(entityName);
1703
+ const escapedName = this.escapeForSql(entityName);
872
1704
  const query = `
873
- MATCH (e {name: '${escapedName}'})-[:APPEARS_IN]->(d:Document)
874
- RETURN d.name, d.title
1705
+ SELECT DISTINCT n.name, n.properties->>'title' as title
1706
+ FROM nodes n
1707
+ INNER JOIN relationships r ON r.target_label = n.label AND r.target_name = n.name
1708
+ WHERE r.source_name = '${escapedName}'
1709
+ AND n.label = 'Document'
875
1710
  `.trim();
876
1711
  const result = await this.graph.query(query);
877
1712
  return (result.resultSet || []).map((row) => ({
@@ -888,10 +1723,13 @@ class CascadeService {
888
1723
  }
889
1724
  async findAffectedByRelationshipChange(entityName) {
890
1725
  try {
891
- const escapedName = this.escapeForCypher(entityName);
1726
+ const escapedName = this.escapeForSql(entityName);
892
1727
  const query = `
893
- MATCH (e {name: '${escapedName}'})-[r]->(d:Document)
894
- RETURN d.name, d.title, type(r) as relType
1728
+ SELECT DISTINCT n.name, n.properties->>'title' as title, r.relation_type
1729
+ FROM nodes n
1730
+ INNER JOIN relationships r ON r.target_label = n.label AND r.target_name = n.name
1731
+ WHERE r.source_name = '${escapedName}'
1732
+ AND n.label = 'Document'
895
1733
  `.trim();
896
1734
  const result = await this.graph.query(query);
897
1735
  return (result.resultSet || []).map((row) => ({
@@ -908,10 +1746,13 @@ class CascadeService {
908
1746
  }
909
1747
  async findAffectedByDocumentDeletion(documentPath) {
910
1748
  try {
911
- const escapedPath = this.escapeForCypher(documentPath);
1749
+ const escapedPath = this.escapeForSql(documentPath);
912
1750
  const query = `
913
- MATCH (d:Document)-[r]->(deleted:Document {name: '${escapedPath}'})
914
- RETURN d.name, type(r) as relType
1751
+ SELECT DISTINCT n.name, r.relation_type
1752
+ FROM nodes n
1753
+ INNER JOIN relationships r ON r.source_label = n.label AND r.source_name = n.name
1754
+ WHERE r.target_name = '${escapedPath}'
1755
+ AND n.label = 'Document'
915
1756
  `.trim();
916
1757
  const result = await this.graph.query(query);
917
1758
  return (result.resultSet || []).map((row) => ({
@@ -998,12 +1839,12 @@ class CascadeService {
998
1839
  return action;
999
1840
  }
1000
1841
  }
1001
- escapeForCypher(value) {
1002
- return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/"/g, "\\\"");
1842
+ escapeForSql(value) {
1843
+ return value.replace(/'/g, "''");
1003
1844
  }
1004
1845
  }
1005
1846
  CascadeService = __legacyDecorateClassTS([
1006
- Injectable4(),
1847
+ Injectable9(),
1007
1848
  __legacyMetadataTS("design:paramtypes", [
1008
1849
  typeof GraphService === "undefined" ? Object : GraphService,
1009
1850
  typeof DocumentParserService === "undefined" ? Object : DocumentParserService
@@ -1011,12 +1852,11 @@ CascadeService = __legacyDecorateClassTS([
1011
1852
  ], CascadeService);
1012
1853
 
1013
1854
  // src/sync/path-resolver.service.ts
1014
- import { Injectable as Injectable5, Logger as Logger4 } from "@nestjs/common";
1015
- import { resolve as resolve3, isAbsolute } from "path";
1016
1855
  import { existsSync as existsSync2 } from "fs";
1856
+ import { isAbsolute, resolve as resolve4 } from "path";
1857
+ import { Injectable as Injectable10 } from "@nestjs/common";
1017
1858
  class PathResolverService {
1018
1859
  parser;
1019
- logger = new Logger4(PathResolverService.name);
1020
1860
  constructor(parser) {
1021
1861
  this.parser = parser;
1022
1862
  }
@@ -1029,10 +1869,10 @@ class PathResolverService {
1029
1869
  if (isAbsolute(userPath)) {
1030
1870
  resolvedPath = userPath;
1031
1871
  } else {
1032
- const fromCwd = resolve3(process.cwd(), userPath);
1033
- const fromDocs = resolve3(this.getDocsPath(), userPath);
1872
+ const fromCwd = resolve4(process.cwd(), userPath);
1873
+ const fromDocs = resolve4(this.getDocsPath(), userPath);
1034
1874
  const docsPrefix = "docs/";
1035
- const strippedFromDocs = userPath.startsWith(docsPrefix) ? resolve3(this.getDocsPath(), userPath.slice(docsPrefix.length)) : null;
1875
+ const strippedFromDocs = userPath.startsWith(docsPrefix) ? resolve4(this.getDocsPath(), userPath.slice(docsPrefix.length)) : null;
1036
1876
  if (this.isUnderDocs(fromCwd) && existsSync2(fromCwd)) {
1037
1877
  resolvedPath = fromCwd;
1038
1878
  } else if (strippedFromDocs && existsSync2(strippedFromDocs)) {
@@ -1053,268 +1893,31 @@ class PathResolverService {
1053
1893
  if (requireExists && !existsSync2(resolvedPath)) {
1054
1894
  throw new Error(`Path "${userPath}" does not exist (resolved to: ${resolvedPath})`);
1055
1895
  }
1056
- return resolvedPath;
1057
- }
1058
- resolveDocPaths(userPaths, options = {}) {
1059
- return userPaths.map((p) => this.resolveDocPath(p, options));
1060
- }
1061
- isUnderDocs(absolutePath) {
1062
- const docsPath = this.getDocsPath();
1063
- const normalizedPath = absolutePath.replace(/\/$/, "");
1064
- const normalizedDocs = docsPath.replace(/\/$/, "");
1065
- return normalizedPath.startsWith(normalizedDocs + "/") || normalizedPath === normalizedDocs;
1066
- }
1067
- getRelativePath(absolutePath) {
1068
- const docsPath = this.getDocsPath();
1069
- if (absolutePath.startsWith(docsPath)) {
1070
- return absolutePath.slice(docsPath.length + 1);
1071
- }
1072
- return absolutePath;
1073
- }
1074
- }
1075
- PathResolverService = __legacyDecorateClassTS([
1076
- Injectable5(),
1077
- __legacyMetadataTS("design:paramtypes", [
1078
- typeof DocumentParserService === "undefined" ? Object : DocumentParserService
1079
- ])
1080
- ], PathResolverService);
1081
-
1082
- // src/embedding/embedding.service.ts
1083
- import { Injectable as Injectable6, Logger as Logger5 } from "@nestjs/common";
1084
- import { ConfigService as ConfigService2 } from "@nestjs/config";
1085
-
1086
- // src/embedding/embedding.types.ts
1087
- var DEFAULT_EMBEDDING_CONFIG = {
1088
- provider: "voyage",
1089
- model: "voyage-3.5-lite",
1090
- dimensions: 512
1091
- };
1092
-
1093
- // src/embedding/providers/openai.provider.ts
1094
- class OpenAIEmbeddingProvider {
1095
- name = "openai";
1096
- dimensions;
1097
- model;
1098
- apiKey;
1099
- baseUrl = "https://api.openai.com/v1";
1100
- constructor(config) {
1101
- const apiKey = config?.apiKey || process.env.OPENAI_API_KEY;
1102
- if (!apiKey) {
1103
- throw new Error("OpenAI API key is required. Set OPENAI_API_KEY environment variable or pass apiKey in config.");
1104
- }
1105
- this.apiKey = apiKey;
1106
- this.model = config?.model || "text-embedding-3-small";
1107
- this.dimensions = config?.dimensions || 1536;
1108
- }
1109
- async generateEmbedding(text) {
1110
- const embeddings = await this.generateEmbeddings([text]);
1111
- return embeddings[0];
1112
- }
1113
- async generateEmbeddings(texts) {
1114
- if (!texts || texts.length === 0) {
1115
- return [];
1116
- }
1117
- try {
1118
- const response = await fetch(`${this.baseUrl}/embeddings`, {
1119
- method: "POST",
1120
- headers: {
1121
- "Content-Type": "application/json",
1122
- Authorization: `Bearer ${this.apiKey}`
1123
- },
1124
- body: JSON.stringify({
1125
- model: this.model,
1126
- input: texts,
1127
- dimensions: this.dimensions
1128
- })
1129
- });
1130
- if (!response.ok) {
1131
- const error = await response.json().catch(() => ({}));
1132
- throw new Error(`OpenAI API error: ${response.status} ${JSON.stringify(error)}`);
1133
- }
1134
- const data = await response.json();
1135
- const sortedData = data.data.sort((a, b) => a.index - b.index);
1136
- return sortedData.map((item) => item.embedding);
1137
- } catch (error) {
1138
- if (error instanceof Error) {
1139
- throw new Error(`Failed to generate embeddings: ${error.message}`);
1140
- }
1141
- throw error;
1142
- }
1143
- }
1144
- }
1145
-
1146
- // src/embedding/providers/voyage.provider.ts
1147
- class VoyageEmbeddingProvider {
1148
- name = "voyage";
1149
- dimensions;
1150
- model;
1151
- apiKey;
1152
- inputType;
1153
- baseUrl = "https://api.voyageai.com/v1";
1154
- constructor(config) {
1155
- const apiKey = config?.apiKey || process.env.VOYAGE_API_KEY;
1156
- if (!apiKey) {
1157
- throw new Error("Voyage API key is required. Set VOYAGE_API_KEY environment variable or pass apiKey in config.");
1158
- }
1159
- this.apiKey = apiKey;
1160
- this.model = config?.model || "voyage-3.5-lite";
1161
- this.dimensions = config?.dimensions || 512;
1162
- this.inputType = config?.inputType || "document";
1163
- }
1164
- async generateEmbedding(text) {
1165
- const embeddings = await this.generateEmbeddings([text]);
1166
- return embeddings[0];
1167
- }
1168
- async generateEmbeddings(texts) {
1169
- if (!texts || texts.length === 0) {
1170
- return [];
1171
- }
1172
- try {
1173
- const response = await fetch(`${this.baseUrl}/embeddings`, {
1174
- method: "POST",
1175
- headers: {
1176
- "Content-Type": "application/json",
1177
- Authorization: `Bearer ${this.apiKey}`
1178
- },
1179
- body: JSON.stringify({
1180
- model: this.model,
1181
- input: texts,
1182
- output_dimension: this.dimensions,
1183
- input_type: this.inputType
1184
- })
1185
- });
1186
- if (!response.ok) {
1187
- const error = await response.json().catch(() => ({}));
1188
- throw new Error(`Voyage API error: ${response.status} ${JSON.stringify(error)}`);
1189
- }
1190
- const data = await response.json();
1191
- const sortedData = data.data.sort((a, b) => a.index - b.index);
1192
- return sortedData.map((item) => item.embedding);
1193
- } catch (error) {
1194
- if (error instanceof Error) {
1195
- throw new Error(`Failed to generate embeddings: ${error.message}`);
1196
- }
1197
- throw error;
1198
- }
1199
- }
1200
- }
1201
-
1202
- // src/embedding/providers/mock.provider.ts
1203
- class MockEmbeddingProvider {
1204
- name = "mock";
1205
- dimensions;
1206
- constructor(dimensions = 1536) {
1207
- this.dimensions = dimensions;
1208
- }
1209
- async generateEmbedding(text) {
1210
- return this.generateDeterministicEmbedding(text);
1211
- }
1212
- async generateEmbeddings(texts) {
1213
- return Promise.all(texts.map((text) => this.generateEmbedding(text)));
1214
- }
1215
- generateDeterministicEmbedding(text) {
1216
- const embedding = new Array(this.dimensions);
1217
- let hash = 0;
1218
- for (let i = 0;i < text.length; i++) {
1219
- hash = (hash << 5) - hash + text.charCodeAt(i);
1220
- hash = hash & hash;
1221
- }
1222
- for (let i = 0;i < this.dimensions; i++) {
1223
- const seed = hash * (i + 1) ^ i * 31;
1224
- embedding[i] = (seed % 2000 - 1000) / 1000;
1225
- }
1226
- const magnitude = Math.sqrt(embedding.reduce((sum, val) => sum + val * val, 0));
1227
- return embedding.map((val) => val / magnitude);
1228
- }
1229
- }
1230
-
1231
- // src/embedding/embedding.service.ts
1232
- class EmbeddingService {
1233
- configService;
1234
- logger = new Logger5(EmbeddingService.name);
1235
- provider;
1236
- config;
1237
- constructor(configService) {
1238
- this.configService = configService;
1239
- this.config = this.loadConfig();
1240
- this.provider = this.createProvider();
1241
- this.logger.log(`Initialized embedding service with provider: ${this.provider.name}`);
1242
- }
1243
- loadConfig() {
1244
- const providerEnv = this.configService.get("EMBEDDING_PROVIDER");
1245
- const modelEnv = this.configService.get("EMBEDDING_MODEL");
1246
- const dimensionsEnv = this.configService.get("EMBEDDING_DIMENSIONS");
1247
- const provider = providerEnv ?? DEFAULT_EMBEDDING_CONFIG.provider;
1248
- let apiKey;
1249
- if (provider === "voyage") {
1250
- apiKey = this.configService.get("VOYAGE_API_KEY");
1251
- } else if (provider === "openai") {
1252
- apiKey = this.configService.get("OPENAI_API_KEY");
1253
- }
1254
- return {
1255
- provider,
1256
- apiKey,
1257
- model: modelEnv ?? DEFAULT_EMBEDDING_CONFIG.model,
1258
- dimensions: dimensionsEnv ? Number(dimensionsEnv) : DEFAULT_EMBEDDING_CONFIG.dimensions
1259
- };
1260
- }
1261
- createProvider() {
1262
- switch (this.config.provider) {
1263
- case "openai":
1264
- if (!this.config.apiKey) {
1265
- throw new Error("OPENAI_API_KEY environment variable is required for embeddings. " + "Set it in .env or use --no-embeddings to skip embedding generation.");
1266
- }
1267
- return new OpenAIEmbeddingProvider({
1268
- apiKey: this.config.apiKey,
1269
- model: this.config.model,
1270
- dimensions: this.config.dimensions
1271
- });
1272
- case "mock":
1273
- return new MockEmbeddingProvider(this.config.dimensions);
1274
- case "voyage":
1275
- if (!this.config.apiKey) {
1276
- throw new Error("VOYAGE_API_KEY environment variable is required for embeddings. " + "Set it in .env or use --no-embeddings to skip embedding generation.");
1277
- }
1278
- return new VoyageEmbeddingProvider({
1279
- apiKey: this.config.apiKey,
1280
- model: this.config.model,
1281
- dimensions: this.config.dimensions
1282
- });
1283
- case "nomic":
1284
- throw new Error(`Provider ${this.config.provider} not yet implemented. Use 'voyage', 'openai', or 'mock'.`);
1285
- default:
1286
- throw new Error(`Unknown embedding provider: ${this.config.provider}. Use 'voyage', 'openai', or 'mock'.`);
1287
- }
1288
- }
1289
- getProviderName() {
1290
- return this.provider.name;
1896
+ return resolvedPath;
1291
1897
  }
1292
- getDimensions() {
1293
- return this.provider.dimensions;
1898
+ resolveDocPaths(userPaths, options = {}) {
1899
+ return userPaths.map((p) => this.resolveDocPath(p, options));
1294
1900
  }
1295
- async generateEmbedding(text) {
1296
- if (!text || text.trim().length === 0) {
1297
- throw new Error("Cannot generate embedding for empty text");
1298
- }
1299
- return this.provider.generateEmbedding(text);
1901
+ isUnderDocs(absolutePath) {
1902
+ const docsPath = this.getDocsPath();
1903
+ const normalizedPath = absolutePath.replace(/\/$/, "");
1904
+ const normalizedDocs = docsPath.replace(/\/$/, "");
1905
+ return normalizedPath.startsWith(`${normalizedDocs}/`) || normalizedPath === normalizedDocs;
1300
1906
  }
1301
- async generateEmbeddings(texts) {
1302
- const validTexts = texts.filter((t) => t && t.trim().length > 0);
1303
- if (validTexts.length === 0) {
1304
- return [];
1907
+ getRelativePath(absolutePath) {
1908
+ const docsPath = this.getDocsPath();
1909
+ if (absolutePath.startsWith(docsPath)) {
1910
+ return absolutePath.slice(docsPath.length + 1);
1305
1911
  }
1306
- return this.provider.generateEmbeddings(validTexts);
1307
- }
1308
- isRealProvider() {
1309
- return this.provider.name !== "mock";
1912
+ return absolutePath;
1310
1913
  }
1311
1914
  }
1312
- EmbeddingService = __legacyDecorateClassTS([
1313
- Injectable6(),
1915
+ PathResolverService = __legacyDecorateClassTS([
1916
+ Injectable10(),
1314
1917
  __legacyMetadataTS("design:paramtypes", [
1315
- typeof ConfigService2 === "undefined" ? Object : ConfigService2
1918
+ typeof DocumentParserService === "undefined" ? Object : DocumentParserService
1316
1919
  ])
1317
- ], EmbeddingService);
1920
+ ], PathResolverService);
1318
1921
 
1319
1922
  // src/sync/sync.service.ts
1320
1923
  var ENTITY_TYPES = [
@@ -1326,37 +1929,8 @@ var ENTITY_TYPES = [
1326
1929
  "Person",
1327
1930
  "Organization"
1328
1931
  ];
1329
- function validateDocuments(docs) {
1330
- const errors = [];
1331
- const entityIndex = new Map;
1332
- for (const doc of docs) {
1333
- for (const entity of doc.entities) {
1334
- if (!entityIndex.has(entity.name)) {
1335
- entityIndex.set(entity.name, new Set);
1336
- }
1337
- entityIndex.get(entity.name).add(doc.path);
1338
- }
1339
- }
1340
- for (const doc of docs) {
1341
- for (const rel of doc.relationships) {
1342
- if (rel.source !== doc.path && !entityIndex.has(rel.source)) {
1343
- errors.push({
1344
- path: doc.path,
1345
- error: `Relationship source "${rel.source}" not found in any document`
1346
- });
1347
- }
1348
- const isDocPath = rel.target.endsWith(".md");
1349
- const isKnownEntity = entityIndex.has(rel.target);
1350
- const isSelfReference = rel.target === doc.path;
1351
- if (!isDocPath && !isKnownEntity && !isSelfReference) {
1352
- errors.push({
1353
- path: doc.path,
1354
- error: `Relationship target "${rel.target}" not found as entity`
1355
- });
1356
- }
1357
- }
1358
- }
1359
- return errors;
1932
+ function validateDocuments2(docs) {
1933
+ return validateDocuments(docs);
1360
1934
  }
1361
1935
 
1362
1936
  class SyncService {
@@ -1366,7 +1940,7 @@ class SyncService {
1366
1940
  cascade;
1367
1941
  pathResolver;
1368
1942
  embeddingService;
1369
- logger = new Logger6(SyncService.name);
1943
+ logger = new Logger5(SyncService.name);
1370
1944
  constructor(manifest, parser, graph, cascade, pathResolver, embeddingService) {
1371
1945
  this.manifest = manifest;
1372
1946
  this.parser = parser;
@@ -1421,7 +1995,7 @@ class SyncService {
1421
1995
  }
1422
1996
  }
1423
1997
  }
1424
- const validationErrors = validateDocuments(docsToSync);
1998
+ const validationErrors = validateDocuments2(docsToSync);
1425
1999
  if (validationErrors.length > 0) {
1426
2000
  for (const err of validationErrors) {
1427
2001
  result.errors.push(err);
@@ -1431,7 +2005,7 @@ class SyncService {
1431
2005
  result.duration = Date.now() - startTime;
1432
2006
  return result;
1433
2007
  }
1434
- const uniqueEntities = this.collectUniqueEntities(docsToSync);
2008
+ const uniqueEntities = collectUniqueEntities(docsToSync);
1435
2009
  if (options.verbose) {
1436
2010
  this.logger.log(`Collected ${uniqueEntities.size} unique entities from ${docsToSync.length} documents`);
1437
2011
  }
@@ -1508,7 +2082,7 @@ class SyncService {
1508
2082
  changes.push({
1509
2083
  path: docPath,
1510
2084
  changeType,
1511
- reason: this.getChangeReason(changeType)
2085
+ reason: getChangeReason(changeType)
1512
2086
  });
1513
2087
  trackedPaths.delete(docPath);
1514
2088
  } catch (error) {
@@ -1537,9 +2111,9 @@ class SyncService {
1537
2111
  await this.graph.deleteDocumentRelationships(doc.path);
1538
2112
  const documentProps = {
1539
2113
  name: doc.path,
1540
- title: doc.title,
2114
+ title: doc.title ?? "",
1541
2115
  contentHash: doc.contentHash,
1542
- tags: doc.tags
2116
+ tags: doc.tags ?? []
1543
2117
  };
1544
2118
  if (doc.summary)
1545
2119
  documentProps.summary = doc.summary;
@@ -1561,7 +2135,7 @@ class SyncService {
1561
2135
  let embeddingGenerated = false;
1562
2136
  if (options.embeddings && !options.skipEmbeddings && this.embeddingService) {
1563
2137
  try {
1564
- const textForEmbedding = this.composeEmbeddingText(doc);
2138
+ const textForEmbedding = composeDocumentEmbeddingText(doc);
1565
2139
  if (textForEmbedding.trim()) {
1566
2140
  const embedding = await this.embeddingService.generateEmbedding(textForEmbedding);
1567
2141
  await this.graph.updateNodeEmbedding("Document", doc.path, embedding);
@@ -1623,9 +2197,9 @@ class SyncService {
1623
2197
  }
1624
2198
  return embeddingGenerated;
1625
2199
  }
1626
- async removeDocument(path) {
1627
- await this.graph.deleteDocumentRelationships(path);
1628
- await this.graph.deleteNode("Document", path);
2200
+ async removeDocument(path2) {
2201
+ await this.graph.deleteDocumentRelationships(path2);
2202
+ await this.graph.deleteNode("Document", path2);
1629
2203
  }
1630
2204
  async processChange(change, options, preloadedDoc) {
1631
2205
  const cascadeWarnings = [];
@@ -1672,14 +2246,10 @@ class SyncService {
1672
2246
  }
1673
2247
  return cascadeWarnings;
1674
2248
  }
1675
- async clearGraph() {
1676
- await this.graph.query("MATCH (n) DETACH DELETE n");
1677
- this.logger.log("Graph cleared");
1678
- }
1679
2249
  async clearManifest() {
1680
2250
  const manifest = await this.manifest.load();
1681
- for (const path of Object.keys(manifest.documents)) {
1682
- this.manifest.removeEntry(path);
2251
+ for (const path2 of Object.keys(manifest.documents)) {
2252
+ this.manifest.removeEntry(path2);
1683
2253
  }
1684
2254
  this.logger.log("Manifest cleared");
1685
2255
  }
@@ -1694,80 +2264,23 @@ class SyncService {
1694
2264
  }
1695
2265
  this.logger.log(`Marked ${normalizedPaths.length} document(s) for re-sync`);
1696
2266
  }
1697
- composeEmbeddingText(doc) {
1698
- const parts = [];
1699
- if (doc.title) {
1700
- parts.push(`Title: ${doc.title}`);
1701
- }
1702
- if (doc.topic) {
1703
- parts.push(`Topic: ${doc.topic}`);
1704
- }
1705
- if (doc.tags && doc.tags.length > 0) {
1706
- parts.push(`Tags: ${doc.tags.join(", ")}`);
1707
- }
1708
- if (doc.entities && doc.entities.length > 0) {
1709
- const entityNames = doc.entities.map((e) => e.name).join(", ");
1710
- parts.push(`Entities: ${entityNames}`);
1711
- }
1712
- if (doc.summary) {
1713
- parts.push(doc.summary);
1714
- } else {
1715
- parts.push(doc.content.slice(0, 500));
1716
- }
1717
- return parts.join(" | ");
1718
- }
1719
- getChangeReason(changeType) {
1720
- switch (changeType) {
1721
- case "new":
1722
- return "New document";
1723
- case "updated":
1724
- return "Content or frontmatter changed";
1725
- case "deleted":
1726
- return "File no longer exists";
1727
- case "unchanged":
1728
- return "No changes detected";
1729
- }
1730
- }
1731
- async getOldDocumentFromManifest(path) {
2267
+ async getOldDocumentFromManifest(path2) {
1732
2268
  try {
1733
2269
  const manifest = await this.manifest.load();
1734
- const entry = manifest.documents[path];
2270
+ const entry = manifest.documents[path2];
1735
2271
  if (!entry) {
1736
2272
  return null;
1737
2273
  }
1738
2274
  try {
1739
- return await this.parser.parseDocument(path);
2275
+ return await this.parser.parseDocument(path2);
1740
2276
  } catch {
1741
2277
  return null;
1742
2278
  }
1743
2279
  } catch (error) {
1744
- this.logger.warn(`Failed to retrieve old document for ${path}: ${error instanceof Error ? error.message : String(error)}`);
2280
+ this.logger.warn(`Failed to retrieve old document for ${path2}: ${error instanceof Error ? error.message : String(error)}`);
1745
2281
  return null;
1746
2282
  }
1747
2283
  }
1748
- collectUniqueEntities(docs) {
1749
- const entities = new Map;
1750
- for (const doc of docs) {
1751
- for (const entity of doc.entities) {
1752
- const key = `${entity.type}:${entity.name}`;
1753
- if (!entities.has(key)) {
1754
- entities.set(key, {
1755
- type: entity.type,
1756
- name: entity.name,
1757
- description: entity.description,
1758
- documentPaths: [doc.path]
1759
- });
1760
- } else {
1761
- const existing = entities.get(key);
1762
- existing.documentPaths.push(doc.path);
1763
- if (entity.description && (!existing.description || entity.description.length > existing.description.length)) {
1764
- existing.description = entity.description;
1765
- }
1766
- }
1767
- }
1768
- }
1769
- return entities;
1770
- }
1771
2284
  async syncEntities(entities, options) {
1772
2285
  let embeddingsGenerated = 0;
1773
2286
  for (const [_key, entity] of entities) {
@@ -1780,7 +2293,7 @@ class SyncService {
1780
2293
  await this.graph.upsertNode(entity.type, entityProps);
1781
2294
  if (options.embeddings && !options.skipEmbeddings && this.embeddingService) {
1782
2295
  try {
1783
- const text = this.composeEntityEmbeddingText(entity);
2296
+ const text = composeEntityEmbeddingText(entity);
1784
2297
  const embedding = await this.embeddingService.generateEmbedding(text);
1785
2298
  await this.graph.updateNodeEmbedding(entity.type, entity.name, embedding);
1786
2299
  embeddingsGenerated++;
@@ -1793,13 +2306,6 @@ class SyncService {
1793
2306
  }
1794
2307
  return embeddingsGenerated;
1795
2308
  }
1796
- composeEntityEmbeddingText(entity) {
1797
- const parts = [`${entity.type}: ${entity.name}`];
1798
- if (entity.description) {
1799
- parts.push(entity.description);
1800
- }
1801
- return parts.join(". ");
1802
- }
1803
2309
  async createEntityVectorIndices() {
1804
2310
  if (!this.embeddingService)
1805
2311
  return;
@@ -1814,7 +2320,7 @@ class SyncService {
1814
2320
  }
1815
2321
  }
1816
2322
  SyncService = __legacyDecorateClassTS([
1817
- Injectable7(),
2323
+ Injectable11(),
1818
2324
  __legacyMetadataTS("design:paramtypes", [
1819
2325
  typeof ManifestService === "undefined" ? Object : ManifestService,
1820
2326
  typeof DocumentParserService === "undefined" ? Object : DocumentParserService,
@@ -1825,170 +2331,98 @@ SyncService = __legacyDecorateClassTS([
1825
2331
  ])
1826
2332
  ], SyncService);
1827
2333
 
1828
- // src/sync/ontology.service.ts
1829
- import { Injectable as Injectable8 } from "@nestjs/common";
1830
- class OntologyService {
1831
- parser;
1832
- constructor(parser) {
1833
- this.parser = parser;
1834
- }
1835
- async deriveOntology() {
1836
- const docs = await this.parser.parseAllDocuments();
1837
- return this.deriveFromDocuments(docs);
2334
+ // src/commands/status.command.ts
2335
+ class StatusCommand extends CommandRunner4 {
2336
+ syncService;
2337
+ manifestService;
2338
+ constructor(syncService, manifestService) {
2339
+ super();
2340
+ this.syncService = syncService;
2341
+ this.manifestService = manifestService;
1838
2342
  }
1839
- deriveFromDocuments(docs) {
1840
- const entityTypeSet = new Set;
1841
- const relationshipTypeSet = new Set;
1842
- const entityCounts = {};
1843
- const relationshipCounts = {};
1844
- const entityExamples = {};
1845
- let documentsWithEntities = 0;
1846
- let documentsWithoutEntities = 0;
1847
- let totalRelationships = 0;
1848
- for (const doc of docs) {
1849
- if (doc.entities.length > 0) {
1850
- documentsWithEntities++;
1851
- } else {
1852
- documentsWithoutEntities++;
1853
- }
1854
- for (const entity of doc.entities) {
1855
- entityTypeSet.add(entity.type);
1856
- entityCounts[entity.type] = (entityCounts[entity.type] || 0) + 1;
1857
- if (!entityExamples[entity.name]) {
1858
- entityExamples[entity.name] = { type: entity.type, documents: [] };
1859
- }
1860
- if (!entityExamples[entity.name].documents.includes(doc.path)) {
1861
- entityExamples[entity.name].documents.push(doc.path);
1862
- }
2343
+ async run(_inputs, options) {
2344
+ try {
2345
+ await this.manifestService.load();
2346
+ const changes = await this.syncService.detectChanges();
2347
+ const newDocs = changes.filter((c) => c.changeType === "new");
2348
+ const updatedDocs = changes.filter((c) => c.changeType === "updated");
2349
+ const deletedDocs = changes.filter((c) => c.changeType === "deleted");
2350
+ const unchangedDocs = changes.filter((c) => c.changeType === "unchanged");
2351
+ const pendingCount = newDocs.length + updatedDocs.length + deletedDocs.length;
2352
+ console.log(`
2353
+ \uD83D\uDCCA Graph Status
2354
+ `);
2355
+ if (newDocs.length > 0) {
2356
+ console.log(`New (${newDocs.length}):`);
2357
+ newDocs.forEach((doc) => {
2358
+ console.log(` + ${doc.path}`);
2359
+ });
2360
+ console.log();
1863
2361
  }
1864
- for (const rel of doc.relationships) {
1865
- relationshipTypeSet.add(rel.relation);
1866
- relationshipCounts[rel.relation] = (relationshipCounts[rel.relation] || 0) + 1;
1867
- totalRelationships++;
2362
+ if (updatedDocs.length > 0) {
2363
+ console.log(`Updated (${updatedDocs.length}):`);
2364
+ updatedDocs.forEach((doc) => {
2365
+ console.log(` ~ ${doc.path}`);
2366
+ });
2367
+ console.log();
1868
2368
  }
1869
- }
1870
- return {
1871
- entityTypes: Array.from(entityTypeSet).sort(),
1872
- relationshipTypes: Array.from(relationshipTypeSet).sort(),
1873
- entityCounts,
1874
- relationshipCounts,
1875
- totalEntities: Object.keys(entityExamples).length,
1876
- totalRelationships,
1877
- documentsWithEntities,
1878
- documentsWithoutEntities,
1879
- entityExamples
1880
- };
1881
- }
1882
- printSummary(ontology) {
1883
- console.log(`
1884
- Derived Ontology Summary
1885
- `);
1886
- console.log(`Documents: ${ontology.documentsWithEntities} with entities, ${ontology.documentsWithoutEntities} without`);
1887
- console.log(`Unique Entities: ${ontology.totalEntities}`);
1888
- console.log(`Total Relationships: ${ontology.totalRelationships}`);
1889
- console.log(`
1890
- Entity Types:`);
1891
- for (const type of ontology.entityTypes) {
1892
- console.log(` ${type}: ${ontology.entityCounts[type]} instances`);
1893
- }
1894
- console.log(`
1895
- Relationship Types:`);
1896
- for (const type of ontology.relationshipTypes) {
1897
- console.log(` ${type}: ${ontology.relationshipCounts[type]} instances`);
1898
- }
1899
- console.log(`
1900
- Top Entities (by document count):`);
1901
- const sorted = Object.entries(ontology.entityExamples).sort((a, b) => b[1].documents.length - a[1].documents.length).slice(0, 10);
1902
- for (const [name, info] of sorted) {
1903
- console.log(` ${name} (${info.type}): ${info.documents.length} docs`);
1904
- }
1905
- }
1906
- }
1907
- OntologyService = __legacyDecorateClassTS([
1908
- Injectable8(),
1909
- __legacyMetadataTS("design:paramtypes", [
1910
- typeof DocumentParserService === "undefined" ? Object : DocumentParserService
1911
- ])
1912
- ], OntologyService);
1913
-
1914
- // src/embedding/embedding.module.ts
1915
- import { Module as Module2 } from "@nestjs/common";
1916
- import { ConfigModule } from "@nestjs/config";
1917
- class EmbeddingModule {
1918
- }
1919
- EmbeddingModule = __legacyDecorateClassTS([
1920
- Module2({
1921
- imports: [ConfigModule],
1922
- providers: [EmbeddingService],
1923
- exports: [EmbeddingService]
1924
- })
1925
- ], EmbeddingModule);
1926
-
1927
- // src/sync/sync.module.ts
1928
- class SyncModule {
1929
- }
1930
- SyncModule = __legacyDecorateClassTS([
1931
- Module3({
1932
- imports: [GraphModule, EmbeddingModule],
1933
- providers: [
1934
- SyncService,
1935
- ManifestService,
1936
- DocumentParserService,
1937
- OntologyService,
1938
- CascadeService,
1939
- PathResolverService
1940
- ],
1941
- exports: [
1942
- SyncService,
1943
- ManifestService,
1944
- DocumentParserService,
1945
- OntologyService,
1946
- CascadeService,
1947
- PathResolverService
1948
- ]
1949
- })
1950
- ], SyncModule);
1951
-
1952
- // src/query/query.module.ts
1953
- import { Module as Module4 } from "@nestjs/common";
1954
-
1955
- // src/query/query.service.ts
1956
- import { Injectable as Injectable9, Logger as Logger7 } from "@nestjs/common";
1957
- class QueryService {
1958
- graphService;
1959
- logger = new Logger7(QueryService.name);
1960
- constructor(graphService) {
1961
- this.graphService = graphService;
2369
+ if (deletedDocs.length > 0) {
2370
+ console.log(`Deleted (${deletedDocs.length}):`);
2371
+ deletedDocs.forEach((doc) => {
2372
+ console.log(` - ${doc.path}`);
2373
+ });
2374
+ console.log();
2375
+ }
2376
+ if (options.verbose && unchangedDocs.length > 0) {
2377
+ console.log(`Unchanged (${unchangedDocs.length}):`);
2378
+ unchangedDocs.forEach((doc) => {
2379
+ console.log(` \xB7 ${doc.path}`);
2380
+ });
2381
+ console.log();
2382
+ }
2383
+ if (pendingCount === 0) {
2384
+ console.log(`\u2705 All documents are in sync
2385
+ `);
2386
+ } else {
2387
+ console.log(`Total: ${pendingCount} document(s) need syncing`);
2388
+ console.log("\uD83D\uDCA1 Run `lattice sync` to apply changes\n");
2389
+ }
2390
+ process.exit(0);
2391
+ } catch (error) {
2392
+ console.error("Error:", error instanceof Error ? error.message : String(error));
2393
+ process.exit(1);
2394
+ }
1962
2395
  }
1963
- async query(cypher) {
1964
- this.logger.debug(`Executing query: ${cypher}`);
1965
- return await this.graphService.query(cypher);
2396
+ parseVerbose() {
2397
+ return true;
1966
2398
  }
1967
2399
  }
1968
- QueryService = __legacyDecorateClassTS([
1969
- Injectable9(),
2400
+ __legacyDecorateClassTS([
2401
+ Option3({
2402
+ flags: "-v, --verbose",
2403
+ description: "Show all documents including unchanged"
2404
+ }),
2405
+ __legacyMetadataTS("design:type", Function),
2406
+ __legacyMetadataTS("design:paramtypes", []),
2407
+ __legacyMetadataTS("design:returntype", Boolean)
2408
+ ], StatusCommand.prototype, "parseVerbose", null);
2409
+ StatusCommand = __legacyDecorateClassTS([
2410
+ Injectable12(),
2411
+ Command4({
2412
+ name: "status",
2413
+ description: "Show documents that need syncing (new or updated)"
2414
+ }),
1970
2415
  __legacyMetadataTS("design:paramtypes", [
1971
- typeof GraphService === "undefined" ? Object : GraphService
2416
+ typeof SyncService === "undefined" ? Object : SyncService,
2417
+ typeof ManifestService === "undefined" ? Object : ManifestService
1972
2418
  ])
1973
- ], QueryService);
1974
-
1975
- // src/query/query.module.ts
1976
- class QueryModule {
1977
- }
1978
- QueryModule = __legacyDecorateClassTS([
1979
- Module4({
1980
- imports: [GraphModule, EmbeddingModule],
1981
- providers: [QueryService, GraphService],
1982
- exports: [QueryService, GraphService]
1983
- })
1984
- ], QueryModule);
1985
-
2419
+ ], StatusCommand);
1986
2420
  // src/commands/sync.command.ts
1987
- import { Injectable as Injectable10 } from "@nestjs/common";
1988
- import { Command, CommandRunner, Option } from "nest-commander";
1989
2421
  import { watch } from "fs";
1990
- import { join } from "path";
1991
- class SyncCommand extends CommandRunner {
2422
+ import { join as join2 } from "path";
2423
+ import { Injectable as Injectable13 } from "@nestjs/common";
2424
+ import { Command as Command5, CommandRunner as CommandRunner5, Option as Option4 } from "nest-commander";
2425
+ class SyncCommand extends CommandRunner5 {
1992
2426
  syncService;
1993
2427
  watcher = null;
1994
2428
  isShuttingDown = false;
@@ -2110,8 +2544,8 @@ class SyncCommand extends CommandRunner {
2110
2544
  \uD83D\uDC41\uFE0F Watch mode enabled
2111
2545
  `);
2112
2546
  this.watcher = watch(docsPath, { recursive: true }, (event, filename) => {
2113
- if (filename && filename.endsWith(".md")) {
2114
- const fullPath = join(docsPath, filename);
2547
+ if (filename?.endsWith(".md")) {
2548
+ const fullPath = join2(docsPath, filename);
2115
2549
  trackedFiles.add(fullPath);
2116
2550
  debouncedSync();
2117
2551
  }
@@ -2150,464 +2584,166 @@ class SyncCommand extends CommandRunner {
2150
2584
  result.errors.forEach((e) => {
2151
2585
  console.log(` ${e.path}: ${e.error}`);
2152
2586
  });
2153
- }
2154
- if (result.changes && result.changes.length > 0) {
2155
- console.log(`
2156
- \uD83D\uDCDD Changes:
2157
- `);
2158
- result.changes.forEach((c) => {
2159
- const icon = {
2160
- new: "\u2795",
2161
- updated: "\uD83D\uDD04",
2162
- deleted: "\uD83D\uDDD1\uFE0F",
2163
- unchanged: "\u23ED\uFE0F"
2164
- }[c.changeType];
2165
- console.log(` ${icon} ${c.changeType}: ${c.path}`);
2166
- if (c.reason) {
2167
- console.log(` ${c.reason}`);
2168
- }
2169
- });
2170
- }
2171
- if (result.cascadeWarnings && result.cascadeWarnings.length > 0) {
2172
- console.log(`
2173
- \u26A0\uFE0F Cascade Impacts Detected:
2174
- `);
2175
- const warningsByTrigger = new Map;
2176
- for (const warning of result.cascadeWarnings) {
2177
- const existing = warningsByTrigger.get(warning.trigger) || [];
2178
- existing.push(warning);
2179
- warningsByTrigger.set(warning.trigger, existing);
2180
- }
2181
- for (const [trigger, warnings] of warningsByTrigger) {
2182
- const triggerLabel = trigger.split("_").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
2183
- console.log(` \uD83D\uDCCC ${triggerLabel}
2184
- `);
2185
- for (const analysis of warnings) {
2186
- console.log(` ${analysis.summary}`);
2187
- console.log(` Source: ${analysis.sourceDocument}
2188
- `);
2189
- if (analysis.affectedDocuments.length > 0) {
2190
- for (const affected of analysis.affectedDocuments) {
2191
- const icon = affected.confidence === "high" ? "\uD83D\uDD34" : affected.confidence === "medium" ? "\uD83D\uDFE1" : "\uD83D\uDFE2";
2192
- console.log(` ${icon} [${affected.confidence.toUpperCase()}] ${affected.path}`);
2193
- console.log(` ${affected.reason}`);
2194
- const suggestedAction = affected.suggestedAction.split("_").join(" ").replace(/\b\w/g, (char) => char.toUpperCase());
2195
- console.log(` \u2192 ${suggestedAction}`);
2196
- }
2197
- } else {
2198
- console.log(` \u2139\uFE0F No directly affected documents detected`);
2199
- }
2200
- console.log();
2201
- }
2202
- }
2203
- console.log(` \uD83D\uDCA1 Run /update-related to apply suggested changes
2204
- `);
2205
- }
2206
- if (isWatchMode) {
2207
- console.log(`
2208
- \u23F3 Watching for changes... (Ctrl+C to stop)
2209
- `);
2210
- }
2211
- }
2212
- parseForce() {
2213
- return true;
2214
- }
2215
- parseDryRun() {
2216
- return true;
2217
- }
2218
- parseVerbose() {
2219
- return true;
2220
- }
2221
- parseWatch() {
2222
- return true;
2223
- }
2224
- parseDiff() {
2225
- return true;
2226
- }
2227
- parseSkipCascade() {
2228
- return true;
2229
- }
2230
- parseNoEmbeddings() {
2231
- return false;
2232
- }
2233
- }
2234
- __legacyDecorateClassTS([
2235
- Option({
2236
- flags: "-f, --force",
2237
- description: "Force re-sync: with paths, clears only those docs; without paths, rebuilds entire graph"
2238
- }),
2239
- __legacyMetadataTS("design:type", Function),
2240
- __legacyMetadataTS("design:paramtypes", []),
2241
- __legacyMetadataTS("design:returntype", Boolean)
2242
- ], SyncCommand.prototype, "parseForce", null);
2243
- __legacyDecorateClassTS([
2244
- Option({
2245
- flags: "-d, --dry-run",
2246
- description: "Show what would change without applying"
2247
- }),
2248
- __legacyMetadataTS("design:type", Function),
2249
- __legacyMetadataTS("design:paramtypes", []),
2250
- __legacyMetadataTS("design:returntype", Boolean)
2251
- ], SyncCommand.prototype, "parseDryRun", null);
2252
- __legacyDecorateClassTS([
2253
- Option({
2254
- flags: "-v, --verbose",
2255
- description: "Show detailed output"
2256
- }),
2257
- __legacyMetadataTS("design:type", Function),
2258
- __legacyMetadataTS("design:paramtypes", []),
2259
- __legacyMetadataTS("design:returntype", Boolean)
2260
- ], SyncCommand.prototype, "parseVerbose", null);
2261
- __legacyDecorateClassTS([
2262
- Option({
2263
- flags: "-w, --watch",
2264
- description: "Watch for file changes and sync automatically"
2265
- }),
2266
- __legacyMetadataTS("design:type", Function),
2267
- __legacyMetadataTS("design:paramtypes", []),
2268
- __legacyMetadataTS("design:returntype", Boolean)
2269
- ], SyncCommand.prototype, "parseWatch", null);
2270
- __legacyDecorateClassTS([
2271
- Option({
2272
- flags: "--diff",
2273
- description: "Show only changed documents (alias for --dry-run)"
2274
- }),
2275
- __legacyMetadataTS("design:type", Function),
2276
- __legacyMetadataTS("design:paramtypes", []),
2277
- __legacyMetadataTS("design:returntype", Boolean)
2278
- ], SyncCommand.prototype, "parseDiff", null);
2279
- __legacyDecorateClassTS([
2280
- Option({
2281
- flags: "--skip-cascade",
2282
- description: "Skip cascade analysis (faster for large repos)"
2283
- }),
2284
- __legacyMetadataTS("design:type", Function),
2285
- __legacyMetadataTS("design:paramtypes", []),
2286
- __legacyMetadataTS("design:returntype", Boolean)
2287
- ], SyncCommand.prototype, "parseSkipCascade", null);
2288
- __legacyDecorateClassTS([
2289
- Option({
2290
- flags: "--no-embeddings",
2291
- description: "Disable embedding generation during sync"
2292
- }),
2293
- __legacyMetadataTS("design:type", Function),
2294
- __legacyMetadataTS("design:paramtypes", []),
2295
- __legacyMetadataTS("design:returntype", Boolean)
2296
- ], SyncCommand.prototype, "parseNoEmbeddings", null);
2297
- SyncCommand = __legacyDecorateClassTS([
2298
- Injectable10(),
2299
- Command({
2300
- name: "sync",
2301
- arguments: "[paths...]",
2302
- description: "Synchronize documents to the knowledge graph"
2303
- }),
2304
- __legacyMetadataTS("design:paramtypes", [
2305
- typeof SyncService === "undefined" ? Object : SyncService
2306
- ])
2307
- ], SyncCommand);
2308
- // src/commands/status.command.ts
2309
- import { Injectable as Injectable11 } from "@nestjs/common";
2310
- import { Command as Command2, CommandRunner as CommandRunner2, Option as Option2 } from "nest-commander";
2311
- class StatusCommand extends CommandRunner2 {
2312
- syncService;
2313
- manifestService;
2314
- constructor(syncService, manifestService) {
2315
- super();
2316
- this.syncService = syncService;
2317
- this.manifestService = manifestService;
2318
- }
2319
- async run(_inputs, options) {
2320
- try {
2321
- await this.manifestService.load();
2322
- const changes = await this.syncService.detectChanges();
2323
- const newDocs = changes.filter((c) => c.changeType === "new");
2324
- const updatedDocs = changes.filter((c) => c.changeType === "updated");
2325
- const deletedDocs = changes.filter((c) => c.changeType === "deleted");
2326
- const unchangedDocs = changes.filter((c) => c.changeType === "unchanged");
2327
- const pendingCount = newDocs.length + updatedDocs.length + deletedDocs.length;
2328
- console.log(`
2329
- \uD83D\uDCCA Graph Status
2330
- `);
2331
- if (newDocs.length > 0) {
2332
- console.log(`New (${newDocs.length}):`);
2333
- newDocs.forEach((doc) => {
2334
- console.log(` + ${doc.path}`);
2335
- });
2336
- console.log();
2337
- }
2338
- if (updatedDocs.length > 0) {
2339
- console.log(`Updated (${updatedDocs.length}):`);
2340
- updatedDocs.forEach((doc) => {
2341
- console.log(` ~ ${doc.path}`);
2342
- });
2343
- console.log();
2344
- }
2345
- if (deletedDocs.length > 0) {
2346
- console.log(`Deleted (${deletedDocs.length}):`);
2347
- deletedDocs.forEach((doc) => {
2348
- console.log(` - ${doc.path}`);
2349
- });
2350
- console.log();
2351
- }
2352
- if (options.verbose && unchangedDocs.length > 0) {
2353
- console.log(`Unchanged (${unchangedDocs.length}):`);
2354
- unchangedDocs.forEach((doc) => {
2355
- console.log(` \xB7 ${doc.path}`);
2356
- });
2357
- console.log();
2587
+ }
2588
+ if (result.changes && result.changes.length > 0) {
2589
+ console.log(`
2590
+ \uD83D\uDCDD Changes:
2591
+ `);
2592
+ const icons = {
2593
+ new: "\u2795",
2594
+ updated: "\uD83D\uDD04",
2595
+ deleted: "\uD83D\uDDD1\uFE0F",
2596
+ unchanged: "\u23ED\uFE0F"
2597
+ };
2598
+ result.changes.forEach((c) => {
2599
+ const icon = icons[c.changeType];
2600
+ console.log(` ${icon} ${c.changeType}: ${c.path}`);
2601
+ if (c.reason) {
2602
+ console.log(` ${c.reason}`);
2603
+ }
2604
+ });
2605
+ }
2606
+ if (result.cascadeWarnings && result.cascadeWarnings.length > 0) {
2607
+ console.log(`
2608
+ \u26A0\uFE0F Cascade Impacts Detected:
2609
+ `);
2610
+ const warningsByTrigger = new Map;
2611
+ for (const warning of result.cascadeWarnings) {
2612
+ const existing = warningsByTrigger.get(warning.trigger) || [];
2613
+ existing.push(warning);
2614
+ warningsByTrigger.set(warning.trigger, existing);
2358
2615
  }
2359
- if (pendingCount === 0) {
2360
- console.log(`\u2705 All documents are in sync
2616
+ for (const [trigger, warnings] of warningsByTrigger) {
2617
+ const triggerLabel = trigger.split("_").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
2618
+ console.log(` \uD83D\uDCCC ${triggerLabel}
2361
2619
  `);
2362
- } else {
2363
- console.log(`Total: ${pendingCount} document(s) need syncing`);
2364
- console.log("\uD83D\uDCA1 Run `lattice sync` to apply changes\n");
2620
+ for (const analysis of warnings) {
2621
+ console.log(` ${analysis.summary}`);
2622
+ console.log(` Source: ${analysis.sourceDocument}
2623
+ `);
2624
+ if (analysis.affectedDocuments.length > 0) {
2625
+ for (const affected of analysis.affectedDocuments) {
2626
+ const icon = affected.confidence === "high" ? "\uD83D\uDD34" : affected.confidence === "medium" ? "\uD83D\uDFE1" : "\uD83D\uDFE2";
2627
+ console.log(` ${icon} [${affected.confidence.toUpperCase()}] ${affected.path}`);
2628
+ console.log(` ${affected.reason}`);
2629
+ const suggestedAction = affected.suggestedAction.split("_").join(" ").replace(/\b\w/g, (char) => char.toUpperCase());
2630
+ console.log(` \u2192 ${suggestedAction}`);
2631
+ }
2632
+ } else {
2633
+ console.log(` \u2139\uFE0F No directly affected documents detected`);
2634
+ }
2635
+ console.log();
2636
+ }
2365
2637
  }
2366
- process.exit(0);
2367
- } catch (error) {
2368
- console.error("Error:", error instanceof Error ? error.message : String(error));
2369
- process.exit(1);
2638
+ console.log(` \uD83D\uDCA1 Run /update-related to apply suggested changes
2639
+ `);
2640
+ }
2641
+ if (isWatchMode) {
2642
+ console.log(`
2643
+ \u23F3 Watching for changes... (Ctrl+C to stop)
2644
+ `);
2370
2645
  }
2371
2646
  }
2647
+ parseForce() {
2648
+ return true;
2649
+ }
2650
+ parseDryRun() {
2651
+ return true;
2652
+ }
2372
2653
  parseVerbose() {
2373
2654
  return true;
2374
2655
  }
2656
+ parseWatch() {
2657
+ return true;
2658
+ }
2659
+ parseDiff() {
2660
+ return true;
2661
+ }
2662
+ parseSkipCascade() {
2663
+ return true;
2664
+ }
2665
+ parseNoEmbeddings() {
2666
+ return false;
2667
+ }
2375
2668
  }
2376
2669
  __legacyDecorateClassTS([
2377
- Option2({
2378
- flags: "-v, --verbose",
2379
- description: "Show all documents including unchanged"
2670
+ Option4({
2671
+ flags: "-f, --force",
2672
+ description: "Force re-sync: with paths, clears only those docs; without paths, rebuilds entire graph"
2380
2673
  }),
2381
2674
  __legacyMetadataTS("design:type", Function),
2382
2675
  __legacyMetadataTS("design:paramtypes", []),
2383
2676
  __legacyMetadataTS("design:returntype", Boolean)
2384
- ], StatusCommand.prototype, "parseVerbose", null);
2385
- StatusCommand = __legacyDecorateClassTS([
2386
- Injectable11(),
2387
- Command2({
2388
- name: "status",
2389
- description: "Show documents that need syncing (new or updated)"
2677
+ ], SyncCommand.prototype, "parseForce", null);
2678
+ __legacyDecorateClassTS([
2679
+ Option4({
2680
+ flags: "-d, --dry-run",
2681
+ description: "Show what would change without applying"
2390
2682
  }),
2391
- __legacyMetadataTS("design:paramtypes", [
2392
- typeof SyncService === "undefined" ? Object : SyncService,
2393
- typeof ManifestService === "undefined" ? Object : ManifestService
2394
- ])
2395
- ], StatusCommand);
2396
- // src/commands/query.command.ts
2397
- import { Injectable as Injectable12 } from "@nestjs/common";
2398
- import { Command as Command3, CommandRunner as CommandRunner3, Option as Option3 } from "nest-commander";
2399
- class SearchCommand extends CommandRunner3 {
2400
- graphService;
2401
- embeddingService;
2402
- constructor(graphService, embeddingService) {
2403
- super();
2404
- this.graphService = graphService;
2405
- this.embeddingService = embeddingService;
2406
- }
2407
- async run(inputs, options) {
2408
- const query = inputs[0];
2409
- const limit = Math.min(parseInt(options.limit || "20", 10), 100);
2410
- try {
2411
- const queryEmbedding = await this.embeddingService.generateEmbedding(query);
2412
- let results;
2413
- if (options.label) {
2414
- const labelResults = await this.graphService.vectorSearch(options.label, queryEmbedding, limit);
2415
- results = labelResults.map((r) => ({
2416
- name: r.name,
2417
- label: options.label,
2418
- title: r.title,
2419
- score: r.score
2420
- }));
2421
- } else {
2422
- results = await this.graphService.vectorSearchAll(queryEmbedding, limit);
2423
- }
2424
- const labelSuffix = options.label ? ` (${options.label})` : "";
2425
- console.log(`
2426
- === Semantic Search Results for "${query}"${labelSuffix} ===
2427
- `);
2428
- if (results.length === 0) {
2429
- console.log(`No results found.
2430
- `);
2431
- if (options.label) {
2432
- console.log(`Tip: Try without --label to search all entity types.
2433
- `);
2434
- }
2435
- process.exit(0);
2436
- }
2437
- results.forEach((result, idx) => {
2438
- console.log(`${idx + 1}. [${result.label}] ${result.name}`);
2439
- if (result.title) {
2440
- console.log(` Title: ${result.title}`);
2441
- }
2442
- if (result.description && result.label !== "Document") {
2443
- const desc = result.description.length > 80 ? result.description.slice(0, 80) + "..." : result.description;
2444
- console.log(` ${desc}`);
2445
- }
2446
- console.log(` Similarity: ${(result.score * 100).toFixed(2)}%`);
2447
- });
2448
- console.log();
2449
- process.exit(0);
2450
- } catch (error) {
2451
- const errorMsg = error instanceof Error ? error.message : String(error);
2452
- console.error("Error:", errorMsg);
2453
- if (errorMsg.includes("no embeddings") || errorMsg.includes("vector")) {
2454
- console.log(`
2455
- Note: Semantic search requires embeddings to be generated first.`);
2456
- console.log(`Run 'lattice sync' to generate embeddings for documents.
2457
- `);
2458
- }
2459
- process.exit(1);
2460
- }
2461
- }
2462
- parseLabel(value) {
2463
- return value;
2464
- }
2465
- parseLimit(value) {
2466
- return value;
2467
- }
2468
- }
2683
+ __legacyMetadataTS("design:type", Function),
2684
+ __legacyMetadataTS("design:paramtypes", []),
2685
+ __legacyMetadataTS("design:returntype", Boolean)
2686
+ ], SyncCommand.prototype, "parseDryRun", null);
2469
2687
  __legacyDecorateClassTS([
2470
- Option3({
2471
- flags: "-l, --label <label>",
2472
- description: "Filter by entity label (e.g., Technology, Concept, Document)"
2688
+ Option4({
2689
+ flags: "-v, --verbose",
2690
+ description: "Show detailed output"
2473
2691
  }),
2474
2692
  __legacyMetadataTS("design:type", Function),
2475
- __legacyMetadataTS("design:paramtypes", [
2476
- String
2477
- ]),
2478
- __legacyMetadataTS("design:returntype", String)
2479
- ], SearchCommand.prototype, "parseLabel", null);
2693
+ __legacyMetadataTS("design:paramtypes", []),
2694
+ __legacyMetadataTS("design:returntype", Boolean)
2695
+ ], SyncCommand.prototype, "parseVerbose", null);
2480
2696
  __legacyDecorateClassTS([
2481
- Option3({
2482
- flags: "--limit <n>",
2483
- description: "Limit results",
2484
- defaultValue: "20"
2697
+ Option4({
2698
+ flags: "-w, --watch",
2699
+ description: "Watch for file changes and sync automatically"
2485
2700
  }),
2486
2701
  __legacyMetadataTS("design:type", Function),
2487
- __legacyMetadataTS("design:paramtypes", [
2488
- String
2489
- ]),
2490
- __legacyMetadataTS("design:returntype", String)
2491
- ], SearchCommand.prototype, "parseLimit", null);
2492
- SearchCommand = __legacyDecorateClassTS([
2493
- Injectable12(),
2494
- Command3({
2495
- name: "search",
2496
- arguments: "<query>",
2497
- description: "Semantic search across the knowledge graph"
2702
+ __legacyMetadataTS("design:paramtypes", []),
2703
+ __legacyMetadataTS("design:returntype", Boolean)
2704
+ ], SyncCommand.prototype, "parseWatch", null);
2705
+ __legacyDecorateClassTS([
2706
+ Option4({
2707
+ flags: "--diff",
2708
+ description: "Show only changed documents (alias for --dry-run)"
2498
2709
  }),
2499
- __legacyMetadataTS("design:paramtypes", [
2500
- typeof GraphService === "undefined" ? Object : GraphService,
2501
- typeof EmbeddingService === "undefined" ? Object : EmbeddingService
2502
- ])
2503
- ], SearchCommand);
2504
-
2505
- class RelsCommand extends CommandRunner3 {
2506
- graphService;
2507
- constructor(graphService) {
2508
- super();
2509
- this.graphService = graphService;
2510
- }
2511
- async run(inputs) {
2512
- const name = inputs[0];
2513
- try {
2514
- const escapedName = name.replace(/'/g, "\\'");
2515
- const cypher = `MATCH (a { name: '${escapedName}' })-[r]-(b) RETURN a, r, b`;
2516
- const result = await this.graphService.query(cypher);
2517
- const results = result.resultSet || [];
2518
- console.log(`
2519
- === Relationships for "${name}" ===
2520
- `);
2521
- if (results.length === 0) {
2522
- console.log(`No relationships found.
2523
- `);
2524
- process.exit(0);
2525
- }
2526
- const incoming = [];
2527
- const outgoing = [];
2528
- results.forEach((row) => {
2529
- const [source, rel, target] = row;
2530
- const sourceObj = Object.fromEntries(source);
2531
- const targetObj = Object.fromEntries(target);
2532
- const relObj = Object.fromEntries(rel);
2533
- const sourceProps = Object.fromEntries(sourceObj.properties || []);
2534
- const targetProps = Object.fromEntries(targetObj.properties || []);
2535
- const sourceName = sourceProps.name || "unknown";
2536
- const targetName = targetProps.name || "unknown";
2537
- const relType = relObj.type || "UNKNOWN";
2538
- if (sourceName === name) {
2539
- outgoing.push(` -[${relType}]-> ${targetName}`);
2540
- } else {
2541
- incoming.push(` <-[${relType}]- ${sourceName}`);
2542
- }
2543
- });
2544
- if (outgoing.length > 0) {
2545
- console.log("Outgoing:");
2546
- outgoing.forEach((r) => console.log(r));
2547
- }
2548
- if (incoming.length > 0) {
2549
- if (outgoing.length > 0)
2550
- console.log();
2551
- console.log("Incoming:");
2552
- incoming.forEach((r) => console.log(r));
2553
- }
2554
- console.log();
2555
- process.exit(0);
2556
- } catch (error) {
2557
- console.error("Error:", error instanceof Error ? error.message : String(error));
2558
- process.exit(1);
2559
- }
2560
- }
2561
- }
2562
- RelsCommand = __legacyDecorateClassTS([
2563
- Injectable12(),
2564
- Command3({
2565
- name: "rels",
2566
- arguments: "<name>",
2567
- description: "Show relationships for a node"
2710
+ __legacyMetadataTS("design:type", Function),
2711
+ __legacyMetadataTS("design:paramtypes", []),
2712
+ __legacyMetadataTS("design:returntype", Boolean)
2713
+ ], SyncCommand.prototype, "parseDiff", null);
2714
+ __legacyDecorateClassTS([
2715
+ Option4({
2716
+ flags: "--skip-cascade",
2717
+ description: "Skip cascade analysis (faster for large repos)"
2568
2718
  }),
2569
- __legacyMetadataTS("design:paramtypes", [
2570
- typeof GraphService === "undefined" ? Object : GraphService
2571
- ])
2572
- ], RelsCommand);
2573
-
2574
- class CypherCommand extends CommandRunner3 {
2575
- graphService;
2576
- constructor(graphService) {
2577
- super();
2578
- this.graphService = graphService;
2579
- }
2580
- async run(inputs) {
2581
- const query = inputs[0];
2582
- try {
2583
- const result = await this.graphService.query(query);
2584
- console.log(`
2585
- === Cypher Query Results ===
2586
- `);
2587
- console.log(JSON.stringify(result, null, 2));
2588
- console.log();
2589
- process.exit(0);
2590
- } catch (error) {
2591
- console.error("Error:", error instanceof Error ? error.message : String(error));
2592
- process.exit(1);
2593
- }
2594
- }
2595
- }
2596
- CypherCommand = __legacyDecorateClassTS([
2597
- Injectable12(),
2598
- Command3({
2599
- name: "cypher",
2600
- arguments: "<query>",
2601
- description: "Execute raw Cypher query"
2719
+ __legacyMetadataTS("design:type", Function),
2720
+ __legacyMetadataTS("design:paramtypes", []),
2721
+ __legacyMetadataTS("design:returntype", Boolean)
2722
+ ], SyncCommand.prototype, "parseSkipCascade", null);
2723
+ __legacyDecorateClassTS([
2724
+ Option4({
2725
+ flags: "--no-embeddings",
2726
+ description: "Disable embedding generation during sync"
2727
+ }),
2728
+ __legacyMetadataTS("design:type", Function),
2729
+ __legacyMetadataTS("design:paramtypes", []),
2730
+ __legacyMetadataTS("design:returntype", Boolean)
2731
+ ], SyncCommand.prototype, "parseNoEmbeddings", null);
2732
+ SyncCommand = __legacyDecorateClassTS([
2733
+ Injectable13(),
2734
+ Command5({
2735
+ name: "sync",
2736
+ arguments: "[paths...]",
2737
+ description: "Synchronize documents to the knowledge graph"
2602
2738
  }),
2603
2739
  __legacyMetadataTS("design:paramtypes", [
2604
- typeof GraphService === "undefined" ? Object : GraphService
2740
+ typeof SyncService === "undefined" ? Object : SyncService
2605
2741
  ])
2606
- ], CypherCommand);
2742
+ ], SyncCommand);
2607
2743
  // src/commands/validate.command.ts
2608
- import { Injectable as Injectable13 } from "@nestjs/common";
2609
- import { Command as Command4, CommandRunner as CommandRunner4, Option as Option4 } from "nest-commander";
2610
- class ValidateCommand extends CommandRunner4 {
2744
+ import { Injectable as Injectable14 } from "@nestjs/common";
2745
+ import { Command as Command6, CommandRunner as CommandRunner6, Option as Option5 } from "nest-commander";
2746
+ class ValidateCommand extends CommandRunner6 {
2611
2747
  parserService;
2612
2748
  constructor(parserService) {
2613
2749
  super();
@@ -2629,13 +2765,15 @@ class ValidateCommand extends CommandRunner4 {
2629
2765
  const entityIndex = new Map;
2630
2766
  for (const doc of docs) {
2631
2767
  for (const entity of doc.entities) {
2632
- if (!entityIndex.has(entity.name)) {
2633
- entityIndex.set(entity.name, new Set);
2768
+ let docPaths = entityIndex.get(entity.name);
2769
+ if (!docPaths) {
2770
+ docPaths = new Set;
2771
+ entityIndex.set(entity.name, docPaths);
2634
2772
  }
2635
- entityIndex.get(entity.name).add(doc.path);
2773
+ docPaths.add(doc.path);
2636
2774
  }
2637
2775
  }
2638
- const validationErrors = validateDocuments(docs);
2776
+ const validationErrors = validateDocuments2(docs);
2639
2777
  for (const err of validationErrors) {
2640
2778
  issues.push({
2641
2779
  type: "error",
@@ -2673,7 +2811,7 @@ class ValidateCommand extends CommandRunner4 {
2673
2811
  }
2674
2812
  }
2675
2813
  __legacyDecorateClassTS([
2676
- Option4({
2814
+ Option5({
2677
2815
  flags: "--fix",
2678
2816
  description: "Show suggestions for common issues"
2679
2817
  }),
@@ -2682,8 +2820,8 @@ __legacyDecorateClassTS([
2682
2820
  __legacyMetadataTS("design:returntype", Boolean)
2683
2821
  ], ValidateCommand.prototype, "parseFix", null);
2684
2822
  ValidateCommand = __legacyDecorateClassTS([
2685
- Injectable13(),
2686
- Command4({
2823
+ Injectable14(),
2824
+ Command6({
2687
2825
  name: "validate",
2688
2826
  description: "Validate entity references and relationships across documents"
2689
2827
  }),
@@ -2691,138 +2829,90 @@ ValidateCommand = __legacyDecorateClassTS([
2691
2829
  typeof DocumentParserService === "undefined" ? Object : DocumentParserService
2692
2830
  ])
2693
2831
  ], ValidateCommand);
2694
- // src/commands/ontology.command.ts
2695
- import { Injectable as Injectable14 } from "@nestjs/common";
2696
- import { Command as Command5, CommandRunner as CommandRunner5 } from "nest-commander";
2697
- class OntologyCommand extends CommandRunner5 {
2698
- ontologyService;
2699
- constructor(ontologyService) {
2700
- super();
2701
- this.ontologyService = ontologyService;
2832
+ // src/embedding/embedding.module.ts
2833
+ import { Module } from "@nestjs/common";
2834
+ import { ConfigModule } from "@nestjs/config";
2835
+ class EmbeddingModule {
2836
+ }
2837
+ EmbeddingModule = __legacyDecorateClassTS([
2838
+ Module({
2839
+ imports: [ConfigModule],
2840
+ providers: [EmbeddingService],
2841
+ exports: [EmbeddingService]
2842
+ })
2843
+ ], EmbeddingModule);
2844
+
2845
+ // src/graph/graph.module.ts
2846
+ import { Module as Module2 } from "@nestjs/common";
2847
+ class GraphModule {
2848
+ }
2849
+ GraphModule = __legacyDecorateClassTS([
2850
+ Module2({
2851
+ providers: [GraphService],
2852
+ exports: [GraphService]
2853
+ })
2854
+ ], GraphModule);
2855
+
2856
+ // src/query/query.module.ts
2857
+ import { Module as Module3 } from "@nestjs/common";
2858
+
2859
+ // src/query/query.service.ts
2860
+ import { Injectable as Injectable15, Logger as Logger6 } from "@nestjs/common";
2861
+ class QueryService {
2862
+ graphService;
2863
+ logger = new Logger6(QueryService.name);
2864
+ constructor(graphService) {
2865
+ this.graphService = graphService;
2702
2866
  }
2703
- async run() {
2704
- try {
2705
- const ontology = await this.ontologyService.deriveOntology();
2706
- this.ontologyService.printSummary(ontology);
2707
- process.exit(0);
2708
- } catch (error) {
2709
- console.error(`
2710
- \u274C Ontology derivation failed:`, error instanceof Error ? error.message : String(error));
2711
- process.exit(1);
2712
- }
2867
+ async query(sql) {
2868
+ this.logger.debug(`Executing query: ${sql}`);
2869
+ return await this.graphService.query(sql);
2713
2870
  }
2714
2871
  }
2715
- OntologyCommand = __legacyDecorateClassTS([
2716
- Injectable14(),
2717
- Command5({
2718
- name: "ontology",
2719
- description: "Derive and display ontology from all documents"
2720
- }),
2872
+ QueryService = __legacyDecorateClassTS([
2873
+ Injectable15(),
2721
2874
  __legacyMetadataTS("design:paramtypes", [
2722
- typeof OntologyService === "undefined" ? Object : OntologyService
2875
+ typeof GraphService === "undefined" ? Object : GraphService
2723
2876
  ])
2724
- ], OntologyCommand);
2725
- // src/commands/init.command.ts
2726
- import { Injectable as Injectable15 } from "@nestjs/common";
2727
- import { Command as Command6, CommandRunner as CommandRunner6, Option as Option5 } from "nest-commander";
2728
- import * as fs from "fs/promises";
2729
- import * as path from "path";
2730
- import { fileURLToPath } from "url";
2731
- import { homedir } from "os";
2732
- var __filename2 = fileURLToPath(import.meta.url);
2733
- var __dirname2 = path.dirname(__filename2);
2734
- var COMMANDS = ["research.md", "graph-sync.md", "entity-extract.md"];
2877
+ ], QueryService);
2735
2878
 
2736
- class InitCommand extends CommandRunner6 {
2737
- async run(_inputs, options) {
2738
- try {
2739
- const targetDir = options.global ? path.join(homedir(), ".claude", "commands") : path.join(process.cwd(), ".claude", "commands");
2740
- let commandsSourceDir = path.resolve(__dirname2, "..", "commands");
2741
- try {
2742
- await fs.access(commandsSourceDir);
2743
- } catch {
2744
- commandsSourceDir = path.resolve(__dirname2, "..", "..", "commands");
2745
- }
2746
- try {
2747
- await fs.access(commandsSourceDir);
2748
- } catch {
2749
- console.error("Error: Commands source directory not found at", commandsSourceDir);
2750
- console.error("This may indicate a corrupted installation. Try reinstalling @zabaca/lattice.");
2751
- process.exit(1);
2752
- }
2753
- await fs.mkdir(targetDir, { recursive: true });
2754
- let copied = 0;
2755
- let skipped = 0;
2756
- const installed = [];
2757
- for (const file of COMMANDS) {
2758
- const sourcePath = path.join(commandsSourceDir, file);
2759
- const targetPath = path.join(targetDir, file);
2760
- try {
2761
- await fs.access(sourcePath);
2762
- try {
2763
- await fs.access(targetPath);
2764
- const sourceContent = await fs.readFile(sourcePath, "utf-8");
2765
- const targetContent = await fs.readFile(targetPath, "utf-8");
2766
- if (sourceContent === targetContent) {
2767
- skipped++;
2768
- continue;
2769
- }
2770
- } catch {}
2771
- await fs.copyFile(sourcePath, targetPath);
2772
- installed.push(file);
2773
- copied++;
2774
- } catch (err) {
2775
- console.error(`Warning: Could not copy ${file}:`, err instanceof Error ? err.message : String(err));
2776
- }
2777
- }
2778
- console.log();
2779
- console.log(`\u2705 Lattice commands installed to ${targetDir}`);
2780
- console.log();
2781
- if (copied > 0) {
2782
- console.log(`Installed ${copied} command(s):`);
2783
- installed.forEach((f) => {
2784
- const name = f.replace(".md", "");
2785
- console.log(` - /${name}`);
2786
- });
2787
- }
2788
- if (skipped > 0) {
2789
- console.log(`Skipped ${skipped} unchanged command(s)`);
2790
- }
2791
- console.log();
2792
- console.log("Available commands in Claude Code:");
2793
- console.log(" /research <topic> - AI-assisted research workflow");
2794
- console.log(" /graph-sync - Extract entities and sync to graph");
2795
- console.log(" /entity-extract - Extract entities from a single document");
2796
- console.log();
2797
- if (!options.global) {
2798
- console.log("\uD83D\uDCA1 Tip: Use 'lattice init --global' to install for all projects");
2799
- }
2800
- process.exit(0);
2801
- } catch (error) {
2802
- console.error("Error:", error instanceof Error ? error.message : String(error));
2803
- process.exit(1);
2804
- }
2805
- }
2806
- parseGlobal() {
2807
- return true;
2808
- }
2879
+ // src/query/query.module.ts
2880
+ class QueryModule {
2809
2881
  }
2810
- __legacyDecorateClassTS([
2811
- Option5({
2812
- flags: "-g, --global",
2813
- description: "Install to ~/.claude/commands/ (available in all projects)"
2814
- }),
2815
- __legacyMetadataTS("design:type", Function),
2816
- __legacyMetadataTS("design:paramtypes", []),
2817
- __legacyMetadataTS("design:returntype", Boolean)
2818
- ], InitCommand.prototype, "parseGlobal", null);
2819
- InitCommand = __legacyDecorateClassTS([
2820
- Injectable15(),
2821
- Command6({
2822
- name: "init",
2823
- description: "Install Claude Code slash commands for Lattice"
2882
+ QueryModule = __legacyDecorateClassTS([
2883
+ Module3({
2884
+ imports: [GraphModule, EmbeddingModule],
2885
+ providers: [QueryService, GraphService],
2886
+ exports: [QueryService, GraphService]
2824
2887
  })
2825
- ], InitCommand);
2888
+ ], QueryModule);
2889
+
2890
+ // src/sync/sync.module.ts
2891
+ import { Module as Module4 } from "@nestjs/common";
2892
+ class SyncModule {
2893
+ }
2894
+ SyncModule = __legacyDecorateClassTS([
2895
+ Module4({
2896
+ imports: [GraphModule, EmbeddingModule],
2897
+ providers: [
2898
+ SyncService,
2899
+ ManifestService,
2900
+ DocumentParserService,
2901
+ OntologyService,
2902
+ CascadeService,
2903
+ PathResolverService
2904
+ ],
2905
+ exports: [
2906
+ SyncService,
2907
+ ManifestService,
2908
+ DocumentParserService,
2909
+ OntologyService,
2910
+ CascadeService,
2911
+ PathResolverService
2912
+ ]
2913
+ })
2914
+ ], SyncModule);
2915
+
2826
2916
  // src/app.module.ts
2827
2917
  class AppModule {
2828
2918
  }
@@ -2842,7 +2932,7 @@ AppModule = __legacyDecorateClassTS([
2842
2932
  StatusCommand,
2843
2933
  SearchCommand,
2844
2934
  RelsCommand,
2845
- CypherCommand,
2935
+ SqlCommand,
2846
2936
  ValidateCommand,
2847
2937
  OntologyCommand,
2848
2938
  InitCommand