septum 0.1.0 → 0.1.2
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 -21
- package/README.md +100 -100
- package/bin/septum.ts +4 -4
- package/package.json +63 -62
- package/src/cli/commands/check.ts +161 -161
- package/src/cli/commands/hook.ts +188 -188
- package/src/cli/commands/ingest.ts +29 -29
- package/src/cli/commands/init.ts +211 -211
- package/src/cli/commands/locate.ts +61 -61
- package/src/cli/commands/query.ts +64 -64
- package/src/cli/commands/serve.ts +5 -5
- package/src/cli/commands/slice.ts +90 -90
- package/src/cli/commands/sync.ts +31 -31
- package/src/cli/index.ts +199 -178
- package/src/cli/templates/hooks.ts +75 -75
- package/src/cli/templates/rules.ts +38 -38
- package/src/cli/templates/skill.ts +66 -66
- package/src/core/boundary/evaluator.ts +210 -210
- package/src/core/config/loader.ts +84 -84
- package/src/core/config/schema.ts +39 -39
- package/src/core/database/client.ts +44 -132
- package/src/core/database/repositories/dependency.repository.ts +279 -279
- package/src/core/database/repositories/domain.repository.ts +198 -198
- package/src/core/database/repositories/file.repository.ts +99 -99
- package/src/core/database/repositories/meta.repository.ts +33 -33
- package/src/core/database/repositories/symbol.repository.ts +424 -424
- package/src/core/database/repository.ts +242 -242
- package/src/core/database/schema.ts +120 -0
- package/src/core/discovery/topology-detector.ts +418 -418
- package/src/core/ingestion/hasher.ts +5 -5
- package/src/core/ingestion/pipeline.ts +438 -438
- package/src/core/parser/boundary-tracker.ts +128 -128
- package/src/core/parser/extractors/base.ts +60 -60
- package/src/core/parser/extractors/frontend.ts +194 -194
- package/src/core/parser/extractors/go.ts +176 -176
- package/src/core/parser/extractors/laravel-semantic.ts +426 -426
- package/src/core/parser/extractors/mcp-cli-semantic.ts +199 -199
- package/src/core/parser/extractors/nestjs-semantic.ts +119 -119
- package/src/core/parser/extractors/php.ts +149 -149
- package/src/core/parser/extractors/python.ts +226 -226
- package/src/core/parser/extractors/registry.ts +49 -49
- package/src/core/parser/extractors/semantic-extractor.interface.ts +21 -21
- package/src/core/parser/extractors/typescript.ts +177 -177
- package/src/core/parser/tree-sitter.ts +39 -39
- package/src/core/resolver/call-graph-tracer.ts +668 -668
- package/src/core/resolver/module-resolver.ts +342 -323
- package/src/core/resolver/symbol-locator.ts +543 -405
- package/src/core/resolver/vertical-slice-tracer.ts +237 -236
- package/src/core/session/session-manager.ts +56 -56
- package/src/core/telemetry/telemetry.ts +86 -86
- package/src/index.ts +18 -18
- package/src/mcp/schemas.ts +68 -68
- package/src/mcp/server.ts +612 -602
- package/src/mcp/tools/check-boundary.ts +102 -102
- package/src/mcp/tools/get-domain-catalog.ts +91 -91
- package/src/mcp/tools/get-feature-context.ts +116 -116
- package/src/mcp/tools/get-symbol-hotspots.ts +49 -49
- package/src/mcp/tools/get-symbol-impact.ts +26 -26
- package/src/mcp/tools/get-symbol.ts +63 -63
- package/src/mcp/tools/locate-symbol.ts +74 -74
- package/src/mcp/tools/register-domain.ts +64 -64
- package/src/mcp/tools/trace-vertical-slice.ts +102 -102
- package/src/types/index.ts +296 -296
- package/src/version.ts +3 -0
- package/src/core/database/schema.sql +0 -92
|
@@ -1,198 +1,198 @@
|
|
|
1
|
-
import type { Database } from "bun:sqlite";
|
|
2
|
-
import type {
|
|
3
|
-
DomainCatalogResponse,
|
|
4
|
-
DomainConfig,
|
|
5
|
-
DomainRecord,
|
|
6
|
-
FileRecord,
|
|
7
|
-
SymbolKind,
|
|
8
|
-
SymbolRecord,
|
|
9
|
-
} from "../../../types/index.ts";
|
|
10
|
-
|
|
11
|
-
export class DomainRepository {
|
|
12
|
-
constructor(private db: Database) {}
|
|
13
|
-
|
|
14
|
-
public upsertDomain(name: string, config: DomainConfig): number {
|
|
15
|
-
const normalizedName = name.trim();
|
|
16
|
-
const existing = this.getDomainByName(normalizedName);
|
|
17
|
-
|
|
18
|
-
// Merge dependencies and archetypes if existing and incoming is empty
|
|
19
|
-
let allowed = config.allowed_dependencies ?? [];
|
|
20
|
-
let forbidden = config.forbidden_dependencies ?? [];
|
|
21
|
-
let archetypes = config.archetypes ?? {};
|
|
22
|
-
|
|
23
|
-
if (existing) {
|
|
24
|
-
if (allowed.length === 0 && existing.allowed_deps_json && existing.allowed_deps_json !== "[]") {
|
|
25
|
-
try {
|
|
26
|
-
allowed = JSON.parse(existing.allowed_deps_json);
|
|
27
|
-
} catch {}
|
|
28
|
-
}
|
|
29
|
-
if (forbidden.length === 0 && existing.forbidden_deps_json && existing.forbidden_deps_json !== "[]") {
|
|
30
|
-
try {
|
|
31
|
-
forbidden = JSON.parse(existing.forbidden_deps_json);
|
|
32
|
-
} catch {}
|
|
33
|
-
}
|
|
34
|
-
if (Object.keys(archetypes).length === 0 && existing.archetypes_json && existing.archetypes_json !== "{}") {
|
|
35
|
-
try {
|
|
36
|
-
archetypes = JSON.parse(existing.archetypes_json);
|
|
37
|
-
} catch {}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const allowedJson = JSON.stringify(allowed);
|
|
42
|
-
const forbiddenJson = JSON.stringify(forbidden);
|
|
43
|
-
const archetypesJson = JSON.stringify(archetypes);
|
|
44
|
-
|
|
45
|
-
const query = `
|
|
46
|
-
INSERT INTO domains (name, root_path, allowed_deps_json, forbidden_deps_json, archetypes_json)
|
|
47
|
-
VALUES (?, ?, ?, ?, ?)
|
|
48
|
-
ON CONFLICT(name) DO UPDATE SET
|
|
49
|
-
root_path = excluded.root_path,
|
|
50
|
-
allowed_deps_json = excluded.allowed_deps_json,
|
|
51
|
-
forbidden_deps_json = excluded.forbidden_deps_json,
|
|
52
|
-
archetypes_json = excluded.archetypes_json,
|
|
53
|
-
updated_at = CURRENT_TIMESTAMP
|
|
54
|
-
`;
|
|
55
|
-
this.db.query(query).run(normalizedName, config.root, allowedJson, forbiddenJson, archetypesJson);
|
|
56
|
-
|
|
57
|
-
const record = this.getDomainByName(normalizedName);
|
|
58
|
-
return record?.id ?? 0;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
public getDomainByName(name: string): DomainRecord | null {
|
|
62
|
-
return this.db
|
|
63
|
-
.query<DomainRecord, [string]>("SELECT * FROM domains WHERE LOWER(name) = LOWER(?)")
|
|
64
|
-
.get(name.trim());
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
public getAllDomains(): DomainRecord[] {
|
|
68
|
-
return this.db.query<DomainRecord, []>("SELECT * FROM domains ORDER BY name ASC").all();
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
public getDomainCatalog(
|
|
72
|
-
domainName: string,
|
|
73
|
-
archetypeFilter?: string
|
|
74
|
-
): DomainCatalogResponse | null {
|
|
75
|
-
const domain = this.getDomainByName(domainName);
|
|
76
|
-
if (!domain) return null;
|
|
77
|
-
|
|
78
|
-
const allowedDeps: string[] = JSON.parse(domain.allowed_deps_json || "[]");
|
|
79
|
-
const forbiddenDeps: string[] = JSON.parse(domain.forbidden_deps_json || "[]");
|
|
80
|
-
|
|
81
|
-
let filesQuery = "SELECT * FROM files WHERE domain_id = ?";
|
|
82
|
-
const params: [number, string?] = [domain.id];
|
|
83
|
-
|
|
84
|
-
if (archetypeFilter) {
|
|
85
|
-
filesQuery += " AND archetype = ?";
|
|
86
|
-
params.push(archetypeFilter);
|
|
87
|
-
}
|
|
88
|
-
filesQuery += " ORDER BY path ASC";
|
|
89
|
-
|
|
90
|
-
const files = this.db.query<FileRecord, any>(filesQuery).all(...params);
|
|
91
|
-
|
|
92
|
-
const catalogFiles: DomainCatalogResponse["files"] = [];
|
|
93
|
-
|
|
94
|
-
for (const file of files) {
|
|
95
|
-
const symbols = this.db
|
|
96
|
-
.query<SymbolRecord, [number]>(
|
|
97
|
-
"SELECT * FROM symbols WHERE file_id = ? ORDER BY line_start ASC"
|
|
98
|
-
)
|
|
99
|
-
.all(file.id);
|
|
100
|
-
|
|
101
|
-
const symbolMap = new Map<
|
|
102
|
-
string,
|
|
103
|
-
{
|
|
104
|
-
name: string;
|
|
105
|
-
kind: SymbolKind;
|
|
106
|
-
properties: string[];
|
|
107
|
-
methods: string[];
|
|
108
|
-
signatures: string[];
|
|
109
|
-
}
|
|
110
|
-
>();
|
|
111
|
-
|
|
112
|
-
for (const sym of symbols) {
|
|
113
|
-
if (
|
|
114
|
-
sym.kind === "class" ||
|
|
115
|
-
sym.kind === "interface" ||
|
|
116
|
-
sym.kind === "trait" ||
|
|
117
|
-
sym.kind === "enum"
|
|
118
|
-
) {
|
|
119
|
-
if (!symbolMap.has(sym.name)) {
|
|
120
|
-
symbolMap.set(sym.name, {
|
|
121
|
-
name: sym.name,
|
|
122
|
-
kind: sym.kind,
|
|
123
|
-
properties: [],
|
|
124
|
-
methods: [],
|
|
125
|
-
signatures: [sym.signature],
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
} else if (sym.kind === "method") {
|
|
129
|
-
let targetContainer = Array.from(symbolMap.values()).pop();
|
|
130
|
-
if (sym.name.includes("::")) {
|
|
131
|
-
const containerName = sym.name.split("::")[0];
|
|
132
|
-
if (symbolMap.has(containerName)) {
|
|
133
|
-
targetContainer = symbolMap.get(containerName);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
if (targetContainer) {
|
|
137
|
-
targetContainer.methods.push(sym.signature);
|
|
138
|
-
} else {
|
|
139
|
-
symbolMap.set(sym.name, {
|
|
140
|
-
name: sym.name,
|
|
141
|
-
kind: sym.kind,
|
|
142
|
-
properties: [],
|
|
143
|
-
methods: [],
|
|
144
|
-
signatures: [sym.signature],
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
} else if (sym.kind === "property") {
|
|
148
|
-
let targetContainer = Array.from(symbolMap.values()).pop();
|
|
149
|
-
if (sym.name.includes("::")) {
|
|
150
|
-
const containerName = sym.name.split("::")[0];
|
|
151
|
-
if (symbolMap.has(containerName)) {
|
|
152
|
-
targetContainer = symbolMap.get(containerName);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
if (targetContainer) {
|
|
156
|
-
targetContainer.properties.push(sym.signature);
|
|
157
|
-
} else {
|
|
158
|
-
symbolMap.set(sym.name, {
|
|
159
|
-
name: sym.name,
|
|
160
|
-
kind: sym.kind,
|
|
161
|
-
properties: [sym.signature],
|
|
162
|
-
methods: [],
|
|
163
|
-
signatures: [sym.signature],
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
} else {
|
|
167
|
-
symbolMap.set(sym.name, {
|
|
168
|
-
name: sym.name,
|
|
169
|
-
kind: sym.kind,
|
|
170
|
-
properties: [],
|
|
171
|
-
methods: [],
|
|
172
|
-
signatures: [sym.signature],
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
catalogFiles.push({
|
|
178
|
-
path: file.path,
|
|
179
|
-
archetype: file.archetype,
|
|
180
|
-
symbols: Array.from(symbolMap.values()).map((s) => ({
|
|
181
|
-
name: s.name,
|
|
182
|
-
kind: s.kind,
|
|
183
|
-
properties: s.properties.length > 0 ? s.properties : undefined,
|
|
184
|
-
methods: s.methods.length > 0 ? s.methods : undefined,
|
|
185
|
-
signatures: s.signatures.length > 0 ? s.signatures : undefined,
|
|
186
|
-
})),
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
return {
|
|
191
|
-
domain: domain.name,
|
|
192
|
-
root: domain.root_path,
|
|
193
|
-
allowed_dependencies: allowedDeps,
|
|
194
|
-
forbidden_dependencies: forbiddenDeps,
|
|
195
|
-
files: catalogFiles,
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
}
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
import type {
|
|
3
|
+
DomainCatalogResponse,
|
|
4
|
+
DomainConfig,
|
|
5
|
+
DomainRecord,
|
|
6
|
+
FileRecord,
|
|
7
|
+
SymbolKind,
|
|
8
|
+
SymbolRecord,
|
|
9
|
+
} from "../../../types/index.ts";
|
|
10
|
+
|
|
11
|
+
export class DomainRepository {
|
|
12
|
+
constructor(private db: Database) {}
|
|
13
|
+
|
|
14
|
+
public upsertDomain(name: string, config: DomainConfig): number {
|
|
15
|
+
const normalizedName = name.trim();
|
|
16
|
+
const existing = this.getDomainByName(normalizedName);
|
|
17
|
+
|
|
18
|
+
// Merge dependencies and archetypes if existing and incoming is empty
|
|
19
|
+
let allowed = config.allowed_dependencies ?? [];
|
|
20
|
+
let forbidden = config.forbidden_dependencies ?? [];
|
|
21
|
+
let archetypes = config.archetypes ?? {};
|
|
22
|
+
|
|
23
|
+
if (existing) {
|
|
24
|
+
if (allowed.length === 0 && existing.allowed_deps_json && existing.allowed_deps_json !== "[]") {
|
|
25
|
+
try {
|
|
26
|
+
allowed = JSON.parse(existing.allowed_deps_json);
|
|
27
|
+
} catch {}
|
|
28
|
+
}
|
|
29
|
+
if (forbidden.length === 0 && existing.forbidden_deps_json && existing.forbidden_deps_json !== "[]") {
|
|
30
|
+
try {
|
|
31
|
+
forbidden = JSON.parse(existing.forbidden_deps_json);
|
|
32
|
+
} catch {}
|
|
33
|
+
}
|
|
34
|
+
if (Object.keys(archetypes).length === 0 && existing.archetypes_json && existing.archetypes_json !== "{}") {
|
|
35
|
+
try {
|
|
36
|
+
archetypes = JSON.parse(existing.archetypes_json);
|
|
37
|
+
} catch {}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const allowedJson = JSON.stringify(allowed);
|
|
42
|
+
const forbiddenJson = JSON.stringify(forbidden);
|
|
43
|
+
const archetypesJson = JSON.stringify(archetypes);
|
|
44
|
+
|
|
45
|
+
const query = `
|
|
46
|
+
INSERT INTO domains (name, root_path, allowed_deps_json, forbidden_deps_json, archetypes_json)
|
|
47
|
+
VALUES (?, ?, ?, ?, ?)
|
|
48
|
+
ON CONFLICT(name) DO UPDATE SET
|
|
49
|
+
root_path = excluded.root_path,
|
|
50
|
+
allowed_deps_json = excluded.allowed_deps_json,
|
|
51
|
+
forbidden_deps_json = excluded.forbidden_deps_json,
|
|
52
|
+
archetypes_json = excluded.archetypes_json,
|
|
53
|
+
updated_at = CURRENT_TIMESTAMP
|
|
54
|
+
`;
|
|
55
|
+
this.db.query(query).run(normalizedName, config.root, allowedJson, forbiddenJson, archetypesJson);
|
|
56
|
+
|
|
57
|
+
const record = this.getDomainByName(normalizedName);
|
|
58
|
+
return record?.id ?? 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public getDomainByName(name: string): DomainRecord | null {
|
|
62
|
+
return this.db
|
|
63
|
+
.query<DomainRecord, [string]>("SELECT * FROM domains WHERE LOWER(name) = LOWER(?)")
|
|
64
|
+
.get(name.trim());
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
public getAllDomains(): DomainRecord[] {
|
|
68
|
+
return this.db.query<DomainRecord, []>("SELECT * FROM domains ORDER BY name ASC").all();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
public getDomainCatalog(
|
|
72
|
+
domainName: string,
|
|
73
|
+
archetypeFilter?: string
|
|
74
|
+
): DomainCatalogResponse | null {
|
|
75
|
+
const domain = this.getDomainByName(domainName);
|
|
76
|
+
if (!domain) return null;
|
|
77
|
+
|
|
78
|
+
const allowedDeps: string[] = JSON.parse(domain.allowed_deps_json || "[]");
|
|
79
|
+
const forbiddenDeps: string[] = JSON.parse(domain.forbidden_deps_json || "[]");
|
|
80
|
+
|
|
81
|
+
let filesQuery = "SELECT * FROM files WHERE domain_id = ?";
|
|
82
|
+
const params: [number, string?] = [domain.id];
|
|
83
|
+
|
|
84
|
+
if (archetypeFilter) {
|
|
85
|
+
filesQuery += " AND archetype = ?";
|
|
86
|
+
params.push(archetypeFilter);
|
|
87
|
+
}
|
|
88
|
+
filesQuery += " ORDER BY path ASC";
|
|
89
|
+
|
|
90
|
+
const files = this.db.query<FileRecord, any>(filesQuery).all(...params);
|
|
91
|
+
|
|
92
|
+
const catalogFiles: DomainCatalogResponse["files"] = [];
|
|
93
|
+
|
|
94
|
+
for (const file of files) {
|
|
95
|
+
const symbols = this.db
|
|
96
|
+
.query<SymbolRecord, [number]>(
|
|
97
|
+
"SELECT * FROM symbols WHERE file_id = ? ORDER BY line_start ASC"
|
|
98
|
+
)
|
|
99
|
+
.all(file.id);
|
|
100
|
+
|
|
101
|
+
const symbolMap = new Map<
|
|
102
|
+
string,
|
|
103
|
+
{
|
|
104
|
+
name: string;
|
|
105
|
+
kind: SymbolKind;
|
|
106
|
+
properties: string[];
|
|
107
|
+
methods: string[];
|
|
108
|
+
signatures: string[];
|
|
109
|
+
}
|
|
110
|
+
>();
|
|
111
|
+
|
|
112
|
+
for (const sym of symbols) {
|
|
113
|
+
if (
|
|
114
|
+
sym.kind === "class" ||
|
|
115
|
+
sym.kind === "interface" ||
|
|
116
|
+
sym.kind === "trait" ||
|
|
117
|
+
sym.kind === "enum"
|
|
118
|
+
) {
|
|
119
|
+
if (!symbolMap.has(sym.name)) {
|
|
120
|
+
symbolMap.set(sym.name, {
|
|
121
|
+
name: sym.name,
|
|
122
|
+
kind: sym.kind,
|
|
123
|
+
properties: [],
|
|
124
|
+
methods: [],
|
|
125
|
+
signatures: [sym.signature],
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
} else if (sym.kind === "method") {
|
|
129
|
+
let targetContainer = Array.from(symbolMap.values()).pop();
|
|
130
|
+
if (sym.name.includes("::")) {
|
|
131
|
+
const containerName = sym.name.split("::")[0];
|
|
132
|
+
if (symbolMap.has(containerName)) {
|
|
133
|
+
targetContainer = symbolMap.get(containerName);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (targetContainer) {
|
|
137
|
+
targetContainer.methods.push(sym.signature);
|
|
138
|
+
} else {
|
|
139
|
+
symbolMap.set(sym.name, {
|
|
140
|
+
name: sym.name,
|
|
141
|
+
kind: sym.kind,
|
|
142
|
+
properties: [],
|
|
143
|
+
methods: [],
|
|
144
|
+
signatures: [sym.signature],
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
} else if (sym.kind === "property") {
|
|
148
|
+
let targetContainer = Array.from(symbolMap.values()).pop();
|
|
149
|
+
if (sym.name.includes("::")) {
|
|
150
|
+
const containerName = sym.name.split("::")[0];
|
|
151
|
+
if (symbolMap.has(containerName)) {
|
|
152
|
+
targetContainer = symbolMap.get(containerName);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (targetContainer) {
|
|
156
|
+
targetContainer.properties.push(sym.signature);
|
|
157
|
+
} else {
|
|
158
|
+
symbolMap.set(sym.name, {
|
|
159
|
+
name: sym.name,
|
|
160
|
+
kind: sym.kind,
|
|
161
|
+
properties: [sym.signature],
|
|
162
|
+
methods: [],
|
|
163
|
+
signatures: [sym.signature],
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
symbolMap.set(sym.name, {
|
|
168
|
+
name: sym.name,
|
|
169
|
+
kind: sym.kind,
|
|
170
|
+
properties: [],
|
|
171
|
+
methods: [],
|
|
172
|
+
signatures: [sym.signature],
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
catalogFiles.push({
|
|
178
|
+
path: file.path,
|
|
179
|
+
archetype: file.archetype,
|
|
180
|
+
symbols: Array.from(symbolMap.values()).map((s) => ({
|
|
181
|
+
name: s.name,
|
|
182
|
+
kind: s.kind,
|
|
183
|
+
properties: s.properties.length > 0 ? s.properties : undefined,
|
|
184
|
+
methods: s.methods.length > 0 ? s.methods : undefined,
|
|
185
|
+
signatures: s.signatures.length > 0 ? s.signatures : undefined,
|
|
186
|
+
})),
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
domain: domain.name,
|
|
192
|
+
root: domain.root_path,
|
|
193
|
+
allowed_dependencies: allowedDeps,
|
|
194
|
+
forbidden_dependencies: forbiddenDeps,
|
|
195
|
+
files: catalogFiles,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -1,99 +1,99 @@
|
|
|
1
|
-
import type { Database } from "bun:sqlite";
|
|
2
|
-
import type { ArchetypeKind, DomainRecord, FileRecord } from "../../../types/index.ts";
|
|
3
|
-
|
|
4
|
-
export class FileRepository {
|
|
5
|
-
constructor(private db: Database) {}
|
|
6
|
-
|
|
7
|
-
public getFileByPath(path: string): FileRecord | null {
|
|
8
|
-
return this.db
|
|
9
|
-
.query<FileRecord, [string]>("SELECT * FROM files WHERE path = ?")
|
|
10
|
-
.get(path);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
public getFileById(id: number): FileRecord | null {
|
|
14
|
-
return this.db
|
|
15
|
-
.query<FileRecord, [number]>("SELECT * FROM files WHERE id = ?")
|
|
16
|
-
.get(id);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
public getFilesByDomain(domainId: number): FileRecord[] {
|
|
20
|
-
return this.db
|
|
21
|
-
.query<FileRecord, [number]>("SELECT * FROM files WHERE domain_id = ? ORDER BY path ASC")
|
|
22
|
-
.all(domainId);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
public upsertFile(
|
|
26
|
-
domainId: number,
|
|
27
|
-
path: string,
|
|
28
|
-
archetype: ArchetypeKind,
|
|
29
|
-
contentHash: string,
|
|
30
|
-
lineCount: number,
|
|
31
|
-
mtimeMs: number = 0,
|
|
32
|
-
sizeBytes: number = 0
|
|
33
|
-
): number {
|
|
34
|
-
const existing = this.getFileByPath(path);
|
|
35
|
-
if (existing) {
|
|
36
|
-
this.db
|
|
37
|
-
.query(
|
|
38
|
-
`UPDATE files
|
|
39
|
-
SET domain_id = ?, archetype = ?, content_hash = ?, line_count = ?, mtime_ms = ?, size_bytes = ?, last_scanned_at = CURRENT_TIMESTAMP
|
|
40
|
-
WHERE id = ?`
|
|
41
|
-
)
|
|
42
|
-
.run(domainId, archetype, contentHash, lineCount, mtimeMs, sizeBytes, existing.id);
|
|
43
|
-
return existing.id;
|
|
44
|
-
} else {
|
|
45
|
-
const result = this.db
|
|
46
|
-
.query(
|
|
47
|
-
`INSERT INTO files (domain_id, path, archetype, content_hash, line_count, mtime_ms, size_bytes)
|
|
48
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
49
|
-
)
|
|
50
|
-
.run(domainId, path, archetype, contentHash, lineCount, mtimeMs, sizeBytes);
|
|
51
|
-
return Number(result.lastInsertRowid);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
public updateFileMetadataOnly(id: number, mtimeMs: number, sizeBytes: number): void {
|
|
56
|
-
this.db
|
|
57
|
-
.query(
|
|
58
|
-
`UPDATE files
|
|
59
|
-
SET mtime_ms = ?, size_bytes = ?, last_scanned_at = CURRENT_TIMESTAMP
|
|
60
|
-
WHERE id = ?`
|
|
61
|
-
)
|
|
62
|
-
.run(mtimeMs, sizeBytes, id);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
public deleteFilesNotInPaths(domainId: number, validPaths: string[]): number {
|
|
66
|
-
if (validPaths.length === 0) {
|
|
67
|
-
const res = this.db.query("DELETE FROM files WHERE domain_id = ?").run(domainId);
|
|
68
|
-
return Number(res.changes);
|
|
69
|
-
}
|
|
70
|
-
const existingFiles = this.getFilesByDomain(domainId);
|
|
71
|
-
const validSet = new Set(validPaths);
|
|
72
|
-
let deletedCount = 0;
|
|
73
|
-
for (const f of existingFiles) {
|
|
74
|
-
if (!validSet.has(f.path)) {
|
|
75
|
-
this.db.query("DELETE FROM files WHERE id = ?").run(f.id);
|
|
76
|
-
deletedCount++;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
return deletedCount;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
public deleteFile(path: string): void {
|
|
83
|
-
this.db.query("DELETE FROM files WHERE path = ?").run(path);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
public getFileWithDomain(
|
|
87
|
-
filePath: string
|
|
88
|
-
): { file: FileRecord; domain: DomainRecord } | null {
|
|
89
|
-
const file = this.getFileByPath(filePath);
|
|
90
|
-
if (!file) return null;
|
|
91
|
-
|
|
92
|
-
const domain = this.db
|
|
93
|
-
.query<DomainRecord, [number]>("SELECT * FROM domains WHERE id = ?")
|
|
94
|
-
.get(file.domain_id);
|
|
95
|
-
if (!domain) return null;
|
|
96
|
-
|
|
97
|
-
return { file, domain };
|
|
98
|
-
}
|
|
99
|
-
}
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
import type { ArchetypeKind, DomainRecord, FileRecord } from "../../../types/index.ts";
|
|
3
|
+
|
|
4
|
+
export class FileRepository {
|
|
5
|
+
constructor(private db: Database) {}
|
|
6
|
+
|
|
7
|
+
public getFileByPath(path: string): FileRecord | null {
|
|
8
|
+
return this.db
|
|
9
|
+
.query<FileRecord, [string]>("SELECT * FROM files WHERE path = ?")
|
|
10
|
+
.get(path);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
public getFileById(id: number): FileRecord | null {
|
|
14
|
+
return this.db
|
|
15
|
+
.query<FileRecord, [number]>("SELECT * FROM files WHERE id = ?")
|
|
16
|
+
.get(id);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
public getFilesByDomain(domainId: number): FileRecord[] {
|
|
20
|
+
return this.db
|
|
21
|
+
.query<FileRecord, [number]>("SELECT * FROM files WHERE domain_id = ? ORDER BY path ASC")
|
|
22
|
+
.all(domainId);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public upsertFile(
|
|
26
|
+
domainId: number,
|
|
27
|
+
path: string,
|
|
28
|
+
archetype: ArchetypeKind,
|
|
29
|
+
contentHash: string,
|
|
30
|
+
lineCount: number,
|
|
31
|
+
mtimeMs: number = 0,
|
|
32
|
+
sizeBytes: number = 0
|
|
33
|
+
): number {
|
|
34
|
+
const existing = this.getFileByPath(path);
|
|
35
|
+
if (existing) {
|
|
36
|
+
this.db
|
|
37
|
+
.query(
|
|
38
|
+
`UPDATE files
|
|
39
|
+
SET domain_id = ?, archetype = ?, content_hash = ?, line_count = ?, mtime_ms = ?, size_bytes = ?, last_scanned_at = CURRENT_TIMESTAMP
|
|
40
|
+
WHERE id = ?`
|
|
41
|
+
)
|
|
42
|
+
.run(domainId, archetype, contentHash, lineCount, mtimeMs, sizeBytes, existing.id);
|
|
43
|
+
return existing.id;
|
|
44
|
+
} else {
|
|
45
|
+
const result = this.db
|
|
46
|
+
.query(
|
|
47
|
+
`INSERT INTO files (domain_id, path, archetype, content_hash, line_count, mtime_ms, size_bytes)
|
|
48
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
49
|
+
)
|
|
50
|
+
.run(domainId, path, archetype, contentHash, lineCount, mtimeMs, sizeBytes);
|
|
51
|
+
return Number(result.lastInsertRowid);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public updateFileMetadataOnly(id: number, mtimeMs: number, sizeBytes: number): void {
|
|
56
|
+
this.db
|
|
57
|
+
.query(
|
|
58
|
+
`UPDATE files
|
|
59
|
+
SET mtime_ms = ?, size_bytes = ?, last_scanned_at = CURRENT_TIMESTAMP
|
|
60
|
+
WHERE id = ?`
|
|
61
|
+
)
|
|
62
|
+
.run(mtimeMs, sizeBytes, id);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
public deleteFilesNotInPaths(domainId: number, validPaths: string[]): number {
|
|
66
|
+
if (validPaths.length === 0) {
|
|
67
|
+
const res = this.db.query("DELETE FROM files WHERE domain_id = ?").run(domainId);
|
|
68
|
+
return Number(res.changes);
|
|
69
|
+
}
|
|
70
|
+
const existingFiles = this.getFilesByDomain(domainId);
|
|
71
|
+
const validSet = new Set(validPaths);
|
|
72
|
+
let deletedCount = 0;
|
|
73
|
+
for (const f of existingFiles) {
|
|
74
|
+
if (!validSet.has(f.path)) {
|
|
75
|
+
this.db.query("DELETE FROM files WHERE id = ?").run(f.id);
|
|
76
|
+
deletedCount++;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return deletedCount;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
public deleteFile(path: string): void {
|
|
83
|
+
this.db.query("DELETE FROM files WHERE path = ?").run(path);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
public getFileWithDomain(
|
|
87
|
+
filePath: string
|
|
88
|
+
): { file: FileRecord; domain: DomainRecord } | null {
|
|
89
|
+
const file = this.getFileByPath(filePath);
|
|
90
|
+
if (!file) return null;
|
|
91
|
+
|
|
92
|
+
const domain = this.db
|
|
93
|
+
.query<DomainRecord, [number]>("SELECT * FROM domains WHERE id = ?")
|
|
94
|
+
.get(file.domain_id);
|
|
95
|
+
if (!domain) return null;
|
|
96
|
+
|
|
97
|
+
return { file, domain };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
import type { Database } from "bun:sqlite";
|
|
2
|
-
|
|
3
|
-
export class MetaRepository {
|
|
4
|
-
constructor(private db: Database) {}
|
|
5
|
-
|
|
6
|
-
public getMeta(key: string): string | null {
|
|
7
|
-
const row = this.db
|
|
8
|
-
.query<{ value: string }, [string]>("SELECT value FROM repo_meta WHERE key = ?")
|
|
9
|
-
.get(key);
|
|
10
|
-
return row ? row.value : null;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
public setMeta(key: string, value: string): void {
|
|
14
|
-
this.db
|
|
15
|
-
.query(
|
|
16
|
-
`INSERT INTO repo_meta (key, value, updated_at)
|
|
17
|
-
VALUES (?, ?, CURRENT_TIMESTAMP)
|
|
18
|
-
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP`
|
|
19
|
-
)
|
|
20
|
-
.run(key, value);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
public getAllMeta(): Record<string, string> {
|
|
24
|
-
const rows = this.db
|
|
25
|
-
.query<{ key: string; value: string }, []>("SELECT key, value FROM repo_meta")
|
|
26
|
-
.all();
|
|
27
|
-
const result: Record<string, string> = {};
|
|
28
|
-
for (const r of rows) {
|
|
29
|
-
result[r.key] = r.value;
|
|
30
|
-
}
|
|
31
|
-
return result;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
|
|
3
|
+
export class MetaRepository {
|
|
4
|
+
constructor(private db: Database) {}
|
|
5
|
+
|
|
6
|
+
public getMeta(key: string): string | null {
|
|
7
|
+
const row = this.db
|
|
8
|
+
.query<{ value: string }, [string]>("SELECT value FROM repo_meta WHERE key = ?")
|
|
9
|
+
.get(key);
|
|
10
|
+
return row ? row.value : null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
public setMeta(key: string, value: string): void {
|
|
14
|
+
this.db
|
|
15
|
+
.query(
|
|
16
|
+
`INSERT INTO repo_meta (key, value, updated_at)
|
|
17
|
+
VALUES (?, ?, CURRENT_TIMESTAMP)
|
|
18
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP`
|
|
19
|
+
)
|
|
20
|
+
.run(key, value);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public getAllMeta(): Record<string, string> {
|
|
24
|
+
const rows = this.db
|
|
25
|
+
.query<{ key: string; value: string }, []>("SELECT key, value FROM repo_meta")
|
|
26
|
+
.all();
|
|
27
|
+
const result: Record<string, string> = {};
|
|
28
|
+
for (const r of rows) {
|
|
29
|
+
result[r.key] = r.value;
|
|
30
|
+
}
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
}
|