amalfa 1.0.36 → 1.0.38
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 +7 -1
- package/package.json +11 -7
- package/src/cli.ts +66 -0
- package/src/daemon/sonar-agent.ts +6 -124
- package/src/daemon/sonar-server.ts +133 -0
- package/src/ember/README.md +23 -0
- package/src/ember/analyzer.ts +120 -0
- package/src/ember/generator.ts +25 -0
- package/src/ember/index.ts +106 -0
- package/src/ember/squasher.ts +71 -0
- package/src/ember/types.ts +26 -0
- package/src/pipeline/AmalfaIngestor.ts +9 -19
- package/src/resonance/DatabaseFactory.ts +1 -1
- package/src/resonance/drizzle/README.md +25 -0
- package/src/resonance/drizzle/migrations/0000_happy_thaddeus_ross.sql +30 -0
- package/src/resonance/drizzle/migrations/meta/0000_snapshot.json +199 -0
- package/src/resonance/drizzle/migrations/meta/_journal.json +13 -0
- package/src/resonance/drizzle/schema.ts +60 -0
package/README.md
CHANGED
|
@@ -244,7 +244,13 @@ Agents generate knowledge through structured reflection. Amalfa provides semanti
|
|
|
244
244
|
- [ ] Git-based auditing for augmentations
|
|
245
245
|
- [ ] Automated file watcher updates
|
|
246
246
|
|
|
247
|
-
###
|
|
247
|
+
### 🚧 Phase 2: Ember Service (Automated Enrichment)
|
|
248
|
+
- ✅ **Analyzer** - Louvain community detection & heuristics
|
|
249
|
+
- ✅ **Sidecar Generator** - Safe proposal mechanism (`.ember.json`)
|
|
250
|
+
- ✅ **Squasher** - Robust metadata merging (preserves user content)
|
|
251
|
+
- ✅ **CLI** - `amalfa ember scan/squash` commands
|
|
252
|
+
|
|
253
|
+
### 📋 Phase 3: Latent Space Organization (Planned)
|
|
248
254
|
|
|
249
255
|
- [ ] Document clustering (HDBSCAN)
|
|
250
256
|
- [ ] Cluster label generation
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "amalfa",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.38",
|
|
4
4
|
"description": "Local-first knowledge graph engine for AI agents. Transforms markdown into searchable memory with MCP protocol.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/pjsvis/amalfa#readme",
|
|
@@ -45,9 +45,9 @@
|
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@biomejs/biome": "2.3.8",
|
|
47
47
|
"@types/bun": "1.3.4",
|
|
48
|
-
"only-allow": "
|
|
49
|
-
"pino-pretty": "
|
|
50
|
-
"typescript": "
|
|
48
|
+
"only-allow": "1.2.2",
|
|
49
|
+
"pino-pretty": "13.1.3",
|
|
50
|
+
"typescript": "5.9.3"
|
|
51
51
|
},
|
|
52
52
|
"scripts": {
|
|
53
53
|
"precommit": "bun run scripts/maintenance/pre-commit.ts",
|
|
@@ -64,9 +64,13 @@
|
|
|
64
64
|
},
|
|
65
65
|
"dependencies": {
|
|
66
66
|
"@modelcontextprotocol/sdk": "1.25.2",
|
|
67
|
+
"drizzle-kit": "0.31.8",
|
|
68
|
+
"drizzle-orm": "0.45.1",
|
|
67
69
|
"fastembed": "2.0.0",
|
|
68
|
-
"graphology": "
|
|
69
|
-
"graphology-library": "
|
|
70
|
-
"
|
|
70
|
+
"graphology": "0.26.0",
|
|
71
|
+
"graphology-library": "0.8.0",
|
|
72
|
+
"gray-matter": "^4.0.3",
|
|
73
|
+
"hono": "4.11.3",
|
|
74
|
+
"pino": "10.1.0"
|
|
71
75
|
}
|
|
72
76
|
}
|
package/src/cli.ts
CHANGED
|
@@ -50,6 +50,7 @@ Commands:
|
|
|
50
50
|
daemon <action> Manage file watcher (start|stop|status|restart)
|
|
51
51
|
vector <action> Manage vector daemon (start|stop|status|restart)
|
|
52
52
|
sonar <action> Manage Sonar AI agent (start|stop|status|restart)
|
|
53
|
+
ember <action> Manage Ember enrichment service (scan|squash)
|
|
53
54
|
scripts list List available scripts and their descriptions
|
|
54
55
|
servers [--dot] Show status of all AMALFA services (--dot for graph)
|
|
55
56
|
|
|
@@ -821,6 +822,67 @@ async function cmdValidate() {
|
|
|
821
822
|
}
|
|
822
823
|
}
|
|
823
824
|
|
|
825
|
+
async function cmdEmber() {
|
|
826
|
+
const rawAction = args[1] || "help";
|
|
827
|
+
const action =
|
|
828
|
+
rawAction === "--help" || rawAction === "-h" ? "help" : rawAction;
|
|
829
|
+
|
|
830
|
+
if (action === "help") {
|
|
831
|
+
console.log(`
|
|
832
|
+
EMBER - Automated Enrichment Service
|
|
833
|
+
|
|
834
|
+
Usage:
|
|
835
|
+
amalfa ember scan [--dry-run] Analyze files and generate sidecars
|
|
836
|
+
amalfa ember squash Merge sidecars into markdown files
|
|
837
|
+
amalfa ember status Show pending sidecars (TODO)
|
|
838
|
+
`);
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
const { ResonanceDB } = await import("./resonance/db");
|
|
843
|
+
const { EmberService } = await import("./ember/index");
|
|
844
|
+
const { loadConfig } = await import("./config/defaults");
|
|
845
|
+
|
|
846
|
+
// Check DB
|
|
847
|
+
const dbPath = await getDbPath();
|
|
848
|
+
if (!existsSync(dbPath)) {
|
|
849
|
+
console.error("❌ Database not found. Run 'amalfa init' first.");
|
|
850
|
+
process.exit(1);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
const db = new ResonanceDB(dbPath);
|
|
854
|
+
const appConfig = await loadConfig();
|
|
855
|
+
|
|
856
|
+
const emberConfig = {
|
|
857
|
+
enabled: true,
|
|
858
|
+
sources: appConfig.sources || ["./docs"],
|
|
859
|
+
minConfidence: 0.7,
|
|
860
|
+
backupDir: ".amalfa/backups",
|
|
861
|
+
excludePatterns: appConfig.excludePatterns || [],
|
|
862
|
+
};
|
|
863
|
+
|
|
864
|
+
const ember = new EmberService(db, emberConfig);
|
|
865
|
+
|
|
866
|
+
try {
|
|
867
|
+
if (action === "scan") {
|
|
868
|
+
const dryRun = args.includes("--dry-run");
|
|
869
|
+
await ember.runFullSweep(dryRun);
|
|
870
|
+
} else if (action === "squash") {
|
|
871
|
+
await ember.squashAll();
|
|
872
|
+
} else if (action === "status") {
|
|
873
|
+
console.log("Checking pending sidecars... (Not yet implemented)");
|
|
874
|
+
} else {
|
|
875
|
+
console.error(`❌ Unknown action: ${action}`);
|
|
876
|
+
process.exit(1);
|
|
877
|
+
}
|
|
878
|
+
} catch (e) {
|
|
879
|
+
console.error("❌ Ember command failed:", e);
|
|
880
|
+
process.exit(1);
|
|
881
|
+
} finally {
|
|
882
|
+
db.close();
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
824
886
|
async function cmdDoctor() {
|
|
825
887
|
console.log("🩺 AMALFA Health Check\n");
|
|
826
888
|
|
|
@@ -944,6 +1006,10 @@ async function main() {
|
|
|
944
1006
|
await cmdSonar();
|
|
945
1007
|
break;
|
|
946
1008
|
|
|
1009
|
+
case "ember":
|
|
1010
|
+
await cmdEmber();
|
|
1011
|
+
break;
|
|
1012
|
+
|
|
947
1013
|
case "scripts":
|
|
948
1014
|
await cmdScripts();
|
|
949
1015
|
break;
|
|
@@ -24,27 +24,14 @@ import { ServiceLifecycle } from "@src/utils/ServiceLifecycle";
|
|
|
24
24
|
import { inferenceState } from "./sonar-inference";
|
|
25
25
|
import {
|
|
26
26
|
handleBatchEnhancement,
|
|
27
|
-
handleChat,
|
|
28
|
-
handleContextExtraction,
|
|
29
27
|
handleGardenTask,
|
|
30
|
-
handleMetadataEnhancement,
|
|
31
28
|
handleResearchTask,
|
|
32
|
-
handleResultReranking,
|
|
33
|
-
handleSearchAnalysis,
|
|
34
29
|
handleSynthesisTask,
|
|
35
30
|
handleTimelineTask,
|
|
36
31
|
type SonarContext,
|
|
37
32
|
} from "./sonar-logic";
|
|
38
33
|
import { getTaskModel } from "./sonar-strategies";
|
|
39
|
-
import type {
|
|
40
|
-
ChatRequest,
|
|
41
|
-
ChatSession,
|
|
42
|
-
MetadataEnhanceRequest,
|
|
43
|
-
SearchAnalyzeRequest,
|
|
44
|
-
SearchContextRequest,
|
|
45
|
-
SearchRerankRequest,
|
|
46
|
-
SonarTask,
|
|
47
|
-
} from "./sonar-types";
|
|
34
|
+
import type { ChatSession, SonarTask } from "./sonar-types";
|
|
48
35
|
|
|
49
36
|
const args = process.argv.slice(2);
|
|
50
37
|
const command = args[0] || "serve";
|
|
@@ -128,123 +115,18 @@ async function main() {
|
|
|
128
115
|
}
|
|
129
116
|
}
|
|
130
117
|
|
|
118
|
+
import { createSonarApp } from "./sonar-server";
|
|
119
|
+
|
|
131
120
|
/**
|
|
132
|
-
* Start Bun HTTP Server
|
|
121
|
+
* Start Bun HTTP Server via Hono
|
|
133
122
|
*/
|
|
134
123
|
function startServer(port: number) {
|
|
135
|
-
const corsHeaders = {
|
|
136
|
-
"Access-Control-Allow-Origin": "*",
|
|
137
|
-
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
138
|
-
"Access-Control-Allow-Headers": "Content-Type",
|
|
139
|
-
};
|
|
140
|
-
|
|
141
124
|
const context: SonarContext = { db, graphEngine, gardener, chatSessions };
|
|
125
|
+
const app = createSonarApp(context);
|
|
142
126
|
|
|
143
127
|
Bun.serve({
|
|
144
128
|
port,
|
|
145
|
-
|
|
146
|
-
if (req.method === "OPTIONS")
|
|
147
|
-
return new Response(null, { headers: corsHeaders });
|
|
148
|
-
const url = new URL(req.url);
|
|
149
|
-
|
|
150
|
-
// Health check
|
|
151
|
-
if (url.pathname === "/health") {
|
|
152
|
-
const cfg = await loadConfig();
|
|
153
|
-
const provider = cfg.sonar.cloud?.enabled ? "cloud" : "local";
|
|
154
|
-
const model = cfg.sonar.cloud?.enabled
|
|
155
|
-
? cfg.sonar.cloud.model
|
|
156
|
-
: inferenceState.ollamaModel || cfg.sonar.model;
|
|
157
|
-
return Response.json(
|
|
158
|
-
{
|
|
159
|
-
status: "ok",
|
|
160
|
-
ollama: inferenceState.ollamaAvailable,
|
|
161
|
-
provider,
|
|
162
|
-
model,
|
|
163
|
-
},
|
|
164
|
-
{ headers: corsHeaders },
|
|
165
|
-
);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Chat endpoint
|
|
169
|
-
if (url.pathname === "/chat" && req.method === "POST") {
|
|
170
|
-
try {
|
|
171
|
-
const body = (await req.json()) as ChatRequest;
|
|
172
|
-
const { sessionId, message, model } = body;
|
|
173
|
-
const result = await handleChat(sessionId, message, context, model);
|
|
174
|
-
return Response.json(result, { headers: corsHeaders });
|
|
175
|
-
} catch (error) {
|
|
176
|
-
return Response.json(
|
|
177
|
-
{ error: String(error) },
|
|
178
|
-
{ status: 500, headers: corsHeaders },
|
|
179
|
-
);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Metadata enhancement endpoint
|
|
184
|
-
if (url.pathname === "/metadata/enhance" && req.method === "POST") {
|
|
185
|
-
try {
|
|
186
|
-
const body = (await req.json()) as MetadataEnhanceRequest;
|
|
187
|
-
const { docId } = body;
|
|
188
|
-
await handleMetadataEnhancement(docId, context);
|
|
189
|
-
return Response.json({ status: "success" }, { headers: corsHeaders });
|
|
190
|
-
} catch (error) {
|
|
191
|
-
return Response.json(
|
|
192
|
-
{ error: String(error) },
|
|
193
|
-
{ status: 500, headers: corsHeaders },
|
|
194
|
-
);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// Graph Stats endpoint
|
|
199
|
-
if (url.pathname === "/graph/stats" && req.method === "GET") {
|
|
200
|
-
return Response.json(graphEngine.getStats(), { headers: corsHeaders });
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// Search endpoints (analysis, rerank, context)
|
|
204
|
-
if (url.pathname === "/search/analyze" && req.method === "POST") {
|
|
205
|
-
try {
|
|
206
|
-
const body = (await req.json()) as SearchAnalyzeRequest;
|
|
207
|
-
const { query } = body;
|
|
208
|
-
const result = await handleSearchAnalysis(query, context);
|
|
209
|
-
return Response.json(result, { headers: corsHeaders });
|
|
210
|
-
} catch (error) {
|
|
211
|
-
return Response.json(
|
|
212
|
-
{ error: String(error) },
|
|
213
|
-
{ status: 500, headers: corsHeaders },
|
|
214
|
-
);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
if (url.pathname === "/search/rerank" && req.method === "POST") {
|
|
219
|
-
try {
|
|
220
|
-
const body = (await req.json()) as SearchRerankRequest;
|
|
221
|
-
const { results, query, intent } = body;
|
|
222
|
-
const result = await handleResultReranking(results, query, intent);
|
|
223
|
-
return Response.json(result, { headers: corsHeaders });
|
|
224
|
-
} catch (error) {
|
|
225
|
-
return Response.json(
|
|
226
|
-
{ error: String(error) },
|
|
227
|
-
{ status: 500, headers: corsHeaders },
|
|
228
|
-
);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
if (url.pathname === "/search/context" && req.method === "POST") {
|
|
233
|
-
try {
|
|
234
|
-
const body = (await req.json()) as SearchContextRequest;
|
|
235
|
-
const { result, query } = body;
|
|
236
|
-
const contextResult = await handleContextExtraction(result, query);
|
|
237
|
-
return Response.json(contextResult, { headers: corsHeaders });
|
|
238
|
-
} catch (error) {
|
|
239
|
-
return Response.json(
|
|
240
|
-
{ error: String(error) },
|
|
241
|
-
{ status: 500, headers: corsHeaders },
|
|
242
|
-
);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
return new Response("Not Found", { status: 404, headers: corsHeaders });
|
|
247
|
-
},
|
|
129
|
+
fetch: app.fetch,
|
|
248
130
|
});
|
|
249
131
|
|
|
250
132
|
log.info(`Server started on port ${port}`);
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { loadConfig } from "@src/config/defaults";
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import { cors } from "hono/cors";
|
|
4
|
+
import { inferenceState } from "./sonar-inference";
|
|
5
|
+
import {
|
|
6
|
+
handleChat,
|
|
7
|
+
handleContextExtraction,
|
|
8
|
+
handleMetadataEnhancement,
|
|
9
|
+
handleResultReranking,
|
|
10
|
+
handleSearchAnalysis,
|
|
11
|
+
type SonarContext,
|
|
12
|
+
} from "./sonar-logic";
|
|
13
|
+
import type {
|
|
14
|
+
ChatRequest,
|
|
15
|
+
MetadataEnhanceRequest,
|
|
16
|
+
SearchAnalyzeRequest,
|
|
17
|
+
SearchContextRequest,
|
|
18
|
+
SearchRerankRequest,
|
|
19
|
+
} from "./sonar-types";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Creates the Hono application for the Sonar Agent
|
|
23
|
+
*/
|
|
24
|
+
export function createSonarApp(context: SonarContext) {
|
|
25
|
+
const app = new Hono();
|
|
26
|
+
|
|
27
|
+
// Global Middleware
|
|
28
|
+
app.use(
|
|
29
|
+
"*",
|
|
30
|
+
cors({
|
|
31
|
+
origin: "*",
|
|
32
|
+
allowMethods: ["GET", "POST", "OPTIONS"],
|
|
33
|
+
allowHeaders: ["Content-Type"],
|
|
34
|
+
}),
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Health Check
|
|
39
|
+
*/
|
|
40
|
+
app.get("/health", async (c) => {
|
|
41
|
+
const cfg = await loadConfig();
|
|
42
|
+
const provider = cfg.sonar.cloud?.enabled ? "cloud" : "local";
|
|
43
|
+
const model = cfg.sonar.cloud?.enabled
|
|
44
|
+
? cfg.sonar.cloud.model
|
|
45
|
+
: inferenceState.ollamaModel || cfg.sonar.model;
|
|
46
|
+
|
|
47
|
+
return c.json({
|
|
48
|
+
status: "ok",
|
|
49
|
+
ollama: inferenceState.ollamaAvailable,
|
|
50
|
+
provider,
|
|
51
|
+
model,
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Chat Interface
|
|
57
|
+
*/
|
|
58
|
+
app.post("/chat", async (c) => {
|
|
59
|
+
try {
|
|
60
|
+
const body = await c.req.json<ChatRequest>();
|
|
61
|
+
const { sessionId, message, model } = body;
|
|
62
|
+
const result = await handleChat(sessionId, message, context, model);
|
|
63
|
+
return c.json(result);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
return c.json({ error: String(error) }, 500);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Metadata Enhancement
|
|
71
|
+
*/
|
|
72
|
+
app.post("/metadata/enhance", async (c) => {
|
|
73
|
+
try {
|
|
74
|
+
const body = await c.req.json<MetadataEnhanceRequest>();
|
|
75
|
+
const { docId } = body;
|
|
76
|
+
await handleMetadataEnhancement(docId, context);
|
|
77
|
+
return c.json({ status: "success" });
|
|
78
|
+
} catch (error) {
|
|
79
|
+
return c.json({ error: String(error) }, 500);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Graph Stats
|
|
85
|
+
*/
|
|
86
|
+
app.get("/graph/stats", (c) => {
|
|
87
|
+
return c.json(context.graphEngine.getStats());
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Search: Query Analysis
|
|
92
|
+
*/
|
|
93
|
+
app.post("/search/analyze", async (c) => {
|
|
94
|
+
try {
|
|
95
|
+
const body = await c.req.json<SearchAnalyzeRequest>();
|
|
96
|
+
const { query } = body;
|
|
97
|
+
const result = await handleSearchAnalysis(query, context);
|
|
98
|
+
return c.json(result);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
return c.json({ error: String(error) }, 500);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Search: Reranking
|
|
106
|
+
*/
|
|
107
|
+
app.post("/search/rerank", async (c) => {
|
|
108
|
+
try {
|
|
109
|
+
const body = await c.req.json<SearchRerankRequest>();
|
|
110
|
+
const { results, query, intent } = body;
|
|
111
|
+
const result = await handleResultReranking(results, query, intent);
|
|
112
|
+
return c.json(result);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
return c.json({ error: String(error) }, 500);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Search: Context Extraction
|
|
120
|
+
*/
|
|
121
|
+
app.post("/search/context", async (c) => {
|
|
122
|
+
try {
|
|
123
|
+
const body = await c.req.json<SearchContextRequest>();
|
|
124
|
+
const { result, query } = body;
|
|
125
|
+
const contextResult = await handleContextExtraction(result, query);
|
|
126
|
+
return c.json(contextResult);
|
|
127
|
+
} catch (error) {
|
|
128
|
+
return c.json({ error: String(error) }, 500);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
return app;
|
|
133
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
|
|
2
|
+
# Ember Service
|
|
3
|
+
|
|
4
|
+
Automated enrichment service for the Amalfa Knowledge Graph.
|
|
5
|
+
|
|
6
|
+
## Stability Clause
|
|
7
|
+
|
|
8
|
+
> **Warning**
|
|
9
|
+
> This module is responsible for modifying user data (markdown files).
|
|
10
|
+
>
|
|
11
|
+
> * **Do not modify** `squasher.ts` without explicit regression testing.
|
|
12
|
+
> * **Do not change** the sidecar format without updating `types.ts` and `generator.ts`.
|
|
13
|
+
> * **Always use** `safe-dump` equivalent (e.g., `gray-matter`) when writing back files.
|
|
14
|
+
|
|
15
|
+
## Architecture
|
|
16
|
+
|
|
17
|
+
- **Analyzer**: Scans graph for enrichment opportunities.
|
|
18
|
+
- **Generator**: Writes changes to `.ember.json` sidecar files.
|
|
19
|
+
- **Squasher**: Merges sidecars into `.md` files safely.
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
Included in the main Amalfa daemon. Can be triggered via CLI.
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { GraphEngine } from "@src/core/GraphEngine";
|
|
2
|
+
import type { ResonanceDB } from "@src/resonance/db";
|
|
3
|
+
import { getLogger } from "@src/utils/Logger";
|
|
4
|
+
import type { EmberSidecar } from "./types";
|
|
5
|
+
|
|
6
|
+
export class EmberAnalyzer {
|
|
7
|
+
private log = getLogger("EmberAnalyzer");
|
|
8
|
+
private graphEngine: GraphEngine;
|
|
9
|
+
private communities: Record<string, number> | null = null;
|
|
10
|
+
private isGraphLoaded = false;
|
|
11
|
+
|
|
12
|
+
constructor(private db: ResonanceDB) {
|
|
13
|
+
this.graphEngine = new GraphEngine();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Pre-load graph data for batch analysis
|
|
18
|
+
*/
|
|
19
|
+
async prepare() {
|
|
20
|
+
this.log.info("Loading graph engine for analysis...");
|
|
21
|
+
await this.graphEngine.load(this.db.getRawDb());
|
|
22
|
+
this.communities = this.graphEngine.detectCommunities();
|
|
23
|
+
this.isGraphLoaded = true;
|
|
24
|
+
this.log.info("Graph engine ready.");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Analyze a file and generate enrichment proposals
|
|
29
|
+
*/
|
|
30
|
+
async analyze(
|
|
31
|
+
filePath: string,
|
|
32
|
+
content: string,
|
|
33
|
+
): Promise<EmberSidecar | null> {
|
|
34
|
+
this.log.info(`Analyzing ${filePath}...`);
|
|
35
|
+
|
|
36
|
+
// Lazy load if not ready
|
|
37
|
+
if (!this.isGraphLoaded) {
|
|
38
|
+
await this.prepare();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 1. Identify Node in Graph
|
|
42
|
+
const filename = filePath.split("/").pop() || "unknown";
|
|
43
|
+
const id = filename
|
|
44
|
+
.replace(/\.(md|ts|js)$/, "")
|
|
45
|
+
.toLowerCase()
|
|
46
|
+
.replace(/[^a-z0-9-]/g, "-");
|
|
47
|
+
|
|
48
|
+
const node = this.db.getNode(id);
|
|
49
|
+
if (!node) {
|
|
50
|
+
this.log.warn(`Node ${id} not found in graph. Skipping analysis.`);
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const proposedTags: string[] = [];
|
|
55
|
+
const proposedLinks: string[] = [];
|
|
56
|
+
|
|
57
|
+
// 2. Community-based Tag Suggestion
|
|
58
|
+
if (this.communities && this.communities[id] !== undefined) {
|
|
59
|
+
const communityId = this.communities[id];
|
|
60
|
+
const communityNodes = Object.entries(this.communities)
|
|
61
|
+
.filter(([_, comm]) => comm === communityId)
|
|
62
|
+
.map(([nId]) => nId);
|
|
63
|
+
|
|
64
|
+
// Only analyze if community is large enough
|
|
65
|
+
if (communityNodes.length > 2) {
|
|
66
|
+
const tagFreq = new Map<string, number>();
|
|
67
|
+
let neighborCount = 0;
|
|
68
|
+
|
|
69
|
+
// Analyze neighbors specifically (stronger signal than whole community)
|
|
70
|
+
const neighbors = this.graphEngine.getNeighbors(id);
|
|
71
|
+
|
|
72
|
+
for (const neighborId of neighbors) {
|
|
73
|
+
const neighbor = this.db.getNode(neighborId);
|
|
74
|
+
const nTags = (neighbor?.meta?.tags as string[]) || [];
|
|
75
|
+
|
|
76
|
+
for (const tag of nTags) {
|
|
77
|
+
tagFreq.set(tag, (tagFreq.get(tag) || 0) + 1);
|
|
78
|
+
}
|
|
79
|
+
neighborCount++;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Suggest tags present in > 50% of neighbors
|
|
83
|
+
if (neighborCount > 0) {
|
|
84
|
+
for (const [tag, count] of tagFreq.entries()) {
|
|
85
|
+
if (count / neighborCount >= 0.5) {
|
|
86
|
+
const currentTags = (node.meta?.tags as string[]) || [];
|
|
87
|
+
if (!currentTags.includes(tag) && !proposedTags.includes(tag)) {
|
|
88
|
+
proposedTags.push(tag);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 3. Heuristics (Stub detection)
|
|
97
|
+
const tags = (node.meta?.tags as string[]) || [];
|
|
98
|
+
if (content.length < 100 && !tags.includes("stub")) {
|
|
99
|
+
proposedTags.push("stub");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// If no meaningful changes, return null
|
|
103
|
+
if (proposedTags.length === 0 && proposedLinks.length === 0) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 4. Construct Sidecar
|
|
108
|
+
const sidecar: EmberSidecar = {
|
|
109
|
+
targetFile: filePath,
|
|
110
|
+
generatedAt: new Date().toISOString(),
|
|
111
|
+
confidence: 0.8,
|
|
112
|
+
changes: {
|
|
113
|
+
tags: proposedTags.length > 0 ? { add: proposedTags } : undefined,
|
|
114
|
+
links: proposedLinks.length > 0 ? { add: proposedLinks } : undefined,
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
return sidecar;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { getLogger } from "@src/utils/Logger";
|
|
2
|
+
import type { EmberSidecar } from "./types";
|
|
3
|
+
|
|
4
|
+
export class EmberGenerator {
|
|
5
|
+
private log = getLogger("EmberGenerator");
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Write the sidecar file to disk
|
|
9
|
+
*/
|
|
10
|
+
async generate(sidecar: EmberSidecar): Promise<string> {
|
|
11
|
+
const sidecarPath = `${sidecar.targetFile}.ember.json`;
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
await Bun.write(sidecarPath, JSON.stringify(sidecar, null, 2));
|
|
15
|
+
this.log.info(`Generated sidecar: ${sidecarPath}`);
|
|
16
|
+
return sidecarPath;
|
|
17
|
+
} catch (error) {
|
|
18
|
+
this.log.error(
|
|
19
|
+
{ err: error, file: sidecarPath },
|
|
20
|
+
"Failed to write sidecar",
|
|
21
|
+
);
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import type { ResonanceDB } from "@src/resonance/db";
|
|
3
|
+
import { getLogger } from "@src/utils/Logger";
|
|
4
|
+
import { Glob } from "bun";
|
|
5
|
+
import { EmberAnalyzer } from "./analyzer";
|
|
6
|
+
import { EmberGenerator } from "./generator";
|
|
7
|
+
import { EmberSquasher } from "./squasher";
|
|
8
|
+
import type { EmberConfig } from "./types";
|
|
9
|
+
|
|
10
|
+
export class EmberService {
|
|
11
|
+
private analyzer: EmberAnalyzer;
|
|
12
|
+
private generator: EmberGenerator;
|
|
13
|
+
private squasher: EmberSquasher;
|
|
14
|
+
private log = getLogger("EmberService");
|
|
15
|
+
|
|
16
|
+
constructor(
|
|
17
|
+
db: ResonanceDB,
|
|
18
|
+
private config: EmberConfig,
|
|
19
|
+
) {
|
|
20
|
+
this.analyzer = new EmberAnalyzer(db);
|
|
21
|
+
this.generator = new EmberGenerator();
|
|
22
|
+
this.squasher = new EmberSquasher();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Run a full sweep of all configured sources
|
|
27
|
+
*/
|
|
28
|
+
async runFullSweep(dryRun = false) {
|
|
29
|
+
this.log.info("Starting full Ember sweep...");
|
|
30
|
+
|
|
31
|
+
const files = await this.discoverFiles();
|
|
32
|
+
let enrichedCount = 0;
|
|
33
|
+
|
|
34
|
+
for (const file of files) {
|
|
35
|
+
const content = await Bun.file(file).text();
|
|
36
|
+
const sidecar = await this.analyzer.analyze(file, content);
|
|
37
|
+
|
|
38
|
+
if (sidecar) {
|
|
39
|
+
if (dryRun) {
|
|
40
|
+
this.log.info(`[Dry Run] Would generate sidecar for ${file}`);
|
|
41
|
+
console.log(JSON.stringify(sidecar, null, 2));
|
|
42
|
+
} else {
|
|
43
|
+
await this.generator.generate(sidecar);
|
|
44
|
+
enrichedCount++;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
this.log.info(`Sweep complete. Enriched ${enrichedCount} files.`);
|
|
50
|
+
return enrichedCount;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Squash all pending sidecars
|
|
55
|
+
*/
|
|
56
|
+
async squashAll() {
|
|
57
|
+
this.log.info("Squashing all pending sidecars...");
|
|
58
|
+
let count = 0;
|
|
59
|
+
|
|
60
|
+
// Simpler scan:
|
|
61
|
+
const sidecars = await this.findSidecars();
|
|
62
|
+
for (const sidecarPath of sidecars) {
|
|
63
|
+
await this.squasher.squash(sidecarPath);
|
|
64
|
+
count++;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
this.log.info(`Squashed ${count} sidecars.`);
|
|
68
|
+
return count;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private async findSidecars(): Promise<string[]> {
|
|
72
|
+
const sidecars: string[] = [];
|
|
73
|
+
const glob = new Glob("**/*.ember.json");
|
|
74
|
+
// Scan sources
|
|
75
|
+
for (const source of this.config.sources) {
|
|
76
|
+
// Assuming source is like "./docs"
|
|
77
|
+
const sourcePath = join(process.cwd(), source);
|
|
78
|
+
for (const file of glob.scanSync({ cwd: sourcePath })) {
|
|
79
|
+
sidecars.push(join(sourcePath, file));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return sidecars;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private async discoverFiles(): Promise<string[]> {
|
|
86
|
+
const files: string[] = [];
|
|
87
|
+
const glob = new Glob("**/*.{md,mdx}"); // Only markdown for now
|
|
88
|
+
|
|
89
|
+
for (const source of this.config.sources) {
|
|
90
|
+
const sourcePath = join(process.cwd(), source);
|
|
91
|
+
try {
|
|
92
|
+
for (const file of glob.scanSync({ cwd: sourcePath })) {
|
|
93
|
+
const shouldExclude = this.config.excludePatterns.some((p) =>
|
|
94
|
+
file.includes(p),
|
|
95
|
+
);
|
|
96
|
+
if (!shouldExclude) {
|
|
97
|
+
files.push(join(sourcePath, file));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
} catch (e) {
|
|
101
|
+
this.log.warn({ source: sourcePath, err: e }, "Failed to scan source");
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return files;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { unlink } from "node:fs/promises";
|
|
2
|
+
import { getLogger } from "@src/utils/Logger";
|
|
3
|
+
import matter from "gray-matter";
|
|
4
|
+
import type { EmberSidecar } from "./types";
|
|
5
|
+
|
|
6
|
+
export class EmberSquasher {
|
|
7
|
+
private log = getLogger("EmberSquasher");
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Apply the sidecar changes to the target file
|
|
11
|
+
*/
|
|
12
|
+
async squash(sidecarPath: string): Promise<void> {
|
|
13
|
+
try {
|
|
14
|
+
// 1. Read Sidecar
|
|
15
|
+
const sidecarContent = await Bun.file(sidecarPath).text();
|
|
16
|
+
const sidecar: EmberSidecar = JSON.parse(sidecarContent);
|
|
17
|
+
|
|
18
|
+
const targetPath = sidecar.targetFile;
|
|
19
|
+
|
|
20
|
+
// 2. Read Target File
|
|
21
|
+
const fileContent = await Bun.file(targetPath).text();
|
|
22
|
+
|
|
23
|
+
// 3. Parse with gray-matter
|
|
24
|
+
const parsed = matter(fileContent);
|
|
25
|
+
const data = parsed.data || {};
|
|
26
|
+
|
|
27
|
+
// 4. Apply Changes
|
|
28
|
+
if (sidecar.changes.tags) {
|
|
29
|
+
const currentTags = (
|
|
30
|
+
Array.isArray(data.tags) ? data.tags : []
|
|
31
|
+
) as string[];
|
|
32
|
+
const toAdd = sidecar.changes.tags.add || [];
|
|
33
|
+
const toRemove = sidecar.changes.tags.remove || [];
|
|
34
|
+
|
|
35
|
+
const newTags = new Set(currentTags);
|
|
36
|
+
for (const t of toAdd) {
|
|
37
|
+
newTags.add(t);
|
|
38
|
+
}
|
|
39
|
+
for (const t of toRemove) {
|
|
40
|
+
newTags.delete(t);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
data.tags = Array.from(newTags);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (sidecar.changes.frontmatter) {
|
|
47
|
+
Object.assign(data, sidecar.changes.frontmatter);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (sidecar.changes.summary) {
|
|
51
|
+
data.summary = sidecar.changes.summary;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 5. Reconstruct File
|
|
55
|
+
const newContent = matter.stringify(parsed.content, data);
|
|
56
|
+
|
|
57
|
+
// 6. Write Back
|
|
58
|
+
await Bun.write(targetPath, newContent);
|
|
59
|
+
this.log.info(`Squashed sidecar into ${targetPath}`);
|
|
60
|
+
|
|
61
|
+
// 7. Cleanup Sidecar
|
|
62
|
+
await unlink(sidecarPath);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
this.log.error(
|
|
65
|
+
{ err: error, file: sidecarPath },
|
|
66
|
+
"Failed to squash sidecar",
|
|
67
|
+
);
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface EmberSidecar {
|
|
2
|
+
targetFile: string;
|
|
3
|
+
generatedAt: string;
|
|
4
|
+
confidence: number;
|
|
5
|
+
changes: {
|
|
6
|
+
tags?: {
|
|
7
|
+
add: string[];
|
|
8
|
+
remove?: string[];
|
|
9
|
+
};
|
|
10
|
+
frontmatter?: Record<string, unknown>;
|
|
11
|
+
summary?: string;
|
|
12
|
+
links?: {
|
|
13
|
+
add: string[]; // List of IDs or Titles to add to 'related'
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface EmberConfig {
|
|
19
|
+
enabled: boolean;
|
|
20
|
+
sources: string[];
|
|
21
|
+
minConfidence: number;
|
|
22
|
+
backupDir: string;
|
|
23
|
+
excludePatterns: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type EnrichmentType = "tag" | "link" | "summary" | "metadata";
|
|
@@ -12,6 +12,7 @@ import { Embedder } from "@src/resonance/services/embedder";
|
|
|
12
12
|
import { SimpleTokenizerService as TokenizerService } from "@src/resonance/services/simpleTokenizer";
|
|
13
13
|
import { getLogger } from "@src/utils/Logger";
|
|
14
14
|
import { Glob } from "bun";
|
|
15
|
+
import matter from "gray-matter";
|
|
15
16
|
|
|
16
17
|
export interface IngestionResult {
|
|
17
18
|
success: boolean;
|
|
@@ -236,11 +237,12 @@ export class AmalfaIngestor {
|
|
|
236
237
|
tokenizer: TokenizerService,
|
|
237
238
|
): Promise<void> {
|
|
238
239
|
try {
|
|
239
|
-
const
|
|
240
|
+
const rawContent = await Bun.file(filePath).text();
|
|
240
241
|
|
|
241
|
-
// Parse frontmatter
|
|
242
|
-
const
|
|
243
|
-
const frontmatter =
|
|
242
|
+
// Parse frontmatter with gray-matter
|
|
243
|
+
const parsed = matter(rawContent);
|
|
244
|
+
const frontmatter = parsed.data || {};
|
|
245
|
+
const content = parsed.content;
|
|
244
246
|
|
|
245
247
|
// Generate ID from filename
|
|
246
248
|
const filename = filePath.split("/").pop() || "unknown";
|
|
@@ -251,7 +253,7 @@ export class AmalfaIngestor {
|
|
|
251
253
|
|
|
252
254
|
// Skip if content unchanged (hash check)
|
|
253
255
|
const hasher = new Bun.CryptoHasher("md5");
|
|
254
|
-
hasher.update(
|
|
256
|
+
hasher.update(rawContent.trim());
|
|
255
257
|
const currentHash = hasher.digest("hex");
|
|
256
258
|
const storedHash = this.db.getNodeHash(id);
|
|
257
259
|
|
|
@@ -268,6 +270,8 @@ export class AmalfaIngestor {
|
|
|
268
270
|
// Extract semantic tokens
|
|
269
271
|
const tokens = tokenizer.extract(content);
|
|
270
272
|
|
|
273
|
+
// Insert node
|
|
274
|
+
|
|
271
275
|
// Insert node
|
|
272
276
|
const node: Node = {
|
|
273
277
|
id,
|
|
@@ -295,18 +299,4 @@ export class AmalfaIngestor {
|
|
|
295
299
|
this.log.warn({ err: e, file: filePath }, "⚠️ Failed to process file");
|
|
296
300
|
}
|
|
297
301
|
}
|
|
298
|
-
|
|
299
|
-
/**
|
|
300
|
-
* Parse YAML-like frontmatter
|
|
301
|
-
*/
|
|
302
|
-
private parseFrontmatter(text: string): Record<string, unknown> {
|
|
303
|
-
const meta: Record<string, unknown> = {};
|
|
304
|
-
text.split("\n").forEach((line) => {
|
|
305
|
-
const [key, ...vals] = line.split(":");
|
|
306
|
-
if (key && vals.length) {
|
|
307
|
-
meta[key.trim()] = vals.join(":").trim();
|
|
308
|
-
}
|
|
309
|
-
});
|
|
310
|
-
return meta;
|
|
311
|
-
}
|
|
312
302
|
}
|
|
@@ -13,7 +13,7 @@ import { Database } from "bun:sqlite";
|
|
|
13
13
|
export const DatabaseFactory = {
|
|
14
14
|
/**
|
|
15
15
|
* Connects specifically to the main Resonance Graph database.
|
|
16
|
-
*
|
|
16
|
+
* Kept for convenience and backward compatibility.
|
|
17
17
|
*/
|
|
18
18
|
connectToResonance(
|
|
19
19
|
dbPath: string = ".amalfa/resonance.db",
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Drizzle Schema Management
|
|
2
|
+
|
|
3
|
+
## ⚠️ CRITICAL ARCHITECTURE RULE
|
|
4
|
+
|
|
5
|
+
**Drizzle ORM is used HERE strictly for:**
|
|
6
|
+
1. **Schema Definition:** Single source of truth for table structures (`schema.ts`).
|
|
7
|
+
2. **Migrations:** Generating and running SQL migrations (`drizzle-kit`).
|
|
8
|
+
|
|
9
|
+
**Drizzle ORM is NOT used for:**
|
|
10
|
+
- **Runtime Queries:** Do NOT use Drizzle `db.select()` query builders in critical paths.
|
|
11
|
+
- **Data Access:** All runtime data access uses `bun:sqlite` with raw parameterized SQL (`db.prepare()`).
|
|
12
|
+
|
|
13
|
+
## Why?
|
|
14
|
+
Our **FAFCAS** (Fast, Autonomous, Fractionally-Connected Agent System) architecture demands raw IO performance. We treat SQLite as a raw storage engine. The overhead of an ORM query builder—even a light one—is unnecessary.
|
|
15
|
+
|
|
16
|
+
## ⛔ Agent Guardrail
|
|
17
|
+
If you are an AI Agent reading this:
|
|
18
|
+
- **DO NOT** refactor the existing `ResonanceDB` SQL queries to use Drizzle.
|
|
19
|
+
- **DO NOT** introduce Drizzle usage into `GraphEngine` or `VectorEngine`.
|
|
20
|
+
- **ONLY** modify this folder if the Database Schema (tables/columns) needs to change.
|
|
21
|
+
|
|
22
|
+
## Workflow for Schema Changes
|
|
23
|
+
1. Edit `schema.ts`
|
|
24
|
+
2. Run `bunx drizzle-kit generate`
|
|
25
|
+
3. Run migrations (automated via scripts)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
CREATE TABLE `edges` (
|
|
2
|
+
`source` text NOT NULL,
|
|
3
|
+
`target` text NOT NULL,
|
|
4
|
+
`type` text NOT NULL,
|
|
5
|
+
`confidence` real DEFAULT 1,
|
|
6
|
+
`veracity` real DEFAULT 1,
|
|
7
|
+
`context_source` text,
|
|
8
|
+
PRIMARY KEY(`source`, `target`, `type`)
|
|
9
|
+
);
|
|
10
|
+
--> statement-breakpoint
|
|
11
|
+
CREATE INDEX `idx_edges_source` ON `edges` (`source`);--> statement-breakpoint
|
|
12
|
+
CREATE INDEX `idx_edges_target` ON `edges` (`target`);--> statement-breakpoint
|
|
13
|
+
CREATE TABLE `ember_state` (
|
|
14
|
+
`file_path` text PRIMARY KEY NOT NULL,
|
|
15
|
+
`last_analyzed` text,
|
|
16
|
+
`sidecar_created` integer,
|
|
17
|
+
`confidence` real
|
|
18
|
+
);
|
|
19
|
+
--> statement-breakpoint
|
|
20
|
+
CREATE TABLE `nodes` (
|
|
21
|
+
`id` text PRIMARY KEY NOT NULL,
|
|
22
|
+
`type` text,
|
|
23
|
+
`title` text,
|
|
24
|
+
`domain` text,
|
|
25
|
+
`layer` text,
|
|
26
|
+
`embedding` blob,
|
|
27
|
+
`hash` text,
|
|
28
|
+
`meta` text,
|
|
29
|
+
`date` text
|
|
30
|
+
);
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "6",
|
|
3
|
+
"dialect": "sqlite",
|
|
4
|
+
"id": "577c9f07-f198-49d2-9fa3-f222a6ecee23",
|
|
5
|
+
"prevId": "00000000-0000-0000-0000-000000000000",
|
|
6
|
+
"tables": {
|
|
7
|
+
"edges": {
|
|
8
|
+
"name": "edges",
|
|
9
|
+
"columns": {
|
|
10
|
+
"source": {
|
|
11
|
+
"name": "source",
|
|
12
|
+
"type": "text",
|
|
13
|
+
"primaryKey": false,
|
|
14
|
+
"notNull": true,
|
|
15
|
+
"autoincrement": false
|
|
16
|
+
},
|
|
17
|
+
"target": {
|
|
18
|
+
"name": "target",
|
|
19
|
+
"type": "text",
|
|
20
|
+
"primaryKey": false,
|
|
21
|
+
"notNull": true,
|
|
22
|
+
"autoincrement": false
|
|
23
|
+
},
|
|
24
|
+
"type": {
|
|
25
|
+
"name": "type",
|
|
26
|
+
"type": "text",
|
|
27
|
+
"primaryKey": false,
|
|
28
|
+
"notNull": true,
|
|
29
|
+
"autoincrement": false
|
|
30
|
+
},
|
|
31
|
+
"confidence": {
|
|
32
|
+
"name": "confidence",
|
|
33
|
+
"type": "real",
|
|
34
|
+
"primaryKey": false,
|
|
35
|
+
"notNull": false,
|
|
36
|
+
"autoincrement": false,
|
|
37
|
+
"default": 1
|
|
38
|
+
},
|
|
39
|
+
"veracity": {
|
|
40
|
+
"name": "veracity",
|
|
41
|
+
"type": "real",
|
|
42
|
+
"primaryKey": false,
|
|
43
|
+
"notNull": false,
|
|
44
|
+
"autoincrement": false,
|
|
45
|
+
"default": 1
|
|
46
|
+
},
|
|
47
|
+
"context_source": {
|
|
48
|
+
"name": "context_source",
|
|
49
|
+
"type": "text",
|
|
50
|
+
"primaryKey": false,
|
|
51
|
+
"notNull": false,
|
|
52
|
+
"autoincrement": false
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"indexes": {
|
|
56
|
+
"idx_edges_source": {
|
|
57
|
+
"name": "idx_edges_source",
|
|
58
|
+
"columns": ["source"],
|
|
59
|
+
"isUnique": false
|
|
60
|
+
},
|
|
61
|
+
"idx_edges_target": {
|
|
62
|
+
"name": "idx_edges_target",
|
|
63
|
+
"columns": ["target"],
|
|
64
|
+
"isUnique": false
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"foreignKeys": {},
|
|
68
|
+
"compositePrimaryKeys": {
|
|
69
|
+
"edges_source_target_type_pk": {
|
|
70
|
+
"columns": ["source", "target", "type"],
|
|
71
|
+
"name": "edges_source_target_type_pk"
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
"uniqueConstraints": {},
|
|
75
|
+
"checkConstraints": {}
|
|
76
|
+
},
|
|
77
|
+
"ember_state": {
|
|
78
|
+
"name": "ember_state",
|
|
79
|
+
"columns": {
|
|
80
|
+
"file_path": {
|
|
81
|
+
"name": "file_path",
|
|
82
|
+
"type": "text",
|
|
83
|
+
"primaryKey": true,
|
|
84
|
+
"notNull": true,
|
|
85
|
+
"autoincrement": false
|
|
86
|
+
},
|
|
87
|
+
"last_analyzed": {
|
|
88
|
+
"name": "last_analyzed",
|
|
89
|
+
"type": "text",
|
|
90
|
+
"primaryKey": false,
|
|
91
|
+
"notNull": false,
|
|
92
|
+
"autoincrement": false
|
|
93
|
+
},
|
|
94
|
+
"sidecar_created": {
|
|
95
|
+
"name": "sidecar_created",
|
|
96
|
+
"type": "integer",
|
|
97
|
+
"primaryKey": false,
|
|
98
|
+
"notNull": false,
|
|
99
|
+
"autoincrement": false
|
|
100
|
+
},
|
|
101
|
+
"confidence": {
|
|
102
|
+
"name": "confidence",
|
|
103
|
+
"type": "real",
|
|
104
|
+
"primaryKey": false,
|
|
105
|
+
"notNull": false,
|
|
106
|
+
"autoincrement": false
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
"indexes": {},
|
|
110
|
+
"foreignKeys": {},
|
|
111
|
+
"compositePrimaryKeys": {},
|
|
112
|
+
"uniqueConstraints": {},
|
|
113
|
+
"checkConstraints": {}
|
|
114
|
+
},
|
|
115
|
+
"nodes": {
|
|
116
|
+
"name": "nodes",
|
|
117
|
+
"columns": {
|
|
118
|
+
"id": {
|
|
119
|
+
"name": "id",
|
|
120
|
+
"type": "text",
|
|
121
|
+
"primaryKey": true,
|
|
122
|
+
"notNull": true,
|
|
123
|
+
"autoincrement": false
|
|
124
|
+
},
|
|
125
|
+
"type": {
|
|
126
|
+
"name": "type",
|
|
127
|
+
"type": "text",
|
|
128
|
+
"primaryKey": false,
|
|
129
|
+
"notNull": false,
|
|
130
|
+
"autoincrement": false
|
|
131
|
+
},
|
|
132
|
+
"title": {
|
|
133
|
+
"name": "title",
|
|
134
|
+
"type": "text",
|
|
135
|
+
"primaryKey": false,
|
|
136
|
+
"notNull": false,
|
|
137
|
+
"autoincrement": false
|
|
138
|
+
},
|
|
139
|
+
"domain": {
|
|
140
|
+
"name": "domain",
|
|
141
|
+
"type": "text",
|
|
142
|
+
"primaryKey": false,
|
|
143
|
+
"notNull": false,
|
|
144
|
+
"autoincrement": false
|
|
145
|
+
},
|
|
146
|
+
"layer": {
|
|
147
|
+
"name": "layer",
|
|
148
|
+
"type": "text",
|
|
149
|
+
"primaryKey": false,
|
|
150
|
+
"notNull": false,
|
|
151
|
+
"autoincrement": false
|
|
152
|
+
},
|
|
153
|
+
"embedding": {
|
|
154
|
+
"name": "embedding",
|
|
155
|
+
"type": "blob",
|
|
156
|
+
"primaryKey": false,
|
|
157
|
+
"notNull": false,
|
|
158
|
+
"autoincrement": false
|
|
159
|
+
},
|
|
160
|
+
"hash": {
|
|
161
|
+
"name": "hash",
|
|
162
|
+
"type": "text",
|
|
163
|
+
"primaryKey": false,
|
|
164
|
+
"notNull": false,
|
|
165
|
+
"autoincrement": false
|
|
166
|
+
},
|
|
167
|
+
"meta": {
|
|
168
|
+
"name": "meta",
|
|
169
|
+
"type": "text",
|
|
170
|
+
"primaryKey": false,
|
|
171
|
+
"notNull": false,
|
|
172
|
+
"autoincrement": false
|
|
173
|
+
},
|
|
174
|
+
"date": {
|
|
175
|
+
"name": "date",
|
|
176
|
+
"type": "text",
|
|
177
|
+
"primaryKey": false,
|
|
178
|
+
"notNull": false,
|
|
179
|
+
"autoincrement": false
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
"indexes": {},
|
|
183
|
+
"foreignKeys": {},
|
|
184
|
+
"compositePrimaryKeys": {},
|
|
185
|
+
"uniqueConstraints": {},
|
|
186
|
+
"checkConstraints": {}
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
"views": {},
|
|
190
|
+
"enums": {},
|
|
191
|
+
"_meta": {
|
|
192
|
+
"schemas": {},
|
|
193
|
+
"tables": {},
|
|
194
|
+
"columns": {}
|
|
195
|
+
},
|
|
196
|
+
"internal": {
|
|
197
|
+
"indexes": {}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import {
|
|
2
|
+
blob,
|
|
3
|
+
index,
|
|
4
|
+
integer,
|
|
5
|
+
primaryKey,
|
|
6
|
+
real,
|
|
7
|
+
sqliteTable,
|
|
8
|
+
text,
|
|
9
|
+
} from "drizzle-orm/sqlite-core";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* NODES Table
|
|
13
|
+
* Core entity storage. Now "Hollow" (no content).
|
|
14
|
+
*/
|
|
15
|
+
export const nodes = sqliteTable("nodes", {
|
|
16
|
+
id: text("id").primaryKey(),
|
|
17
|
+
type: text("type"),
|
|
18
|
+
title: text("title"),
|
|
19
|
+
domain: text("domain"),
|
|
20
|
+
layer: text("layer"),
|
|
21
|
+
// Embeddings are stored as raw BLOBs (Float32Array bytes)
|
|
22
|
+
embedding: blob("embedding"),
|
|
23
|
+
hash: text("hash"),
|
|
24
|
+
meta: text("meta"), // JSON string
|
|
25
|
+
date: text("date"), // ISO string or YYYY-MM-DD
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* EDGES Table
|
|
30
|
+
* Defines relationships between nodes.
|
|
31
|
+
*/
|
|
32
|
+
export const edges = sqliteTable(
|
|
33
|
+
"edges",
|
|
34
|
+
{
|
|
35
|
+
source: text("source").notNull(),
|
|
36
|
+
target: text("target").notNull(),
|
|
37
|
+
type: text("type").notNull(),
|
|
38
|
+
confidence: real("confidence").default(1.0),
|
|
39
|
+
veracity: real("veracity").default(1.0),
|
|
40
|
+
contextSource: text("context_source"),
|
|
41
|
+
},
|
|
42
|
+
(table) => ({
|
|
43
|
+
// Composite Primary Key
|
|
44
|
+
pk: primaryKey({ columns: [table.source, table.target, table.type] }),
|
|
45
|
+
// Indices for traversal speed
|
|
46
|
+
sourceIdx: index("idx_edges_source").on(table.source),
|
|
47
|
+
targetIdx: index("idx_edges_target").on(table.target),
|
|
48
|
+
}),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* EMBER STATE Table (Pilot)
|
|
53
|
+
* Tracks the state of the Ember Service (automated enrichment).
|
|
54
|
+
*/
|
|
55
|
+
export const emberState = sqliteTable("ember_state", {
|
|
56
|
+
filePath: text("file_path").primaryKey(),
|
|
57
|
+
lastAnalyzed: text("last_analyzed"),
|
|
58
|
+
sidecarCreated: integer("sidecar_created", { mode: "boolean" }),
|
|
59
|
+
confidence: real("confidence"),
|
|
60
|
+
});
|