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.
Files changed (70) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +105 -105
  3. package/bin/septum.ts +4 -4
  4. package/package.json +63 -63
  5. package/src/cli/commands/check.ts +161 -161
  6. package/src/cli/commands/hook.ts +216 -188
  7. package/src/cli/commands/ingest.ts +29 -29
  8. package/src/cli/commands/init.ts +213 -211
  9. package/src/cli/commands/locate.ts +61 -61
  10. package/src/cli/commands/query.ts +104 -64
  11. package/src/cli/commands/serve.ts +5 -5
  12. package/src/cli/commands/slice.ts +50 -48
  13. package/src/cli/commands/sync.ts +31 -31
  14. package/src/cli/index.ts +220 -210
  15. package/src/cli/templates/hooks.ts +75 -75
  16. package/src/cli/templates/rules.ts +50 -38
  17. package/src/cli/templates/skill.ts +65 -69
  18. package/src/core/boundary/evaluator.ts +429 -210
  19. package/src/core/config/loader.ts +117 -97
  20. package/src/core/config/schema.ts +39 -39
  21. package/src/core/database/client.ts +44 -44
  22. package/src/core/database/repositories/dependency.repository.ts +552 -349
  23. package/src/core/database/repositories/domain.repository.ts +275 -198
  24. package/src/core/database/repositories/file.repository.ts +117 -106
  25. package/src/core/database/repositories/meta.repository.ts +33 -33
  26. package/src/core/database/repositories/symbol.repository.ts +482 -443
  27. package/src/core/database/repository.ts +281 -260
  28. package/src/core/database/schema.ts +154 -153
  29. package/src/core/discovery/topology-detector.ts +486 -486
  30. package/src/core/ingestion/hasher.ts +5 -5
  31. package/src/core/ingestion/pipeline.ts +606 -535
  32. package/src/core/parser/boundary-tracker.ts +433 -288
  33. package/src/core/parser/extractors/base.ts +62 -62
  34. package/src/core/parser/extractors/blade.ts +158 -158
  35. package/src/core/parser/extractors/frontend.ts +147 -194
  36. package/src/core/parser/extractors/go.ts +408 -180
  37. package/src/core/parser/extractors/laravel-semantic.ts +1045 -901
  38. package/src/core/parser/extractors/mcp-cli-semantic.ts +328 -199
  39. package/src/core/parser/extractors/nestjs-semantic.ts +134 -119
  40. package/src/core/parser/extractors/php.ts +415 -153
  41. package/src/core/parser/extractors/python.ts +460 -228
  42. package/src/core/parser/extractors/registry.ts +70 -70
  43. package/src/core/parser/extractors/semantic-extractor.interface.ts +21 -21
  44. package/src/core/parser/extractors/typescript.ts +668 -181
  45. package/src/core/parser/tree-sitter.ts +41 -41
  46. package/src/core/parser/wasm-loader.ts +271 -0
  47. package/src/core/resolver/call-graph-tracer.ts +698 -668
  48. package/src/core/resolver/module-resolver.ts +479 -342
  49. package/src/core/resolver/path-resolver.ts +147 -147
  50. package/src/core/resolver/symbol-locator.ts +543 -543
  51. package/src/core/resolver/vertical-slice-tracer.ts +323 -285
  52. package/src/core/session/session-manager.ts +56 -56
  53. package/src/core/telemetry/telemetry.ts +86 -86
  54. package/src/core/topology/environment-detector.ts +109 -6
  55. package/src/index.ts +21 -21
  56. package/src/mcp/schemas.ts +695 -286
  57. package/src/mcp/server.ts +927 -716
  58. package/src/mcp/tools/check-boundary.ts +307 -152
  59. package/src/mcp/tools/get-domain-catalog.ts +155 -121
  60. package/src/mcp/tools/get-environment-topology.ts +3 -6
  61. package/src/mcp/tools/get-feature-context.ts +227 -116
  62. package/src/mcp/tools/get-symbol-hotspots.ts +54 -59
  63. package/src/mcp/tools/get-symbol-impact.ts +28 -26
  64. package/src/mcp/tools/get-symbol.ts +65 -63
  65. package/src/mcp/tools/locate-symbol.ts +169 -190
  66. package/src/mcp/tools/register-domain.ts +59 -64
  67. package/src/mcp/tools/register-feature.ts +91 -0
  68. package/src/mcp/tools/trace-vertical-slice.ts +74 -48
  69. package/src/types/index.ts +366 -315
  70. package/src/version.ts +3 -3
