@zabaca/lattice 0.3.3 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main.js +1587 -1521
- package/package.json +64 -59
package/dist/main.js
CHANGED
|
@@ -23,26 +23,789 @@ 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/
|
|
27
|
-
import
|
|
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 FalkorDBConfigSchema = z.object({
|
|
144
|
+
host: z.string().min(1).default("localhost"),
|
|
145
|
+
port: z.coerce.number().int().positive().default(6379),
|
|
146
|
+
graphName: z.string().min(1).default("research_knowledge")
|
|
147
|
+
});
|
|
148
|
+
var EmbeddingConfigSchema = z.object({
|
|
149
|
+
provider: z.enum(["openai", "voyage", "nomic", "mock"]).default("voyage"),
|
|
150
|
+
apiKey: z.string().optional(),
|
|
151
|
+
model: z.string().min(1).default("voyage-3.5-lite"),
|
|
152
|
+
dimensions: z.coerce.number().int().positive().default(512)
|
|
153
|
+
});
|
|
154
|
+
var DocsConfigSchema = z.object({
|
|
155
|
+
projectRoot: z.string().default(process.cwd()),
|
|
156
|
+
docsPath: z.string().default("docs")
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// src/utils/frontmatter.ts
|
|
160
|
+
import matter from "gray-matter";
|
|
161
|
+
import { z as z2 } from "zod";
|
|
162
|
+
var EntityTypeSchema = z2.enum([
|
|
163
|
+
"Topic",
|
|
164
|
+
"Technology",
|
|
165
|
+
"Concept",
|
|
166
|
+
"Tool",
|
|
167
|
+
"Process",
|
|
168
|
+
"Person",
|
|
169
|
+
"Organization",
|
|
170
|
+
"Document"
|
|
171
|
+
]);
|
|
172
|
+
var RelationTypeSchema = z2.enum(["REFERENCES"]);
|
|
173
|
+
var EntitySchema = z2.object({
|
|
174
|
+
name: z2.string().min(1),
|
|
175
|
+
type: EntityTypeSchema,
|
|
176
|
+
description: z2.string().optional()
|
|
177
|
+
});
|
|
178
|
+
var RelationshipSchema = z2.object({
|
|
179
|
+
source: z2.string().min(1),
|
|
180
|
+
relation: RelationTypeSchema,
|
|
181
|
+
target: z2.string().min(1)
|
|
182
|
+
});
|
|
183
|
+
var GraphMetadataSchema = z2.object({
|
|
184
|
+
importance: z2.enum(["high", "medium", "low"]).optional(),
|
|
185
|
+
domain: z2.string().optional()
|
|
186
|
+
});
|
|
187
|
+
var validateDateFormat = (dateStr) => {
|
|
188
|
+
const match = dateStr.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
|
189
|
+
if (!match)
|
|
190
|
+
return false;
|
|
191
|
+
const [, yearStr, monthStr, dayStr] = match;
|
|
192
|
+
const year = parseInt(yearStr, 10);
|
|
193
|
+
const month = parseInt(monthStr, 10);
|
|
194
|
+
const day = parseInt(dayStr, 10);
|
|
195
|
+
if (month < 1 || month > 12)
|
|
196
|
+
return false;
|
|
197
|
+
if (day < 1 || day > 31)
|
|
198
|
+
return false;
|
|
199
|
+
const daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
200
|
+
if (year % 4 === 0 && year % 100 !== 0 || year % 400 === 0) {
|
|
201
|
+
daysInMonth[1] = 29;
|
|
202
|
+
}
|
|
203
|
+
return day <= daysInMonth[month - 1];
|
|
204
|
+
};
|
|
205
|
+
var FrontmatterSchema = z2.object({
|
|
206
|
+
created: z2.string().refine(validateDateFormat, "Date must be in YYYY-MM-DD format"),
|
|
207
|
+
updated: z2.string().refine(validateDateFormat, "Date must be in YYYY-MM-DD format"),
|
|
208
|
+
status: z2.enum(["draft", "ongoing", "complete"]).optional(),
|
|
209
|
+
topic: z2.string().optional(),
|
|
210
|
+
tags: z2.array(z2.string()).optional(),
|
|
211
|
+
summary: z2.string().optional(),
|
|
212
|
+
entities: z2.array(EntitySchema).optional(),
|
|
213
|
+
relationships: z2.array(RelationshipSchema).optional(),
|
|
214
|
+
graph: GraphMetadataSchema.optional()
|
|
215
|
+
}).passthrough();
|
|
216
|
+
function parseFrontmatter(content) {
|
|
217
|
+
try {
|
|
218
|
+
const { data, content: markdown } = matter(content);
|
|
219
|
+
if (Object.keys(data).length === 0) {
|
|
220
|
+
return {
|
|
221
|
+
frontmatter: null,
|
|
222
|
+
content: markdown.trim(),
|
|
223
|
+
raw: content
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
const normalizedData = normalizeData(data);
|
|
227
|
+
const validated = FrontmatterSchema.safeParse(normalizedData);
|
|
228
|
+
return {
|
|
229
|
+
frontmatter: validated.success ? validated.data : normalizedData,
|
|
230
|
+
content: markdown.trim(),
|
|
231
|
+
raw: content
|
|
232
|
+
};
|
|
233
|
+
} catch (error) {
|
|
234
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
235
|
+
throw new Error(`YAML parsing error: ${errorMessage}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
function normalizeData(data) {
|
|
239
|
+
if (data instanceof Date) {
|
|
240
|
+
return data.toISOString().split("T")[0];
|
|
241
|
+
}
|
|
242
|
+
if (Array.isArray(data)) {
|
|
243
|
+
return data.map(normalizeData);
|
|
244
|
+
}
|
|
245
|
+
if (data !== null && typeof data === "object") {
|
|
246
|
+
const normalized = {};
|
|
247
|
+
for (const [key, value] of Object.entries(data)) {
|
|
248
|
+
normalized[key] = normalizeData(value);
|
|
249
|
+
}
|
|
250
|
+
return normalized;
|
|
251
|
+
}
|
|
252
|
+
return data;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// src/sync/document-parser.service.ts
|
|
256
|
+
class DocumentParserService {
|
|
257
|
+
logger = new Logger(DocumentParserService.name);
|
|
258
|
+
docsPath;
|
|
259
|
+
constructor() {
|
|
260
|
+
const config = DocsConfigSchema.parse({
|
|
261
|
+
projectRoot: process.env.PROJECT_ROOT,
|
|
262
|
+
docsPath: process.env.DOCS_PATH
|
|
263
|
+
});
|
|
264
|
+
if (config.docsPath.startsWith("/")) {
|
|
265
|
+
this.docsPath = config.docsPath;
|
|
266
|
+
} else {
|
|
267
|
+
this.docsPath = resolve2(config.projectRoot, config.docsPath);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
getDocsPath() {
|
|
271
|
+
return this.docsPath;
|
|
272
|
+
}
|
|
273
|
+
async discoverDocuments() {
|
|
274
|
+
const pattern = `${this.docsPath}/**/*.md`;
|
|
275
|
+
const files = await glob(pattern, {
|
|
276
|
+
ignore: ["**/node_modules/**", "**/.git/**"]
|
|
277
|
+
});
|
|
278
|
+
return files.sort();
|
|
279
|
+
}
|
|
280
|
+
async parseDocument(filePath) {
|
|
281
|
+
const content = await readFile2(filePath, "utf-8");
|
|
282
|
+
const parsed = parseFrontmatter(content);
|
|
283
|
+
const title = this.extractTitle(content, filePath);
|
|
284
|
+
const contentHash = this.computeHash(content);
|
|
285
|
+
const frontmatterHash = this.computeHash(JSON.stringify(parsed.frontmatter || {}));
|
|
286
|
+
const entities = this.extractEntities(parsed.frontmatter, filePath);
|
|
287
|
+
const relationships = this.extractRelationships(parsed.frontmatter, filePath);
|
|
288
|
+
const graphMetadata = this.extractGraphMetadata(parsed.frontmatter);
|
|
289
|
+
return {
|
|
290
|
+
path: filePath,
|
|
291
|
+
title,
|
|
292
|
+
content: parsed.content,
|
|
293
|
+
contentHash,
|
|
294
|
+
frontmatterHash,
|
|
295
|
+
summary: parsed.frontmatter?.summary,
|
|
296
|
+
topic: parsed.frontmatter?.topic,
|
|
297
|
+
entities,
|
|
298
|
+
relationships,
|
|
299
|
+
graphMetadata,
|
|
300
|
+
tags: parsed.frontmatter?.tags || [],
|
|
301
|
+
created: parsed.frontmatter?.created,
|
|
302
|
+
updated: parsed.frontmatter?.updated,
|
|
303
|
+
status: parsed.frontmatter?.status
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
async parseAllDocuments() {
|
|
307
|
+
const { docs } = await this.parseAllDocumentsWithErrors();
|
|
308
|
+
return docs;
|
|
309
|
+
}
|
|
310
|
+
async parseAllDocumentsWithErrors() {
|
|
311
|
+
const files = await this.discoverDocuments();
|
|
312
|
+
const docs = [];
|
|
313
|
+
const errors = [];
|
|
314
|
+
for (const file of files) {
|
|
315
|
+
try {
|
|
316
|
+
const parsed = await this.parseDocument(file);
|
|
317
|
+
docs.push(parsed);
|
|
318
|
+
} catch (error) {
|
|
319
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
320
|
+
errors.push({ path: file, error: errorMsg });
|
|
321
|
+
this.logger.warn(`Failed to parse ${file}: ${error}`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return { docs, errors };
|
|
325
|
+
}
|
|
326
|
+
extractTitle(content, filePath) {
|
|
327
|
+
const h1Match = content.match(/^#\s+(.+)$/m);
|
|
328
|
+
if (h1Match) {
|
|
329
|
+
return h1Match[1];
|
|
330
|
+
}
|
|
331
|
+
const parts = filePath.split("/");
|
|
332
|
+
return parts[parts.length - 1].replace(".md", "");
|
|
333
|
+
}
|
|
334
|
+
extractEntities(frontmatter, docPath) {
|
|
335
|
+
const fm = frontmatter;
|
|
336
|
+
if (!fm?.entities || !Array.isArray(fm.entities)) {
|
|
337
|
+
return [];
|
|
338
|
+
}
|
|
339
|
+
const validEntities = [];
|
|
340
|
+
const errors = [];
|
|
341
|
+
for (let i = 0;i < fm.entities.length; i++) {
|
|
342
|
+
const e = fm.entities[i];
|
|
343
|
+
const result = EntitySchema.safeParse(e);
|
|
344
|
+
if (result.success) {
|
|
345
|
+
validEntities.push(result.data);
|
|
346
|
+
} else {
|
|
347
|
+
const entityPreview = typeof e === "string" ? `"${e}"` : JSON.stringify(e);
|
|
348
|
+
errors.push(`Entity[${i}]: ${entityPreview} - Expected object with {name, type}, got ${typeof e}`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
if (errors.length > 0) {
|
|
352
|
+
const errorMsg = `Invalid entity schema in ${docPath}:
|
|
353
|
+
${errors.join(`
|
|
354
|
+
`)}`;
|
|
355
|
+
throw new Error(errorMsg);
|
|
356
|
+
}
|
|
357
|
+
return validEntities;
|
|
358
|
+
}
|
|
359
|
+
extractRelationships(frontmatter, docPath) {
|
|
360
|
+
const fm = frontmatter;
|
|
361
|
+
if (!fm?.relationships || !Array.isArray(fm.relationships)) {
|
|
362
|
+
return [];
|
|
363
|
+
}
|
|
364
|
+
const validRelationships = [];
|
|
365
|
+
const errors = [];
|
|
366
|
+
const validRelationTypes = RelationTypeSchema.options;
|
|
367
|
+
for (let i = 0;i < fm.relationships.length; i++) {
|
|
368
|
+
const r = fm.relationships[i];
|
|
369
|
+
const result = RelationshipSchema.safeParse(r);
|
|
370
|
+
if (result.success) {
|
|
371
|
+
const rel = result.data;
|
|
372
|
+
if (rel.source === "this") {
|
|
373
|
+
rel.source = docPath;
|
|
374
|
+
}
|
|
375
|
+
if (rel.target === "this") {
|
|
376
|
+
rel.target = docPath;
|
|
377
|
+
}
|
|
378
|
+
validRelationships.push(rel);
|
|
379
|
+
} else {
|
|
380
|
+
if (typeof r === "string") {
|
|
381
|
+
errors.push(`Relationship[${i}]: "${r}" - Expected object with {source, relation, target}, got string`);
|
|
382
|
+
} else if (typeof r === "object" && r !== null) {
|
|
383
|
+
const issues = [];
|
|
384
|
+
if (!r.source)
|
|
385
|
+
issues.push("missing source");
|
|
386
|
+
if (!r.target)
|
|
387
|
+
issues.push("missing target");
|
|
388
|
+
if (!r.relation) {
|
|
389
|
+
issues.push("missing relation");
|
|
390
|
+
} else if (!validRelationTypes.includes(r.relation)) {
|
|
391
|
+
issues.push(`invalid relation "${r.relation}" (allowed: ${validRelationTypes.join(", ")})`);
|
|
392
|
+
}
|
|
393
|
+
errors.push(`Relationship[${i}]: ${issues.join(", ")}`);
|
|
394
|
+
} else {
|
|
395
|
+
errors.push(`Relationship[${i}]: Expected object, got ${typeof r}`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
if (errors.length > 0) {
|
|
400
|
+
const errorMsg = `Invalid relationship schema in ${docPath}:
|
|
401
|
+
${errors.join(`
|
|
402
|
+
`)}`;
|
|
403
|
+
throw new Error(errorMsg);
|
|
404
|
+
}
|
|
405
|
+
return validRelationships;
|
|
406
|
+
}
|
|
407
|
+
extractGraphMetadata(frontmatter) {
|
|
408
|
+
const fm = frontmatter;
|
|
409
|
+
if (!fm?.graph) {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
const result = GraphMetadataSchema.safeParse(fm.graph);
|
|
413
|
+
return result.success ? result.data : undefined;
|
|
414
|
+
}
|
|
415
|
+
computeHash(content) {
|
|
416
|
+
return createHash("sha256").update(content).digest("hex");
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
DocumentParserService = __legacyDecorateClassTS([
|
|
420
|
+
Injectable2(),
|
|
421
|
+
__legacyMetadataTS("design:paramtypes", [])
|
|
422
|
+
], DocumentParserService);
|
|
423
|
+
|
|
424
|
+
// src/sync/ontology.service.ts
|
|
425
|
+
class OntologyService {
|
|
426
|
+
parser;
|
|
427
|
+
constructor(parser) {
|
|
428
|
+
this.parser = parser;
|
|
429
|
+
}
|
|
430
|
+
async deriveOntology() {
|
|
431
|
+
const docs = await this.parser.parseAllDocuments();
|
|
432
|
+
return this.deriveFromDocuments(docs);
|
|
433
|
+
}
|
|
434
|
+
deriveFromDocuments(docs) {
|
|
435
|
+
const entityTypeSet = new Set;
|
|
436
|
+
const relationshipTypeSet = new Set;
|
|
437
|
+
const entityCounts = {};
|
|
438
|
+
const relationshipCounts = {};
|
|
439
|
+
const entityExamples = {};
|
|
440
|
+
let documentsWithEntities = 0;
|
|
441
|
+
let documentsWithoutEntities = 0;
|
|
442
|
+
let totalRelationships = 0;
|
|
443
|
+
for (const doc of docs) {
|
|
444
|
+
if (doc.entities.length > 0) {
|
|
445
|
+
documentsWithEntities++;
|
|
446
|
+
} else {
|
|
447
|
+
documentsWithoutEntities++;
|
|
448
|
+
}
|
|
449
|
+
for (const entity of doc.entities) {
|
|
450
|
+
entityTypeSet.add(entity.type);
|
|
451
|
+
entityCounts[entity.type] = (entityCounts[entity.type] || 0) + 1;
|
|
452
|
+
if (!entityExamples[entity.name]) {
|
|
453
|
+
entityExamples[entity.name] = { type: entity.type, documents: [] };
|
|
454
|
+
}
|
|
455
|
+
if (!entityExamples[entity.name].documents.includes(doc.path)) {
|
|
456
|
+
entityExamples[entity.name].documents.push(doc.path);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
for (const rel of doc.relationships) {
|
|
460
|
+
relationshipTypeSet.add(rel.relation);
|
|
461
|
+
relationshipCounts[rel.relation] = (relationshipCounts[rel.relation] || 0) + 1;
|
|
462
|
+
totalRelationships++;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return {
|
|
466
|
+
entityTypes: Array.from(entityTypeSet).sort(),
|
|
467
|
+
relationshipTypes: Array.from(relationshipTypeSet).sort(),
|
|
468
|
+
entityCounts,
|
|
469
|
+
relationshipCounts,
|
|
470
|
+
totalEntities: Object.keys(entityExamples).length,
|
|
471
|
+
totalRelationships,
|
|
472
|
+
documentsWithEntities,
|
|
473
|
+
documentsWithoutEntities,
|
|
474
|
+
entityExamples
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
printSummary(ontology) {
|
|
478
|
+
console.log(`
|
|
479
|
+
Derived Ontology Summary
|
|
480
|
+
`);
|
|
481
|
+
console.log(`Documents: ${ontology.documentsWithEntities} with entities, ${ontology.documentsWithoutEntities} without`);
|
|
482
|
+
console.log(`Unique Entities: ${ontology.totalEntities}`);
|
|
483
|
+
console.log(`Total Relationships: ${ontology.totalRelationships}`);
|
|
484
|
+
console.log(`
|
|
485
|
+
Entity Types:`);
|
|
486
|
+
for (const type of ontology.entityTypes) {
|
|
487
|
+
console.log(` ${type}: ${ontology.entityCounts[type]} instances`);
|
|
488
|
+
}
|
|
489
|
+
console.log(`
|
|
490
|
+
Relationship Types:`);
|
|
491
|
+
for (const type of ontology.relationshipTypes) {
|
|
492
|
+
console.log(` ${type}: ${ontology.relationshipCounts[type]} instances`);
|
|
493
|
+
}
|
|
494
|
+
console.log(`
|
|
495
|
+
Top Entities (by document count):`);
|
|
496
|
+
const sorted = Object.entries(ontology.entityExamples).sort((a, b) => b[1].documents.length - a[1].documents.length).slice(0, 10);
|
|
497
|
+
for (const [name, info] of sorted) {
|
|
498
|
+
console.log(` ${name} (${info.type}): ${info.documents.length} docs`);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
OntologyService = __legacyDecorateClassTS([
|
|
503
|
+
Injectable3(),
|
|
504
|
+
__legacyMetadataTS("design:paramtypes", [
|
|
505
|
+
typeof DocumentParserService === "undefined" ? Object : DocumentParserService
|
|
506
|
+
])
|
|
507
|
+
], OntologyService);
|
|
508
|
+
|
|
509
|
+
// src/commands/ontology.command.ts
|
|
510
|
+
class OntologyCommand extends CommandRunner2 {
|
|
511
|
+
ontologyService;
|
|
512
|
+
constructor(ontologyService) {
|
|
513
|
+
super();
|
|
514
|
+
this.ontologyService = ontologyService;
|
|
515
|
+
}
|
|
516
|
+
async run() {
|
|
517
|
+
try {
|
|
518
|
+
const ontology = await this.ontologyService.deriveOntology();
|
|
519
|
+
this.ontologyService.printSummary(ontology);
|
|
520
|
+
process.exit(0);
|
|
521
|
+
} catch (error) {
|
|
522
|
+
console.error(`
|
|
523
|
+
\u274C Ontology derivation failed:`, error instanceof Error ? error.message : String(error));
|
|
524
|
+
process.exit(1);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
OntologyCommand = __legacyDecorateClassTS([
|
|
529
|
+
Injectable4(),
|
|
530
|
+
Command2({
|
|
531
|
+
name: "ontology",
|
|
532
|
+
description: "Derive and display ontology from all documents"
|
|
533
|
+
}),
|
|
534
|
+
__legacyMetadataTS("design:paramtypes", [
|
|
535
|
+
typeof OntologyService === "undefined" ? Object : OntologyService
|
|
536
|
+
])
|
|
537
|
+
], OntologyCommand);
|
|
538
|
+
// src/commands/query.command.ts
|
|
539
|
+
import { Injectable as Injectable7 } from "@nestjs/common";
|
|
540
|
+
import { Command as Command3, CommandRunner as CommandRunner3, Option as Option2 } from "nest-commander";
|
|
541
|
+
|
|
542
|
+
// src/embedding/embedding.service.ts
|
|
543
|
+
import { Injectable as Injectable5, Logger as Logger2 } from "@nestjs/common";
|
|
544
|
+
import { ConfigService } from "@nestjs/config";
|
|
545
|
+
|
|
546
|
+
// src/embedding/embedding.types.ts
|
|
547
|
+
var DEFAULT_EMBEDDING_CONFIG = {
|
|
548
|
+
provider: "voyage",
|
|
549
|
+
model: "voyage-3.5-lite",
|
|
550
|
+
dimensions: 512
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
// src/embedding/providers/mock.provider.ts
|
|
554
|
+
class MockEmbeddingProvider {
|
|
555
|
+
name = "mock";
|
|
556
|
+
dimensions;
|
|
557
|
+
constructor(dimensions = 1536) {
|
|
558
|
+
this.dimensions = dimensions;
|
|
559
|
+
}
|
|
560
|
+
async generateEmbedding(text) {
|
|
561
|
+
return this.generateDeterministicEmbedding(text);
|
|
562
|
+
}
|
|
563
|
+
async generateEmbeddings(texts) {
|
|
564
|
+
return Promise.all(texts.map((text) => this.generateEmbedding(text)));
|
|
565
|
+
}
|
|
566
|
+
generateDeterministicEmbedding(text) {
|
|
567
|
+
const embedding = new Array(this.dimensions);
|
|
568
|
+
let hash = 0;
|
|
569
|
+
for (let i = 0;i < text.length; i++) {
|
|
570
|
+
hash = (hash << 5) - hash + text.charCodeAt(i);
|
|
571
|
+
hash = hash & hash;
|
|
572
|
+
}
|
|
573
|
+
for (let i = 0;i < this.dimensions; i++) {
|
|
574
|
+
const seed = hash * (i + 1) ^ i * 31;
|
|
575
|
+
embedding[i] = (seed % 2000 - 1000) / 1000;
|
|
576
|
+
}
|
|
577
|
+
const magnitude = Math.sqrt(embedding.reduce((sum, val) => sum + val * val, 0));
|
|
578
|
+
return embedding.map((val) => val / magnitude);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// src/embedding/providers/openai.provider.ts
|
|
583
|
+
class OpenAIEmbeddingProvider {
|
|
584
|
+
name = "openai";
|
|
585
|
+
dimensions;
|
|
586
|
+
model;
|
|
587
|
+
apiKey;
|
|
588
|
+
baseUrl = "https://api.openai.com/v1";
|
|
589
|
+
constructor(config) {
|
|
590
|
+
const apiKey = config?.apiKey || process.env.OPENAI_API_KEY;
|
|
591
|
+
if (!apiKey) {
|
|
592
|
+
throw new Error("OpenAI API key is required. Set OPENAI_API_KEY environment variable or pass apiKey in config.");
|
|
593
|
+
}
|
|
594
|
+
this.apiKey = apiKey;
|
|
595
|
+
this.model = config?.model || "text-embedding-3-small";
|
|
596
|
+
this.dimensions = config?.dimensions || 1536;
|
|
597
|
+
}
|
|
598
|
+
async generateEmbedding(text) {
|
|
599
|
+
const embeddings = await this.generateEmbeddings([text]);
|
|
600
|
+
return embeddings[0];
|
|
601
|
+
}
|
|
602
|
+
async generateEmbeddings(texts) {
|
|
603
|
+
if (!texts || texts.length === 0) {
|
|
604
|
+
return [];
|
|
605
|
+
}
|
|
606
|
+
try {
|
|
607
|
+
const response = await fetch(`${this.baseUrl}/embeddings`, {
|
|
608
|
+
method: "POST",
|
|
609
|
+
headers: {
|
|
610
|
+
"Content-Type": "application/json",
|
|
611
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
612
|
+
},
|
|
613
|
+
body: JSON.stringify({
|
|
614
|
+
model: this.model,
|
|
615
|
+
input: texts,
|
|
616
|
+
dimensions: this.dimensions
|
|
617
|
+
})
|
|
618
|
+
});
|
|
619
|
+
if (!response.ok) {
|
|
620
|
+
const error = await response.json().catch(() => ({}));
|
|
621
|
+
throw new Error(`OpenAI API error: ${response.status} ${JSON.stringify(error)}`);
|
|
622
|
+
}
|
|
623
|
+
const data = await response.json();
|
|
624
|
+
const sortedData = data.data.sort((a, b) => a.index - b.index);
|
|
625
|
+
return sortedData.map((item) => item.embedding);
|
|
626
|
+
} catch (error) {
|
|
627
|
+
if (error instanceof Error) {
|
|
628
|
+
throw new Error(`Failed to generate embeddings: ${error.message}`);
|
|
629
|
+
}
|
|
630
|
+
throw error;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// src/schemas/embedding.schemas.ts
|
|
636
|
+
import { z as z3 } from "zod";
|
|
637
|
+
var VoyageEmbeddingResponseSchema = z3.object({
|
|
638
|
+
object: z3.string(),
|
|
639
|
+
data: z3.array(z3.object({
|
|
640
|
+
object: z3.string(),
|
|
641
|
+
embedding: z3.array(z3.number()),
|
|
642
|
+
index: z3.number().int().nonnegative()
|
|
643
|
+
})),
|
|
644
|
+
model: z3.string(),
|
|
645
|
+
usage: z3.object({
|
|
646
|
+
total_tokens: z3.number().int().positive()
|
|
647
|
+
})
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
// src/embedding/providers/voyage.provider.ts
|
|
651
|
+
class VoyageEmbeddingProvider {
|
|
652
|
+
name = "voyage";
|
|
653
|
+
dimensions;
|
|
654
|
+
model;
|
|
655
|
+
apiKey;
|
|
656
|
+
inputType;
|
|
657
|
+
baseUrl = "https://api.voyageai.com/v1";
|
|
658
|
+
constructor(config) {
|
|
659
|
+
const apiKey = config?.apiKey || process.env.VOYAGE_API_KEY;
|
|
660
|
+
if (!apiKey) {
|
|
661
|
+
throw new Error("Voyage API key is required. Set VOYAGE_API_KEY environment variable or pass apiKey in config.");
|
|
662
|
+
}
|
|
663
|
+
this.apiKey = apiKey;
|
|
664
|
+
this.model = config?.model || "voyage-3.5-lite";
|
|
665
|
+
this.dimensions = config?.dimensions || 512;
|
|
666
|
+
this.inputType = config?.inputType || "document";
|
|
667
|
+
}
|
|
668
|
+
async generateEmbedding(text) {
|
|
669
|
+
const embeddings = await this.generateEmbeddings([text]);
|
|
670
|
+
return embeddings[0];
|
|
671
|
+
}
|
|
672
|
+
async generateEmbeddings(texts) {
|
|
673
|
+
if (!texts || texts.length === 0) {
|
|
674
|
+
return [];
|
|
675
|
+
}
|
|
676
|
+
try {
|
|
677
|
+
const response = await fetch(`${this.baseUrl}/embeddings`, {
|
|
678
|
+
method: "POST",
|
|
679
|
+
headers: {
|
|
680
|
+
"Content-Type": "application/json",
|
|
681
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
682
|
+
},
|
|
683
|
+
body: JSON.stringify({
|
|
684
|
+
model: this.model,
|
|
685
|
+
input: texts,
|
|
686
|
+
output_dimension: this.dimensions,
|
|
687
|
+
input_type: this.inputType
|
|
688
|
+
})
|
|
689
|
+
});
|
|
690
|
+
if (!response.ok) {
|
|
691
|
+
const error = await response.json().catch(() => ({}));
|
|
692
|
+
throw new Error(`Voyage API error: ${response.status} ${JSON.stringify(error)}`);
|
|
693
|
+
}
|
|
694
|
+
const data = VoyageEmbeddingResponseSchema.parse(await response.json());
|
|
695
|
+
const sortedData = data.data.sort((a, b) => a.index - b.index);
|
|
696
|
+
return sortedData.map((item) => item.embedding);
|
|
697
|
+
} catch (error) {
|
|
698
|
+
if (error instanceof Error) {
|
|
699
|
+
throw new Error(`Failed to generate embeddings: ${error.message}`);
|
|
700
|
+
}
|
|
701
|
+
throw error;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// src/embedding/embedding.service.ts
|
|
707
|
+
class EmbeddingService {
|
|
708
|
+
configService;
|
|
709
|
+
logger = new Logger2(EmbeddingService.name);
|
|
710
|
+
provider;
|
|
711
|
+
config;
|
|
712
|
+
constructor(configService) {
|
|
713
|
+
this.configService = configService;
|
|
714
|
+
this.config = this.loadConfig();
|
|
715
|
+
this.provider = this.createProvider();
|
|
716
|
+
this.logger.log(`Initialized embedding service with provider: ${this.provider.name}`);
|
|
717
|
+
}
|
|
718
|
+
loadConfig() {
|
|
719
|
+
const providerEnv = this.configService.get("EMBEDDING_PROVIDER");
|
|
720
|
+
const provider = providerEnv ?? DEFAULT_EMBEDDING_CONFIG.provider;
|
|
721
|
+
let apiKey;
|
|
722
|
+
if (provider === "voyage") {
|
|
723
|
+
apiKey = this.configService.get("VOYAGE_API_KEY");
|
|
724
|
+
} else if (provider === "openai") {
|
|
725
|
+
apiKey = this.configService.get("OPENAI_API_KEY");
|
|
726
|
+
}
|
|
727
|
+
return EmbeddingConfigSchema.parse({
|
|
728
|
+
provider: providerEnv,
|
|
729
|
+
apiKey,
|
|
730
|
+
model: this.configService.get("EMBEDDING_MODEL"),
|
|
731
|
+
dimensions: this.configService.get("EMBEDDING_DIMENSIONS")
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
createProvider() {
|
|
735
|
+
switch (this.config.provider) {
|
|
736
|
+
case "openai":
|
|
737
|
+
if (!this.config.apiKey) {
|
|
738
|
+
throw new Error("OPENAI_API_KEY environment variable is required for embeddings. " + "Set it in .env or use --no-embeddings to skip embedding generation.");
|
|
739
|
+
}
|
|
740
|
+
return new OpenAIEmbeddingProvider({
|
|
741
|
+
apiKey: this.config.apiKey,
|
|
742
|
+
model: this.config.model,
|
|
743
|
+
dimensions: this.config.dimensions
|
|
744
|
+
});
|
|
745
|
+
case "mock":
|
|
746
|
+
return new MockEmbeddingProvider(this.config.dimensions);
|
|
747
|
+
case "voyage":
|
|
748
|
+
if (!this.config.apiKey) {
|
|
749
|
+
throw new Error("VOYAGE_API_KEY environment variable is required for embeddings. " + "Set it in .env or use --no-embeddings to skip embedding generation.");
|
|
750
|
+
}
|
|
751
|
+
return new VoyageEmbeddingProvider({
|
|
752
|
+
apiKey: this.config.apiKey,
|
|
753
|
+
model: this.config.model,
|
|
754
|
+
dimensions: this.config.dimensions
|
|
755
|
+
});
|
|
756
|
+
case "nomic":
|
|
757
|
+
throw new Error(`Provider ${this.config.provider} not yet implemented. Use 'voyage', 'openai', or 'mock'.`);
|
|
758
|
+
default:
|
|
759
|
+
throw new Error(`Unknown embedding provider: ${this.config.provider}. Use 'voyage', 'openai', or 'mock'.`);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
getProviderName() {
|
|
763
|
+
return this.provider.name;
|
|
764
|
+
}
|
|
765
|
+
getDimensions() {
|
|
766
|
+
return this.provider.dimensions;
|
|
767
|
+
}
|
|
768
|
+
async generateEmbedding(text) {
|
|
769
|
+
if (!text || text.trim().length === 0) {
|
|
770
|
+
throw new Error("Cannot generate embedding for empty text");
|
|
771
|
+
}
|
|
772
|
+
return this.provider.generateEmbedding(text);
|
|
773
|
+
}
|
|
774
|
+
async generateEmbeddings(texts) {
|
|
775
|
+
const validTexts = texts.filter((t) => t && t.trim().length > 0);
|
|
776
|
+
if (validTexts.length === 0) {
|
|
777
|
+
return [];
|
|
778
|
+
}
|
|
779
|
+
return this.provider.generateEmbeddings(validTexts);
|
|
780
|
+
}
|
|
781
|
+
isRealProvider() {
|
|
782
|
+
return this.provider.name !== "mock";
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
EmbeddingService = __legacyDecorateClassTS([
|
|
786
|
+
Injectable5(),
|
|
787
|
+
__legacyMetadataTS("design:paramtypes", [
|
|
788
|
+
typeof ConfigService === "undefined" ? Object : ConfigService
|
|
789
|
+
])
|
|
790
|
+
], EmbeddingService);
|
|
28
791
|
|
|
29
792
|
// src/graph/graph.service.ts
|
|
30
|
-
import { Injectable, Logger } from "@nestjs/common";
|
|
31
|
-
import { ConfigService } from "@nestjs/config";
|
|
793
|
+
import { Injectable as Injectable6, Logger as Logger3 } from "@nestjs/common";
|
|
794
|
+
import { ConfigService as ConfigService2 } from "@nestjs/config";
|
|
32
795
|
import Redis from "ioredis";
|
|
33
796
|
class GraphService {
|
|
34
797
|
configService;
|
|
35
|
-
logger = new
|
|
798
|
+
logger = new Logger3(GraphService.name);
|
|
36
799
|
redis = null;
|
|
37
800
|
config;
|
|
38
801
|
connecting = null;
|
|
39
802
|
constructor(configService) {
|
|
40
803
|
this.configService = configService;
|
|
41
|
-
this.config = {
|
|
42
|
-
host: this.configService.get("FALKORDB_HOST"
|
|
43
|
-
port: this.configService.get("FALKORDB_PORT"
|
|
44
|
-
graphName: this.configService.get("GRAPH_NAME"
|
|
45
|
-
};
|
|
804
|
+
this.config = FalkorDBConfigSchema.parse({
|
|
805
|
+
host: this.configService.get("FALKORDB_HOST"),
|
|
806
|
+
port: this.configService.get("FALKORDB_PORT"),
|
|
807
|
+
graphName: this.configService.get("GRAPH_NAME")
|
|
808
|
+
});
|
|
46
809
|
}
|
|
47
810
|
async onModuleDestroy() {
|
|
48
811
|
await this.disconnect();
|
|
@@ -165,7 +928,7 @@ class GraphService {
|
|
|
165
928
|
async deleteDocumentRelationships(documentPath) {
|
|
166
929
|
try {
|
|
167
930
|
const escapedPath = this.escapeCypher(documentPath);
|
|
168
|
-
const cypher = `MATCH ()-[r { documentPath: '${escapedPath}' }]-()
|
|
931
|
+
const cypher = `MATCH ()-[r { documentPath: '${escapedPath}' }]-() DELETE r`;
|
|
169
932
|
await this.query(cypher);
|
|
170
933
|
} catch (error) {
|
|
171
934
|
this.logger.error(`Failed to delete document relationships: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -240,7 +1003,16 @@ class GraphService {
|
|
|
240
1003
|
}
|
|
241
1004
|
}
|
|
242
1005
|
async vectorSearchAll(queryVector, k = 10) {
|
|
243
|
-
const allLabels = [
|
|
1006
|
+
const allLabels = [
|
|
1007
|
+
"Document",
|
|
1008
|
+
"Concept",
|
|
1009
|
+
"Process",
|
|
1010
|
+
"Tool",
|
|
1011
|
+
"Technology",
|
|
1012
|
+
"Organization",
|
|
1013
|
+
"Topic",
|
|
1014
|
+
"Person"
|
|
1015
|
+
];
|
|
244
1016
|
const allResults = [];
|
|
245
1017
|
for (const label of allLabels) {
|
|
246
1018
|
try {
|
|
@@ -324,34 +1096,252 @@ class GraphService {
|
|
|
324
1096
|
}
|
|
325
1097
|
}
|
|
326
1098
|
GraphService = __legacyDecorateClassTS([
|
|
327
|
-
|
|
1099
|
+
Injectable6(),
|
|
328
1100
|
__legacyMetadataTS("design:paramtypes", [
|
|
329
|
-
typeof
|
|
1101
|
+
typeof ConfigService2 === "undefined" ? Object : ConfigService2
|
|
330
1102
|
])
|
|
331
1103
|
], GraphService);
|
|
332
1104
|
|
|
333
|
-
// src/
|
|
334
|
-
class
|
|
1105
|
+
// src/commands/query.command.ts
|
|
1106
|
+
class SearchCommand extends CommandRunner3 {
|
|
1107
|
+
graphService;
|
|
1108
|
+
embeddingService;
|
|
1109
|
+
constructor(graphService, embeddingService) {
|
|
1110
|
+
super();
|
|
1111
|
+
this.graphService = graphService;
|
|
1112
|
+
this.embeddingService = embeddingService;
|
|
1113
|
+
}
|
|
1114
|
+
async run(inputs, options) {
|
|
1115
|
+
const query = inputs[0];
|
|
1116
|
+
const limit = Math.min(parseInt(options.limit || "20", 10), 100);
|
|
1117
|
+
try {
|
|
1118
|
+
const queryEmbedding = await this.embeddingService.generateEmbedding(query);
|
|
1119
|
+
let results;
|
|
1120
|
+
if (options.label) {
|
|
1121
|
+
const labelResults = await this.graphService.vectorSearch(options.label, queryEmbedding, limit);
|
|
1122
|
+
results = labelResults.map((r) => ({
|
|
1123
|
+
name: r.name,
|
|
1124
|
+
label: options.label,
|
|
1125
|
+
title: r.title,
|
|
1126
|
+
score: r.score
|
|
1127
|
+
}));
|
|
1128
|
+
} else {
|
|
1129
|
+
results = await this.graphService.vectorSearchAll(queryEmbedding, limit);
|
|
1130
|
+
}
|
|
1131
|
+
const labelSuffix = options.label ? ` (${options.label})` : "";
|
|
1132
|
+
console.log(`
|
|
1133
|
+
=== Semantic Search Results for "${query}"${labelSuffix} ===
|
|
1134
|
+
`);
|
|
1135
|
+
if (results.length === 0) {
|
|
1136
|
+
console.log(`No results found.
|
|
1137
|
+
`);
|
|
1138
|
+
if (options.label) {
|
|
1139
|
+
console.log(`Tip: Try without --label to search all entity types.
|
|
1140
|
+
`);
|
|
1141
|
+
}
|
|
1142
|
+
process.exit(0);
|
|
1143
|
+
}
|
|
1144
|
+
results.forEach((result, idx) => {
|
|
1145
|
+
console.log(`${idx + 1}. [${result.label}] ${result.name}`);
|
|
1146
|
+
if (result.title) {
|
|
1147
|
+
console.log(` Title: ${result.title}`);
|
|
1148
|
+
}
|
|
1149
|
+
if (result.description && result.label !== "Document") {
|
|
1150
|
+
const desc = result.description.length > 80 ? `${result.description.slice(0, 80)}...` : result.description;
|
|
1151
|
+
console.log(` ${desc}`);
|
|
1152
|
+
}
|
|
1153
|
+
console.log(` Similarity: ${(result.score * 100).toFixed(2)}%`);
|
|
1154
|
+
});
|
|
1155
|
+
console.log();
|
|
1156
|
+
process.exit(0);
|
|
1157
|
+
} catch (error) {
|
|
1158
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1159
|
+
console.error("Error:", errorMsg);
|
|
1160
|
+
if (errorMsg.includes("no embeddings") || errorMsg.includes("vector")) {
|
|
1161
|
+
console.log(`
|
|
1162
|
+
Note: Semantic search requires embeddings to be generated first.`);
|
|
1163
|
+
console.log(`Run 'lattice sync' to generate embeddings for documents.
|
|
1164
|
+
`);
|
|
1165
|
+
}
|
|
1166
|
+
process.exit(1);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
parseLabel(value) {
|
|
1170
|
+
return value;
|
|
1171
|
+
}
|
|
1172
|
+
parseLimit(value) {
|
|
1173
|
+
return value;
|
|
1174
|
+
}
|
|
335
1175
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
})
|
|
341
|
-
|
|
1176
|
+
__legacyDecorateClassTS([
|
|
1177
|
+
Option2({
|
|
1178
|
+
flags: "-l, --label <label>",
|
|
1179
|
+
description: "Filter by entity label (e.g., Technology, Concept, Document)"
|
|
1180
|
+
}),
|
|
1181
|
+
__legacyMetadataTS("design:type", Function),
|
|
1182
|
+
__legacyMetadataTS("design:paramtypes", [
|
|
1183
|
+
String
|
|
1184
|
+
]),
|
|
1185
|
+
__legacyMetadataTS("design:returntype", String)
|
|
1186
|
+
], SearchCommand.prototype, "parseLabel", null);
|
|
1187
|
+
__legacyDecorateClassTS([
|
|
1188
|
+
Option2({
|
|
1189
|
+
flags: "--limit <n>",
|
|
1190
|
+
description: "Limit results",
|
|
1191
|
+
defaultValue: "20"
|
|
1192
|
+
}),
|
|
1193
|
+
__legacyMetadataTS("design:type", Function),
|
|
1194
|
+
__legacyMetadataTS("design:paramtypes", [
|
|
1195
|
+
String
|
|
1196
|
+
]),
|
|
1197
|
+
__legacyMetadataTS("design:returntype", String)
|
|
1198
|
+
], SearchCommand.prototype, "parseLimit", null);
|
|
1199
|
+
SearchCommand = __legacyDecorateClassTS([
|
|
1200
|
+
Injectable7(),
|
|
1201
|
+
Command3({
|
|
1202
|
+
name: "search",
|
|
1203
|
+
arguments: "<query>",
|
|
1204
|
+
description: "Semantic search across the knowledge graph"
|
|
1205
|
+
}),
|
|
1206
|
+
__legacyMetadataTS("design:paramtypes", [
|
|
1207
|
+
typeof GraphService === "undefined" ? Object : GraphService,
|
|
1208
|
+
typeof EmbeddingService === "undefined" ? Object : EmbeddingService
|
|
1209
|
+
])
|
|
1210
|
+
], SearchCommand);
|
|
342
1211
|
|
|
343
|
-
|
|
344
|
-
|
|
1212
|
+
class RelsCommand extends CommandRunner3 {
|
|
1213
|
+
graphService;
|
|
1214
|
+
constructor(graphService) {
|
|
1215
|
+
super();
|
|
1216
|
+
this.graphService = graphService;
|
|
1217
|
+
}
|
|
1218
|
+
async run(inputs) {
|
|
1219
|
+
const name = inputs[0];
|
|
1220
|
+
try {
|
|
1221
|
+
const escapedName = name.replace(/'/g, "\\'");
|
|
1222
|
+
const cypher = `MATCH (a { name: '${escapedName}' })-[r]-(b) RETURN a, r, b`;
|
|
1223
|
+
const result = await this.graphService.query(cypher);
|
|
1224
|
+
const results = result.resultSet || [];
|
|
1225
|
+
console.log(`
|
|
1226
|
+
=== Relationships for "${name}" ===
|
|
1227
|
+
`);
|
|
1228
|
+
if (results.length === 0) {
|
|
1229
|
+
console.log(`No relationships found.
|
|
1230
|
+
`);
|
|
1231
|
+
process.exit(0);
|
|
1232
|
+
}
|
|
1233
|
+
const incoming = [];
|
|
1234
|
+
const outgoing = [];
|
|
1235
|
+
for (const row of results) {
|
|
1236
|
+
const [source, rel, target] = row;
|
|
1237
|
+
const sourceObj = Object.fromEntries(source);
|
|
1238
|
+
const targetObj = Object.fromEntries(target);
|
|
1239
|
+
const relObj = Object.fromEntries(rel);
|
|
1240
|
+
const sourceProps = Object.fromEntries(sourceObj.properties || []);
|
|
1241
|
+
const targetProps = Object.fromEntries(targetObj.properties || []);
|
|
1242
|
+
const sourceName = sourceProps.name || "unknown";
|
|
1243
|
+
const targetName = targetProps.name || "unknown";
|
|
1244
|
+
const relType = relObj.type || "UNKNOWN";
|
|
1245
|
+
if (sourceName === name) {
|
|
1246
|
+
outgoing.push(` -[${relType}]-> ${targetName}`);
|
|
1247
|
+
} else {
|
|
1248
|
+
incoming.push(` <-[${relType}]- ${sourceName}`);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
if (outgoing.length > 0) {
|
|
1252
|
+
console.log("Outgoing:");
|
|
1253
|
+
for (const r of outgoing) {
|
|
1254
|
+
console.log(r);
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
if (incoming.length > 0) {
|
|
1258
|
+
if (outgoing.length > 0)
|
|
1259
|
+
console.log();
|
|
1260
|
+
console.log("Incoming:");
|
|
1261
|
+
for (const r of incoming) {
|
|
1262
|
+
console.log(r);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
console.log();
|
|
1266
|
+
process.exit(0);
|
|
1267
|
+
} catch (error) {
|
|
1268
|
+
console.error("Error:", error instanceof Error ? error.message : String(error));
|
|
1269
|
+
process.exit(1);
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
RelsCommand = __legacyDecorateClassTS([
|
|
1274
|
+
Injectable7(),
|
|
1275
|
+
Command3({
|
|
1276
|
+
name: "rels",
|
|
1277
|
+
arguments: "<name>",
|
|
1278
|
+
description: "Show relationships for a node"
|
|
1279
|
+
}),
|
|
1280
|
+
__legacyMetadataTS("design:paramtypes", [
|
|
1281
|
+
typeof GraphService === "undefined" ? Object : GraphService
|
|
1282
|
+
])
|
|
1283
|
+
], RelsCommand);
|
|
345
1284
|
|
|
346
|
-
|
|
347
|
-
|
|
1285
|
+
class CypherCommand extends CommandRunner3 {
|
|
1286
|
+
graphService;
|
|
1287
|
+
constructor(graphService) {
|
|
1288
|
+
super();
|
|
1289
|
+
this.graphService = graphService;
|
|
1290
|
+
}
|
|
1291
|
+
async run(inputs) {
|
|
1292
|
+
const query = inputs[0];
|
|
1293
|
+
try {
|
|
1294
|
+
const result = await this.graphService.query(query);
|
|
1295
|
+
console.log(`
|
|
1296
|
+
=== Cypher Query Results ===
|
|
1297
|
+
`);
|
|
1298
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1299
|
+
console.log();
|
|
1300
|
+
process.exit(0);
|
|
1301
|
+
} catch (error) {
|
|
1302
|
+
console.error("Error:", error instanceof Error ? error.message : String(error));
|
|
1303
|
+
process.exit(1);
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
CypherCommand = __legacyDecorateClassTS([
|
|
1308
|
+
Injectable7(),
|
|
1309
|
+
Command3({
|
|
1310
|
+
name: "cypher",
|
|
1311
|
+
arguments: "<query>",
|
|
1312
|
+
description: "Execute raw Cypher query"
|
|
1313
|
+
}),
|
|
1314
|
+
__legacyMetadataTS("design:paramtypes", [
|
|
1315
|
+
typeof GraphService === "undefined" ? Object : GraphService
|
|
1316
|
+
])
|
|
1317
|
+
], CypherCommand);
|
|
1318
|
+
// src/commands/status.command.ts
|
|
1319
|
+
import { Injectable as Injectable12 } from "@nestjs/common";
|
|
1320
|
+
import { Command as Command4, CommandRunner as CommandRunner4, Option as Option3 } from "nest-commander";
|
|
348
1321
|
|
|
349
1322
|
// src/sync/manifest.service.ts
|
|
350
|
-
import {
|
|
351
|
-
import { createHash } from "crypto";
|
|
352
|
-
import { readFile, writeFile } from "fs/promises";
|
|
1323
|
+
import { createHash as createHash2 } from "crypto";
|
|
353
1324
|
import { existsSync } from "fs";
|
|
354
|
-
import {
|
|
1325
|
+
import { readFile as readFile3, writeFile } from "fs/promises";
|
|
1326
|
+
import { resolve as resolve3 } from "path";
|
|
1327
|
+
import { Injectable as Injectable8 } from "@nestjs/common";
|
|
1328
|
+
|
|
1329
|
+
// src/schemas/manifest.schemas.ts
|
|
1330
|
+
import { z as z4 } from "zod";
|
|
1331
|
+
var ManifestEntrySchema = z4.object({
|
|
1332
|
+
contentHash: z4.string(),
|
|
1333
|
+
frontmatterHash: z4.string(),
|
|
1334
|
+
lastSynced: z4.string(),
|
|
1335
|
+
entityCount: z4.number().int().nonnegative(),
|
|
1336
|
+
relationshipCount: z4.number().int().nonnegative()
|
|
1337
|
+
});
|
|
1338
|
+
var SyncManifestSchema = z4.object({
|
|
1339
|
+
version: z4.string(),
|
|
1340
|
+
lastSync: z4.string(),
|
|
1341
|
+
documents: z4.record(z4.string(), ManifestEntrySchema)
|
|
1342
|
+
});
|
|
1343
|
+
|
|
1344
|
+
// src/sync/manifest.service.ts
|
|
355
1345
|
function getProjectRoot() {
|
|
356
1346
|
if (process.env.PROJECT_ROOT) {
|
|
357
1347
|
return process.env.PROJECT_ROOT;
|
|
@@ -364,17 +1354,17 @@ class ManifestService {
|
|
|
364
1354
|
manifest = null;
|
|
365
1355
|
constructor() {
|
|
366
1356
|
const docsPath = process.env.DOCS_PATH || "docs";
|
|
367
|
-
this.manifestPath =
|
|
1357
|
+
this.manifestPath = resolve3(getProjectRoot(), docsPath, ".sync-manifest.json");
|
|
368
1358
|
}
|
|
369
1359
|
async load() {
|
|
370
1360
|
try {
|
|
371
1361
|
if (existsSync(this.manifestPath)) {
|
|
372
|
-
const content = await
|
|
373
|
-
this.manifest = JSON.parse(content);
|
|
1362
|
+
const content = await readFile3(this.manifestPath, "utf-8");
|
|
1363
|
+
this.manifest = SyncManifestSchema.parse(JSON.parse(content));
|
|
374
1364
|
} else {
|
|
375
1365
|
this.manifest = this.createEmptyManifest();
|
|
376
1366
|
}
|
|
377
|
-
} catch (
|
|
1367
|
+
} catch (_error) {
|
|
378
1368
|
this.manifest = this.createEmptyManifest();
|
|
379
1369
|
}
|
|
380
1370
|
return this.manifest;
|
|
@@ -388,13 +1378,13 @@ class ManifestService {
|
|
|
388
1378
|
await writeFile(this.manifestPath, content, "utf-8");
|
|
389
1379
|
}
|
|
390
1380
|
getContentHash(content) {
|
|
391
|
-
return
|
|
1381
|
+
return createHash2("sha256").update(content).digest("hex");
|
|
392
1382
|
}
|
|
393
|
-
detectChange(
|
|
1383
|
+
detectChange(path2, contentHash, frontmatterHash) {
|
|
394
1384
|
if (!this.manifest) {
|
|
395
1385
|
throw new Error("Manifest not loaded. Call load() first.");
|
|
396
1386
|
}
|
|
397
|
-
const existing = this.manifest.documents[
|
|
1387
|
+
const existing = this.manifest.documents[path2];
|
|
398
1388
|
if (!existing) {
|
|
399
1389
|
return "new";
|
|
400
1390
|
}
|
|
@@ -403,11 +1393,11 @@ class ManifestService {
|
|
|
403
1393
|
}
|
|
404
1394
|
return "updated";
|
|
405
1395
|
}
|
|
406
|
-
updateEntry(
|
|
1396
|
+
updateEntry(path2, contentHash, frontmatterHash, entityCount, relationshipCount) {
|
|
407
1397
|
if (!this.manifest) {
|
|
408
1398
|
throw new Error("Manifest not loaded. Call load() first.");
|
|
409
1399
|
}
|
|
410
|
-
this.manifest.documents[
|
|
1400
|
+
this.manifest.documents[path2] = {
|
|
411
1401
|
contentHash,
|
|
412
1402
|
frontmatterHash,
|
|
413
1403
|
lastSynced: new Date().toISOString(),
|
|
@@ -415,11 +1405,11 @@ class ManifestService {
|
|
|
415
1405
|
relationshipCount
|
|
416
1406
|
};
|
|
417
1407
|
}
|
|
418
|
-
removeEntry(
|
|
1408
|
+
removeEntry(path2) {
|
|
419
1409
|
if (!this.manifest) {
|
|
420
1410
|
throw new Error("Manifest not loaded. Call load() first.");
|
|
421
1411
|
}
|
|
422
|
-
delete this.manifest.documents[
|
|
1412
|
+
delete this.manifest.documents[path2];
|
|
423
1413
|
}
|
|
424
1414
|
getTrackedPaths() {
|
|
425
1415
|
if (!this.manifest) {
|
|
@@ -436,291 +1426,118 @@ class ManifestService {
|
|
|
436
1426
|
}
|
|
437
1427
|
}
|
|
438
1428
|
ManifestService = __legacyDecorateClassTS([
|
|
439
|
-
|
|
1429
|
+
Injectable8(),
|
|
440
1430
|
__legacyMetadataTS("design:paramtypes", [])
|
|
441
1431
|
], ManifestService);
|
|
442
1432
|
|
|
443
|
-
// src/sync/
|
|
444
|
-
import { Injectable as
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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;
|
|
1433
|
+
// src/sync/sync.service.ts
|
|
1434
|
+
import { Injectable as Injectable11, Logger as Logger5 } from "@nestjs/common";
|
|
1435
|
+
// src/pure/embedding-text.ts
|
|
1436
|
+
function composeDocumentEmbeddingText(doc) {
|
|
1437
|
+
const parts = [];
|
|
1438
|
+
if (doc.title) {
|
|
1439
|
+
parts.push(`Title: ${doc.title}`);
|
|
495
1440
|
}
|
|
496
|
-
|
|
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}`);
|
|
1441
|
+
if (doc.topic) {
|
|
1442
|
+
parts.push(`Topic: ${doc.topic}`);
|
|
529
1443
|
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
if (data instanceof Date) {
|
|
533
|
-
return data.toISOString().split("T")[0];
|
|
1444
|
+
if (doc.tags && doc.tags.length > 0) {
|
|
1445
|
+
parts.push(`Tags: ${doc.tags.join(", ")}`);
|
|
534
1446
|
}
|
|
535
|
-
if (
|
|
536
|
-
|
|
1447
|
+
if (doc.entities && doc.entities.length > 0) {
|
|
1448
|
+
const entityNames = doc.entities.map((e) => e.name).join(", ");
|
|
1449
|
+
parts.push(`Entities: ${entityNames}`);
|
|
537
1450
|
}
|
|
538
|
-
if (
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
}
|
|
543
|
-
return normalized;
|
|
1451
|
+
if (doc.summary) {
|
|
1452
|
+
parts.push(doc.summary);
|
|
1453
|
+
} else {
|
|
1454
|
+
parts.push(doc.content.slice(0, 500));
|
|
544
1455
|
}
|
|
545
|
-
return
|
|
1456
|
+
return parts.join(" | ");
|
|
546
1457
|
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
return process.env.PROJECT_ROOT;
|
|
1458
|
+
function composeEntityEmbeddingText(entity) {
|
|
1459
|
+
const parts = [`${entity.type}: ${entity.name}`];
|
|
1460
|
+
if (entity.description) {
|
|
1461
|
+
parts.push(entity.description);
|
|
552
1462
|
}
|
|
553
|
-
return
|
|
1463
|
+
return parts.join(". ");
|
|
554
1464
|
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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);
|
|
1465
|
+
function collectUniqueEntities(docs) {
|
|
1466
|
+
const entities = new Map;
|
|
1467
|
+
for (const doc of docs) {
|
|
1468
|
+
for (const entity of doc.entities) {
|
|
1469
|
+
const key = `${entity.type}:${entity.name}`;
|
|
1470
|
+
const existing = entities.get(key);
|
|
1471
|
+
if (!existing) {
|
|
1472
|
+
entities.set(key, {
|
|
1473
|
+
type: entity.type,
|
|
1474
|
+
name: entity.name,
|
|
1475
|
+
description: entity.description,
|
|
1476
|
+
documentPaths: [doc.path]
|
|
1477
|
+
});
|
|
642
1478
|
} else {
|
|
643
|
-
|
|
644
|
-
|
|
1479
|
+
existing.documentPaths.push(doc.path);
|
|
1480
|
+
if (entity.description && (!existing.description || entity.description.length > existing.description.length)) {
|
|
1481
|
+
existing.description = entity.description;
|
|
1482
|
+
}
|
|
645
1483
|
}
|
|
646
1484
|
}
|
|
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
1485
|
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
for (
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
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
|
-
}
|
|
1486
|
+
return entities;
|
|
1487
|
+
}
|
|
1488
|
+
// src/pure/validation.ts
|
|
1489
|
+
function validateDocuments(docs) {
|
|
1490
|
+
const errors = [];
|
|
1491
|
+
const entityIndex = new Map;
|
|
1492
|
+
for (const doc of docs) {
|
|
1493
|
+
for (const entity of doc.entities) {
|
|
1494
|
+
let docSet = entityIndex.get(entity.name);
|
|
1495
|
+
if (!docSet) {
|
|
1496
|
+
docSet = new Set;
|
|
1497
|
+
entityIndex.set(entity.name, docSet);
|
|
692
1498
|
}
|
|
1499
|
+
docSet.add(doc.path);
|
|
693
1500
|
}
|
|
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
1501
|
}
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
1502
|
+
for (const doc of docs) {
|
|
1503
|
+
for (const rel of doc.relationships) {
|
|
1504
|
+
if (rel.source !== doc.path && !entityIndex.has(rel.source)) {
|
|
1505
|
+
errors.push({
|
|
1506
|
+
path: doc.path,
|
|
1507
|
+
error: `Relationship source "${rel.source}" not found in any document`
|
|
1508
|
+
});
|
|
1509
|
+
}
|
|
1510
|
+
const isDocPath = rel.target.endsWith(".md");
|
|
1511
|
+
const isKnownEntity = entityIndex.has(rel.target);
|
|
1512
|
+
const isSelfReference = rel.target === doc.path;
|
|
1513
|
+
if (!isDocPath && !isKnownEntity && !isSelfReference) {
|
|
1514
|
+
errors.push({
|
|
1515
|
+
path: doc.path,
|
|
1516
|
+
error: `Relationship target "${rel.target}" not found as entity`
|
|
1517
|
+
});
|
|
1518
|
+
}
|
|
705
1519
|
}
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
1520
|
+
}
|
|
1521
|
+
return errors;
|
|
1522
|
+
}
|
|
1523
|
+
function getChangeReason(changeType) {
|
|
1524
|
+
switch (changeType) {
|
|
1525
|
+
case "new":
|
|
1526
|
+
return "New document";
|
|
1527
|
+
case "updated":
|
|
1528
|
+
return "Content or frontmatter changed";
|
|
1529
|
+
case "deleted":
|
|
1530
|
+
return "File no longer exists";
|
|
1531
|
+
case "unchanged":
|
|
1532
|
+
return "No changes detected";
|
|
711
1533
|
}
|
|
712
1534
|
}
|
|
713
|
-
DocumentParserService = __legacyDecorateClassTS([
|
|
714
|
-
Injectable3(),
|
|
715
|
-
__legacyMetadataTS("design:paramtypes", [])
|
|
716
|
-
], DocumentParserService);
|
|
717
|
-
|
|
718
1535
|
// src/sync/cascade.service.ts
|
|
719
|
-
import { Injectable as
|
|
1536
|
+
import { Injectable as Injectable9, Logger as Logger4 } from "@nestjs/common";
|
|
720
1537
|
class CascadeService {
|
|
721
1538
|
graph;
|
|
722
1539
|
_parser;
|
|
723
|
-
logger = new
|
|
1540
|
+
logger = new Logger4(CascadeService.name);
|
|
724
1541
|
constructor(graph, _parser) {
|
|
725
1542
|
this.graph = graph;
|
|
726
1543
|
this._parser = _parser;
|
|
@@ -871,450 +1688,212 @@ class CascadeService {
|
|
|
871
1688
|
const escapedName = this.escapeForCypher(entityName);
|
|
872
1689
|
const query = `
|
|
873
1690
|
MATCH (e {name: '${escapedName}'})-[:APPEARS_IN]->(d:Document)
|
|
874
|
-
RETURN d.name, d.title
|
|
875
|
-
`.trim();
|
|
876
|
-
const result = await this.graph.query(query);
|
|
877
|
-
return (result.resultSet || []).map((row) => ({
|
|
878
|
-
path: row[0],
|
|
879
|
-
reason: `References "${entityName}" with type "${oldType}" (now "${newType}")`,
|
|
880
|
-
suggestedAction: "review_content",
|
|
881
|
-
confidence: "medium",
|
|
882
|
-
affectedEntities: [entityName]
|
|
883
|
-
}));
|
|
884
|
-
} catch (error) {
|
|
885
|
-
this.logger.warn(`Failed to find documents affected by type change: ${error instanceof Error ? error.message : String(error)}`);
|
|
886
|
-
return [];
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
async findAffectedByRelationshipChange(entityName) {
|
|
890
|
-
try {
|
|
891
|
-
const escapedName = this.escapeForCypher(entityName);
|
|
892
|
-
const query = `
|
|
893
|
-
MATCH (e {name: '${escapedName}'})-[r]->(d:Document)
|
|
894
|
-
RETURN d.name, d.title, type(r) as relType
|
|
895
|
-
`.trim();
|
|
896
|
-
const result = await this.graph.query(query);
|
|
897
|
-
return (result.resultSet || []).map((row) => ({
|
|
898
|
-
path: row[0],
|
|
899
|
-
reason: `Has relationship with "${entityName}"`,
|
|
900
|
-
suggestedAction: "review_content",
|
|
901
|
-
confidence: "medium",
|
|
902
|
-
affectedEntities: [entityName]
|
|
903
|
-
}));
|
|
904
|
-
} catch (error) {
|
|
905
|
-
this.logger.warn(`Failed to find documents affected by relationship change: ${error instanceof Error ? error.message : String(error)}`);
|
|
906
|
-
return [];
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
async findAffectedByDocumentDeletion(documentPath) {
|
|
910
|
-
try {
|
|
911
|
-
const escapedPath = this.escapeForCypher(documentPath);
|
|
912
|
-
const query = `
|
|
913
|
-
MATCH (d:Document)-[r]->(deleted:Document {name: '${escapedPath}'})
|
|
914
|
-
RETURN d.name, type(r) as relType
|
|
915
|
-
`.trim();
|
|
916
|
-
const result = await this.graph.query(query);
|
|
917
|
-
return (result.resultSet || []).map((row) => ({
|
|
918
|
-
path: row[0],
|
|
919
|
-
reason: `Links to deleted document "${documentPath}"`,
|
|
920
|
-
suggestedAction: "remove_reference",
|
|
921
|
-
confidence: "high",
|
|
922
|
-
affectedEntities: []
|
|
923
|
-
}));
|
|
924
|
-
} catch (error) {
|
|
925
|
-
this.logger.warn(`Failed to find documents affected by document deletion: ${error instanceof Error ? error.message : String(error)}`);
|
|
926
|
-
return [];
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
async analyzeDocumentChange(oldDoc, newDoc) {
|
|
930
|
-
if (!oldDoc) {
|
|
931
|
-
return [];
|
|
932
|
-
}
|
|
933
|
-
const analyses = [];
|
|
934
|
-
const renames = this.detectEntityRenames(oldDoc, newDoc);
|
|
935
|
-
const deletions = this.detectEntityDeletions(oldDoc, newDoc);
|
|
936
|
-
const typeChanges = this.detectEntityTypeChanges(oldDoc, newDoc);
|
|
937
|
-
for (const change of renames) {
|
|
938
|
-
const analysis = await this.analyzeEntityChange(change);
|
|
939
|
-
if (analysis.affectedDocuments.length > 0) {
|
|
940
|
-
analyses.push(analysis);
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
for (const change of deletions) {
|
|
944
|
-
const analysis = await this.analyzeEntityChange(change);
|
|
945
|
-
if (analysis.affectedDocuments.length > 0) {
|
|
946
|
-
analyses.push(analysis);
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
for (const change of typeChanges) {
|
|
950
|
-
const analysis = await this.analyzeEntityChange(change);
|
|
951
|
-
if (analysis.affectedDocuments.length > 0) {
|
|
952
|
-
analyses.push(analysis);
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
|
-
return analyses;
|
|
956
|
-
}
|
|
957
|
-
formatWarnings(analyses) {
|
|
958
|
-
if (analyses.length === 0) {
|
|
959
|
-
return "";
|
|
960
|
-
}
|
|
961
|
-
const lines = [];
|
|
962
|
-
lines.push(`
|
|
963
|
-
=== Cascade Impact Detected ===
|
|
964
|
-
`);
|
|
965
|
-
for (const analysis of analyses) {
|
|
966
|
-
lines.push(analysis.summary);
|
|
967
|
-
lines.push(` Source: ${analysis.sourceDocument}`);
|
|
968
|
-
lines.push("");
|
|
969
|
-
if (analysis.affectedDocuments.length > 0) {
|
|
970
|
-
lines.push(` Affected documents (${analysis.affectedDocuments.length}):`);
|
|
971
|
-
for (const doc of analysis.affectedDocuments) {
|
|
972
|
-
lines.push(` [${doc.confidence}] ${doc.path}`);
|
|
973
|
-
lines.push(` ${doc.reason}`);
|
|
974
|
-
lines.push(` -> Suggested: ${this.formatSuggestedAction(doc.suggestedAction, analysis)}`);
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
lines.push("");
|
|
978
|
-
}
|
|
979
|
-
return lines.join(`
|
|
980
|
-
`);
|
|
981
|
-
}
|
|
982
|
-
formatSuggestedAction(action, analysis) {
|
|
983
|
-
switch (action) {
|
|
984
|
-
case "update_reference":
|
|
985
|
-
if (analysis.trigger === "entity_renamed") {
|
|
986
|
-
const match = analysis.summary.match(/renamed to "([^"]+)"/);
|
|
987
|
-
const newName = match ? match[1] : "new name";
|
|
988
|
-
return `Update reference to "${newName}"`;
|
|
989
|
-
}
|
|
990
|
-
return "Update reference";
|
|
991
|
-
case "remove_reference":
|
|
992
|
-
return "Remove broken reference";
|
|
993
|
-
case "review_content":
|
|
994
|
-
return "Review content for consistency";
|
|
995
|
-
case "add_entity":
|
|
996
|
-
return "Consider adding entity definition";
|
|
997
|
-
default:
|
|
998
|
-
return action;
|
|
999
|
-
}
|
|
1000
|
-
}
|
|
1001
|
-
escapeForCypher(value) {
|
|
1002
|
-
return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/"/g, "\\\"");
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1005
|
-
CascadeService = __legacyDecorateClassTS([
|
|
1006
|
-
Injectable4(),
|
|
1007
|
-
__legacyMetadataTS("design:paramtypes", [
|
|
1008
|
-
typeof GraphService === "undefined" ? Object : GraphService,
|
|
1009
|
-
typeof DocumentParserService === "undefined" ? Object : DocumentParserService
|
|
1010
|
-
])
|
|
1011
|
-
], CascadeService);
|
|
1012
|
-
|
|
1013
|
-
// 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
|
-
import { existsSync as existsSync2 } from "fs";
|
|
1017
|
-
class PathResolverService {
|
|
1018
|
-
parser;
|
|
1019
|
-
logger = new Logger4(PathResolverService.name);
|
|
1020
|
-
constructor(parser) {
|
|
1021
|
-
this.parser = parser;
|
|
1022
|
-
}
|
|
1023
|
-
getDocsPath() {
|
|
1024
|
-
return this.parser.getDocsPath();
|
|
1025
|
-
}
|
|
1026
|
-
resolveDocPath(userPath, options = {}) {
|
|
1027
|
-
const { requireExists = true, requireInDocs = true } = options;
|
|
1028
|
-
let resolvedPath;
|
|
1029
|
-
if (isAbsolute(userPath)) {
|
|
1030
|
-
resolvedPath = userPath;
|
|
1031
|
-
} else {
|
|
1032
|
-
const fromCwd = resolve3(process.cwd(), userPath);
|
|
1033
|
-
const fromDocs = resolve3(this.getDocsPath(), userPath);
|
|
1034
|
-
const docsPrefix = "docs/";
|
|
1035
|
-
const strippedFromDocs = userPath.startsWith(docsPrefix) ? resolve3(this.getDocsPath(), userPath.slice(docsPrefix.length)) : null;
|
|
1036
|
-
if (this.isUnderDocs(fromCwd) && existsSync2(fromCwd)) {
|
|
1037
|
-
resolvedPath = fromCwd;
|
|
1038
|
-
} else if (strippedFromDocs && existsSync2(strippedFromDocs)) {
|
|
1039
|
-
resolvedPath = strippedFromDocs;
|
|
1040
|
-
} else if (existsSync2(fromDocs)) {
|
|
1041
|
-
resolvedPath = fromDocs;
|
|
1042
|
-
} else if (this.isUnderDocs(fromCwd)) {
|
|
1043
|
-
resolvedPath = fromCwd;
|
|
1044
|
-
} else if (strippedFromDocs) {
|
|
1045
|
-
resolvedPath = strippedFromDocs;
|
|
1046
|
-
} else {
|
|
1047
|
-
resolvedPath = fromDocs;
|
|
1048
|
-
}
|
|
1049
|
-
}
|
|
1050
|
-
if (requireInDocs && !this.isUnderDocs(resolvedPath)) {
|
|
1051
|
-
throw new Error(`Path "${userPath}" resolves to "${resolvedPath}" which is outside the docs directory (${this.getDocsPath()})`);
|
|
1052
|
-
}
|
|
1053
|
-
if (requireExists && !existsSync2(resolvedPath)) {
|
|
1054
|
-
throw new Error(`Path "${userPath}" does not exist (resolved to: ${resolvedPath})`);
|
|
1055
|
-
}
|
|
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);
|
|
1691
|
+
RETURN d.name, d.title
|
|
1692
|
+
`.trim();
|
|
1693
|
+
const result = await this.graph.query(query);
|
|
1694
|
+
return (result.resultSet || []).map((row) => ({
|
|
1695
|
+
path: row[0],
|
|
1696
|
+
reason: `References "${entityName}" with type "${oldType}" (now "${newType}")`,
|
|
1697
|
+
suggestedAction: "review_content",
|
|
1698
|
+
confidence: "medium",
|
|
1699
|
+
affectedEntities: [entityName]
|
|
1700
|
+
}));
|
|
1137
1701
|
} catch (error) {
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
}
|
|
1141
|
-
throw error;
|
|
1702
|
+
this.logger.warn(`Failed to find documents affected by type change: ${error instanceof Error ? error.message : String(error)}`);
|
|
1703
|
+
return [];
|
|
1142
1704
|
}
|
|
1143
1705
|
}
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1706
|
+
async findAffectedByRelationshipChange(entityName) {
|
|
1707
|
+
try {
|
|
1708
|
+
const escapedName = this.escapeForCypher(entityName);
|
|
1709
|
+
const query = `
|
|
1710
|
+
MATCH (e {name: '${escapedName}'})-[r]->(d:Document)
|
|
1711
|
+
RETURN d.name, d.title, type(r) as relType
|
|
1712
|
+
`.trim();
|
|
1713
|
+
const result = await this.graph.query(query);
|
|
1714
|
+
return (result.resultSet || []).map((row) => ({
|
|
1715
|
+
path: row[0],
|
|
1716
|
+
reason: `Has relationship with "${entityName}"`,
|
|
1717
|
+
suggestedAction: "review_content",
|
|
1718
|
+
confidence: "medium",
|
|
1719
|
+
affectedEntities: [entityName]
|
|
1720
|
+
}));
|
|
1721
|
+
} catch (error) {
|
|
1722
|
+
this.logger.warn(`Failed to find documents affected by relationship change: ${error instanceof Error ? error.message : String(error)}`);
|
|
1723
|
+
return [];
|
|
1158
1724
|
}
|
|
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
1725
|
}
|
|
1164
|
-
async
|
|
1165
|
-
|
|
1166
|
-
|
|
1726
|
+
async findAffectedByDocumentDeletion(documentPath) {
|
|
1727
|
+
try {
|
|
1728
|
+
const escapedPath = this.escapeForCypher(documentPath);
|
|
1729
|
+
const query = `
|
|
1730
|
+
MATCH (d:Document)-[r]->(deleted:Document {name: '${escapedPath}'})
|
|
1731
|
+
RETURN d.name, type(r) as relType
|
|
1732
|
+
`.trim();
|
|
1733
|
+
const result = await this.graph.query(query);
|
|
1734
|
+
return (result.resultSet || []).map((row) => ({
|
|
1735
|
+
path: row[0],
|
|
1736
|
+
reason: `Links to deleted document "${documentPath}"`,
|
|
1737
|
+
suggestedAction: "remove_reference",
|
|
1738
|
+
confidence: "high",
|
|
1739
|
+
affectedEntities: []
|
|
1740
|
+
}));
|
|
1741
|
+
} catch (error) {
|
|
1742
|
+
this.logger.warn(`Failed to find documents affected by document deletion: ${error instanceof Error ? error.message : String(error)}`);
|
|
1743
|
+
return [];
|
|
1744
|
+
}
|
|
1167
1745
|
}
|
|
1168
|
-
async
|
|
1169
|
-
if (!
|
|
1746
|
+
async analyzeDocumentChange(oldDoc, newDoc) {
|
|
1747
|
+
if (!oldDoc) {
|
|
1170
1748
|
return [];
|
|
1171
1749
|
}
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
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}`);
|
|
1750
|
+
const analyses = [];
|
|
1751
|
+
const renames = this.detectEntityRenames(oldDoc, newDoc);
|
|
1752
|
+
const deletions = this.detectEntityDeletions(oldDoc, newDoc);
|
|
1753
|
+
const typeChanges = this.detectEntityTypeChanges(oldDoc, newDoc);
|
|
1754
|
+
for (const change of renames) {
|
|
1755
|
+
const analysis = await this.analyzeEntityChange(change);
|
|
1756
|
+
if (analysis.affectedDocuments.length > 0) {
|
|
1757
|
+
analyses.push(analysis);
|
|
1196
1758
|
}
|
|
1197
|
-
throw error;
|
|
1198
1759
|
}
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
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;
|
|
1760
|
+
for (const change of deletions) {
|
|
1761
|
+
const analysis = await this.analyzeEntityChange(change);
|
|
1762
|
+
if (analysis.affectedDocuments.length > 0) {
|
|
1763
|
+
analyses.push(analysis);
|
|
1764
|
+
}
|
|
1221
1765
|
}
|
|
1222
|
-
for (
|
|
1223
|
-
const
|
|
1224
|
-
|
|
1766
|
+
for (const change of typeChanges) {
|
|
1767
|
+
const analysis = await this.analyzeEntityChange(change);
|
|
1768
|
+
if (analysis.affectedDocuments.length > 0) {
|
|
1769
|
+
analyses.push(analysis);
|
|
1770
|
+
}
|
|
1225
1771
|
}
|
|
1226
|
-
|
|
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}`);
|
|
1772
|
+
return analyses;
|
|
1242
1773
|
}
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
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");
|
|
1774
|
+
formatWarnings(analyses) {
|
|
1775
|
+
if (analyses.length === 0) {
|
|
1776
|
+
return "";
|
|
1253
1777
|
}
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
}
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1778
|
+
const lines = [];
|
|
1779
|
+
lines.push(`
|
|
1780
|
+
=== Cascade Impact Detected ===
|
|
1781
|
+
`);
|
|
1782
|
+
for (const analysis of analyses) {
|
|
1783
|
+
lines.push(analysis.summary);
|
|
1784
|
+
lines.push(` Source: ${analysis.sourceDocument}`);
|
|
1785
|
+
lines.push("");
|
|
1786
|
+
if (analysis.affectedDocuments.length > 0) {
|
|
1787
|
+
lines.push(` Affected documents (${analysis.affectedDocuments.length}):`);
|
|
1788
|
+
for (const doc of analysis.affectedDocuments) {
|
|
1789
|
+
lines.push(` [${doc.confidence}] ${doc.path}`);
|
|
1790
|
+
lines.push(` ${doc.reason}`);
|
|
1791
|
+
lines.push(` -> Suggested: ${this.formatSuggestedAction(doc.suggestedAction, analysis)}`);
|
|
1266
1792
|
}
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1793
|
+
}
|
|
1794
|
+
lines.push("");
|
|
1795
|
+
}
|
|
1796
|
+
return lines.join(`
|
|
1797
|
+
`);
|
|
1798
|
+
}
|
|
1799
|
+
formatSuggestedAction(action, analysis) {
|
|
1800
|
+
switch (action) {
|
|
1801
|
+
case "update_reference":
|
|
1802
|
+
if (analysis.trigger === "entity_renamed") {
|
|
1803
|
+
const match = analysis.summary.match(/renamed to "([^"]+)"/);
|
|
1804
|
+
const newName = match ? match[1] : "new name";
|
|
1805
|
+
return `Update reference to "${newName}"`;
|
|
1277
1806
|
}
|
|
1278
|
-
return
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
case "
|
|
1284
|
-
|
|
1807
|
+
return "Update reference";
|
|
1808
|
+
case "remove_reference":
|
|
1809
|
+
return "Remove broken reference";
|
|
1810
|
+
case "review_content":
|
|
1811
|
+
return "Review content for consistency";
|
|
1812
|
+
case "add_entity":
|
|
1813
|
+
return "Consider adding entity definition";
|
|
1285
1814
|
default:
|
|
1286
|
-
|
|
1815
|
+
return action;
|
|
1287
1816
|
}
|
|
1288
1817
|
}
|
|
1289
|
-
|
|
1290
|
-
return
|
|
1818
|
+
escapeForCypher(value) {
|
|
1819
|
+
return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/"/g, "\\\"");
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
CascadeService = __legacyDecorateClassTS([
|
|
1823
|
+
Injectable9(),
|
|
1824
|
+
__legacyMetadataTS("design:paramtypes", [
|
|
1825
|
+
typeof GraphService === "undefined" ? Object : GraphService,
|
|
1826
|
+
typeof DocumentParserService === "undefined" ? Object : DocumentParserService
|
|
1827
|
+
])
|
|
1828
|
+
], CascadeService);
|
|
1829
|
+
|
|
1830
|
+
// src/sync/path-resolver.service.ts
|
|
1831
|
+
import { existsSync as existsSync2 } from "fs";
|
|
1832
|
+
import { isAbsolute, resolve as resolve4 } from "path";
|
|
1833
|
+
import { Injectable as Injectable10 } from "@nestjs/common";
|
|
1834
|
+
class PathResolverService {
|
|
1835
|
+
parser;
|
|
1836
|
+
constructor(parser) {
|
|
1837
|
+
this.parser = parser;
|
|
1291
1838
|
}
|
|
1292
|
-
|
|
1293
|
-
return this.
|
|
1839
|
+
getDocsPath() {
|
|
1840
|
+
return this.parser.getDocsPath();
|
|
1294
1841
|
}
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1842
|
+
resolveDocPath(userPath, options = {}) {
|
|
1843
|
+
const { requireExists = true, requireInDocs = true } = options;
|
|
1844
|
+
let resolvedPath;
|
|
1845
|
+
if (isAbsolute(userPath)) {
|
|
1846
|
+
resolvedPath = userPath;
|
|
1847
|
+
} else {
|
|
1848
|
+
const fromCwd = resolve4(process.cwd(), userPath);
|
|
1849
|
+
const fromDocs = resolve4(this.getDocsPath(), userPath);
|
|
1850
|
+
const docsPrefix = "docs/";
|
|
1851
|
+
const strippedFromDocs = userPath.startsWith(docsPrefix) ? resolve4(this.getDocsPath(), userPath.slice(docsPrefix.length)) : null;
|
|
1852
|
+
if (this.isUnderDocs(fromCwd) && existsSync2(fromCwd)) {
|
|
1853
|
+
resolvedPath = fromCwd;
|
|
1854
|
+
} else if (strippedFromDocs && existsSync2(strippedFromDocs)) {
|
|
1855
|
+
resolvedPath = strippedFromDocs;
|
|
1856
|
+
} else if (existsSync2(fromDocs)) {
|
|
1857
|
+
resolvedPath = fromDocs;
|
|
1858
|
+
} else if (this.isUnderDocs(fromCwd)) {
|
|
1859
|
+
resolvedPath = fromCwd;
|
|
1860
|
+
} else if (strippedFromDocs) {
|
|
1861
|
+
resolvedPath = strippedFromDocs;
|
|
1862
|
+
} else {
|
|
1863
|
+
resolvedPath = fromDocs;
|
|
1864
|
+
}
|
|
1298
1865
|
}
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
async generateEmbeddings(texts) {
|
|
1302
|
-
const validTexts = texts.filter((t) => t && t.trim().length > 0);
|
|
1303
|
-
if (validTexts.length === 0) {
|
|
1304
|
-
return [];
|
|
1866
|
+
if (requireInDocs && !this.isUnderDocs(resolvedPath)) {
|
|
1867
|
+
throw new Error(`Path "${userPath}" resolves to "${resolvedPath}" which is outside the docs directory (${this.getDocsPath()})`);
|
|
1305
1868
|
}
|
|
1306
|
-
|
|
1869
|
+
if (requireExists && !existsSync2(resolvedPath)) {
|
|
1870
|
+
throw new Error(`Path "${userPath}" does not exist (resolved to: ${resolvedPath})`);
|
|
1871
|
+
}
|
|
1872
|
+
return resolvedPath;
|
|
1307
1873
|
}
|
|
1308
|
-
|
|
1309
|
-
return this.
|
|
1874
|
+
resolveDocPaths(userPaths, options = {}) {
|
|
1875
|
+
return userPaths.map((p) => this.resolveDocPath(p, options));
|
|
1876
|
+
}
|
|
1877
|
+
isUnderDocs(absolutePath) {
|
|
1878
|
+
const docsPath = this.getDocsPath();
|
|
1879
|
+
const normalizedPath = absolutePath.replace(/\/$/, "");
|
|
1880
|
+
const normalizedDocs = docsPath.replace(/\/$/, "");
|
|
1881
|
+
return normalizedPath.startsWith(`${normalizedDocs}/`) || normalizedPath === normalizedDocs;
|
|
1882
|
+
}
|
|
1883
|
+
getRelativePath(absolutePath) {
|
|
1884
|
+
const docsPath = this.getDocsPath();
|
|
1885
|
+
if (absolutePath.startsWith(docsPath)) {
|
|
1886
|
+
return absolutePath.slice(docsPath.length + 1);
|
|
1887
|
+
}
|
|
1888
|
+
return absolutePath;
|
|
1310
1889
|
}
|
|
1311
1890
|
}
|
|
1312
|
-
|
|
1313
|
-
|
|
1891
|
+
PathResolverService = __legacyDecorateClassTS([
|
|
1892
|
+
Injectable10(),
|
|
1314
1893
|
__legacyMetadataTS("design:paramtypes", [
|
|
1315
|
-
typeof
|
|
1894
|
+
typeof DocumentParserService === "undefined" ? Object : DocumentParserService
|
|
1316
1895
|
])
|
|
1317
|
-
],
|
|
1896
|
+
], PathResolverService);
|
|
1318
1897
|
|
|
1319
1898
|
// src/sync/sync.service.ts
|
|
1320
1899
|
var ENTITY_TYPES = [
|
|
@@ -1326,37 +1905,8 @@ var ENTITY_TYPES = [
|
|
|
1326
1905
|
"Person",
|
|
1327
1906
|
"Organization"
|
|
1328
1907
|
];
|
|
1329
|
-
function
|
|
1330
|
-
|
|
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;
|
|
1908
|
+
function validateDocuments2(docs) {
|
|
1909
|
+
return validateDocuments(docs);
|
|
1360
1910
|
}
|
|
1361
1911
|
|
|
1362
1912
|
class SyncService {
|
|
@@ -1366,7 +1916,7 @@ class SyncService {
|
|
|
1366
1916
|
cascade;
|
|
1367
1917
|
pathResolver;
|
|
1368
1918
|
embeddingService;
|
|
1369
|
-
logger = new
|
|
1919
|
+
logger = new Logger5(SyncService.name);
|
|
1370
1920
|
constructor(manifest, parser, graph, cascade, pathResolver, embeddingService) {
|
|
1371
1921
|
this.manifest = manifest;
|
|
1372
1922
|
this.parser = parser;
|
|
@@ -1421,7 +1971,7 @@ class SyncService {
|
|
|
1421
1971
|
}
|
|
1422
1972
|
}
|
|
1423
1973
|
}
|
|
1424
|
-
const validationErrors =
|
|
1974
|
+
const validationErrors = validateDocuments2(docsToSync);
|
|
1425
1975
|
if (validationErrors.length > 0) {
|
|
1426
1976
|
for (const err of validationErrors) {
|
|
1427
1977
|
result.errors.push(err);
|
|
@@ -1431,7 +1981,7 @@ class SyncService {
|
|
|
1431
1981
|
result.duration = Date.now() - startTime;
|
|
1432
1982
|
return result;
|
|
1433
1983
|
}
|
|
1434
|
-
const uniqueEntities =
|
|
1984
|
+
const uniqueEntities = collectUniqueEntities(docsToSync);
|
|
1435
1985
|
if (options.verbose) {
|
|
1436
1986
|
this.logger.log(`Collected ${uniqueEntities.size} unique entities from ${docsToSync.length} documents`);
|
|
1437
1987
|
}
|
|
@@ -1508,7 +2058,7 @@ class SyncService {
|
|
|
1508
2058
|
changes.push({
|
|
1509
2059
|
path: docPath,
|
|
1510
2060
|
changeType,
|
|
1511
|
-
reason:
|
|
2061
|
+
reason: getChangeReason(changeType)
|
|
1512
2062
|
});
|
|
1513
2063
|
trackedPaths.delete(docPath);
|
|
1514
2064
|
} catch (error) {
|
|
@@ -1537,9 +2087,9 @@ class SyncService {
|
|
|
1537
2087
|
await this.graph.deleteDocumentRelationships(doc.path);
|
|
1538
2088
|
const documentProps = {
|
|
1539
2089
|
name: doc.path,
|
|
1540
|
-
title: doc.title,
|
|
2090
|
+
title: doc.title ?? "",
|
|
1541
2091
|
contentHash: doc.contentHash,
|
|
1542
|
-
tags: doc.tags
|
|
2092
|
+
tags: doc.tags ?? []
|
|
1543
2093
|
};
|
|
1544
2094
|
if (doc.summary)
|
|
1545
2095
|
documentProps.summary = doc.summary;
|
|
@@ -1561,7 +2111,7 @@ class SyncService {
|
|
|
1561
2111
|
let embeddingGenerated = false;
|
|
1562
2112
|
if (options.embeddings && !options.skipEmbeddings && this.embeddingService) {
|
|
1563
2113
|
try {
|
|
1564
|
-
const textForEmbedding =
|
|
2114
|
+
const textForEmbedding = composeDocumentEmbeddingText(doc);
|
|
1565
2115
|
if (textForEmbedding.trim()) {
|
|
1566
2116
|
const embedding = await this.embeddingService.generateEmbedding(textForEmbedding);
|
|
1567
2117
|
await this.graph.updateNodeEmbedding("Document", doc.path, embedding);
|
|
@@ -1623,9 +2173,9 @@ class SyncService {
|
|
|
1623
2173
|
}
|
|
1624
2174
|
return embeddingGenerated;
|
|
1625
2175
|
}
|
|
1626
|
-
async removeDocument(
|
|
1627
|
-
await this.graph.deleteDocumentRelationships(
|
|
1628
|
-
await this.graph.deleteNode("Document",
|
|
2176
|
+
async removeDocument(path2) {
|
|
2177
|
+
await this.graph.deleteDocumentRelationships(path2);
|
|
2178
|
+
await this.graph.deleteNode("Document", path2);
|
|
1629
2179
|
}
|
|
1630
2180
|
async processChange(change, options, preloadedDoc) {
|
|
1631
2181
|
const cascadeWarnings = [];
|
|
@@ -1672,14 +2222,10 @@ class SyncService {
|
|
|
1672
2222
|
}
|
|
1673
2223
|
return cascadeWarnings;
|
|
1674
2224
|
}
|
|
1675
|
-
async clearGraph() {
|
|
1676
|
-
await this.graph.query("MATCH (n) DETACH DELETE n");
|
|
1677
|
-
this.logger.log("Graph cleared");
|
|
1678
|
-
}
|
|
1679
2225
|
async clearManifest() {
|
|
1680
2226
|
const manifest = await this.manifest.load();
|
|
1681
|
-
for (const
|
|
1682
|
-
this.manifest.removeEntry(
|
|
2227
|
+
for (const path2 of Object.keys(manifest.documents)) {
|
|
2228
|
+
this.manifest.removeEntry(path2);
|
|
1683
2229
|
}
|
|
1684
2230
|
this.logger.log("Manifest cleared");
|
|
1685
2231
|
}
|
|
@@ -1694,80 +2240,23 @@ class SyncService {
|
|
|
1694
2240
|
}
|
|
1695
2241
|
this.logger.log(`Marked ${normalizedPaths.length} document(s) for re-sync`);
|
|
1696
2242
|
}
|
|
1697
|
-
|
|
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) {
|
|
2243
|
+
async getOldDocumentFromManifest(path2) {
|
|
1732
2244
|
try {
|
|
1733
2245
|
const manifest = await this.manifest.load();
|
|
1734
|
-
const entry = manifest.documents[
|
|
2246
|
+
const entry = manifest.documents[path2];
|
|
1735
2247
|
if (!entry) {
|
|
1736
2248
|
return null;
|
|
1737
2249
|
}
|
|
1738
2250
|
try {
|
|
1739
|
-
return await this.parser.parseDocument(
|
|
2251
|
+
return await this.parser.parseDocument(path2);
|
|
1740
2252
|
} catch {
|
|
1741
2253
|
return null;
|
|
1742
2254
|
}
|
|
1743
2255
|
} catch (error) {
|
|
1744
|
-
this.logger.warn(`Failed to retrieve old document for ${
|
|
2256
|
+
this.logger.warn(`Failed to retrieve old document for ${path2}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1745
2257
|
return null;
|
|
1746
2258
|
}
|
|
1747
2259
|
}
|
|
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
2260
|
async syncEntities(entities, options) {
|
|
1772
2261
|
let embeddingsGenerated = 0;
|
|
1773
2262
|
for (const [_key, entity] of entities) {
|
|
@@ -1780,7 +2269,7 @@ class SyncService {
|
|
|
1780
2269
|
await this.graph.upsertNode(entity.type, entityProps);
|
|
1781
2270
|
if (options.embeddings && !options.skipEmbeddings && this.embeddingService) {
|
|
1782
2271
|
try {
|
|
1783
|
-
const text =
|
|
2272
|
+
const text = composeEntityEmbeddingText(entity);
|
|
1784
2273
|
const embedding = await this.embeddingService.generateEmbedding(text);
|
|
1785
2274
|
await this.graph.updateNodeEmbedding(entity.type, entity.name, embedding);
|
|
1786
2275
|
embeddingsGenerated++;
|
|
@@ -1793,13 +2282,6 @@ class SyncService {
|
|
|
1793
2282
|
}
|
|
1794
2283
|
return embeddingsGenerated;
|
|
1795
2284
|
}
|
|
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
2285
|
async createEntityVectorIndices() {
|
|
1804
2286
|
if (!this.embeddingService)
|
|
1805
2287
|
return;
|
|
@@ -1814,7 +2296,7 @@ class SyncService {
|
|
|
1814
2296
|
}
|
|
1815
2297
|
}
|
|
1816
2298
|
SyncService = __legacyDecorateClassTS([
|
|
1817
|
-
|
|
2299
|
+
Injectable11(),
|
|
1818
2300
|
__legacyMetadataTS("design:paramtypes", [
|
|
1819
2301
|
typeof ManifestService === "undefined" ? Object : ManifestService,
|
|
1820
2302
|
typeof DocumentParserService === "undefined" ? Object : DocumentParserService,
|
|
@@ -1825,170 +2307,98 @@ SyncService = __legacyDecorateClassTS([
|
|
|
1825
2307
|
])
|
|
1826
2308
|
], SyncService);
|
|
1827
2309
|
|
|
1828
|
-
// src/
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
constructor(
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
const docs = await this.parser.parseAllDocuments();
|
|
1837
|
-
return this.deriveFromDocuments(docs);
|
|
1838
|
-
}
|
|
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
|
-
}
|
|
1863
|
-
}
|
|
1864
|
-
for (const rel of doc.relationships) {
|
|
1865
|
-
relationshipTypeSet.add(rel.relation);
|
|
1866
|
-
relationshipCounts[rel.relation] = (relationshipCounts[rel.relation] || 0) + 1;
|
|
1867
|
-
totalRelationships++;
|
|
1868
|
-
}
|
|
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
|
-
}
|
|
2310
|
+
// src/commands/status.command.ts
|
|
2311
|
+
class StatusCommand extends CommandRunner4 {
|
|
2312
|
+
syncService;
|
|
2313
|
+
manifestService;
|
|
2314
|
+
constructor(syncService, manifestService) {
|
|
2315
|
+
super();
|
|
2316
|
+
this.syncService = syncService;
|
|
2317
|
+
this.manifestService = manifestService;
|
|
1905
2318
|
}
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
}
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
graphService;
|
|
1959
|
-
logger = new Logger7(QueryService.name);
|
|
1960
|
-
constructor(graphService) {
|
|
1961
|
-
this.graphService = graphService;
|
|
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();
|
|
2358
|
+
}
|
|
2359
|
+
if (pendingCount === 0) {
|
|
2360
|
+
console.log(`\u2705 All documents are in sync
|
|
2361
|
+
`);
|
|
2362
|
+
} else {
|
|
2363
|
+
console.log(`Total: ${pendingCount} document(s) need syncing`);
|
|
2364
|
+
console.log("\uD83D\uDCA1 Run `lattice sync` to apply changes\n");
|
|
2365
|
+
}
|
|
2366
|
+
process.exit(0);
|
|
2367
|
+
} catch (error) {
|
|
2368
|
+
console.error("Error:", error instanceof Error ? error.message : String(error));
|
|
2369
|
+
process.exit(1);
|
|
2370
|
+
}
|
|
1962
2371
|
}
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
return await this.graphService.query(cypher);
|
|
2372
|
+
parseVerbose() {
|
|
2373
|
+
return true;
|
|
1966
2374
|
}
|
|
1967
2375
|
}
|
|
1968
|
-
|
|
1969
|
-
|
|
2376
|
+
__legacyDecorateClassTS([
|
|
2377
|
+
Option3({
|
|
2378
|
+
flags: "-v, --verbose",
|
|
2379
|
+
description: "Show all documents including unchanged"
|
|
2380
|
+
}),
|
|
2381
|
+
__legacyMetadataTS("design:type", Function),
|
|
2382
|
+
__legacyMetadataTS("design:paramtypes", []),
|
|
2383
|
+
__legacyMetadataTS("design:returntype", Boolean)
|
|
2384
|
+
], StatusCommand.prototype, "parseVerbose", null);
|
|
2385
|
+
StatusCommand = __legacyDecorateClassTS([
|
|
2386
|
+
Injectable12(),
|
|
2387
|
+
Command4({
|
|
2388
|
+
name: "status",
|
|
2389
|
+
description: "Show documents that need syncing (new or updated)"
|
|
2390
|
+
}),
|
|
1970
2391
|
__legacyMetadataTS("design:paramtypes", [
|
|
1971
|
-
typeof
|
|
2392
|
+
typeof SyncService === "undefined" ? Object : SyncService,
|
|
2393
|
+
typeof ManifestService === "undefined" ? Object : ManifestService
|
|
1972
2394
|
])
|
|
1973
|
-
],
|
|
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
|
-
|
|
2395
|
+
], StatusCommand);
|
|
1986
2396
|
// src/commands/sync.command.ts
|
|
1987
|
-
import { Injectable as Injectable10 } from "@nestjs/common";
|
|
1988
|
-
import { Command, CommandRunner, Option } from "nest-commander";
|
|
1989
2397
|
import { watch } from "fs";
|
|
1990
|
-
import { join } from "path";
|
|
1991
|
-
|
|
2398
|
+
import { join as join2 } from "path";
|
|
2399
|
+
import { Injectable as Injectable13 } from "@nestjs/common";
|
|
2400
|
+
import { Command as Command5, CommandRunner as CommandRunner5, Option as Option4 } from "nest-commander";
|
|
2401
|
+
class SyncCommand extends CommandRunner5 {
|
|
1992
2402
|
syncService;
|
|
1993
2403
|
watcher = null;
|
|
1994
2404
|
isShuttingDown = false;
|
|
@@ -2110,8 +2520,8 @@ class SyncCommand extends CommandRunner {
|
|
|
2110
2520
|
\uD83D\uDC41\uFE0F Watch mode enabled
|
|
2111
2521
|
`);
|
|
2112
2522
|
this.watcher = watch(docsPath, { recursive: true }, (event, filename) => {
|
|
2113
|
-
if (filename
|
|
2114
|
-
const fullPath =
|
|
2523
|
+
if (filename?.endsWith(".md")) {
|
|
2524
|
+
const fullPath = join2(docsPath, filename);
|
|
2115
2525
|
trackedFiles.add(fullPath);
|
|
2116
2526
|
debouncedSync();
|
|
2117
2527
|
}
|
|
@@ -2155,13 +2565,14 @@ class SyncCommand extends CommandRunner {
|
|
|
2155
2565
|
console.log(`
|
|
2156
2566
|
\uD83D\uDCDD Changes:
|
|
2157
2567
|
`);
|
|
2568
|
+
const icons = {
|
|
2569
|
+
new: "\u2795",
|
|
2570
|
+
updated: "\uD83D\uDD04",
|
|
2571
|
+
deleted: "\uD83D\uDDD1\uFE0F",
|
|
2572
|
+
unchanged: "\u23ED\uFE0F"
|
|
2573
|
+
};
|
|
2158
2574
|
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];
|
|
2575
|
+
const icon = icons[c.changeType];
|
|
2165
2576
|
console.log(` ${icon} ${c.changeType}: ${c.path}`);
|
|
2166
2577
|
if (c.reason) {
|
|
2167
2578
|
console.log(` ${c.reason}`);
|
|
@@ -2190,424 +2601,125 @@ class SyncCommand extends CommandRunner {
|
|
|
2190
2601
|
for (const affected of analysis.affectedDocuments) {
|
|
2191
2602
|
const icon = affected.confidence === "high" ? "\uD83D\uDD34" : affected.confidence === "medium" ? "\uD83D\uDFE1" : "\uD83D\uDFE2";
|
|
2192
2603
|
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();
|
|
2604
|
+
console.log(` ${affected.reason}`);
|
|
2605
|
+
const suggestedAction = affected.suggestedAction.split("_").join(" ").replace(/\b\w/g, (char) => char.toUpperCase());
|
|
2606
|
+
console.log(` \u2192 ${suggestedAction}`);
|
|
2607
|
+
}
|
|
2608
|
+
} else {
|
|
2609
|
+
console.log(` \u2139\uFE0F No directly affected documents detected`);
|
|
2610
|
+
}
|
|
2611
|
+
console.log();
|
|
2612
|
+
}
|
|
2358
2613
|
}
|
|
2359
|
-
|
|
2360
|
-
|
|
2614
|
+
console.log(` \uD83D\uDCA1 Run /update-related to apply suggested changes
|
|
2615
|
+
`);
|
|
2616
|
+
}
|
|
2617
|
+
if (isWatchMode) {
|
|
2618
|
+
console.log(`
|
|
2619
|
+
\u23F3 Watching for changes... (Ctrl+C to stop)
|
|
2361
2620
|
`);
|
|
2362
|
-
} else {
|
|
2363
|
-
console.log(`Total: ${pendingCount} document(s) need syncing`);
|
|
2364
|
-
console.log("\uD83D\uDCA1 Run `lattice sync` to apply changes\n");
|
|
2365
|
-
}
|
|
2366
|
-
process.exit(0);
|
|
2367
|
-
} catch (error) {
|
|
2368
|
-
console.error("Error:", error instanceof Error ? error.message : String(error));
|
|
2369
|
-
process.exit(1);
|
|
2370
2621
|
}
|
|
2371
2622
|
}
|
|
2623
|
+
parseForce() {
|
|
2624
|
+
return true;
|
|
2625
|
+
}
|
|
2626
|
+
parseDryRun() {
|
|
2627
|
+
return true;
|
|
2628
|
+
}
|
|
2372
2629
|
parseVerbose() {
|
|
2373
2630
|
return true;
|
|
2374
2631
|
}
|
|
2632
|
+
parseWatch() {
|
|
2633
|
+
return true;
|
|
2634
|
+
}
|
|
2635
|
+
parseDiff() {
|
|
2636
|
+
return true;
|
|
2637
|
+
}
|
|
2638
|
+
parseSkipCascade() {
|
|
2639
|
+
return true;
|
|
2640
|
+
}
|
|
2641
|
+
parseNoEmbeddings() {
|
|
2642
|
+
return false;
|
|
2643
|
+
}
|
|
2375
2644
|
}
|
|
2376
2645
|
__legacyDecorateClassTS([
|
|
2377
|
-
|
|
2378
|
-
flags: "-
|
|
2379
|
-
description: "
|
|
2646
|
+
Option4({
|
|
2647
|
+
flags: "-f, --force",
|
|
2648
|
+
description: "Force re-sync: with paths, clears only those docs; without paths, rebuilds entire graph"
|
|
2380
2649
|
}),
|
|
2381
2650
|
__legacyMetadataTS("design:type", Function),
|
|
2382
2651
|
__legacyMetadataTS("design:paramtypes", []),
|
|
2383
2652
|
__legacyMetadataTS("design:returntype", Boolean)
|
|
2384
|
-
],
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
description: "Show documents that need syncing (new or updated)"
|
|
2653
|
+
], SyncCommand.prototype, "parseForce", null);
|
|
2654
|
+
__legacyDecorateClassTS([
|
|
2655
|
+
Option4({
|
|
2656
|
+
flags: "-d, --dry-run",
|
|
2657
|
+
description: "Show what would change without applying"
|
|
2390
2658
|
}),
|
|
2391
|
-
__legacyMetadataTS("design:
|
|
2392
|
-
|
|
2393
|
-
|
|
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
|
-
}
|
|
2659
|
+
__legacyMetadataTS("design:type", Function),
|
|
2660
|
+
__legacyMetadataTS("design:paramtypes", []),
|
|
2661
|
+
__legacyMetadataTS("design:returntype", Boolean)
|
|
2662
|
+
], SyncCommand.prototype, "parseDryRun", null);
|
|
2469
2663
|
__legacyDecorateClassTS([
|
|
2470
|
-
|
|
2471
|
-
flags: "-
|
|
2472
|
-
description: "
|
|
2664
|
+
Option4({
|
|
2665
|
+
flags: "-v, --verbose",
|
|
2666
|
+
description: "Show detailed output"
|
|
2473
2667
|
}),
|
|
2474
2668
|
__legacyMetadataTS("design:type", Function),
|
|
2475
|
-
__legacyMetadataTS("design:paramtypes", [
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
__legacyMetadataTS("design:returntype", String)
|
|
2479
|
-
], SearchCommand.prototype, "parseLabel", null);
|
|
2669
|
+
__legacyMetadataTS("design:paramtypes", []),
|
|
2670
|
+
__legacyMetadataTS("design:returntype", Boolean)
|
|
2671
|
+
], SyncCommand.prototype, "parseVerbose", null);
|
|
2480
2672
|
__legacyDecorateClassTS([
|
|
2481
|
-
|
|
2482
|
-
flags: "--
|
|
2483
|
-
description: "
|
|
2484
|
-
defaultValue: "20"
|
|
2673
|
+
Option4({
|
|
2674
|
+
flags: "-w, --watch",
|
|
2675
|
+
description: "Watch for file changes and sync automatically"
|
|
2485
2676
|
}),
|
|
2486
2677
|
__legacyMetadataTS("design:type", Function),
|
|
2487
|
-
__legacyMetadataTS("design:paramtypes", [
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
Command3({
|
|
2495
|
-
name: "search",
|
|
2496
|
-
arguments: "<query>",
|
|
2497
|
-
description: "Semantic search across the knowledge graph"
|
|
2678
|
+
__legacyMetadataTS("design:paramtypes", []),
|
|
2679
|
+
__legacyMetadataTS("design:returntype", Boolean)
|
|
2680
|
+
], SyncCommand.prototype, "parseWatch", null);
|
|
2681
|
+
__legacyDecorateClassTS([
|
|
2682
|
+
Option4({
|
|
2683
|
+
flags: "--diff",
|
|
2684
|
+
description: "Show only changed documents (alias for --dry-run)"
|
|
2498
2685
|
}),
|
|
2499
|
-
__legacyMetadataTS("design:
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
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"
|
|
2686
|
+
__legacyMetadataTS("design:type", Function),
|
|
2687
|
+
__legacyMetadataTS("design:paramtypes", []),
|
|
2688
|
+
__legacyMetadataTS("design:returntype", Boolean)
|
|
2689
|
+
], SyncCommand.prototype, "parseDiff", null);
|
|
2690
|
+
__legacyDecorateClassTS([
|
|
2691
|
+
Option4({
|
|
2692
|
+
flags: "--skip-cascade",
|
|
2693
|
+
description: "Skip cascade analysis (faster for large repos)"
|
|
2694
|
+
}),
|
|
2695
|
+
__legacyMetadataTS("design:type", Function),
|
|
2696
|
+
__legacyMetadataTS("design:paramtypes", []),
|
|
2697
|
+
__legacyMetadataTS("design:returntype", Boolean)
|
|
2698
|
+
], SyncCommand.prototype, "parseSkipCascade", null);
|
|
2699
|
+
__legacyDecorateClassTS([
|
|
2700
|
+
Option4({
|
|
2701
|
+
flags: "--no-embeddings",
|
|
2702
|
+
description: "Disable embedding generation during sync"
|
|
2568
2703
|
}),
|
|
2569
|
-
__legacyMetadataTS("design:
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
],
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
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"
|
|
2704
|
+
__legacyMetadataTS("design:type", Function),
|
|
2705
|
+
__legacyMetadataTS("design:paramtypes", []),
|
|
2706
|
+
__legacyMetadataTS("design:returntype", Boolean)
|
|
2707
|
+
], SyncCommand.prototype, "parseNoEmbeddings", null);
|
|
2708
|
+
SyncCommand = __legacyDecorateClassTS([
|
|
2709
|
+
Injectable13(),
|
|
2710
|
+
Command5({
|
|
2711
|
+
name: "sync",
|
|
2712
|
+
arguments: "[paths...]",
|
|
2713
|
+
description: "Synchronize documents to the knowledge graph"
|
|
2602
2714
|
}),
|
|
2603
2715
|
__legacyMetadataTS("design:paramtypes", [
|
|
2604
|
-
typeof
|
|
2716
|
+
typeof SyncService === "undefined" ? Object : SyncService
|
|
2605
2717
|
])
|
|
2606
|
-
],
|
|
2718
|
+
], SyncCommand);
|
|
2607
2719
|
// src/commands/validate.command.ts
|
|
2608
|
-
import { Injectable as
|
|
2609
|
-
import { Command as
|
|
2610
|
-
class ValidateCommand extends
|
|
2720
|
+
import { Injectable as Injectable14 } from "@nestjs/common";
|
|
2721
|
+
import { Command as Command6, CommandRunner as CommandRunner6, Option as Option5 } from "nest-commander";
|
|
2722
|
+
class ValidateCommand extends CommandRunner6 {
|
|
2611
2723
|
parserService;
|
|
2612
2724
|
constructor(parserService) {
|
|
2613
2725
|
super();
|
|
@@ -2629,13 +2741,15 @@ class ValidateCommand extends CommandRunner4 {
|
|
|
2629
2741
|
const entityIndex = new Map;
|
|
2630
2742
|
for (const doc of docs) {
|
|
2631
2743
|
for (const entity of doc.entities) {
|
|
2632
|
-
|
|
2633
|
-
|
|
2744
|
+
let docPaths = entityIndex.get(entity.name);
|
|
2745
|
+
if (!docPaths) {
|
|
2746
|
+
docPaths = new Set;
|
|
2747
|
+
entityIndex.set(entity.name, docPaths);
|
|
2634
2748
|
}
|
|
2635
|
-
|
|
2749
|
+
docPaths.add(doc.path);
|
|
2636
2750
|
}
|
|
2637
2751
|
}
|
|
2638
|
-
const validationErrors =
|
|
2752
|
+
const validationErrors = validateDocuments2(docs);
|
|
2639
2753
|
for (const err of validationErrors) {
|
|
2640
2754
|
issues.push({
|
|
2641
2755
|
type: "error",
|
|
@@ -2673,7 +2787,7 @@ class ValidateCommand extends CommandRunner4 {
|
|
|
2673
2787
|
}
|
|
2674
2788
|
}
|
|
2675
2789
|
__legacyDecorateClassTS([
|
|
2676
|
-
|
|
2790
|
+
Option5({
|
|
2677
2791
|
flags: "--fix",
|
|
2678
2792
|
description: "Show suggestions for common issues"
|
|
2679
2793
|
}),
|
|
@@ -2682,8 +2796,8 @@ __legacyDecorateClassTS([
|
|
|
2682
2796
|
__legacyMetadataTS("design:returntype", Boolean)
|
|
2683
2797
|
], ValidateCommand.prototype, "parseFix", null);
|
|
2684
2798
|
ValidateCommand = __legacyDecorateClassTS([
|
|
2685
|
-
|
|
2686
|
-
|
|
2799
|
+
Injectable14(),
|
|
2800
|
+
Command6({
|
|
2687
2801
|
name: "validate",
|
|
2688
2802
|
description: "Validate entity references and relationships across documents"
|
|
2689
2803
|
}),
|
|
@@ -2691,138 +2805,90 @@ ValidateCommand = __legacyDecorateClassTS([
|
|
|
2691
2805
|
typeof DocumentParserService === "undefined" ? Object : DocumentParserService
|
|
2692
2806
|
])
|
|
2693
2807
|
], ValidateCommand);
|
|
2694
|
-
// src/
|
|
2695
|
-
import {
|
|
2696
|
-
import {
|
|
2697
|
-
class
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2808
|
+
// src/embedding/embedding.module.ts
|
|
2809
|
+
import { Module } from "@nestjs/common";
|
|
2810
|
+
import { ConfigModule } from "@nestjs/config";
|
|
2811
|
+
class EmbeddingModule {
|
|
2812
|
+
}
|
|
2813
|
+
EmbeddingModule = __legacyDecorateClassTS([
|
|
2814
|
+
Module({
|
|
2815
|
+
imports: [ConfigModule],
|
|
2816
|
+
providers: [EmbeddingService],
|
|
2817
|
+
exports: [EmbeddingService]
|
|
2818
|
+
})
|
|
2819
|
+
], EmbeddingModule);
|
|
2820
|
+
|
|
2821
|
+
// src/graph/graph.module.ts
|
|
2822
|
+
import { Module as Module2 } from "@nestjs/common";
|
|
2823
|
+
class GraphModule {
|
|
2824
|
+
}
|
|
2825
|
+
GraphModule = __legacyDecorateClassTS([
|
|
2826
|
+
Module2({
|
|
2827
|
+
providers: [GraphService],
|
|
2828
|
+
exports: [GraphService]
|
|
2829
|
+
})
|
|
2830
|
+
], GraphModule);
|
|
2831
|
+
|
|
2832
|
+
// src/query/query.module.ts
|
|
2833
|
+
import { Module as Module3 } from "@nestjs/common";
|
|
2834
|
+
|
|
2835
|
+
// src/query/query.service.ts
|
|
2836
|
+
import { Injectable as Injectable15, Logger as Logger6 } from "@nestjs/common";
|
|
2837
|
+
class QueryService {
|
|
2838
|
+
graphService;
|
|
2839
|
+
logger = new Logger6(QueryService.name);
|
|
2840
|
+
constructor(graphService) {
|
|
2841
|
+
this.graphService = graphService;
|
|
2702
2842
|
}
|
|
2703
|
-
async
|
|
2704
|
-
|
|
2705
|
-
|
|
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
|
-
}
|
|
2843
|
+
async query(cypher) {
|
|
2844
|
+
this.logger.debug(`Executing query: ${cypher}`);
|
|
2845
|
+
return await this.graphService.query(cypher);
|
|
2713
2846
|
}
|
|
2714
2847
|
}
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
Command5({
|
|
2718
|
-
name: "ontology",
|
|
2719
|
-
description: "Derive and display ontology from all documents"
|
|
2720
|
-
}),
|
|
2848
|
+
QueryService = __legacyDecorateClassTS([
|
|
2849
|
+
Injectable15(),
|
|
2721
2850
|
__legacyMetadataTS("design:paramtypes", [
|
|
2722
|
-
typeof
|
|
2851
|
+
typeof GraphService === "undefined" ? Object : GraphService
|
|
2723
2852
|
])
|
|
2724
|
-
],
|
|
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"];
|
|
2853
|
+
], QueryService);
|
|
2735
2854
|
|
|
2736
|
-
|
|
2737
|
-
|
|
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
|
-
}
|
|
2855
|
+
// src/query/query.module.ts
|
|
2856
|
+
class QueryModule {
|
|
2809
2857
|
}
|
|
2810
|
-
__legacyDecorateClassTS([
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
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"
|
|
2858
|
+
QueryModule = __legacyDecorateClassTS([
|
|
2859
|
+
Module3({
|
|
2860
|
+
imports: [GraphModule, EmbeddingModule],
|
|
2861
|
+
providers: [QueryService, GraphService],
|
|
2862
|
+
exports: [QueryService, GraphService]
|
|
2824
2863
|
})
|
|
2825
|
-
],
|
|
2864
|
+
], QueryModule);
|
|
2865
|
+
|
|
2866
|
+
// src/sync/sync.module.ts
|
|
2867
|
+
import { Module as Module4 } from "@nestjs/common";
|
|
2868
|
+
class SyncModule {
|
|
2869
|
+
}
|
|
2870
|
+
SyncModule = __legacyDecorateClassTS([
|
|
2871
|
+
Module4({
|
|
2872
|
+
imports: [GraphModule, EmbeddingModule],
|
|
2873
|
+
providers: [
|
|
2874
|
+
SyncService,
|
|
2875
|
+
ManifestService,
|
|
2876
|
+
DocumentParserService,
|
|
2877
|
+
OntologyService,
|
|
2878
|
+
CascadeService,
|
|
2879
|
+
PathResolverService
|
|
2880
|
+
],
|
|
2881
|
+
exports: [
|
|
2882
|
+
SyncService,
|
|
2883
|
+
ManifestService,
|
|
2884
|
+
DocumentParserService,
|
|
2885
|
+
OntologyService,
|
|
2886
|
+
CascadeService,
|
|
2887
|
+
PathResolverService
|
|
2888
|
+
]
|
|
2889
|
+
})
|
|
2890
|
+
], SyncModule);
|
|
2891
|
+
|
|
2826
2892
|
// src/app.module.ts
|
|
2827
2893
|
class AppModule {
|
|
2828
2894
|
}
|