@dahch/muninn 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +152 -2
- package/dist/cli.d.ts +23 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +170 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/check.d.ts +3 -0
- package/dist/commands/check.d.ts.map +1 -0
- package/dist/commands/check.js +56 -0
- package/dist/commands/check.js.map +1 -0
- package/dist/commands/memory.d.ts +6 -0
- package/dist/commands/memory.d.ts.map +1 -0
- package/dist/commands/memory.js +325 -0
- package/dist/commands/memory.js.map +1 -0
- package/dist/contracts/compiler.d.ts +71 -0
- package/dist/contracts/compiler.d.ts.map +1 -0
- package/dist/contracts/compiler.js +362 -0
- package/dist/contracts/compiler.js.map +1 -0
- package/dist/contracts/index.d.ts +2 -0
- package/dist/contracts/index.d.ts.map +1 -0
- package/dist/contracts/index.js +2 -0
- package/dist/contracts/index.js.map +1 -0
- package/dist/db/client.d.ts +84 -0
- package/dist/db/client.d.ts.map +1 -0
- package/dist/db/client.js +264 -0
- package/dist/db/client.js.map +1 -0
- package/dist/db/schema.sql +97 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/indexer/ast-indexer.d.ts +55 -0
- package/dist/indexer/ast-indexer.d.ts.map +1 -0
- package/dist/indexer/ast-indexer.js +445 -0
- package/dist/indexer/ast-indexer.js.map +1 -0
- package/dist/indexer/index.d.ts +2 -0
- package/dist/indexer/index.d.ts.map +1 -0
- package/dist/indexer/index.js +2 -0
- package/dist/indexer/index.js.map +1 -0
- package/dist/mcp/index.d.ts +2 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +2 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/server.d.ts +233 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +495 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/service/index.d.ts +2 -0
- package/dist/service/index.d.ts.map +1 -0
- package/dist/service/index.js +2 -0
- package/dist/service/index.js.map +1 -0
- package/dist/service/memory-service.d.ts +203 -0
- package/dist/service/memory-service.d.ts.map +1 -0
- package/dist/service/memory-service.js +891 -0
- package/dist/service/memory-service.js.map +1 -0
- package/dist/util/path-safety.d.ts +94 -0
- package/dist/util/path-safety.d.ts.map +1 -0
- package/dist/util/path-safety.js +162 -0
- package/dist/util/path-safety.js.map +1 -0
- package/dist/util/text.d.ts +9 -0
- package/dist/util/text.d.ts.map +1 -0
- package/dist/util/text.js +19 -0
- package/dist/util/text.js.map +1 -0
- package/package.json +87 -4
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import crypto from "node:crypto";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import Database from "better-sqlite3";
|
|
7
|
+
import { assertDocPath } from "../util/path-safety.js";
|
|
8
|
+
export const SCHEMA_SQL = `-- Muninn Engine Database Schema
|
|
9
|
+
-- Version 1.0.0
|
|
10
|
+
|
|
11
|
+
-- Projects table
|
|
12
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
13
|
+
id TEXT PRIMARY KEY,
|
|
14
|
+
name TEXT NOT NULL,
|
|
15
|
+
git_remote TEXT,
|
|
16
|
+
root_path TEXT NOT NULL UNIQUE,
|
|
17
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
CREATE INDEX IF NOT EXISTS idx_projects_root_path ON projects(root_path);
|
|
21
|
+
|
|
22
|
+
-- Observations table
|
|
23
|
+
CREATE TABLE IF NOT EXISTS observations (
|
|
24
|
+
id TEXT PRIMARY KEY,
|
|
25
|
+
project_id TEXT NOT NULL,
|
|
26
|
+
category TEXT CHECK(category IN ('decision', 'convention', 'discovery', 'bugfix', 'architecture')) NOT NULL,
|
|
27
|
+
title TEXT NOT NULL,
|
|
28
|
+
content TEXT NOT NULL,
|
|
29
|
+
topic_key TEXT,
|
|
30
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
31
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
32
|
+
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
CREATE INDEX IF NOT EXISTS idx_observations_project_id ON observations(project_id);
|
|
36
|
+
CREATE INDEX IF NOT EXISTS idx_observations_updated_at ON observations(updated_at);
|
|
37
|
+
CREATE INDEX IF NOT EXISTS idx_observations_project_updated ON observations(project_id, updated_at DESC, created_at DESC);
|
|
38
|
+
|
|
39
|
+
-- Observations FTS5 virtual table
|
|
40
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS observations_fts USING fts5(
|
|
41
|
+
title,
|
|
42
|
+
content,
|
|
43
|
+
topic_key,
|
|
44
|
+
content='observations',
|
|
45
|
+
content_rowid='rowid'
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
-- Triggers for automatic FTS5 synchronization
|
|
49
|
+
CREATE TRIGGER IF NOT EXISTS obs_ai AFTER INSERT ON observations BEGIN
|
|
50
|
+
INSERT INTO observations_fts(rowid, title, content, topic_key)
|
|
51
|
+
VALUES (new.rowid, new.title, new.content, new.topic_key);
|
|
52
|
+
END;
|
|
53
|
+
|
|
54
|
+
CREATE TRIGGER IF NOT EXISTS obs_ad AFTER DELETE ON observations BEGIN
|
|
55
|
+
INSERT INTO observations_fts(observations_fts, rowid, title, content, topic_key)
|
|
56
|
+
VALUES ('delete', old.rowid, old.title, old.content, old.topic_key);
|
|
57
|
+
END;
|
|
58
|
+
|
|
59
|
+
CREATE TRIGGER IF NOT EXISTS obs_au AFTER UPDATE ON observations BEGIN
|
|
60
|
+
INSERT INTO observations_fts(observations_fts, rowid, title, content, topic_key)
|
|
61
|
+
VALUES ('delete', old.rowid, old.title, old.content, old.topic_key);
|
|
62
|
+
INSERT INTO observations_fts(rowid, title, content, topic_key)
|
|
63
|
+
VALUES (new.rowid, new.title, new.content, new.topic_key);
|
|
64
|
+
END;
|
|
65
|
+
|
|
66
|
+
-- Entities table
|
|
67
|
+
CREATE TABLE IF NOT EXISTS entities (
|
|
68
|
+
id TEXT PRIMARY KEY,
|
|
69
|
+
project_id TEXT NOT NULL,
|
|
70
|
+
entity_type TEXT CHECK(entity_type IN ('file', 'function', 'class', 'interface', 'module')) NOT NULL,
|
|
71
|
+
identifier TEXT NOT NULL,
|
|
72
|
+
file_path TEXT NOT NULL,
|
|
73
|
+
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
CREATE INDEX IF NOT EXISTS idx_entities_project_id ON entities(project_id);
|
|
77
|
+
CREATE INDEX IF NOT EXISTS idx_entities_identifier ON entities(identifier);
|
|
78
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_entities_project_identifier ON entities(project_id, identifier);
|
|
79
|
+
|
|
80
|
+
-- Observation Entities join table
|
|
81
|
+
CREATE TABLE IF NOT EXISTS observation_entities (
|
|
82
|
+
observation_id TEXT NOT NULL,
|
|
83
|
+
entity_id TEXT NOT NULL,
|
|
84
|
+
PRIMARY KEY(observation_id, entity_id),
|
|
85
|
+
FOREIGN KEY(observation_id) REFERENCES observations(id) ON DELETE CASCADE,
|
|
86
|
+
FOREIGN KEY(entity_id) REFERENCES entities(id) ON DELETE CASCADE
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
CREATE INDEX IF NOT EXISTS idx_observation_entities_entity_id ON observation_entities(entity_id);
|
|
90
|
+
|
|
91
|
+
-- Entity Dependencies table
|
|
92
|
+
-- Relaciones topológicas entre símbolos (A llama/importa a B, o A implementa a B)
|
|
93
|
+
CREATE TABLE IF NOT EXISTS entity_dependencies (
|
|
94
|
+
source_entity_id TEXT NOT NULL,
|
|
95
|
+
target_entity_id TEXT NOT NULL,
|
|
96
|
+
relation_type TEXT CHECK(relation_type IN ('imports', 'calls', 'implements', 'extends', 'references')) NOT NULL,
|
|
97
|
+
PRIMARY KEY(source_entity_id, target_entity_id, relation_type),
|
|
98
|
+
FOREIGN KEY(source_entity_id) REFERENCES entities(id) ON DELETE CASCADE,
|
|
99
|
+
FOREIGN KEY(target_entity_id) REFERENCES entities(id) ON DELETE CASCADE
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
CREATE INDEX IF NOT EXISTS idx_entity_deps_source ON entity_dependencies(source_entity_id);
|
|
103
|
+
CREATE INDEX IF NOT EXISTS idx_entity_deps_target ON entity_dependencies(target_entity_id);
|
|
104
|
+
`;
|
|
105
|
+
/**
|
|
106
|
+
* Robustly load schema SQL from disk if available, otherwise fallback to embedded SCHEMA_SQL.
|
|
107
|
+
*/
|
|
108
|
+
export function loadSchemaSql() {
|
|
109
|
+
try {
|
|
110
|
+
const currentDir = typeof __dirname !== "undefined"
|
|
111
|
+
? __dirname
|
|
112
|
+
: path.dirname(fileURLToPath(import.meta.url));
|
|
113
|
+
const schemaFile = path.join(currentDir, "schema.sql");
|
|
114
|
+
if (fs.existsSync(schemaFile)) {
|
|
115
|
+
return fs.readFileSync(schemaFile, "utf-8");
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
// Ignore and fallback to embedded schema
|
|
120
|
+
}
|
|
121
|
+
return SCHEMA_SQL;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Strips embedded basic auth credentials (e.g. https://user:pass@host/... -> https://host/...)
|
|
125
|
+
* from a git remote URL before saving git_remote.
|
|
126
|
+
*/
|
|
127
|
+
export function sanitizeGitRemote(rawUrl) {
|
|
128
|
+
try {
|
|
129
|
+
const parsed = new URL(rawUrl);
|
|
130
|
+
if (parsed.username || parsed.password) {
|
|
131
|
+
if (parsed.protocol === "ssh:" && !parsed.password) {
|
|
132
|
+
return rawUrl;
|
|
133
|
+
}
|
|
134
|
+
parsed.username = "";
|
|
135
|
+
parsed.password = "";
|
|
136
|
+
return parsed.toString();
|
|
137
|
+
}
|
|
138
|
+
return rawUrl;
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
// Regex fallback for non-standard URLs or git SCP-like syntax
|
|
142
|
+
return rawUrl.replace(/^([a-zA-Z+-]+:\/\/)[^/@]+@/, "$1");
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Searches upward from startDir for the nearest directory or parent containing a .git folder or file.
|
|
147
|
+
*/
|
|
148
|
+
export function findGitRoot(startDir = process.cwd()) {
|
|
149
|
+
let current = path.resolve(startDir);
|
|
150
|
+
while (true) {
|
|
151
|
+
const gitPath = path.join(current, ".git");
|
|
152
|
+
if (fs.existsSync(gitPath)) {
|
|
153
|
+
return current;
|
|
154
|
+
}
|
|
155
|
+
const parent = path.dirname(current);
|
|
156
|
+
if (parent === current) {
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
current = parent;
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Inspects a git repository root to extract the default origin remote URL if present.
|
|
165
|
+
*/
|
|
166
|
+
export function tryGetGitRemote(gitRoot) {
|
|
167
|
+
try {
|
|
168
|
+
const configPath = path.join(gitRoot, ".git", "config");
|
|
169
|
+
if (fs.existsSync(configPath)) {
|
|
170
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
171
|
+
const match = content.match(/\[remote "origin"\][^\[]*url\s*=\s*([^\n\r]+)/);
|
|
172
|
+
if (match && match[1]) {
|
|
173
|
+
return sanitizeGitRemote(match[1].trim());
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
// Ignore error
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Resolves the database path for Muninn.
|
|
184
|
+
* - If customPath is given, return it (e.g. ':memory:' or specific path).
|
|
185
|
+
* - Check if cwd (or startDir) or any parent directory has .git. If so, resolve to `<git_root>/.huginn/muninn.db`.
|
|
186
|
+
* - Fallback to `join(homedir(), '.huginn', 'muninn.db')`.
|
|
187
|
+
*/
|
|
188
|
+
export function resolveDatabasePath(customPath, startDir = process.cwd()) {
|
|
189
|
+
if (customPath !== undefined && customPath !== "") {
|
|
190
|
+
return customPath;
|
|
191
|
+
}
|
|
192
|
+
const gitRoot = findGitRoot(startDir);
|
|
193
|
+
if (gitRoot) {
|
|
194
|
+
return path.join(gitRoot, ".huginn", "muninn.db");
|
|
195
|
+
}
|
|
196
|
+
return path.join(os.homedir(), ".huginn", "muninn.db");
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Initializes and returns a SQLite database instance using better-sqlite3.
|
|
200
|
+
* Configures foreign_keys = ON, journal_mode = WAL, busy_timeout = 5000, recursive_triggers = ON, and executes schema.sql DDL.
|
|
201
|
+
*/
|
|
202
|
+
export function getDatabase(dbPath, startDir) {
|
|
203
|
+
// The database and the project record must come from ONE root (ADR-49): the
|
|
204
|
+
// caller passes the same directory it hands to `ensureProject`.
|
|
205
|
+
const resolvedPath = resolveDatabasePath(dbPath, startDir);
|
|
206
|
+
if (resolvedPath !== ":memory:") {
|
|
207
|
+
// A cloned repository can ship `.huginn` — or the database file itself — as a
|
|
208
|
+
// symlink and redirect the SQLite write outside the project (SEC-005). The
|
|
209
|
+
// path is screened exactly like the harness state, the receipts and the
|
|
210
|
+
// promotion backup; the lexical path is kept for the open so callers see the
|
|
211
|
+
// path they named.
|
|
212
|
+
assertDocPath(resolvedPath, {
|
|
213
|
+
projectPath: findGitRoot(startDir ?? process.cwd()) ?? undefined,
|
|
214
|
+
label: "the Muninn database",
|
|
215
|
+
action: "write",
|
|
216
|
+
});
|
|
217
|
+
const dir = path.dirname(resolvedPath);
|
|
218
|
+
if (!fs.existsSync(dir)) {
|
|
219
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const db = new Database(resolvedPath, { timeout: 5000 });
|
|
223
|
+
try {
|
|
224
|
+
db.pragma("foreign_keys = ON");
|
|
225
|
+
db.pragma("journal_mode = WAL");
|
|
226
|
+
db.pragma("busy_timeout = 5000");
|
|
227
|
+
db.pragma("recursive_triggers = ON");
|
|
228
|
+
const schema = loadSchemaSql();
|
|
229
|
+
db.exec(schema);
|
|
230
|
+
}
|
|
231
|
+
catch (err) {
|
|
232
|
+
db.close();
|
|
233
|
+
throw err;
|
|
234
|
+
}
|
|
235
|
+
return db;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Finds an existing project by root_path or creates a new project record idempotently.
|
|
239
|
+
*/
|
|
240
|
+
export function ensureProject(db, options) {
|
|
241
|
+
// Same fallback root as the database resolution (ADR-49): when no explicit
|
|
242
|
+
// root is given, both derive from `startDir` (or the process cwd).
|
|
243
|
+
const base = options?.startDir ? path.resolve(options.startDir) : process.cwd();
|
|
244
|
+
const rootPath = options?.rootPath ? path.resolve(options.rootPath) : (findGitRoot(base) ?? base);
|
|
245
|
+
const existing = db
|
|
246
|
+
.prepare("SELECT id, name, git_remote, root_path, created_at FROM projects WHERE root_path = ?")
|
|
247
|
+
.get(rootPath);
|
|
248
|
+
if (existing) {
|
|
249
|
+
return existing;
|
|
250
|
+
}
|
|
251
|
+
const name = options?.name ?? (path.basename(rootPath) || "project");
|
|
252
|
+
const rawRemote = options?.gitRemote ?? tryGetGitRemote(rootPath) ?? null;
|
|
253
|
+
const gitRemote = rawRemote ? sanitizeGitRemote(rawRemote) : null;
|
|
254
|
+
const id = crypto.randomUUID();
|
|
255
|
+
db.prepare("INSERT INTO projects (id, name, git_remote, root_path) VALUES (?, ?, ?, ?)").run(id, name, gitRemote, rootPath);
|
|
256
|
+
const created = db
|
|
257
|
+
.prepare("SELECT id, name, git_remote, root_path, created_at FROM projects WHERE id = ?")
|
|
258
|
+
.get(id);
|
|
259
|
+
if (!created) {
|
|
260
|
+
throw new Error(`Failed to create project record for rootPath: ${rootPath}`);
|
|
261
|
+
}
|
|
262
|
+
return created;
|
|
263
|
+
}
|
|
264
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/db/client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
-- Muninn Engine Database Schema
|
|
2
|
+
-- Version 1.0.0
|
|
3
|
+
|
|
4
|
+
-- Projects table
|
|
5
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
6
|
+
id TEXT PRIMARY KEY,
|
|
7
|
+
name TEXT NOT NULL,
|
|
8
|
+
git_remote TEXT,
|
|
9
|
+
root_path TEXT NOT NULL UNIQUE,
|
|
10
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
CREATE INDEX IF NOT EXISTS idx_projects_root_path ON projects(root_path);
|
|
14
|
+
|
|
15
|
+
-- Observations table
|
|
16
|
+
CREATE TABLE IF NOT EXISTS observations (
|
|
17
|
+
id TEXT PRIMARY KEY,
|
|
18
|
+
project_id TEXT NOT NULL,
|
|
19
|
+
category TEXT CHECK(category IN ('decision', 'convention', 'discovery', 'bugfix', 'architecture')) NOT NULL,
|
|
20
|
+
title TEXT NOT NULL,
|
|
21
|
+
content TEXT NOT NULL,
|
|
22
|
+
topic_key TEXT,
|
|
23
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
24
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
25
|
+
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
CREATE INDEX IF NOT EXISTS idx_observations_project_id ON observations(project_id);
|
|
29
|
+
CREATE INDEX IF NOT EXISTS idx_observations_updated_at ON observations(updated_at);
|
|
30
|
+
CREATE INDEX IF NOT EXISTS idx_observations_project_updated ON observations(project_id, updated_at DESC, created_at DESC);
|
|
31
|
+
|
|
32
|
+
-- Observations FTS5 virtual table
|
|
33
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS observations_fts USING fts5(
|
|
34
|
+
title,
|
|
35
|
+
content,
|
|
36
|
+
topic_key,
|
|
37
|
+
content='observations',
|
|
38
|
+
content_rowid='rowid'
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
-- Triggers for automatic FTS5 synchronization
|
|
42
|
+
CREATE TRIGGER IF NOT EXISTS obs_ai AFTER INSERT ON observations BEGIN
|
|
43
|
+
INSERT INTO observations_fts(rowid, title, content, topic_key)
|
|
44
|
+
VALUES (new.rowid, new.title, new.content, new.topic_key);
|
|
45
|
+
END;
|
|
46
|
+
|
|
47
|
+
CREATE TRIGGER IF NOT EXISTS obs_ad AFTER DELETE ON observations BEGIN
|
|
48
|
+
INSERT INTO observations_fts(observations_fts, rowid, title, content, topic_key)
|
|
49
|
+
VALUES ('delete', old.rowid, old.title, old.content, old.topic_key);
|
|
50
|
+
END;
|
|
51
|
+
|
|
52
|
+
CREATE TRIGGER IF NOT EXISTS obs_au AFTER UPDATE ON observations BEGIN
|
|
53
|
+
INSERT INTO observations_fts(observations_fts, rowid, title, content, topic_key)
|
|
54
|
+
VALUES ('delete', old.rowid, old.title, old.content, old.topic_key);
|
|
55
|
+
INSERT INTO observations_fts(rowid, title, content, topic_key)
|
|
56
|
+
VALUES (new.rowid, new.title, new.content, new.topic_key);
|
|
57
|
+
END;
|
|
58
|
+
|
|
59
|
+
-- Entities table
|
|
60
|
+
CREATE TABLE IF NOT EXISTS entities (
|
|
61
|
+
id TEXT PRIMARY KEY,
|
|
62
|
+
project_id TEXT NOT NULL,
|
|
63
|
+
entity_type TEXT CHECK(entity_type IN ('file', 'function', 'class', 'interface', 'module')) NOT NULL,
|
|
64
|
+
identifier TEXT NOT NULL,
|
|
65
|
+
file_path TEXT NOT NULL,
|
|
66
|
+
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
CREATE INDEX IF NOT EXISTS idx_entities_project_id ON entities(project_id);
|
|
70
|
+
CREATE INDEX IF NOT EXISTS idx_entities_identifier ON entities(identifier);
|
|
71
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_entities_project_identifier ON entities(project_id, identifier);
|
|
72
|
+
|
|
73
|
+
-- Observation Entities join table
|
|
74
|
+
CREATE TABLE IF NOT EXISTS observation_entities (
|
|
75
|
+
observation_id TEXT NOT NULL,
|
|
76
|
+
entity_id TEXT NOT NULL,
|
|
77
|
+
PRIMARY KEY(observation_id, entity_id),
|
|
78
|
+
FOREIGN KEY(observation_id) REFERENCES observations(id) ON DELETE CASCADE,
|
|
79
|
+
FOREIGN KEY(entity_id) REFERENCES entities(id) ON DELETE CASCADE
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
CREATE INDEX IF NOT EXISTS idx_observation_entities_entity_id ON observation_entities(entity_id);
|
|
83
|
+
|
|
84
|
+
-- Entity Dependencies table
|
|
85
|
+
-- Relaciones topológicas entre símbolos (A llama/importa a B, o A implementa a B)
|
|
86
|
+
CREATE TABLE IF NOT EXISTS entity_dependencies (
|
|
87
|
+
source_entity_id TEXT NOT NULL,
|
|
88
|
+
target_entity_id TEXT NOT NULL,
|
|
89
|
+
relation_type TEXT CHECK(relation_type IN ('imports', 'calls', 'implements', 'extends', 'references')) NOT NULL,
|
|
90
|
+
PRIMARY KEY(source_entity_id, target_entity_id, relation_type),
|
|
91
|
+
FOREIGN KEY(source_entity_id) REFERENCES entities(id) ON DELETE CASCADE,
|
|
92
|
+
FOREIGN KEY(target_entity_id) REFERENCES entities(id) ON DELETE CASCADE
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
CREATE INDEX IF NOT EXISTS idx_entity_deps_source ON entity_dependencies(source_entity_id);
|
|
96
|
+
CREATE INDEX IF NOT EXISTS idx_entity_deps_target ON entity_dependencies(target_entity_id);
|
|
97
|
+
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public API of the Muninn memory engine.
|
|
3
|
+
*
|
|
4
|
+
* Everything below is safe to import from a Node or Bun process:
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* import { MemoryService, indexFilesIntoMuninn } from "@dahch/muninn";
|
|
8
|
+
*
|
|
9
|
+
* const memory = new MemoryService({ projectRoot: process.cwd() });
|
|
10
|
+
* indexFilesIntoMuninn(memory, ["src/app.ts"], { projectRoot: process.cwd() });
|
|
11
|
+
* console.log(memory.search({ query: "auth", limit: 5 }));
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* Subpath entry points (`@dahch/muninn/db`, `/service`, `/indexer`, `/mcp`,
|
|
15
|
+
* `/contracts`, `/util`) expose the same modules without the barrel.
|
|
16
|
+
*/
|
|
17
|
+
export * from "./db/client.js";
|
|
18
|
+
export * from "./service/index.js";
|
|
19
|
+
export * from "./indexer/index.js";
|
|
20
|
+
export * from "./mcp/index.js";
|
|
21
|
+
export * from "./contracts/index.js";
|
|
22
|
+
export * from "./util/text.js";
|
|
23
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,cAAc,gBAAgB,CAAC;AAC/B,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,sBAAsB,CAAC;AACrC,cAAc,gBAAgB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public API of the Muninn memory engine.
|
|
3
|
+
*
|
|
4
|
+
* Everything below is safe to import from a Node or Bun process:
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* import { MemoryService, indexFilesIntoMuninn } from "@dahch/muninn";
|
|
8
|
+
*
|
|
9
|
+
* const memory = new MemoryService({ projectRoot: process.cwd() });
|
|
10
|
+
* indexFilesIntoMuninn(memory, ["src/app.ts"], { projectRoot: process.cwd() });
|
|
11
|
+
* console.log(memory.search({ query: "auth", limit: 5 }));
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* Subpath entry points (`@dahch/muninn/db`, `/service`, `/indexer`, `/mcp`,
|
|
15
|
+
* `/contracts`, `/util`) expose the same modules without the barrel.
|
|
16
|
+
*/
|
|
17
|
+
export * from "./db/client.js";
|
|
18
|
+
export * from "./service/index.js";
|
|
19
|
+
export * from "./indexer/index.js";
|
|
20
|
+
export * from "./mcp/index.js";
|
|
21
|
+
export * from "./contracts/index.js";
|
|
22
|
+
export * from "./util/text.js";
|
|
23
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,cAAc,gBAAgB,CAAC;AAC/B,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,sBAAsB,CAAC;AACrC,cAAc,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { EntityType, DependencyRelationType } from "../db/client.js";
|
|
2
|
+
import type { IMemoryService } from "../service/memory-service.js";
|
|
3
|
+
export interface ExtractedDependency {
|
|
4
|
+
sourceIdentifier: string;
|
|
5
|
+
targetIdentifier: string;
|
|
6
|
+
relationType: DependencyRelationType;
|
|
7
|
+
}
|
|
8
|
+
export interface ExtractedSymbol {
|
|
9
|
+
name: string;
|
|
10
|
+
identifier: string;
|
|
11
|
+
filePath: string;
|
|
12
|
+
entityType: EntityType;
|
|
13
|
+
startLine: number;
|
|
14
|
+
endLine: number;
|
|
15
|
+
isExported: boolean;
|
|
16
|
+
dependencies?: ExtractedDependency[];
|
|
17
|
+
}
|
|
18
|
+
export interface AstExtractionResult {
|
|
19
|
+
filePath: string;
|
|
20
|
+
symbols: ExtractedSymbol[];
|
|
21
|
+
dependencies: ExtractedDependency[];
|
|
22
|
+
}
|
|
23
|
+
export interface IndexSummary {
|
|
24
|
+
indexedFiles: number;
|
|
25
|
+
indexedSymbols: number;
|
|
26
|
+
indexedDependencies: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Normalizes file path to forward slashes.
|
|
30
|
+
*/
|
|
31
|
+
export declare function normalizePath(p: string): string;
|
|
32
|
+
/**
|
|
33
|
+
* Normalizes relative module specifiers against the importing file's directory.
|
|
34
|
+
* Maps .js, .mjs, .cjs to .ts, and .jsx to .tsx, while keeping extensionless specifiers bare.
|
|
35
|
+
*/
|
|
36
|
+
export declare function normalizeModuleSpecifier(filePath: string, specifier: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* Extracts symbols and dependencies from TypeScript / JavaScript source code.
|
|
39
|
+
*/
|
|
40
|
+
export declare function extractSymbolsFromSource(filePath: string, sourceText: string): ExtractedSymbol[];
|
|
41
|
+
/**
|
|
42
|
+
* Comprehensive AST traversal returning both symbols and inter-symbol dependencies.
|
|
43
|
+
*/
|
|
44
|
+
export declare function extractAstData(filePath: string, sourceText: string): AstExtractionResult;
|
|
45
|
+
/**
|
|
46
|
+
* Indexes a collection of files into Muninn's entities and entity_dependencies tables.
|
|
47
|
+
*
|
|
48
|
+
* Execution is cleanly separated into two stages (REV-001):
|
|
49
|
+
* 1. Synchronous file reading, filtering, validation, and AST parsing are performed in-memory outside any database transaction.
|
|
50
|
+
* 2. SQLite batch operations are executed inside a short-lived transaction with zombie entity pruning (REV-003, SEC-003).
|
|
51
|
+
*/
|
|
52
|
+
export declare function indexFilesIntoMuninn(memoryService: IMemoryService, filePaths: string[], options?: {
|
|
53
|
+
projectRoot?: string;
|
|
54
|
+
}): IndexSummary;
|
|
55
|
+
//# sourceMappingURL=ast-indexer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ast-indexer.d.ts","sourceRoot":"","sources":["../../src/indexer/ast-indexer.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,UAAU,EACV,sBAAsB,EAEvB,MAAM,iBAAiB,CAAC;AACzB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAEnE,MAAM,WAAW,mBAAmB;IAClC,gBAAgB,EAAE,MAAM,CAAC;IACzB,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,sBAAsB,CAAC;CACtC;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,UAAU,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,OAAO,CAAC;IACpB,YAAY,CAAC,EAAE,mBAAmB,EAAE,CAAC;CACtC;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,YAAY,EAAE,mBAAmB,EAAE,CAAC;CACrC;AAED,MAAM,WAAW,YAAY;IAC3B,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,mBAAmB,EAAE,MAAM,CAAC;CAC7B;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,GAChB,MAAM,CAiBR;AAED;;GAEG;AACH,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,GACjB,eAAe,EAAE,CAGnB;AAED;;GAEG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,GACjB,mBAAmB,CAyQrB;AAmBD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,aAAa,EAAE,cAAc,EAC7B,SAAS,EAAE,MAAM,EAAE,EACnB,OAAO,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,GACjC,YAAY,CAwOd"}
|