septum 0.1.4 → 0.1.5

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 (67) 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 +188 -188
  7. package/src/cli/commands/ingest.ts +29 -29
  8. package/src/cli/commands/init.ts +211 -211
  9. package/src/cli/commands/locate.ts +61 -61
  10. package/src/cli/commands/query.ts +64 -64
  11. package/src/cli/commands/serve.ts +5 -5
  12. package/src/cli/commands/slice.ts +48 -48
  13. package/src/cli/commands/sync.ts +31 -31
  14. package/src/cli/index.ts +210 -210
  15. package/src/cli/templates/hooks.ts +75 -75
  16. package/src/cli/templates/rules.ts +38 -38
  17. package/src/cli/templates/skill.ts +69 -69
  18. package/src/core/boundary/evaluator.ts +232 -210
  19. package/src/core/config/loader.ts +97 -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 +366 -349
  23. package/src/core/database/repositories/domain.repository.ts +218 -198
  24. package/src/core/database/repositories/file.repository.ts +116 -106
  25. package/src/core/database/repositories/meta.repository.ts +33 -33
  26. package/src/core/database/repositories/symbol.repository.ts +443 -443
  27. package/src/core/database/repository.ts +264 -260
  28. package/src/core/database/schema.ts +153 -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 +584 -535
  32. package/src/core/parser/boundary-tracker.ts +288 -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 +194 -194
  36. package/src/core/parser/extractors/go.ts +229 -180
  37. package/src/core/parser/extractors/laravel-semantic.ts +1039 -901
  38. package/src/core/parser/extractors/mcp-cli-semantic.ts +199 -199
  39. package/src/core/parser/extractors/nestjs-semantic.ts +119 -119
  40. package/src/core/parser/extractors/php.ts +153 -153
  41. package/src/core/parser/extractors/python.ts +228 -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 +181 -181
  45. package/src/core/parser/tree-sitter.ts +41 -41
  46. package/src/core/resolver/call-graph-tracer.ts +666 -668
  47. package/src/core/resolver/module-resolver.ts +408 -342
  48. package/src/core/resolver/path-resolver.ts +147 -147
  49. package/src/core/resolver/symbol-locator.ts +543 -543
  50. package/src/core/resolver/vertical-slice-tracer.ts +301 -285
  51. package/src/core/session/session-manager.ts +56 -56
  52. package/src/core/telemetry/telemetry.ts +86 -86
  53. package/src/index.ts +21 -21
  54. package/src/mcp/schemas.ts +578 -286
  55. package/src/mcp/server.ts +875 -716
  56. package/src/mcp/tools/check-boundary.ts +171 -152
  57. package/src/mcp/tools/get-domain-catalog.ts +145 -121
  58. package/src/mcp/tools/get-feature-context.ts +212 -116
  59. package/src/mcp/tools/get-symbol-hotspots.ts +60 -59
  60. package/src/mcp/tools/get-symbol-impact.ts +28 -26
  61. package/src/mcp/tools/get-symbol.ts +65 -63
  62. package/src/mcp/tools/locate-symbol.ts +171 -190
  63. package/src/mcp/tools/register-domain.ts +68 -64
  64. package/src/mcp/tools/register-feature.ts +100 -0
  65. package/src/mcp/tools/trace-vertical-slice.ts +74 -48
  66. package/src/types/index.ts +335 -315
  67. 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
+ }