@@ -1,62 +1,62 @@
1
- import type { ArchetypeKind, ParsedFileAST } from "../../../types/index.ts";
2
-
3
- export interface CodeExtractor {
4
- canHandle(filePath: string): boolean;
5
- extract(filePath: string, content: string): Promise<ParsedFileAST>;
6
- }
7
-
8
- export function detectArchetype(filePath: string, archetypesMap: Record<string, string>): ArchetypeKind {
9
- const normalized = filePath.replace(/\\/g, "/");
10
-
11
- // Check user-configured archetype glob patterns
12
- for (const [archetype, pattern] of Object.entries(archetypesMap)) {
13
- if (matchesSimpleGlob(normalized, pattern)) {
14
- return archetype as ArchetypeKind;
15
- }
16
- }
17
-
18
- // Built-in convention heuristics
19
- const lower = normalized.toLowerCase();
20
- if (lower.includes("controller")) return "controller";
21
- if (lower.includes("service")) return "service";
22
- if (lower.includes("repository") || lower.includes("repo")) return "repository";
23
- if (lower.includes("model") || lower.includes("entities") || lower.includes("entity")) return "model";
24
- if (lower.includes("action")) return "action";
25
- if (lower.includes("job") || lower.includes("queue")) return "job";
26
- if (lower.includes("event")) return "event";
27
- if (lower.includes("listener")) return "listener";
28
- if (lower.includes("middleware")) return "middleware";
29
- if (lower.includes("request")) return "request";
30
- if (lower.includes("resource")) return "resource";
31
- if (lower.includes(".blade.php") || lower.includes("views/") || lower.includes("templates/")) return "view";
32
- if (lower.includes("route") || lower.includes("router")) return "route";
33
- if (lower.includes("util") || lower.includes("helper")) return "util";
34
-
35
- return "unknown";
36
- }
37
-
38
- function matchesSimpleGlob(path: string, pattern: string): boolean {
39
- const normalizedPath = path.replace(/\\/g, "/").replace(/^\.\//, "");
40
- const normalizedPattern = pattern.replace(/\\/g, "/").replace(/^\.\//, "");
41
-
42
- const escaped = normalizedPattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
43
- const regexStr = escaped
44
- .replace(/\*\*\//g, "(?:.*\\/)?")
45
- .replace(/\*\*/g, ".*")
46
- .replace(/\*/g, "[^/]*")
47
- .replace(/\?/g, "[^/]");
48
-
49
- const regex = new RegExp(`^${regexStr}$`, "i");
50
- if (regex.test(normalizedPath)) {
51
- return true;
52
- }
53
-
54
- // If pattern does not contain directory separators, allow matching against the basename
55
- if (!normalizedPattern.includes("/")) {
56
- const base = normalizedPath.split("/").pop() ?? "";
57
- return regex.test(base);
58
- }
59
-
60
- return false;
61
- }
62
-
1
+ import type { ArchetypeKind, ParsedFileAST } from "../../../types/index.ts";
2
+
3
+ export interface CodeExtractor {
4
+ canHandle(filePath: string): boolean;
5
+ extract(filePath: string, content: string): Promise<ParsedFileAST>;
6
+ }
7
+
8
+ export function detectArchetype(filePath: string, archetypesMap: Record<string, string>): ArchetypeKind {
9
+ const normalized = filePath.replace(/\\/g, "/");
10
+
11
+ // Check user-configured archetype glob patterns
12
+ for (const [archetype, pattern] of Object.entries(archetypesMap)) {
13
+ if (matchesSimpleGlob(normalized, pattern)) {
14
+ return archetype as ArchetypeKind;
15
+ }
16
+ }
17
+
18
+ // Built-in convention heuristics
19
+ const lower = normalized.toLowerCase();
20
+ if (lower.includes("controller")) return "controller";
21
+ if (lower.includes("service")) return "service";
22
+ if (lower.includes("repository") || lower.includes("repo")) return "repository";
23
+ if (lower.includes("model") || lower.includes("entities") || lower.includes("entity")) return "model";
24
+ if (lower.includes("action")) return "action";
25
+ if (lower.includes("job") || lower.includes("queue")) return "job";
26
+ if (lower.includes("event")) return "event";
27
+ if (lower.includes("listener")) return "listener";
28
+ if (lower.includes("middleware")) return "middleware";
29
+ if (lower.includes("request")) return "request";
30
+ if (lower.includes("resource")) return "resource";
31
+ if (lower.includes(".blade.php") || lower.includes("views/") || lower.includes("templates/")) return "view";
32
+ if (lower.includes("route") || lower.includes("router")) return "route";
33
+ if (lower.includes("util") || lower.includes("helper")) return "util";
34
+
35
+ return "unknown";
36
+ }
37
+
38
+ function matchesSimpleGlob(path: string, pattern: string): boolean {
39
+ const normalizedPath = path.replace(/\\/g, "/").replace(/^\.\//, "");
40
+ const normalizedPattern = pattern.replace(/\\/g, "/").replace(/^\.\//, "");
41
+
42
+ const escaped = normalizedPattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
43
+ const regexStr = escaped
44
+ .replace(/\*\*\//g, "(?:.*\\/)?")
45
+ .replace(/\*\*/g, ".*")
46
+ .replace(/\*/g, "[^/]*")
47
+ .replace(/\?/g, "[^/]");
48
+
49
+ const regex = new RegExp(`^${regexStr}$`, "i");
50
+ if (regex.test(normalizedPath)) {
51
+ return true;
52
+ }
53
+
54
+ // If pattern does not contain directory separators, allow matching against the basename
55
+ if (!normalizedPattern.includes("/")) {
56
+ const base = normalizedPath.split("/").pop() ?? "";
57
+ return regex.test(base);
58
+ }
59
+
60
+ return false;
61
+ }
62
+
@@ -1,158 +1,158 @@
1
- import type {
2
- ExtractedDependency,
3
- ExtractedSymbol,
4
- ParsedFileAST,
5
- } from "../../../types/index.ts";
6
- import type { CodeExtractor } from "./base.ts";
7
-
8
- export class BladeExtractor implements CodeExtractor {
9
- public canHandle(filePath: string): boolean {
10
- return /\.blade\.php$/i.test(filePath);
11
- }
12
-
13
- public async extract(filePath: string, content: string): Promise<ParsedFileAST> {
14
- const symbols: ExtractedSymbol[] = [];
15
- const dependencies: ExtractedDependency[] = [];
16
-
17
- const lines = content.split(/\r?\n/);
18
- const lineCount = lines.length;
19
-
20
- // 1. Register the view itself as a top-level symbol
21
- const viewName = this.normalizeViewName(filePath);
22
- if (viewName) {
23
- symbols.push({
24
- name: viewName,
25
- kind: "view",
26
- signature: `@view(${viewName})`,
27
- visibility: "public",
28
- line_start: 1,
29
- line_end: Math.max(1, lineCount),
30
- line_count: Math.max(1, lineCount),
31
- });
32
-
33
- // Also register with view: prefix for explicit queries
34
- symbols.push({
35
- name: `view:${viewName}`,
36
- kind: "view",
37
- signature: `@view(${viewName})`,
38
- visibility: "public",
39
- line_start: 1,
40
- line_end: Math.max(1, lineCount),
41
- line_count: Math.max(1, lineCount),
42
- });
43
- }
44
-
45
- // 2. Line-by-line scanning for Blade directives, components, and DOM IDs
46
- for (let i = 0; i < lines.length; i++) {
47
- const line = lines[i];
48
- const trimmed = line.trim();
49
- const lineNum = i + 1;
50
-
51
- if (!trimmed) continue;
52
-
53
- // Extends: @extends('layouts.app')
54
- const extendsMatch = /@extends\s*\(\s*['"]([^'"]+)['"]\s*\)/.exec(trimmed);
55
- if (extendsMatch) {
56
- dependencies.push({
57
- target: extendsMatch[1],
58
- statement: trimmed,
59
- line_number: lineNum,
60
- is_external: false,
61
- });
62
- }
63
-
64
- // Includes: @include('partials.store_card'), @includeIf, @includeWhen, @includeUnless
65
- const includeRegex = /@include(?:If|When|Unless)?\s*\(\s*['"]([^'"]+)['"]/g;
66
- let incMatch: RegExpExecArray | null;
67
- while ((incMatch = includeRegex.exec(trimmed)) !== null) {
68
- dependencies.push({
69
- target: incMatch[1],
70
- statement: trimmed,
71
- line_number: lineNum,
72
- is_external: false,
73
- });
74
- }
75
-
76
- // Blade Components: <x-store.search-bar or <x-alert
77
- const componentRegex = /<x-([a-zA-Z0-9_.\-]+)/g;
78
- let compMatch: RegExpExecArray | null;
79
- while ((compMatch = componentRegex.exec(trimmed)) !== null) {
80
- const compTag = compMatch[1];
81
- // Convert foo-bar to components.foo-bar or components.foo.bar
82
- const compTarget = `components.${compTag.replace(/-/g, ".")}`;
83
- dependencies.push({
84
- target: compTarget,
85
- statement: trimmed,
86
- line_number: lineNum,
87
- is_external: false,
88
- });
89
- symbols.push({
90
- name: `<x-${compTag}>`,
91
- kind: "component",
92
- signature: `<x-${compTag}>`,
93
- visibility: "public",
94
- line_start: lineNum,
95
- line_end: lineNum,
96
- line_count: 1,
97
- });
98
- }
99
-
100
- // Livewire: @livewire('search-dropdown')
101
- const livewireMatch = /@livewire\s*\(\s*['"]([^'"]+)['"]/.exec(trimmed);
102
- if (livewireMatch) {
103
- dependencies.push({
104
- target: `livewire:${livewireMatch[1]}`,
105
- statement: trimmed,
106
- line_number: lineNum,
107
- is_external: false,
108
- });
109
- }
110
-
111
- // Section: @section('content')
112
- const sectionMatch = /@section\s*\(\s*['"]([^'"]+)['"]/.exec(trimmed);
113
- if (sectionMatch) {
114
- symbols.push({
115
- name: `section:${sectionMatch[1]}`,
116
- kind: "property",
117
- signature: `@section('${sectionMatch[1]}')`,
118
- visibility: "public",
119
- line_start: lineNum,
120
- line_end: lineNum,
121
- line_count: 1,
122
- });
123
- }
124
-
125
- // DOM ID attribute: id="laptop-slider" or id='laptop-slider'
126
- const idMatch = /\bid=["']([a-zA-Z0-9_\-]+)["']/.exec(trimmed);
127
- if (idMatch) {
128
- const elementId = idMatch[1];
129
- symbols.push({
130
- name: `#${elementId}`,
131
- kind: "property",
132
- signature: `id="${elementId}"`,
133
- visibility: "public",
134
- line_start: lineNum,
135
- line_end: lineNum,
136
- line_count: 1,
137
- });
138
- }
139
- }
140
-
141
- return { symbols, dependencies };
142
- }
143
-
144
- /**
145
- * Derive Laravel view name from path, e.g.:
146
- * resources/views/partials/store_card.blade.php -> partials.store_card
147
- */
148
- private normalizeViewName(filePath: string): string | null {
149
- const clean = filePath.replace(/\\/g, "/");
150
- const viewsIdx = clean.indexOf("views/");
151
- if (viewsIdx !== -1) {
152
- const sub = clean.slice(viewsIdx + 6).replace(/\.blade\.php$/i, "");
153
- return sub.replace(/\//g, ".");
154
- }
155
- const base = clean.split("/").pop()?.replace(/\.blade\.php$/i, "");
156
- return base || null;
157
- }
158
- }
1
+ import type {
2
+ ExtractedDependency,
3
+ ExtractedSymbol,
4
+ ParsedFileAST,
5
+ } from "../../../types/index.ts";
6
+ import type { CodeExtractor } from "./base.ts";
7
+
8
+ export class BladeExtractor implements CodeExtractor {
9
+ public canHandle(filePath: string): boolean {
10
+ return /\.blade\.php$/i.test(filePath);
11
+ }
12
+
13
+ public async extract(filePath: string, content: string): Promise<ParsedFileAST> {
14
+ const symbols: ExtractedSymbol[] = [];
15
+ const dependencies: ExtractedDependency[] = [];
16
+
17
+ const lines = content.split(/\r?\n/);
18
+ const lineCount = lines.length;
19
+
20
+ // 1. Register the view itself as a top-level symbol
21
+ const viewName = this.normalizeViewName(filePath);
22
+ if (viewName) {
23
+ symbols.push({
24
+ name: viewName,
25
+ kind: "view",
26
+ signature: `@view(${viewName})`,
27
+ visibility: "public",
28
+ line_start: 1,
29
+ line_end: Math.max(1, lineCount),
30
+ line_count: Math.max(1, lineCount),
31
+ });
32
+
33
+ // Also register with view: prefix for explicit queries
34
+ symbols.push({
35
+ name: `view:${viewName}`,
36
+ kind: "view",
37
+ signature: `@view(${viewName})`,
38
+ visibility: "public",
39
+ line_start: 1,
40
+ line_end: Math.max(1, lineCount),
41
+ line_count: Math.max(1, lineCount),
42
+ });
43
+ }
44
+
45
+ // 2. Line-by-line scanning for Blade directives, components, and DOM IDs
46
+ for (let i = 0; i < lines.length; i++) {
47
+ const line = lines[i];
48
+ const trimmed = line.trim();
49
+ const lineNum = i + 1;
50
+
51
+ if (!trimmed) continue;
52
+
53
+ // Extends: @extends('layouts.app')
54
+ const extendsMatch = /@extends\s*\(\s*['"]([^'"]+)['"]\s*\)/.exec(trimmed);
55
+ if (extendsMatch) {
56
+ dependencies.push({
57
+ target: extendsMatch[1],
58
+ statement: trimmed,
59
+ line_number: lineNum,
60
+ is_external: false,
61
+ });
62
+ }
63
+
64
+ // Includes: @include('partials.store_card'), @includeIf, @includeWhen, @includeUnless
65
+ const includeRegex = /@include(?:If|When|Unless)?\s*\(\s*['"]([^'"]+)['"]/g;
66
+ let incMatch: RegExpExecArray | null;
67
+ while ((incMatch = includeRegex.exec(trimmed)) !== null) {
68
+ dependencies.push({
69
+ target: incMatch[1],
70
+ statement: trimmed,
71
+ line_number: lineNum,
72
+ is_external: false,
73
+ });
74
+ }
75
+
76
+ // Blade Components: <x-store.search-bar or <x-alert
77
+ const componentRegex = /<x-([a-zA-Z0-9_.\-]+)/g;
78
+ let compMatch: RegExpExecArray | null;
79
+ while ((compMatch = componentRegex.exec(trimmed)) !== null) {
80
+ const compTag = compMatch[1];
81
+ // Convert foo-bar to components.foo-bar or components.foo.bar
82
+ const compTarget = `components.${compTag.replace(/-/g, ".")}`;
83
+ dependencies.push({
84
+ target: compTarget,
85
+ statement: trimmed,
86
+ line_number: lineNum,
87
+ is_external: false,
88
+ });
89
+ symbols.push({
90
+ name: `<x-${compTag}>`,
91
+ kind: "component",
92
+ signature: `<x-${compTag}>`,
93
+ visibility: "public",
94
+ line_start: lineNum,
95
+ line_end: lineNum,
96
+ line_count: 1,
97
+ });
98
+ }
99
+
100
+ // Livewire: @livewire('search-dropdown')
101
+ const livewireMatch = /@livewire\s*\(\s*['"]([^'"]+)['"]/.exec(trimmed);
102
+ if (livewireMatch) {
103
+ dependencies.push({
104
+ target: `livewire:${livewireMatch[1]}`,
105
+ statement: trimmed,
106
+ line_number: lineNum,
107
+ is_external: false,
108
+ });
109
+ }
110
+
111
+ // Section: @section('content')
112
+ const sectionMatch = /@section\s*\(\s*['"]([^'"]+)['"]/.exec(trimmed);
113
+ if (sectionMatch) {
114
+ symbols.push({
115
+ name: `section:${sectionMatch[1]}`,
116
+ kind: "property",
117
+ signature: `@section('${sectionMatch[1]}')`,
118
+ visibility: "public",
119
+ line_start: lineNum,
120
+ line_end: lineNum,
121
+ line_count: 1,
122
+ });
123
+ }
124
+
125
+ // DOM ID attribute: id="laptop-slider" or id='laptop-slider'
126
+ const idMatch = /\bid=["']([a-zA-Z0-9_\-]+)["']/.exec(trimmed);
127
+ if (idMatch) {
128
+ const elementId = idMatch[1];
129
+ symbols.push({
130
+ name: `#${elementId}`,
131
+ kind: "property",
132
+ signature: `id="${elementId}"`,
133
+ visibility: "public",
134
+ line_start: lineNum,
135
+ line_end: lineNum,
136
+ line_count: 1,
137
+ });
138
+ }
139
+ }
140
+
141
+ return { symbols, dependencies };
142
+ }
143
+
144
+ /**
145
+ * Derive Laravel view name from path, e.g.:
146
+ * resources/views/partials/store_card.blade.php -> partials.store_card
147
+ */
148
+ private normalizeViewName(filePath: string): string | null {
149
+ const clean = filePath.replace(/\\/g, "/");
150
+ const viewsIdx = clean.indexOf("views/");
151
+ if (viewsIdx !== -1) {
152
+ const sub = clean.slice(viewsIdx + 6).replace(/\.blade\.php$/i, "");
153
+ return sub.replace(/\//g, ".");
154
+ }
155
+ const base = clean.split("/").pop()?.replace(/\.blade\.php$/i, "");
156
+ return base || null;
157
+ }
158
+ }