septum 0.1.4 → 0.1.6
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 +105 -105
- package/bin/septum.ts +4 -4
- package/package.json +63 -63
- package/src/cli/commands/check.ts +161 -161
- package/src/cli/commands/hook.ts +216 -188
- package/src/cli/commands/ingest.ts +29 -29
- package/src/cli/commands/init.ts +213 -211
- package/src/cli/commands/locate.ts +61 -61
- package/src/cli/commands/query.ts +104 -64
- package/src/cli/commands/serve.ts +5 -5
- package/src/cli/commands/slice.ts +50 -48
- package/src/cli/commands/sync.ts +31 -31
- package/src/cli/index.ts +220 -210
- package/src/cli/templates/hooks.ts +75 -75
- package/src/cli/templates/rules.ts +50 -38
- package/src/cli/templates/skill.ts +65 -69
- package/src/core/boundary/evaluator.ts +429 -210
- package/src/core/config/loader.ts +117 -97
- package/src/core/config/schema.ts +39 -39
- package/src/core/database/client.ts +44 -44
- package/src/core/database/repositories/dependency.repository.ts +552 -349
- package/src/core/database/repositories/domain.repository.ts +275 -198
- package/src/core/database/repositories/file.repository.ts +117 -106
- package/src/core/database/repositories/meta.repository.ts +33 -33
- package/src/core/database/repositories/symbol.repository.ts +482 -443
- package/src/core/database/repository.ts +281 -260
- package/src/core/database/schema.ts +154 -153
- package/src/core/discovery/topology-detector.ts +486 -486
- package/src/core/ingestion/hasher.ts +5 -5
- package/src/core/ingestion/pipeline.ts +606 -535
- package/src/core/parser/boundary-tracker.ts +433 -288
- package/src/core/parser/extractors/base.ts +62 -62
- package/src/core/parser/extractors/blade.ts +158 -158
- package/src/core/parser/extractors/frontend.ts +147 -194
- package/src/core/parser/extractors/go.ts +408 -180
- package/src/core/parser/extractors/laravel-semantic.ts +1045 -901
- package/src/core/parser/extractors/mcp-cli-semantic.ts +328 -199
- package/src/core/parser/extractors/nestjs-semantic.ts +134 -119
- package/src/core/parser/extractors/php.ts +415 -153
- package/src/core/parser/extractors/python.ts +460 -228
- package/src/core/parser/extractors/registry.ts +70 -70
- package/src/core/parser/extractors/semantic-extractor.interface.ts +21 -21
- package/src/core/parser/extractors/typescript.ts +668 -181
- package/src/core/parser/tree-sitter.ts +41 -41
- package/src/core/parser/wasm-loader.ts +271 -0
- package/src/core/resolver/call-graph-tracer.ts +698 -668
- package/src/core/resolver/module-resolver.ts +479 -342
- package/src/core/resolver/path-resolver.ts +147 -147
- package/src/core/resolver/symbol-locator.ts +543 -543
- package/src/core/resolver/vertical-slice-tracer.ts +323 -285
- package/src/core/session/session-manager.ts +56 -56
- package/src/core/telemetry/telemetry.ts +86 -86
- package/src/core/topology/environment-detector.ts +109 -6
- package/src/index.ts +21 -21
- package/src/mcp/schemas.ts +695 -286
- package/src/mcp/server.ts +927 -716
- package/src/mcp/tools/check-boundary.ts +307 -152
- package/src/mcp/tools/get-domain-catalog.ts +155 -121
- package/src/mcp/tools/get-environment-topology.ts +3 -6
- package/src/mcp/tools/get-feature-context.ts +227 -116
- package/src/mcp/tools/get-symbol-hotspots.ts +54 -59
- package/src/mcp/tools/get-symbol-impact.ts +28 -26
- package/src/mcp/tools/get-symbol.ts +65 -63
- package/src/mcp/tools/locate-symbol.ts +169 -190
- package/src/mcp/tools/register-domain.ts +59 -64
- package/src/mcp/tools/register-feature.ts +91 -0
- package/src/mcp/tools/trace-vertical-slice.ts +74 -48
- package/src/types/index.ts +366 -315
- package/src/version.ts +3 -3
|
@@ -1,535 +1,606 @@
|
|
|
1
|
-
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
-
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
3
|
-
import type { ParsedFileAST } from "../../types/index.ts";
|
|
4
|
-
import type { ValidatedSeptumConfig } from "../config/schema.ts";
|
|
5
|
-
import type { SeptumRepository } from "../database/repository.ts";
|
|
6
|
-
import { detectArchetype } from "../parser/extractors/base.ts";
|
|
7
|
-
import { SemanticSliceExtractorRegistry } from "../parser/extractors/registry.ts";
|
|
8
|
-
import { TopologyDetector } from "../discovery/topology-detector.ts";
|
|
9
|
-
import { ASTParserEngine } from "../parser/tree-sitter.ts";
|
|
10
|
-
import { CallGraphTracer } from "../resolver/call-graph-tracer.ts";
|
|
11
|
-
import { computeContentHash } from "./hasher.ts";
|
|
12
|
-
|
|
13
|
-
export interface IngestionMetrics {
|
|
14
|
-
domains_processed: number;
|
|
15
|
-
files_scanned: number;
|
|
16
|
-
files_updated: number;
|
|
17
|
-
files_skipped: number;
|
|
18
|
-
symbols_indexed: number;
|
|
19
|
-
dependencies_indexed: number;
|
|
20
|
-
duration_ms: number;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export class IngestionPipeline {
|
|
24
|
-
private repo: SeptumRepository;
|
|
25
|
-
private parserEngine: ASTParserEngine;
|
|
26
|
-
private customWorkspaceRoot?: string;
|
|
27
|
-
|
|
28
|
-
constructor(repo: SeptumRepository, workspaceRoot?: string) {
|
|
29
|
-
this.repo = repo;
|
|
30
|
-
this.parserEngine = new ASTParserEngine();
|
|
31
|
-
if (workspaceRoot) {
|
|
32
|
-
this.customWorkspaceRoot = resolve(workspaceRoot);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
public async run(config: ValidatedSeptumConfig): Promise<IngestionMetrics> {
|
|
37
|
-
const startTime = performance.now();
|
|
38
|
-
let filesScanned = 0;
|
|
39
|
-
let filesUpdated = 0;
|
|
40
|
-
let filesSkipped = 0;
|
|
41
|
-
let symbolsIndexed = 0;
|
|
42
|
-
let dependenciesIndexed = 0;
|
|
43
|
-
|
|
44
|
-
let domainNames = Object.keys(config.domains);
|
|
45
|
-
const projectRoot = this.customWorkspaceRoot || process.cwd();
|
|
46
|
-
|
|
47
|
-
// Auto-discovery mode if no domains explicitly declared in config
|
|
48
|
-
if (domainNames.length === 0) {
|
|
49
|
-
const discovered = this.autoDiscoverDomains(projectRoot);
|
|
50
|
-
for (const [dName, dCfg] of Object.entries(discovered)) {
|
|
51
|
-
config.domains[dName] = {
|
|
52
|
-
root: dCfg.root,
|
|
53
|
-
description: dCfg.description,
|
|
54
|
-
allowed_dependencies: dCfg.allowed_dependencies || [],
|
|
55
|
-
forbidden_dependencies: dCfg.forbidden_dependencies || [],
|
|
56
|
-
archetypes: dCfg.archetypes || {},
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
domainNames = Object.keys(config.domains);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const workspaceRoot = this.resolveWorkspaceRoot(config, domainCfg.root);
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
existingFile &&
|
|
132
|
-
existingFile.
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
domains["
|
|
366
|
-
root: "
|
|
367
|
-
description: "
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
3
|
+
import type { ParsedFileAST } from "../../types/index.ts";
|
|
4
|
+
import type { ValidatedSeptumConfig } from "../config/schema.ts";
|
|
5
|
+
import type { SeptumRepository } from "../database/repository.ts";
|
|
6
|
+
import { detectArchetype } from "../parser/extractors/base.ts";
|
|
7
|
+
import { SemanticSliceExtractorRegistry } from "../parser/extractors/registry.ts";
|
|
8
|
+
import { TopologyDetector } from "../discovery/topology-detector.ts";
|
|
9
|
+
import { ASTParserEngine } from "../parser/tree-sitter.ts";
|
|
10
|
+
import { CallGraphTracer } from "../resolver/call-graph-tracer.ts";
|
|
11
|
+
import { computeContentHash } from "./hasher.ts";
|
|
12
|
+
|
|
13
|
+
export interface IngestionMetrics {
|
|
14
|
+
domains_processed: number;
|
|
15
|
+
files_scanned: number;
|
|
16
|
+
files_updated: number;
|
|
17
|
+
files_skipped: number;
|
|
18
|
+
symbols_indexed: number;
|
|
19
|
+
dependencies_indexed: number;
|
|
20
|
+
duration_ms: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class IngestionPipeline {
|
|
24
|
+
private repo: SeptumRepository;
|
|
25
|
+
private parserEngine: ASTParserEngine;
|
|
26
|
+
private customWorkspaceRoot?: string;
|
|
27
|
+
|
|
28
|
+
constructor(repo: SeptumRepository, workspaceRoot?: string) {
|
|
29
|
+
this.repo = repo;
|
|
30
|
+
this.parserEngine = new ASTParserEngine();
|
|
31
|
+
if (workspaceRoot) {
|
|
32
|
+
this.customWorkspaceRoot = resolve(workspaceRoot);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public async run(config: ValidatedSeptumConfig): Promise<IngestionMetrics> {
|
|
37
|
+
const startTime = performance.now();
|
|
38
|
+
let filesScanned = 0;
|
|
39
|
+
let filesUpdated = 0;
|
|
40
|
+
let filesSkipped = 0;
|
|
41
|
+
let symbolsIndexed = 0;
|
|
42
|
+
let dependenciesIndexed = 0;
|
|
43
|
+
|
|
44
|
+
let domainNames = Object.keys(config.domains);
|
|
45
|
+
const projectRoot = this.customWorkspaceRoot || process.cwd();
|
|
46
|
+
|
|
47
|
+
// Auto-discovery mode if no domains explicitly declared in config
|
|
48
|
+
if (domainNames.length === 0) {
|
|
49
|
+
const discovered = this.autoDiscoverDomains(projectRoot);
|
|
50
|
+
for (const [dName, dCfg] of Object.entries(discovered)) {
|
|
51
|
+
config.domains[dName] = {
|
|
52
|
+
root: dCfg.root,
|
|
53
|
+
description: dCfg.description,
|
|
54
|
+
allowed_dependencies: dCfg.allowed_dependencies || [],
|
|
55
|
+
forbidden_dependencies: dCfg.forbidden_dependencies || [],
|
|
56
|
+
archetypes: dCfg.archetypes || {},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
domainNames = Object.keys(config.domains);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const existingFilesMap = new Map<string, import("../../types/index.ts").FileRecord>();
|
|
63
|
+
for (const f of this.repo.getAllFiles()) {
|
|
64
|
+
existingFilesMap.set(f.path, f);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for (const domainName of domainNames) {
|
|
68
|
+
const domainCfg = config.domains[domainName];
|
|
69
|
+
const domainId = this.repo.upsertDomain(domainName, domainCfg);
|
|
70
|
+
const workspaceRoot = this.resolveWorkspaceRoot(config, domainCfg.root);
|
|
71
|
+
const absDomainRoot = resolve(workspaceRoot, domainCfg.root);
|
|
72
|
+
|
|
73
|
+
if (!existsSync(absDomainRoot)) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const normCurrentRoot = domainCfg.root.replace(/\\/g, "/").replace(/^\.\/?/, "");
|
|
78
|
+
const childDomainRoots = domainNames
|
|
79
|
+
.filter((d) => d !== domainName)
|
|
80
|
+
.map((d) => config.domains[d].root.replace(/\\/g, "/").replace(/^\.\/?/, ""))
|
|
81
|
+
.filter((r) => {
|
|
82
|
+
if (!r) return false;
|
|
83
|
+
if (!normCurrentRoot) return true;
|
|
84
|
+
return r.startsWith(normCurrentRoot + "/");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const collectedFiles = this.collectFiles(
|
|
88
|
+
absDomainRoot,
|
|
89
|
+
workspaceRoot,
|
|
90
|
+
config.settings.ignore_patterns,
|
|
91
|
+
childDomainRoots
|
|
92
|
+
);
|
|
93
|
+
const activeRelPaths = new Set<string>();
|
|
94
|
+
|
|
95
|
+
const pendingMetadataUpdates: Array<{
|
|
96
|
+
id: number;
|
|
97
|
+
mtimeMs: number;
|
|
98
|
+
sizeBytes: number;
|
|
99
|
+
}> = [];
|
|
100
|
+
|
|
101
|
+
const pendingUpdates: Array<{
|
|
102
|
+
fullPath: string;
|
|
103
|
+
relPath: string;
|
|
104
|
+
contentHash: string;
|
|
105
|
+
lineCount: number;
|
|
106
|
+
archetype: ReturnType<typeof detectArchetype>;
|
|
107
|
+
mtimeMs: number;
|
|
108
|
+
sizeBytes: number;
|
|
109
|
+
ast: ParsedFileAST;
|
|
110
|
+
}> = [];
|
|
111
|
+
|
|
112
|
+
for (const { fullPath, relPath } of collectedFiles) {
|
|
113
|
+
filesScanned++;
|
|
114
|
+
activeRelPaths.add(relPath);
|
|
115
|
+
|
|
116
|
+
let stat;
|
|
117
|
+
try {
|
|
118
|
+
stat = statSync(fullPath);
|
|
119
|
+
} catch {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const mtimeMs = Math.round(stat.mtimeMs);
|
|
124
|
+
const sizeBytes = stat.size;
|
|
125
|
+
const existingFile = existingFilesMap.get(relPath);
|
|
126
|
+
|
|
127
|
+
// Tier 1: Fast OS metadata check (inode mtime + size + domain match)
|
|
128
|
+
if (
|
|
129
|
+
existingFile &&
|
|
130
|
+
existingFile.domain_id === domainId &&
|
|
131
|
+
existingFile.mtime_ms === mtimeMs &&
|
|
132
|
+
existingFile.size_bytes === sizeBytes
|
|
133
|
+
) {
|
|
134
|
+
filesSkipped++;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Tier 2: Content Hash verification
|
|
139
|
+
const content = readFileSync(fullPath, "utf-8");
|
|
140
|
+
const currentHash = computeContentHash(content);
|
|
141
|
+
|
|
142
|
+
if (
|
|
143
|
+
existingFile &&
|
|
144
|
+
existingFile.domain_id === domainId &&
|
|
145
|
+
existingFile.content_hash === currentHash
|
|
146
|
+
) {
|
|
147
|
+
// Content identical despite mtime bump (e.g. touch) - queue metadata update for batch transaction
|
|
148
|
+
pendingMetadataUpdates.push({ id: existingFile.id, mtimeMs, sizeBytes });
|
|
149
|
+
filesSkipped++;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
filesUpdated++;
|
|
154
|
+
const lineCount = content.split("\n").length;
|
|
155
|
+
const archetype = detectArchetype(relPath, domainCfg.archetypes ?? {});
|
|
156
|
+
const ast = await this.parserEngine.parseFile(fullPath, content);
|
|
157
|
+
|
|
158
|
+
pendingUpdates.push({
|
|
159
|
+
fullPath,
|
|
160
|
+
relPath,
|
|
161
|
+
contentHash: currentHash,
|
|
162
|
+
lineCount,
|
|
163
|
+
archetype,
|
|
164
|
+
mtimeMs,
|
|
165
|
+
sizeBytes,
|
|
166
|
+
ast,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Batch ACID Transaction per domain
|
|
171
|
+
this.repo.runInTransaction(() => {
|
|
172
|
+
for (const meta of pendingMetadataUpdates) {
|
|
173
|
+
this.repo.updateFileMetadataOnly(meta.id, meta.mtimeMs, meta.sizeBytes);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
for (const item of pendingUpdates) {
|
|
177
|
+
const fileId = this.repo.upsertFile(
|
|
178
|
+
domainId,
|
|
179
|
+
item.relPath,
|
|
180
|
+
item.archetype,
|
|
181
|
+
item.contentHash,
|
|
182
|
+
item.lineCount,
|
|
183
|
+
item.mtimeMs,
|
|
184
|
+
item.sizeBytes
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
this.repo.replaceFileSymbols(fileId, item.ast.symbols);
|
|
188
|
+
this.repo.replaceFileDependencies(fileId, item.ast.dependencies);
|
|
189
|
+
|
|
190
|
+
symbolsIndexed += item.ast.symbols.length;
|
|
191
|
+
dependenciesIndexed += item.ast.dependencies.length;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// State Reconciliation: Prune deleted files from DB using pre-loaded existingFilesMap
|
|
195
|
+
for (const [existingPath, existingRecord] of existingFilesMap.entries()) {
|
|
196
|
+
if (existingRecord.domain_id === domainId && !activeRelPaths.has(existingPath)) {
|
|
197
|
+
this.repo.deleteFileByPath(existingPath);
|
|
198
|
+
existingFilesMap.delete(existingPath);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Framework-Agnostic Semantic Vertical Slice Extraction (via Registry Strategy)
|
|
205
|
+
const detectedTopology = TopologyDetector.detect(projectRoot);
|
|
206
|
+
const semanticSlices = await SemanticSliceExtractorRegistry.extractAllSlices(
|
|
207
|
+
projectRoot,
|
|
208
|
+
detectedTopology.framework
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
const slicesToPersist =
|
|
212
|
+
semanticSlices.length > 0
|
|
213
|
+
? semanticSlices
|
|
214
|
+
: new CallGraphTracer(this.repo, projectRoot).traceAllSlices();
|
|
215
|
+
|
|
216
|
+
this.repo.runInTransaction(() => {
|
|
217
|
+
this.repo.clearVerticalSlices();
|
|
218
|
+
for (const slice of slicesToPersist) {
|
|
219
|
+
this.repo.upsertVerticalSlice(slice);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const durationMs = Math.round(performance.now() - startTime);
|
|
224
|
+
|
|
225
|
+
// Update SSOT repo_meta watermark
|
|
226
|
+
this.repo.runInTransaction(() => {
|
|
227
|
+
this.repo.setMeta("last_sync", new Date().toISOString());
|
|
228
|
+
this.repo.setMeta("files_scanned", String(filesScanned));
|
|
229
|
+
this.repo.setMeta("files_updated", String(filesUpdated));
|
|
230
|
+
this.repo.setMeta("symbols_indexed", String(symbolsIndexed));
|
|
231
|
+
this.repo.setMeta("dependencies_indexed", String(dependenciesIndexed));
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
domains_processed: domainNames.length,
|
|
236
|
+
files_scanned: filesScanned,
|
|
237
|
+
files_updated: filesUpdated,
|
|
238
|
+
files_skipped: filesSkipped,
|
|
239
|
+
symbols_indexed: symbolsIndexed,
|
|
240
|
+
dependencies_indexed: dependenciesIndexed,
|
|
241
|
+
duration_ms: durationMs,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
public autoDiscoverDomains(projectRoot: string): Record<string, import("../../types/index.ts").DomainConfig> {
|
|
246
|
+
const domains: Record<string, import("../../types/index.ts").DomainConfig> = {};
|
|
247
|
+
|
|
248
|
+
// 1. Modular / DDD check
|
|
249
|
+
const candidateDirs = [
|
|
250
|
+
{ name: "domains", rel: "src/domains" },
|
|
251
|
+
{ name: "features", rel: "src/features" },
|
|
252
|
+
{ name: "modules", rel: "src/modules" },
|
|
253
|
+
{ name: "domain", rel: "app/Domain" },
|
|
254
|
+
{ name: "domains_app", rel: "app/Domains" },
|
|
255
|
+
];
|
|
256
|
+
|
|
257
|
+
for (const c of candidateDirs) {
|
|
258
|
+
const fullDir = join(projectRoot, c.rel);
|
|
259
|
+
if (existsSync(fullDir)) {
|
|
260
|
+
try {
|
|
261
|
+
const subdirs = readdirSync(fullDir, { withFileTypes: true })
|
|
262
|
+
.filter((d) => d.isDirectory())
|
|
263
|
+
.map((d) => d.name);
|
|
264
|
+
|
|
265
|
+
if (subdirs.length > 0) {
|
|
266
|
+
for (const sub of subdirs) {
|
|
267
|
+
domains[sub.toLowerCase()] = {
|
|
268
|
+
root: `${c.rel}/${sub}`,
|
|
269
|
+
description: `${sub} domain (auto-discovered)`,
|
|
270
|
+
allowed_dependencies: [],
|
|
271
|
+
forbidden_dependencies: [],
|
|
272
|
+
archetypes: {
|
|
273
|
+
service: "*Service.*",
|
|
274
|
+
model: "*Model.*",
|
|
275
|
+
repository: "*Repository.*",
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
return domains;
|
|
280
|
+
}
|
|
281
|
+
} catch {}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// 2. Framework: Laravel
|
|
286
|
+
const hasArtisan = existsSync(join(projectRoot, "artisan"));
|
|
287
|
+
let isLaravel = hasArtisan;
|
|
288
|
+
if (!isLaravel && existsSync(join(projectRoot, "composer.json"))) {
|
|
289
|
+
try {
|
|
290
|
+
const comp = readFileSync(join(projectRoot, "composer.json"), "utf-8");
|
|
291
|
+
if (comp.includes("laravel/framework")) isLaravel = true;
|
|
292
|
+
} catch {}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (isLaravel) {
|
|
296
|
+
domains["app"] = {
|
|
297
|
+
root: "app",
|
|
298
|
+
description: "Core Laravel Application (auto-discovered)",
|
|
299
|
+
allowed_dependencies: [],
|
|
300
|
+
forbidden_dependencies: [],
|
|
301
|
+
archetypes: {
|
|
302
|
+
controller: "app/Http/Controllers/**",
|
|
303
|
+
service: "app/Services/**",
|
|
304
|
+
model: "app/Models/**",
|
|
305
|
+
repository: "app/Repositories/**",
|
|
306
|
+
action: "app/Actions/**",
|
|
307
|
+
job: "app/Jobs/**",
|
|
308
|
+
event: "app/Events/**",
|
|
309
|
+
listener: "app/Listeners/**",
|
|
310
|
+
request: "app/Http/Requests/**",
|
|
311
|
+
middleware: "app/Http/Middleware/**",
|
|
312
|
+
},
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
if (existsSync(join(projectRoot, "resources/views"))) {
|
|
316
|
+
domains["views"] = {
|
|
317
|
+
root: "resources/views",
|
|
318
|
+
description: "Laravel Blade Templates and UI Views (auto-discovered)",
|
|
319
|
+
allowed_dependencies: [],
|
|
320
|
+
forbidden_dependencies: [],
|
|
321
|
+
archetypes: {
|
|
322
|
+
view: "resources/views/**/*.blade.php",
|
|
323
|
+
},
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (existsSync(join(projectRoot, "routes"))) {
|
|
328
|
+
domains["routes"] = {
|
|
329
|
+
root: "routes",
|
|
330
|
+
description: "Laravel HTTP Route Definitions (auto-discovered)",
|
|
331
|
+
allowed_dependencies: [],
|
|
332
|
+
forbidden_dependencies: [],
|
|
333
|
+
archetypes: {
|
|
334
|
+
route: "routes/**/*.php",
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (existsSync(join(projectRoot, "database"))) {
|
|
340
|
+
domains["database"] = {
|
|
341
|
+
root: "database",
|
|
342
|
+
description: "Laravel Migrations and Database Schemas (auto-discovered)",
|
|
343
|
+
allowed_dependencies: [],
|
|
344
|
+
forbidden_dependencies: [],
|
|
345
|
+
archetypes: {
|
|
346
|
+
migration: "database/migrations/**/*.php",
|
|
347
|
+
},
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return domains;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// 3. Framework: NestJS
|
|
355
|
+
const hasNestCli = existsSync(join(projectRoot, "nest-cli.json"));
|
|
356
|
+
let isNest = hasNestCli;
|
|
357
|
+
if (!isNest && existsSync(join(projectRoot, "package.json"))) {
|
|
358
|
+
try {
|
|
359
|
+
const pkg = readFileSync(join(projectRoot, "package.json"), "utf-8");
|
|
360
|
+
if (pkg.includes("@nestjs/core")) isNest = true;
|
|
361
|
+
} catch {}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (isNest) {
|
|
365
|
+
domains["src"] = {
|
|
366
|
+
root: "src",
|
|
367
|
+
description: "NestJS Source Domain (auto-discovered)",
|
|
368
|
+
allowed_dependencies: [],
|
|
369
|
+
forbidden_dependencies: [],
|
|
370
|
+
archetypes: {
|
|
371
|
+
controller: "src/**/*.controller.ts",
|
|
372
|
+
service: "src/**/*.service.ts",
|
|
373
|
+
repository: "src/**/*.repository.ts",
|
|
374
|
+
},
|
|
375
|
+
};
|
|
376
|
+
return domains;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// 4. Framework: Go
|
|
380
|
+
if (existsSync(join(projectRoot, "go.mod"))) {
|
|
381
|
+
if (existsSync(join(projectRoot, "internal"))) {
|
|
382
|
+
domains["internal"] = {
|
|
383
|
+
root: "internal",
|
|
384
|
+
description: "Internal business logic packages",
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
if (existsSync(join(projectRoot, "pkg"))) {
|
|
388
|
+
domains["pkg"] = {
|
|
389
|
+
root: "pkg",
|
|
390
|
+
description: "Public library packages",
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
if (existsSync(join(projectRoot, "cmd"))) {
|
|
394
|
+
domains["cmd"] = {
|
|
395
|
+
root: "cmd",
|
|
396
|
+
description: "Application command entry points",
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
if (Object.keys(domains).length > 0) return domains;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// 5. General project fallback (src or root)
|
|
403
|
+
if (existsSync(join(projectRoot, "src"))) {
|
|
404
|
+
domains["src"] = {
|
|
405
|
+
root: "src",
|
|
406
|
+
description: "Default Source Domain (auto-discovered)",
|
|
407
|
+
};
|
|
408
|
+
} else {
|
|
409
|
+
domains["root"] = {
|
|
410
|
+
root: ".",
|
|
411
|
+
description: "Root Domain (auto-discovered)",
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return domains;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
private collectFiles(
|
|
419
|
+
dir: string,
|
|
420
|
+
workspaceRoot: string,
|
|
421
|
+
ignorePatterns: string[],
|
|
422
|
+
childDomainRoots: string[] = []
|
|
423
|
+
): Array<{ fullPath: string; relPath: string }> {
|
|
424
|
+
const results: Array<{ fullPath: string; relPath: string }> = [];
|
|
425
|
+
|
|
426
|
+
const walk = (currentDir: string) => {
|
|
427
|
+
let entries: string[] = [];
|
|
428
|
+
try {
|
|
429
|
+
entries = readdirSync(currentDir);
|
|
430
|
+
} catch {
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
for (const entry of entries) {
|
|
435
|
+
const fullPath = join(currentDir, entry);
|
|
436
|
+
const relPath = relative(workspaceRoot, fullPath).replace(/\\/g, "/");
|
|
437
|
+
|
|
438
|
+
if (this.shouldIgnore(relPath, ignorePatterns)) {
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (childDomainRoots.some((child) => relPath === child || relPath.startsWith(child + "/"))) {
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
try {
|
|
447
|
+
const stat = statSync(fullPath);
|
|
448
|
+
if (stat.isDirectory()) {
|
|
449
|
+
walk(fullPath);
|
|
450
|
+
} else if (stat.isFile() && this.isSourceFile(fullPath)) {
|
|
451
|
+
results.push({ fullPath, relPath });
|
|
452
|
+
}
|
|
453
|
+
} catch {
|
|
454
|
+
// ignore unreadable files
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
walk(dir);
|
|
460
|
+
return results;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
private resolveWorkspaceRoot(config: ValidatedSeptumConfig, domainRoot: string): string {
|
|
464
|
+
if (this.customWorkspaceRoot) {
|
|
465
|
+
return this.customWorkspaceRoot;
|
|
466
|
+
}
|
|
467
|
+
const settings = config.settings as Record<string, unknown> | undefined;
|
|
468
|
+
if (typeof settings?.workspace_root === "string" && settings.workspace_root.trim()) {
|
|
469
|
+
return resolve(settings.workspace_root);
|
|
470
|
+
}
|
|
471
|
+
if (config.settings.db_path && config.settings.db_path !== ":memory:") {
|
|
472
|
+
const dbDir = dirname(resolve(config.settings.db_path));
|
|
473
|
+
const candidate = basename(dbDir) === ".septum" ? dirname(dbDir) : dbDir;
|
|
474
|
+
const absDomainRoot = resolve(domainRoot);
|
|
475
|
+
if (absDomainRoot.startsWith(candidate)) {
|
|
476
|
+
return candidate;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
return process.cwd();
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
private shouldIgnore(filePath: string, ignorePatterns: string[]): boolean {
|
|
483
|
+
const normPath = filePath.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
484
|
+
for (const pattern of ignorePatterns) {
|
|
485
|
+
const normPattern = pattern.replace(/\\/g, "/").replace(/^\.\//, "").trim();
|
|
486
|
+
if (!normPattern) continue;
|
|
487
|
+
|
|
488
|
+
if (normPattern === "*" || normPattern === "**") {
|
|
489
|
+
return true;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (normPath === normPattern) return true;
|
|
493
|
+
|
|
494
|
+
if (normPattern.endsWith("/**")) {
|
|
495
|
+
const prefix = normPattern.slice(0, -3);
|
|
496
|
+
if (normPath === prefix || normPath.startsWith(prefix + "/")) return true;
|
|
497
|
+
} else if (normPattern.endsWith("/*")) {
|
|
498
|
+
const prefix = normPattern.slice(0, -2);
|
|
499
|
+
if (normPath === prefix || normPath.startsWith(prefix + "/")) return true;
|
|
500
|
+
} else if (normPath.startsWith(normPattern + "/")) {
|
|
501
|
+
return true;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (normPattern.startsWith("*.") && normPath.endsWith(normPattern.slice(1))) {
|
|
505
|
+
return true;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const segments = normPath.split("/");
|
|
509
|
+
if (segments.includes(normPattern)) {
|
|
510
|
+
return true;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return false;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
private isSourceFile(path: string): boolean {
|
|
517
|
+
return /\.(ts|tsx|js|jsx|mjs|cjs|php|py|go|rs|vue|astro|svelte)$/i.test(path);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* JIT single-file ingestion: quickly verifies and updates a single file's AST,
|
|
522
|
+
* symbols, and dependencies in SQLite if its mtime or content hash has changed.
|
|
523
|
+
* Runs in < 5ms for rapid delta sync without full project scans.
|
|
524
|
+
*/
|
|
525
|
+
public async ingestFile(targetFilePath: string, config: ValidatedSeptumConfig): Promise<boolean> {
|
|
526
|
+
const fullPath = resolve(targetFilePath);
|
|
527
|
+
if (!existsSync(fullPath)) return false;
|
|
528
|
+
|
|
529
|
+
let stat;
|
|
530
|
+
try {
|
|
531
|
+
stat = statSync(fullPath);
|
|
532
|
+
if (!stat.isFile() || !this.isSourceFile(fullPath)) return false;
|
|
533
|
+
} catch {
|
|
534
|
+
return false;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const mtimeMs = Math.round(stat.mtimeMs);
|
|
538
|
+
const sizeBytes = stat.size;
|
|
539
|
+
|
|
540
|
+
const projectRoot = this.customWorkspaceRoot || process.cwd();
|
|
541
|
+
const relPath = relative(projectRoot, fullPath).replace(/\\/g, "/");
|
|
542
|
+
|
|
543
|
+
if (this.shouldIgnore(relPath, config.settings.ignore_patterns ?? [])) {
|
|
544
|
+
return false;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
let targetDomainName: string | null = null;
|
|
548
|
+
let targetDomainCfg = null;
|
|
549
|
+
|
|
550
|
+
for (const [dName, dCfg] of Object.entries(config.domains)) {
|
|
551
|
+
const absRoot = resolve(projectRoot, dCfg.root).replace(/\\/g, "/");
|
|
552
|
+
const normFullPath = fullPath.replace(/\\/g, "/");
|
|
553
|
+
if (normFullPath === absRoot || normFullPath.startsWith(absRoot + "/")) {
|
|
554
|
+
targetDomainName = dName;
|
|
555
|
+
targetDomainCfg = dCfg;
|
|
556
|
+
break;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (!targetDomainName || !targetDomainCfg) {
|
|
561
|
+
return false;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const domainId = this.repo.upsertDomain(targetDomainName, targetDomainCfg);
|
|
565
|
+
const existingFile = this.repo.getFileByPath(relPath);
|
|
566
|
+
|
|
567
|
+
// Tier 1: Fast OS metadata check
|
|
568
|
+
if (
|
|
569
|
+
existingFile &&
|
|
570
|
+
existingFile.mtime_ms === mtimeMs &&
|
|
571
|
+
existingFile.size_bytes === sizeBytes
|
|
572
|
+
) {
|
|
573
|
+
return false; // Completely up to date
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const content = readFileSync(fullPath, "utf-8");
|
|
577
|
+
const currentHash = computeContentHash(content);
|
|
578
|
+
|
|
579
|
+
// Tier 2: Hash verification
|
|
580
|
+
if (existingFile && existingFile.content_hash === currentHash) {
|
|
581
|
+
this.repo.updateFileMetadataOnly(existingFile.id, mtimeMs, sizeBytes);
|
|
582
|
+
return false;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const lineCount = content.split("\n").length;
|
|
586
|
+
const archetype = detectArchetype(relPath, targetDomainCfg.archetypes ?? {});
|
|
587
|
+
const ast = await this.parserEngine.parseFile(fullPath, content);
|
|
588
|
+
|
|
589
|
+
this.repo.runInTransaction(() => {
|
|
590
|
+
const fileId = this.repo.upsertFile(
|
|
591
|
+
domainId,
|
|
592
|
+
relPath,
|
|
593
|
+
archetype,
|
|
594
|
+
currentHash,
|
|
595
|
+
lineCount,
|
|
596
|
+
mtimeMs,
|
|
597
|
+
sizeBytes
|
|
598
|
+
);
|
|
599
|
+
this.repo.replaceFileSymbols(fileId, ast.symbols);
|
|
600
|
+
this.repo.replaceFileDependencies(fileId, ast.dependencies);
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
return true;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|