@zabaca/lattice 0.3.4 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -64
- package/dist/main.js +279 -255
- package/package.json +3 -4
package/README.md
CHANGED
|
@@ -24,30 +24,31 @@ That's it. Two commands to build a knowledge base.
|
|
|
24
24
|
| Feature | Lattice | Other GraphRAG Tools |
|
|
25
25
|
|---------|---------|---------------------|
|
|
26
26
|
| **LLM for extraction** | Your Claude Code subscription | Separate API key + costs |
|
|
27
|
-
| **Setup time** |
|
|
28
|
-
| **
|
|
27
|
+
| **Setup time** | 2 minutes | 30+ minutes |
|
|
28
|
+
| **Database** | Embedded DuckDB (zero config) | Docker containers required |
|
|
29
|
+
| **External dependencies** | None | 2-3 (DB + vector + graph) |
|
|
29
30
|
| **API keys needed** | 1 (Voyage AI for embeddings) | 2-3 (LLM + embedding + rerank) |
|
|
30
31
|
| **Workflow** | `/research` → `/graph-sync` | Custom scripts |
|
|
31
32
|
|
|
32
33
|
---
|
|
33
34
|
|
|
34
|
-
## Quick Start (
|
|
35
|
+
## Quick Start (2 Minutes)
|
|
35
36
|
|
|
36
37
|
### What You Need
|
|
37
38
|
|
|
38
39
|
- **Claude Code** (you probably already have it)
|
|
39
|
-
- **Docker** (for FalkorDB)
|
|
40
40
|
- **Voyage AI API key** ([get one here](https://www.voyageai.com/) - embeddings only, ~$0.01/1M tokens)
|
|
41
41
|
|
|
42
|
-
### 1. Install
|
|
42
|
+
### 1. Install
|
|
43
43
|
|
|
44
44
|
```bash
|
|
45
|
-
bun add -g @zabaca/lattice
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
lattice init --global # Install Claude Code commands
|
|
45
|
+
bun add -g @zabaca/lattice # Install CLI
|
|
46
|
+
export VOYAGE_API_KEY=your-key-here # Set API key
|
|
47
|
+
lattice init --global # Install Claude Code commands
|
|
49
48
|
```
|
|
50
49
|
|
|
50
|
+
That's it. No Docker. No containers. DuckDB is embedded.
|
|
51
|
+
|
|
51
52
|
### 2. Start Researching
|
|
52
53
|
|
|
53
54
|
```bash
|
|
@@ -67,7 +68,7 @@ The `/research` command will:
|
|
|
67
68
|
The `/graph-sync` command will:
|
|
68
69
|
- Detect all new/changed documents
|
|
69
70
|
- Extract entities using Claude Code (your subscription)
|
|
70
|
-
- Sync to
|
|
71
|
+
- Sync to DuckDB for semantic search
|
|
71
72
|
|
|
72
73
|
---
|
|
73
74
|
|
|
@@ -152,6 +153,24 @@ Semantic search across the knowledge graph.
|
|
|
152
153
|
|
|
153
154
|
```bash
|
|
154
155
|
lattice search "query" # Search all entity types
|
|
156
|
+
lattice search "query" -l Tool # Filter by label
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### `lattice sql`
|
|
160
|
+
|
|
161
|
+
Execute raw SQL queries against DuckDB.
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
lattice sql "SELECT * FROM nodes LIMIT 10"
|
|
165
|
+
lattice sql "SELECT label, COUNT(*) FROM nodes GROUP BY label"
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### `lattice rels`
|
|
169
|
+
|
|
170
|
+
Show relationships for a node.
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
lattice rels "TypeScript" # Show all relationships for an entity
|
|
155
174
|
```
|
|
156
175
|
|
|
157
176
|
### `lattice validate`
|
|
@@ -182,15 +201,24 @@ lattice ontology # Show entity types and relationship types
|
|
|
182
201
|
| Variable | Description | Default |
|
|
183
202
|
|----------|-------------|---------|
|
|
184
203
|
| `VOYAGE_API_KEY` | Voyage AI API key for embeddings | *required* |
|
|
185
|
-
| `
|
|
186
|
-
| `
|
|
204
|
+
| `DUCKDB_PATH` | Path to DuckDB database file | `./.lattice.duckdb` |
|
|
205
|
+
| `EMBEDDING_DIMENSIONS` | Embedding vector dimensions | `512` |
|
|
206
|
+
|
|
207
|
+
### Database Location
|
|
208
|
+
|
|
209
|
+
Lattice stores its knowledge graph in a single `.lattice.duckdb` file in your docs directory. This file contains:
|
|
210
|
+
- All extracted entities (nodes)
|
|
211
|
+
- Relationships between entities
|
|
212
|
+
- Vector embeddings for semantic search
|
|
213
|
+
|
|
214
|
+
You can back up, copy, or version control this file like any other.
|
|
187
215
|
|
|
188
216
|
<details>
|
|
189
217
|
<summary><b>How It Works (Technical Details)</b></summary>
|
|
190
218
|
|
|
191
219
|
### Entity Extraction
|
|
192
220
|
|
|
193
|
-
When you run `/graph-sync`, Claude Code extracts entities from your documents and writes them to YAML frontmatter. The Lattice CLI then syncs this to
|
|
221
|
+
When you run `/graph-sync`, Claude Code extracts entities from your documents and writes them to YAML frontmatter. The Lattice CLI then syncs this to DuckDB.
|
|
194
222
|
|
|
195
223
|
```yaml
|
|
196
224
|
---
|
|
@@ -208,57 +236,35 @@ relationships:
|
|
|
208
236
|
|
|
209
237
|
You don't need to write this manually — Claude Code handles it automatically.
|
|
210
238
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
falkordb-data:
|
|
239
|
+
### Database Schema
|
|
240
|
+
|
|
241
|
+
Lattice uses two main tables:
|
|
242
|
+
|
|
243
|
+
```sql
|
|
244
|
+
-- Nodes (entities)
|
|
245
|
+
CREATE TABLE nodes (
|
|
246
|
+
label VARCHAR NOT NULL, -- Entity type: Document, Technology, etc.
|
|
247
|
+
name VARCHAR NOT NULL, -- Unique identifier
|
|
248
|
+
properties JSON, -- Additional metadata
|
|
249
|
+
embedding FLOAT[512], -- Vector for semantic search
|
|
250
|
+
PRIMARY KEY(label, name)
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
-- Relationships
|
|
254
|
+
CREATE TABLE relationships (
|
|
255
|
+
source_label VARCHAR NOT NULL,
|
|
256
|
+
source_name VARCHAR NOT NULL,
|
|
257
|
+
relation_type VARCHAR NOT NULL,
|
|
258
|
+
target_label VARCHAR NOT NULL,
|
|
259
|
+
target_name VARCHAR NOT NULL,
|
|
260
|
+
properties JSON,
|
|
261
|
+
PRIMARY KEY(source_label, source_name, relation_type, target_label, target_name)
|
|
262
|
+
);
|
|
236
263
|
```
|
|
237
264
|
|
|
238
|
-
|
|
239
|
-
docker-compose up -d
|
|
240
|
-
```
|
|
265
|
+
### Vector Search
|
|
241
266
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
<details>
|
|
245
|
-
<summary><b>Kubernetes (k3s)</b></summary>
|
|
246
|
-
|
|
247
|
-
For production deployments, use the provided k3s manifests:
|
|
248
|
-
|
|
249
|
-
```bash
|
|
250
|
-
kubectl apply -f infra/k3s/namespace.yaml
|
|
251
|
-
kubectl apply -f infra/k3s/pv.yaml
|
|
252
|
-
kubectl apply -f infra/k3s/pvc.yaml
|
|
253
|
-
kubectl apply -f infra/k3s/deployment.yaml
|
|
254
|
-
kubectl apply -f infra/k3s/service.yaml
|
|
255
|
-
|
|
256
|
-
# Optional: NodePort for external access
|
|
257
|
-
kubectl apply -f infra/k3s/nodeport-service.yaml
|
|
258
|
-
|
|
259
|
-
# Optional: Ingress
|
|
260
|
-
kubectl apply -f infra/k3s/ingress.yaml
|
|
261
|
-
```
|
|
267
|
+
Lattice uses DuckDB's VSS extension for HNSW-based vector similarity search with cosine distance.
|
|
262
268
|
|
|
263
269
|
</details>
|
|
264
270
|
|
|
@@ -273,7 +279,6 @@ kubectl apply -f infra/k3s/ingress.yaml
|
|
|
273
279
|
|
|
274
280
|
- Node.js >= 18.0.0
|
|
275
281
|
- Bun (recommended) or npm
|
|
276
|
-
- Docker (for FalkorDB)
|
|
277
282
|
|
|
278
283
|
### Setup
|
|
279
284
|
|
|
@@ -282,7 +287,6 @@ git clone https://github.com/Zabaca/lattice.git
|
|
|
282
287
|
cd lattice
|
|
283
288
|
bun install
|
|
284
289
|
cp .env.example .env
|
|
285
|
-
docker-compose -f infra/docker-compose.yaml up -d
|
|
286
290
|
```
|
|
287
291
|
|
|
288
292
|
### Running Locally
|
|
@@ -329,4 +333,4 @@ MIT License - see [LICENSE](LICENSE) for details.
|
|
|
329
333
|
|
|
330
334
|
---
|
|
331
335
|
|
|
332
|
-
Built with [
|
|
336
|
+
Built with [DuckDB](https://duckdb.org/), [Voyage AI](https://www.voyageai.com/), and [Claude Code](https://claude.ai/code)
|
package/dist/main.js
CHANGED
|
@@ -140,10 +140,9 @@ import { glob } from "glob";
|
|
|
140
140
|
|
|
141
141
|
// src/schemas/config.schemas.ts
|
|
142
142
|
import { z } from "zod";
|
|
143
|
-
var
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
graphName: z.string().min(1).default("research_knowledge")
|
|
143
|
+
var DuckDBConfigSchema = z.object({
|
|
144
|
+
dbPath: z.string().optional(),
|
|
145
|
+
embeddingDimensions: z.coerce.number().int().positive().default(512)
|
|
147
146
|
});
|
|
148
147
|
var EmbeddingConfigSchema = z.object({
|
|
149
148
|
provider: z.enum(["openai", "voyage", "nomic", "mock"]).default("voyage"),
|
|
@@ -790,82 +789,124 @@ EmbeddingService = __legacyDecorateClassTS([
|
|
|
790
789
|
], EmbeddingService);
|
|
791
790
|
|
|
792
791
|
// src/graph/graph.service.ts
|
|
792
|
+
import { DuckDBInstance } from "@duckdb/node-api";
|
|
793
793
|
import { Injectable as Injectable6, Logger as Logger3 } from "@nestjs/common";
|
|
794
794
|
import { ConfigService as ConfigService2 } from "@nestjs/config";
|
|
795
|
-
import Redis from "ioredis";
|
|
796
795
|
class GraphService {
|
|
797
796
|
configService;
|
|
798
797
|
logger = new Logger3(GraphService.name);
|
|
799
|
-
|
|
800
|
-
|
|
798
|
+
instance = null;
|
|
799
|
+
connection = null;
|
|
800
|
+
dbPath;
|
|
801
801
|
connecting = null;
|
|
802
|
+
vectorIndexes = new Set;
|
|
803
|
+
embeddingDimensions;
|
|
802
804
|
constructor(configService) {
|
|
803
805
|
this.configService = configService;
|
|
804
|
-
this.
|
|
805
|
-
|
|
806
|
-
port: this.configService.get("FALKORDB_PORT"),
|
|
807
|
-
graphName: this.configService.get("GRAPH_NAME")
|
|
808
|
-
});
|
|
806
|
+
this.dbPath = this.configService.get("DUCKDB_PATH") || "./.lattice.duckdb";
|
|
807
|
+
this.embeddingDimensions = this.configService.get("EMBEDDING_DIMENSIONS") || 512;
|
|
809
808
|
}
|
|
810
809
|
async onModuleDestroy() {
|
|
811
810
|
await this.disconnect();
|
|
812
811
|
}
|
|
813
812
|
async ensureConnected() {
|
|
814
|
-
if (this.
|
|
815
|
-
return this.
|
|
813
|
+
if (this.connection) {
|
|
814
|
+
return this.connection;
|
|
816
815
|
}
|
|
817
816
|
if (this.connecting) {
|
|
818
817
|
await this.connecting;
|
|
819
|
-
|
|
818
|
+
if (!this.connection) {
|
|
819
|
+
throw new Error("Connection failed to establish");
|
|
820
|
+
}
|
|
821
|
+
return this.connection;
|
|
820
822
|
}
|
|
821
823
|
this.connecting = this.connect();
|
|
822
824
|
await this.connecting;
|
|
823
825
|
this.connecting = null;
|
|
824
|
-
|
|
826
|
+
if (!this.connection) {
|
|
827
|
+
throw new Error("Connection failed to establish");
|
|
828
|
+
}
|
|
829
|
+
return this.connection;
|
|
825
830
|
}
|
|
826
831
|
async connect() {
|
|
827
832
|
try {
|
|
828
|
-
this.
|
|
829
|
-
|
|
830
|
-
port: this.config.port,
|
|
831
|
-
maxRetriesPerRequest: 3,
|
|
832
|
-
retryStrategy: (times) => {
|
|
833
|
-
if (times > 3) {
|
|
834
|
-
return null;
|
|
835
|
-
}
|
|
836
|
-
const delay = Math.min(times * 50, 2000);
|
|
837
|
-
return delay;
|
|
838
|
-
},
|
|
839
|
-
lazyConnect: true
|
|
840
|
-
});
|
|
841
|
-
this.redis.on("error", (err) => {
|
|
842
|
-
this.logger.debug(`Redis connection error: ${err.message}`);
|
|
833
|
+
this.instance = await DuckDBInstance.create(this.dbPath, {
|
|
834
|
+
allow_unsigned_extensions: "true"
|
|
843
835
|
});
|
|
844
|
-
await this.
|
|
845
|
-
this.
|
|
836
|
+
this.connection = await this.instance.connect();
|
|
837
|
+
await this.connection.run("INSTALL vss; LOAD vss;");
|
|
838
|
+
await this.connection.run("SET hnsw_enable_experimental_persistence = true;");
|
|
839
|
+
try {
|
|
840
|
+
await this.connection.run("SET custom_extension_repository = 'http://duckpgq.s3.eu-north-1.amazonaws.com';");
|
|
841
|
+
await this.connection.run("FORCE INSTALL 'duckpgq';");
|
|
842
|
+
await this.connection.run("LOAD 'duckpgq';");
|
|
843
|
+
this.logger.log("DuckPGQ extension loaded successfully");
|
|
844
|
+
} catch (e) {
|
|
845
|
+
this.logger.warn(`DuckPGQ extension not available: ${e instanceof Error ? e.message : String(e)}`);
|
|
846
|
+
}
|
|
847
|
+
await this.initializeSchema();
|
|
848
|
+
this.logger.log(`Connected to DuckDB at ${this.dbPath}`);
|
|
846
849
|
} catch (error) {
|
|
847
|
-
this.
|
|
848
|
-
this.
|
|
850
|
+
this.connection = null;
|
|
851
|
+
this.instance = null;
|
|
852
|
+
this.logger.error(`Failed to connect to DuckDB: ${error instanceof Error ? error.message : String(error)}`);
|
|
849
853
|
throw error;
|
|
850
854
|
}
|
|
851
855
|
}
|
|
852
856
|
async disconnect() {
|
|
853
|
-
if (this.
|
|
854
|
-
|
|
855
|
-
this.
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
857
|
+
if (this.connection) {
|
|
858
|
+
this.connection.closeSync();
|
|
859
|
+
this.connection = null;
|
|
860
|
+
this.logger.log("Disconnected from DuckDB");
|
|
861
|
+
}
|
|
862
|
+
if (this.instance) {
|
|
863
|
+
this.instance = null;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
async initializeSchema() {
|
|
867
|
+
if (!this.connection) {
|
|
868
|
+
throw new Error("Cannot initialize schema: not connected");
|
|
869
|
+
}
|
|
870
|
+
const conn = this.connection;
|
|
871
|
+
await conn.run(`
|
|
872
|
+
CREATE TABLE IF NOT EXISTS nodes (
|
|
873
|
+
label VARCHAR NOT NULL,
|
|
874
|
+
name VARCHAR NOT NULL,
|
|
875
|
+
properties JSON,
|
|
876
|
+
embedding FLOAT[${this.embeddingDimensions}],
|
|
877
|
+
created_at TIMESTAMP DEFAULT NOW(),
|
|
878
|
+
updated_at TIMESTAMP DEFAULT NOW(),
|
|
879
|
+
PRIMARY KEY(label, name)
|
|
880
|
+
)
|
|
881
|
+
`);
|
|
882
|
+
await conn.run(`
|
|
883
|
+
CREATE TABLE IF NOT EXISTS relationships (
|
|
884
|
+
source_label VARCHAR NOT NULL,
|
|
885
|
+
source_name VARCHAR NOT NULL,
|
|
886
|
+
relation_type VARCHAR NOT NULL,
|
|
887
|
+
target_label VARCHAR NOT NULL,
|
|
888
|
+
target_name VARCHAR NOT NULL,
|
|
889
|
+
properties JSON,
|
|
890
|
+
created_at TIMESTAMP DEFAULT NOW(),
|
|
891
|
+
PRIMARY KEY(source_label, source_name, relation_type, target_label, target_name)
|
|
892
|
+
)
|
|
893
|
+
`);
|
|
894
|
+
await conn.run("CREATE INDEX IF NOT EXISTS idx_nodes_label ON nodes(label)");
|
|
895
|
+
await conn.run("CREATE INDEX IF NOT EXISTS idx_nodes_label_name ON nodes(label, name)");
|
|
896
|
+
await conn.run("CREATE INDEX IF NOT EXISTS idx_rels_source ON relationships(source_label, source_name)");
|
|
897
|
+
await conn.run("CREATE INDEX IF NOT EXISTS idx_rels_target ON relationships(target_label, target_name)");
|
|
898
|
+
}
|
|
899
|
+
async query(sql, _params) {
|
|
859
900
|
try {
|
|
860
|
-
const
|
|
861
|
-
const
|
|
862
|
-
const
|
|
901
|
+
const conn = await this.ensureConnected();
|
|
902
|
+
const reader = await conn.runAndReadAll(sql);
|
|
903
|
+
const rows = reader.getRows();
|
|
863
904
|
return {
|
|
864
|
-
resultSet:
|
|
865
|
-
stats:
|
|
905
|
+
resultSet: rows,
|
|
906
|
+
stats: undefined
|
|
866
907
|
};
|
|
867
908
|
} catch (error) {
|
|
868
|
-
this.logger.error(`
|
|
909
|
+
this.logger.error(`Query failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
869
910
|
throw error;
|
|
870
911
|
}
|
|
871
912
|
}
|
|
@@ -875,18 +916,15 @@ class GraphService {
|
|
|
875
916
|
if (!name) {
|
|
876
917
|
throw new Error("Node must have a 'name' property");
|
|
877
918
|
}
|
|
878
|
-
const
|
|
879
|
-
const
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
}).join(", ");
|
|
888
|
-
const cypher = `MERGE (n:\`${escapedLabel}\` { name: '${escapedName}' }) ` + `SET ${propAssignments}`;
|
|
889
|
-
await this.query(cypher);
|
|
919
|
+
const conn = await this.ensureConnected();
|
|
920
|
+
const propsJson = JSON.stringify(otherProps);
|
|
921
|
+
await conn.run(`
|
|
922
|
+
INSERT INTO nodes (label, name, properties)
|
|
923
|
+
VALUES ('${this.escape(String(label))}', '${this.escape(String(name))}', '${this.escape(propsJson)}')
|
|
924
|
+
ON CONFLICT (label, name) DO UPDATE SET
|
|
925
|
+
properties = EXCLUDED.properties,
|
|
926
|
+
updated_at = NOW()
|
|
927
|
+
`);
|
|
890
928
|
} catch (error) {
|
|
891
929
|
this.logger.error(`Failed to upsert node: ${error instanceof Error ? error.message : String(error)}`);
|
|
892
930
|
throw error;
|
|
@@ -894,21 +932,31 @@ class GraphService {
|
|
|
894
932
|
}
|
|
895
933
|
async upsertRelationship(sourceLabel, sourceName, relation, targetLabel, targetName, properties) {
|
|
896
934
|
try {
|
|
897
|
-
const
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
935
|
+
const conn = await this.ensureConnected();
|
|
936
|
+
await conn.run(`
|
|
937
|
+
INSERT INTO nodes (label, name, properties)
|
|
938
|
+
VALUES ('${this.escape(sourceLabel)}', '${this.escape(sourceName)}', '{}')
|
|
939
|
+
ON CONFLICT (label, name) DO NOTHING
|
|
940
|
+
`);
|
|
941
|
+
await conn.run(`
|
|
942
|
+
INSERT INTO nodes (label, name, properties)
|
|
943
|
+
VALUES ('${this.escape(targetLabel)}', '${this.escape(targetName)}', '{}')
|
|
944
|
+
ON CONFLICT (label, name) DO NOTHING
|
|
945
|
+
`);
|
|
946
|
+
const propsJson = properties ? JSON.stringify(properties) : "{}";
|
|
947
|
+
await conn.run(`
|
|
948
|
+
INSERT INTO relationships (source_label, source_name, relation_type, target_label, target_name, properties)
|
|
949
|
+
VALUES (
|
|
950
|
+
'${this.escape(sourceLabel)}',
|
|
951
|
+
'${this.escape(sourceName)}',
|
|
952
|
+
'${this.escape(relation)}',
|
|
953
|
+
'${this.escape(targetLabel)}',
|
|
954
|
+
'${this.escape(targetName)}',
|
|
955
|
+
'${this.escape(propsJson)}'
|
|
956
|
+
)
|
|
957
|
+
ON CONFLICT (source_label, source_name, relation_type, target_label, target_name) DO UPDATE SET
|
|
958
|
+
properties = EXCLUDED.properties
|
|
959
|
+
`);
|
|
912
960
|
} catch (error) {
|
|
913
961
|
this.logger.error(`Failed to upsert relationship: ${error instanceof Error ? error.message : String(error)}`);
|
|
914
962
|
throw error;
|
|
@@ -916,10 +964,16 @@ class GraphService {
|
|
|
916
964
|
}
|
|
917
965
|
async deleteNode(label, name) {
|
|
918
966
|
try {
|
|
919
|
-
const
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
967
|
+
const conn = await this.ensureConnected();
|
|
968
|
+
await conn.run(`
|
|
969
|
+
DELETE FROM relationships
|
|
970
|
+
WHERE (source_label = '${this.escape(label)}' AND source_name = '${this.escape(name)}')
|
|
971
|
+
OR (target_label = '${this.escape(label)}' AND target_name = '${this.escape(name)}')
|
|
972
|
+
`);
|
|
973
|
+
await conn.run(`
|
|
974
|
+
DELETE FROM nodes
|
|
975
|
+
WHERE label = '${this.escape(label)}' AND name = '${this.escape(name)}'
|
|
976
|
+
`);
|
|
923
977
|
} catch (error) {
|
|
924
978
|
this.logger.error(`Failed to delete node: ${error instanceof Error ? error.message : String(error)}`);
|
|
925
979
|
throw error;
|
|
@@ -927,9 +981,11 @@ class GraphService {
|
|
|
927
981
|
}
|
|
928
982
|
async deleteDocumentRelationships(documentPath) {
|
|
929
983
|
try {
|
|
930
|
-
const
|
|
931
|
-
|
|
932
|
-
|
|
984
|
+
const conn = await this.ensureConnected();
|
|
985
|
+
await conn.run(`
|
|
986
|
+
DELETE FROM relationships
|
|
987
|
+
WHERE properties->>'documentPath' = '${this.escape(documentPath)}'
|
|
988
|
+
`);
|
|
933
989
|
} catch (error) {
|
|
934
990
|
this.logger.error(`Failed to delete document relationships: ${error instanceof Error ? error.message : String(error)}`);
|
|
935
991
|
throw error;
|
|
@@ -937,11 +993,18 @@ class GraphService {
|
|
|
937
993
|
}
|
|
938
994
|
async findNodesByLabel(label, limit) {
|
|
939
995
|
try {
|
|
940
|
-
const
|
|
996
|
+
const conn = await this.ensureConnected();
|
|
941
997
|
const limitClause = limit ? ` LIMIT ${limit}` : "";
|
|
942
|
-
const
|
|
943
|
-
|
|
944
|
-
|
|
998
|
+
const reader = await conn.runAndReadAll(`
|
|
999
|
+
SELECT name, properties
|
|
1000
|
+
FROM nodes
|
|
1001
|
+
WHERE label = '${this.escape(label)}'${limitClause}
|
|
1002
|
+
`);
|
|
1003
|
+
return reader.getRows().map((row) => {
|
|
1004
|
+
const [name, properties] = row;
|
|
1005
|
+
const props = properties ? JSON.parse(properties) : {};
|
|
1006
|
+
return { name, ...props };
|
|
1007
|
+
});
|
|
945
1008
|
} catch (error) {
|
|
946
1009
|
this.logger.error(`Failed to find nodes by label: ${error instanceof Error ? error.message : String(error)}`);
|
|
947
1010
|
return [];
|
|
@@ -949,10 +1012,17 @@ class GraphService {
|
|
|
949
1012
|
}
|
|
950
1013
|
async findRelationships(nodeName) {
|
|
951
1014
|
try {
|
|
952
|
-
const
|
|
953
|
-
const
|
|
954
|
-
|
|
955
|
-
|
|
1015
|
+
const conn = await this.ensureConnected();
|
|
1016
|
+
const reader = await conn.runAndReadAll(`
|
|
1017
|
+
SELECT relation_type, target_name, source_name
|
|
1018
|
+
FROM relationships
|
|
1019
|
+
WHERE source_name = '${this.escape(nodeName)}'
|
|
1020
|
+
OR target_name = '${this.escape(nodeName)}'
|
|
1021
|
+
`);
|
|
1022
|
+
return reader.getRows().map((row) => {
|
|
1023
|
+
const [relType, targetName, sourceName] = row;
|
|
1024
|
+
return [relType, sourceName === nodeName ? targetName : sourceName];
|
|
1025
|
+
});
|
|
956
1026
|
} catch (error) {
|
|
957
1027
|
this.logger.error(`Failed to find relationships: ${error instanceof Error ? error.message : String(error)}`);
|
|
958
1028
|
return [];
|
|
@@ -960,27 +1030,39 @@ class GraphService {
|
|
|
960
1030
|
}
|
|
961
1031
|
async createVectorIndex(label, property, dimensions) {
|
|
962
1032
|
try {
|
|
963
|
-
const
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
1033
|
+
const indexKey = `${label}_${property}`;
|
|
1034
|
+
if (this.vectorIndexes.has(indexKey)) {
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
const conn = await this.ensureConnected();
|
|
1038
|
+
try {
|
|
1039
|
+
await conn.run(`
|
|
1040
|
+
CREATE INDEX idx_embedding_${this.escape(label)}
|
|
1041
|
+
ON nodes USING HNSW (embedding)
|
|
1042
|
+
WITH (metric = 'cosine')
|
|
1043
|
+
`);
|
|
1044
|
+
} catch {
|
|
1045
|
+
this.logger.debug(`Vector index on ${label}.${property} already exists`);
|
|
1046
|
+
}
|
|
1047
|
+
this.vectorIndexes.add(indexKey);
|
|
967
1048
|
this.logger.log(`Created vector index on ${label}.${property} with ${dimensions} dimensions`);
|
|
968
1049
|
} catch (error) {
|
|
969
1050
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
970
|
-
if (!errorMessage.includes("already
|
|
1051
|
+
if (!errorMessage.includes("already exists")) {
|
|
971
1052
|
this.logger.error(`Failed to create vector index: ${errorMessage}`);
|
|
972
1053
|
throw error;
|
|
973
1054
|
}
|
|
974
|
-
this.logger.debug(`Vector index on ${label}.${property} already exists`);
|
|
975
1055
|
}
|
|
976
1056
|
}
|
|
977
1057
|
async updateNodeEmbedding(label, name, embedding) {
|
|
978
1058
|
try {
|
|
979
|
-
const
|
|
980
|
-
const escapedName = this.escapeCypher(name);
|
|
1059
|
+
const conn = await this.ensureConnected();
|
|
981
1060
|
const vectorStr = `[${embedding.join(", ")}]`;
|
|
982
|
-
|
|
983
|
-
|
|
1061
|
+
await conn.run(`
|
|
1062
|
+
UPDATE nodes
|
|
1063
|
+
SET embedding = ${vectorStr}::FLOAT[${this.embeddingDimensions}]
|
|
1064
|
+
WHERE label = '${this.escape(label)}' AND name = '${this.escape(name)}'
|
|
1065
|
+
`);
|
|
984
1066
|
} catch (error) {
|
|
985
1067
|
this.logger.error(`Failed to update node embedding: ${error instanceof Error ? error.message : String(error)}`);
|
|
986
1068
|
throw error;
|
|
@@ -988,111 +1070,66 @@ class GraphService {
|
|
|
988
1070
|
}
|
|
989
1071
|
async vectorSearch(label, queryVector, k = 10) {
|
|
990
1072
|
try {
|
|
991
|
-
const
|
|
1073
|
+
const conn = await this.ensureConnected();
|
|
992
1074
|
const vectorStr = `[${queryVector.join(", ")}]`;
|
|
993
|
-
const
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1075
|
+
const reader = await conn.runAndReadAll(`
|
|
1076
|
+
SELECT
|
|
1077
|
+
name,
|
|
1078
|
+
properties->>'title' as title,
|
|
1079
|
+
array_cosine_similarity(embedding, ${vectorStr}::FLOAT[${this.embeddingDimensions}]) as similarity
|
|
1080
|
+
FROM nodes
|
|
1081
|
+
WHERE label = '${this.escape(label)}'
|
|
1082
|
+
AND embedding IS NOT NULL
|
|
1083
|
+
ORDER BY similarity DESC
|
|
1084
|
+
LIMIT ${k}
|
|
1085
|
+
`);
|
|
1086
|
+
return reader.getRows().map((row) => {
|
|
1087
|
+
const [name, title, similarity] = row;
|
|
1088
|
+
return {
|
|
1089
|
+
name,
|
|
1090
|
+
title: title || undefined,
|
|
1091
|
+
score: similarity
|
|
1092
|
+
};
|
|
1093
|
+
});
|
|
1000
1094
|
} catch (error) {
|
|
1001
1095
|
this.logger.error(`Vector search failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1002
1096
|
throw error;
|
|
1003
1097
|
}
|
|
1004
1098
|
}
|
|
1005
1099
|
async vectorSearchAll(queryVector, k = 10) {
|
|
1006
|
-
const allLabels = [
|
|
1007
|
-
"Document",
|
|
1008
|
-
"Concept",
|
|
1009
|
-
"Process",
|
|
1010
|
-
"Tool",
|
|
1011
|
-
"Technology",
|
|
1012
|
-
"Organization",
|
|
1013
|
-
"Topic",
|
|
1014
|
-
"Person"
|
|
1015
|
-
];
|
|
1016
1100
|
const allResults = [];
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1101
|
+
const conn = await this.ensureConnected();
|
|
1102
|
+
const vectorStr = `[${queryVector.join(", ")}]`;
|
|
1103
|
+
try {
|
|
1104
|
+
const reader = await conn.runAndReadAll(`
|
|
1105
|
+
SELECT
|
|
1106
|
+
name,
|
|
1107
|
+
label,
|
|
1108
|
+
properties->>'title' as title,
|
|
1109
|
+
properties->>'description' as description,
|
|
1110
|
+
array_cosine_similarity(embedding, ${vectorStr}::FLOAT[${this.embeddingDimensions}]) as similarity
|
|
1111
|
+
FROM nodes
|
|
1112
|
+
WHERE embedding IS NOT NULL
|
|
1113
|
+
ORDER BY similarity DESC
|
|
1114
|
+
LIMIT ${k}
|
|
1115
|
+
`);
|
|
1116
|
+
for (const row of reader.getRows()) {
|
|
1117
|
+
const [name, label, title, description, similarity] = row;
|
|
1118
|
+
allResults.push({
|
|
1119
|
+
name,
|
|
1025
1120
|
label,
|
|
1026
|
-
title:
|
|
1027
|
-
description:
|
|
1028
|
-
score:
|
|
1029
|
-
})
|
|
1030
|
-
allResults.push(...labelResults);
|
|
1031
|
-
} catch (error) {
|
|
1032
|
-
this.logger.debug(`Vector search for ${label} skipped: ${error instanceof Error ? error.message : String(error)}`);
|
|
1121
|
+
title: title || undefined,
|
|
1122
|
+
description: description || undefined,
|
|
1123
|
+
score: similarity
|
|
1124
|
+
});
|
|
1033
1125
|
}
|
|
1126
|
+
} catch (error) {
|
|
1127
|
+
this.logger.debug(`Vector search failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1034
1128
|
}
|
|
1035
1129
|
return allResults.sort((a, b) => b.score - a.score).slice(0, k);
|
|
1036
1130
|
}
|
|
1037
|
-
|
|
1038
|
-
return value.replace(
|
|
1039
|
-
}
|
|
1040
|
-
escapeCypherValue(value) {
|
|
1041
|
-
if (value === null || value === undefined) {
|
|
1042
|
-
return "null";
|
|
1043
|
-
}
|
|
1044
|
-
if (typeof value === "string") {
|
|
1045
|
-
const escaped = this.escapeCypher(value);
|
|
1046
|
-
return `'${escaped}'`;
|
|
1047
|
-
}
|
|
1048
|
-
if (typeof value === "number" || typeof value === "boolean") {
|
|
1049
|
-
return String(value);
|
|
1050
|
-
}
|
|
1051
|
-
if (Array.isArray(value)) {
|
|
1052
|
-
return `[${value.map((v) => this.escapeCypherValue(v)).join(", ")}]`;
|
|
1053
|
-
}
|
|
1054
|
-
if (typeof value === "object") {
|
|
1055
|
-
const pairs = Object.entries(value).map(([k, v]) => `${k}: ${this.escapeCypherValue(v)}`).join(", ");
|
|
1056
|
-
return `{${pairs}}`;
|
|
1057
|
-
}
|
|
1058
|
-
return String(value);
|
|
1059
|
-
}
|
|
1060
|
-
parseStats(result) {
|
|
1061
|
-
if (!Array.isArray(result) || result.length < 3) {
|
|
1062
|
-
return;
|
|
1063
|
-
}
|
|
1064
|
-
const statsStr = result[2];
|
|
1065
|
-
if (!statsStr || typeof statsStr !== "string") {
|
|
1066
|
-
return;
|
|
1067
|
-
}
|
|
1068
|
-
const stats = {
|
|
1069
|
-
nodesCreated: 0,
|
|
1070
|
-
nodesDeleted: 0,
|
|
1071
|
-
relationshipsCreated: 0,
|
|
1072
|
-
relationshipsDeleted: 0,
|
|
1073
|
-
propertiesSet: 0
|
|
1074
|
-
};
|
|
1075
|
-
const nodeCreatedMatch = statsStr.match(/Nodes created: (\d+)/);
|
|
1076
|
-
if (nodeCreatedMatch) {
|
|
1077
|
-
stats.nodesCreated = parseInt(nodeCreatedMatch[1], 10);
|
|
1078
|
-
}
|
|
1079
|
-
const nodeDeletedMatch = statsStr.match(/Nodes deleted: (\d+)/);
|
|
1080
|
-
if (nodeDeletedMatch) {
|
|
1081
|
-
stats.nodesDeleted = parseInt(nodeDeletedMatch[1], 10);
|
|
1082
|
-
}
|
|
1083
|
-
const relCreatedMatch = statsStr.match(/Relationships created: (\d+)/);
|
|
1084
|
-
if (relCreatedMatch) {
|
|
1085
|
-
stats.relationshipsCreated = parseInt(relCreatedMatch[1], 10);
|
|
1086
|
-
}
|
|
1087
|
-
const relDeletedMatch = statsStr.match(/Relationships deleted: (\d+)/);
|
|
1088
|
-
if (relDeletedMatch) {
|
|
1089
|
-
stats.relationshipsDeleted = parseInt(relDeletedMatch[1], 10);
|
|
1090
|
-
}
|
|
1091
|
-
const propSetMatch = statsStr.match(/Properties set: (\d+)/);
|
|
1092
|
-
if (propSetMatch) {
|
|
1093
|
-
stats.propertiesSet = parseInt(propSetMatch[1], 10);
|
|
1094
|
-
}
|
|
1095
|
-
return stats;
|
|
1131
|
+
escape(value) {
|
|
1132
|
+
return value.replace(/'/g, "''");
|
|
1096
1133
|
}
|
|
1097
1134
|
}
|
|
1098
1135
|
GraphService = __legacyDecorateClassTS([
|
|
@@ -1218,49 +1255,19 @@ class RelsCommand extends CommandRunner3 {
|
|
|
1218
1255
|
async run(inputs) {
|
|
1219
1256
|
const name = inputs[0];
|
|
1220
1257
|
try {
|
|
1221
|
-
const
|
|
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 || [];
|
|
1258
|
+
const relationships = await this.graphService.findRelationships(name);
|
|
1225
1259
|
console.log(`
|
|
1226
1260
|
=== Relationships for "${name}" ===
|
|
1227
1261
|
`);
|
|
1228
|
-
if (
|
|
1262
|
+
if (relationships.length === 0) {
|
|
1229
1263
|
console.log(`No relationships found.
|
|
1230
1264
|
`);
|
|
1231
1265
|
process.exit(0);
|
|
1232
1266
|
}
|
|
1233
|
-
|
|
1234
|
-
const
|
|
1235
|
-
|
|
1236
|
-
|
|
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
|
-
}
|
|
1267
|
+
console.log("Relationships:");
|
|
1268
|
+
for (const rel of relationships) {
|
|
1269
|
+
const [relType, targetName] = rel;
|
|
1270
|
+
console.log(` -[${relType}]-> ${targetName}`);
|
|
1264
1271
|
}
|
|
1265
1272
|
console.log();
|
|
1266
1273
|
process.exit(0);
|
|
@@ -1282,7 +1289,7 @@ RelsCommand = __legacyDecorateClassTS([
|
|
|
1282
1289
|
])
|
|
1283
1290
|
], RelsCommand);
|
|
1284
1291
|
|
|
1285
|
-
class
|
|
1292
|
+
class SqlCommand extends CommandRunner3 {
|
|
1286
1293
|
graphService;
|
|
1287
1294
|
constructor(graphService) {
|
|
1288
1295
|
super();
|
|
@@ -1293,9 +1300,10 @@ class CypherCommand extends CommandRunner3 {
|
|
|
1293
1300
|
try {
|
|
1294
1301
|
const result = await this.graphService.query(query);
|
|
1295
1302
|
console.log(`
|
|
1296
|
-
===
|
|
1303
|
+
=== SQL Query Results ===
|
|
1297
1304
|
`);
|
|
1298
|
-
|
|
1305
|
+
const replacer = (_key, value) => typeof value === "bigint" ? Number(value) : value;
|
|
1306
|
+
console.log(JSON.stringify(result, replacer, 2));
|
|
1299
1307
|
console.log();
|
|
1300
1308
|
process.exit(0);
|
|
1301
1309
|
} catch (error) {
|
|
@@ -1304,17 +1312,17 @@ class CypherCommand extends CommandRunner3 {
|
|
|
1304
1312
|
}
|
|
1305
1313
|
}
|
|
1306
1314
|
}
|
|
1307
|
-
|
|
1315
|
+
SqlCommand = __legacyDecorateClassTS([
|
|
1308
1316
|
Injectable7(),
|
|
1309
1317
|
Command3({
|
|
1310
|
-
name: "
|
|
1318
|
+
name: "sql",
|
|
1311
1319
|
arguments: "<query>",
|
|
1312
|
-
description: "Execute raw
|
|
1320
|
+
description: "Execute raw SQL query against DuckDB"
|
|
1313
1321
|
}),
|
|
1314
1322
|
__legacyMetadataTS("design:paramtypes", [
|
|
1315
1323
|
typeof GraphService === "undefined" ? Object : GraphService
|
|
1316
1324
|
])
|
|
1317
|
-
],
|
|
1325
|
+
], SqlCommand);
|
|
1318
1326
|
// src/commands/status.command.ts
|
|
1319
1327
|
import { Injectable as Injectable12 } from "@nestjs/common";
|
|
1320
1328
|
import { Command as Command4, CommandRunner as CommandRunner4, Option as Option3 } from "nest-commander";
|
|
@@ -1432,6 +1440,7 @@ ManifestService = __legacyDecorateClassTS([
|
|
|
1432
1440
|
|
|
1433
1441
|
// src/sync/sync.service.ts
|
|
1434
1442
|
import { Injectable as Injectable11, Logger as Logger5 } from "@nestjs/common";
|
|
1443
|
+
|
|
1435
1444
|
// src/pure/embedding-text.ts
|
|
1436
1445
|
function composeDocumentEmbeddingText(doc) {
|
|
1437
1446
|
const parts = [];
|
|
@@ -1645,10 +1654,13 @@ class CascadeService {
|
|
|
1645
1654
|
}
|
|
1646
1655
|
async findAffectedByRename(entityName, _newName) {
|
|
1647
1656
|
try {
|
|
1648
|
-
const escapedName = this.
|
|
1657
|
+
const escapedName = this.escapeForSql(entityName);
|
|
1649
1658
|
const query = `
|
|
1650
|
-
|
|
1651
|
-
|
|
1659
|
+
SELECT DISTINCT n.name, n.properties->>'title' as title
|
|
1660
|
+
FROM nodes n
|
|
1661
|
+
INNER JOIN relationships r ON r.target_label = n.label AND r.target_name = n.name
|
|
1662
|
+
WHERE r.source_name = '${escapedName}'
|
|
1663
|
+
AND n.label = 'Document'
|
|
1652
1664
|
`.trim();
|
|
1653
1665
|
const result = await this.graph.query(query);
|
|
1654
1666
|
return (result.resultSet || []).map((row) => ({
|
|
@@ -1665,10 +1677,13 @@ class CascadeService {
|
|
|
1665
1677
|
}
|
|
1666
1678
|
async findAffectedByDeletion(entityName) {
|
|
1667
1679
|
try {
|
|
1668
|
-
const escapedName = this.
|
|
1680
|
+
const escapedName = this.escapeForSql(entityName);
|
|
1669
1681
|
const query = `
|
|
1670
|
-
|
|
1671
|
-
|
|
1682
|
+
SELECT DISTINCT n.name, n.properties->>'title' as title
|
|
1683
|
+
FROM nodes n
|
|
1684
|
+
INNER JOIN relationships r ON r.target_label = n.label AND r.target_name = n.name
|
|
1685
|
+
WHERE r.source_name = '${escapedName}'
|
|
1686
|
+
AND n.label = 'Document'
|
|
1672
1687
|
`.trim();
|
|
1673
1688
|
const result = await this.graph.query(query);
|
|
1674
1689
|
return (result.resultSet || []).map((row) => ({
|
|
@@ -1685,10 +1700,13 @@ class CascadeService {
|
|
|
1685
1700
|
}
|
|
1686
1701
|
async findAffectedByTypeChange(entityName, oldType, newType) {
|
|
1687
1702
|
try {
|
|
1688
|
-
const escapedName = this.
|
|
1703
|
+
const escapedName = this.escapeForSql(entityName);
|
|
1689
1704
|
const query = `
|
|
1690
|
-
|
|
1691
|
-
|
|
1705
|
+
SELECT DISTINCT n.name, n.properties->>'title' as title
|
|
1706
|
+
FROM nodes n
|
|
1707
|
+
INNER JOIN relationships r ON r.target_label = n.label AND r.target_name = n.name
|
|
1708
|
+
WHERE r.source_name = '${escapedName}'
|
|
1709
|
+
AND n.label = 'Document'
|
|
1692
1710
|
`.trim();
|
|
1693
1711
|
const result = await this.graph.query(query);
|
|
1694
1712
|
return (result.resultSet || []).map((row) => ({
|
|
@@ -1705,10 +1723,13 @@ class CascadeService {
|
|
|
1705
1723
|
}
|
|
1706
1724
|
async findAffectedByRelationshipChange(entityName) {
|
|
1707
1725
|
try {
|
|
1708
|
-
const escapedName = this.
|
|
1726
|
+
const escapedName = this.escapeForSql(entityName);
|
|
1709
1727
|
const query = `
|
|
1710
|
-
|
|
1711
|
-
|
|
1728
|
+
SELECT DISTINCT n.name, n.properties->>'title' as title, r.relation_type
|
|
1729
|
+
FROM nodes n
|
|
1730
|
+
INNER JOIN relationships r ON r.target_label = n.label AND r.target_name = n.name
|
|
1731
|
+
WHERE r.source_name = '${escapedName}'
|
|
1732
|
+
AND n.label = 'Document'
|
|
1712
1733
|
`.trim();
|
|
1713
1734
|
const result = await this.graph.query(query);
|
|
1714
1735
|
return (result.resultSet || []).map((row) => ({
|
|
@@ -1725,10 +1746,13 @@ class CascadeService {
|
|
|
1725
1746
|
}
|
|
1726
1747
|
async findAffectedByDocumentDeletion(documentPath) {
|
|
1727
1748
|
try {
|
|
1728
|
-
const escapedPath = this.
|
|
1749
|
+
const escapedPath = this.escapeForSql(documentPath);
|
|
1729
1750
|
const query = `
|
|
1730
|
-
|
|
1731
|
-
|
|
1751
|
+
SELECT DISTINCT n.name, r.relation_type
|
|
1752
|
+
FROM nodes n
|
|
1753
|
+
INNER JOIN relationships r ON r.source_label = n.label AND r.source_name = n.name
|
|
1754
|
+
WHERE r.target_name = '${escapedPath}'
|
|
1755
|
+
AND n.label = 'Document'
|
|
1732
1756
|
`.trim();
|
|
1733
1757
|
const result = await this.graph.query(query);
|
|
1734
1758
|
return (result.resultSet || []).map((row) => ({
|
|
@@ -1815,8 +1839,8 @@ class CascadeService {
|
|
|
1815
1839
|
return action;
|
|
1816
1840
|
}
|
|
1817
1841
|
}
|
|
1818
|
-
|
|
1819
|
-
return value.replace(
|
|
1842
|
+
escapeForSql(value) {
|
|
1843
|
+
return value.replace(/'/g, "''");
|
|
1820
1844
|
}
|
|
1821
1845
|
}
|
|
1822
1846
|
CascadeService = __legacyDecorateClassTS([
|
|
@@ -2840,9 +2864,9 @@ class QueryService {
|
|
|
2840
2864
|
constructor(graphService) {
|
|
2841
2865
|
this.graphService = graphService;
|
|
2842
2866
|
}
|
|
2843
|
-
async query(
|
|
2844
|
-
this.logger.debug(`Executing query: ${
|
|
2845
|
-
return await this.graphService.query(
|
|
2867
|
+
async query(sql) {
|
|
2868
|
+
this.logger.debug(`Executing query: ${sql}`);
|
|
2869
|
+
return await this.graphService.query(sql);
|
|
2846
2870
|
}
|
|
2847
2871
|
}
|
|
2848
2872
|
QueryService = __legacyDecorateClassTS([
|
|
@@ -2908,7 +2932,7 @@ AppModule = __legacyDecorateClassTS([
|
|
|
2908
2932
|
StatusCommand,
|
|
2909
2933
|
SearchCommand,
|
|
2910
2934
|
RelsCommand,
|
|
2911
|
-
|
|
2935
|
+
SqlCommand,
|
|
2912
2936
|
ValidateCommand,
|
|
2913
2937
|
OntologyCommand,
|
|
2914
2938
|
InitCommand
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zabaca/lattice",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Human-initiated, AI-powered knowledge graph for markdown documentation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
},
|
|
27
27
|
"keywords": [
|
|
28
28
|
"knowledge-graph",
|
|
29
|
-
"
|
|
29
|
+
"duckdb",
|
|
30
30
|
"documentation",
|
|
31
31
|
"markdown",
|
|
32
32
|
"ai",
|
|
@@ -43,13 +43,13 @@
|
|
|
43
43
|
"url": "https://github.com/Zabaca/lattice.git"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
+
"@duckdb/node-api": "1.3.1-alpha.23",
|
|
46
47
|
"@nestjs/common": "^10.0.0",
|
|
47
48
|
"@nestjs/config": "^3.0.0",
|
|
48
49
|
"@nestjs/core": "^10.0.0",
|
|
49
50
|
"chalk": "^5.3.0",
|
|
50
51
|
"glob": "^10.3.0",
|
|
51
52
|
"gray-matter": "^4.0.3",
|
|
52
|
-
"ioredis": "^5.3.0",
|
|
53
53
|
"nest-commander": "^3.12.0",
|
|
54
54
|
"reflect-metadata": "^0.2.0",
|
|
55
55
|
"rxjs": "^7.8.0",
|
|
@@ -59,7 +59,6 @@
|
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"@biomejs/biome": "^2.2.6",
|
|
62
|
-
"@tkoehlerlg/bun-mock-extended": "^1.0.0",
|
|
63
62
|
"@types/bun": "latest",
|
|
64
63
|
"typescript": "^5.0.0"
|
|
65
64
|
}
|