septum 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +100 -0
  3. package/bin/septum.ts +4 -0
  4. package/package.json +62 -0
  5. package/src/cli/commands/check.ts +161 -0
  6. package/src/cli/commands/hook.ts +188 -0
  7. package/src/cli/commands/ingest.ts +29 -0
  8. package/src/cli/commands/init.ts +211 -0
  9. package/src/cli/commands/locate.ts +61 -0
  10. package/src/cli/commands/query.ts +64 -0
  11. package/src/cli/commands/serve.ts +5 -0
  12. package/src/cli/commands/slice.ts +90 -0
  13. package/src/cli/commands/sync.ts +31 -0
  14. package/src/cli/index.ts +178 -0
  15. package/src/cli/templates/hooks.ts +75 -0
  16. package/src/cli/templates/rules.ts +38 -0
  17. package/src/cli/templates/skill.ts +66 -0
  18. package/src/core/boundary/evaluator.ts +210 -0
  19. package/src/core/config/loader.ts +84 -0
  20. package/src/core/config/schema.ts +39 -0
  21. package/src/core/database/client.ts +132 -0
  22. package/src/core/database/repositories/dependency.repository.ts +279 -0
  23. package/src/core/database/repositories/domain.repository.ts +198 -0
  24. package/src/core/database/repositories/file.repository.ts +99 -0
  25. package/src/core/database/repositories/meta.repository.ts +33 -0
  26. package/src/core/database/repositories/symbol.repository.ts +424 -0
  27. package/src/core/database/repository.ts +242 -0
  28. package/src/core/database/schema.sql +92 -0
  29. package/src/core/discovery/topology-detector.ts +418 -0
  30. package/src/core/ingestion/hasher.ts +5 -0
  31. package/src/core/ingestion/pipeline.ts +438 -0
  32. package/src/core/parser/boundary-tracker.ts +128 -0
  33. package/src/core/parser/extractors/base.ts +60 -0
  34. package/src/core/parser/extractors/frontend.ts +194 -0
  35. package/src/core/parser/extractors/go.ts +176 -0
  36. package/src/core/parser/extractors/laravel-semantic.ts +426 -0
  37. package/src/core/parser/extractors/mcp-cli-semantic.ts +199 -0
  38. package/src/core/parser/extractors/nestjs-semantic.ts +119 -0
  39. package/src/core/parser/extractors/php.ts +149 -0
  40. package/src/core/parser/extractors/python.ts +226 -0
  41. package/src/core/parser/extractors/registry.ts +49 -0
  42. package/src/core/parser/extractors/semantic-extractor.interface.ts +21 -0
  43. package/src/core/parser/extractors/typescript.ts +177 -0
  44. package/src/core/parser/tree-sitter.ts +39 -0
  45. package/src/core/resolver/call-graph-tracer.ts +668 -0
  46. package/src/core/resolver/module-resolver.ts +323 -0
  47. package/src/core/resolver/symbol-locator.ts +405 -0
  48. package/src/core/resolver/vertical-slice-tracer.ts +236 -0
  49. package/src/core/session/session-manager.ts +56 -0
  50. package/src/core/telemetry/telemetry.ts +86 -0
  51. package/src/index.ts +18 -0
  52. package/src/mcp/schemas.ts +68 -0
  53. package/src/mcp/server.ts +602 -0
  54. package/src/mcp/tools/check-boundary.ts +102 -0
  55. package/src/mcp/tools/get-domain-catalog.ts +91 -0
  56. package/src/mcp/tools/get-feature-context.ts +116 -0
  57. package/src/mcp/tools/get-symbol-hotspots.ts +49 -0
  58. package/src/mcp/tools/get-symbol-impact.ts +26 -0
  59. package/src/mcp/tools/get-symbol.ts +63 -0
  60. package/src/mcp/tools/locate-symbol.ts +74 -0
  61. package/src/mcp/tools/register-domain.ts +64 -0
  62. package/src/mcp/tools/trace-vertical-slice.ts +102 -0
  63. package/src/types/index.ts +296 -0
@@ -0,0 +1,75 @@
1
+ export const SEPTUM_HOOKS_CONFIG = {
2
+ "septum-boundary-guard": {
3
+ "PreToolUse": [
4
+ {
5
+ "matcher": "write_to_file|replace_file_content",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "sh -c 'HOOK=\"$(git rev-parse --show-toplevel 2>/dev/null)/.agents/hooks/septum-pre-write.sh\"; if [ -f \"$HOOK\" ]; then exec \"$HOOK\"; else exit 0; fi'",
10
+ "timeout": 10,
11
+ },
12
+ ],
13
+ },
14
+ ],
15
+ "PostToolUse": [
16
+ {
17
+ "matcher": "write_to_file|replace_file_content",
18
+ "hooks": [
19
+ {
20
+ "type": "command",
21
+ "command": "sh -c 'HOOK=\"$(git rev-parse --show-toplevel 2>/dev/null)/.agents/hooks/septum-post-write.sh\"; if [ -f \"$HOOK\" ]; then exec \"$HOOK\"; else exit 0; fi'",
22
+ "timeout": 15,
23
+ },
24
+ ],
25
+ },
26
+ ],
27
+ },
28
+ };
29
+
30
+ export const SEPTUM_PRE_WRITE_SCRIPT = `#!/usr/bin/env sh
31
+ # Septum 3-Gate Boundary Guard Pre-Write Hook
32
+ # Intercepts write_to_file and replace_file_content before disk mutation
33
+
34
+ ROOT_DIR="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
35
+ if [ ! -f "$ROOT_DIR/.septum/septum.db" ]; then
36
+ exit 0
37
+ fi
38
+
39
+ # Delegate directly to Septum's active 3-gate hook engine
40
+ if command -v bun >/dev/null 2>&1 && [ -f "$ROOT_DIR/bin/septum.ts" ]; then
41
+ exec bun run "$ROOT_DIR/bin/septum.ts" hook pre-write
42
+ elif command -v septum >/dev/null 2>&1; then
43
+ exec septum hook pre-write
44
+ elif [ -f "$ROOT_DIR/node_modules/.bin/septum" ]; then
45
+ exec "$ROOT_DIR/node_modules/.bin/septum" hook pre-write
46
+ elif command -v bunx >/dev/null 2>&1; then
47
+ exec bunx septum hook pre-write
48
+ fi
49
+
50
+ exit 0
51
+ `;
52
+
53
+ export const SEPTUM_POST_WRITE_SCRIPT = `#!/usr/bin/env sh
54
+ # Septum Automated Incremental Post-Write Hook
55
+ # Syncs modified AST into SQLite SSOT after file mutations
56
+
57
+ ROOT_DIR="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
58
+ if [ ! -f "$ROOT_DIR/.septum/septum.db" ]; then
59
+ exit 0
60
+ fi
61
+
62
+ # Trigger background incremental sync
63
+ if command -v bun >/dev/null 2>&1 && [ -f "$ROOT_DIR/bin/septum.ts" ]; then
64
+ bun run "$ROOT_DIR/bin/septum.ts" sync --json >/dev/null 2>&1 &
65
+ elif command -v septum >/dev/null 2>&1; then
66
+ septum sync --json >/dev/null 2>&1 &
67
+ elif [ -f "$ROOT_DIR/node_modules/.bin/septum" ]; then
68
+ "$ROOT_DIR/node_modules/.bin/septum" sync --json >/dev/null 2>&1 &
69
+ elif command -v bunx >/dev/null 2>&1; then
70
+ bunx septum sync --json >/dev/null 2>&1 &
71
+ fi
72
+
73
+ exit 0
74
+ `;
75
+
@@ -0,0 +1,38 @@
1
+ export const SEPTUM_RULES_TEMPLATE = `# Septum Bounded-Context & Anti-Poisoning Partition Rules
2
+
3
+ > **Prinsip Utama:** Proyek ini menggunakan **Septum** untuk menegakkan batasan arsitektur (bounded-context) secara deterministik. Setiap AI Coding Agent wajib mematuhi aturan batas domain dan touchpoint berikut.
4
+
5
+ ---
6
+
7
+ ## 1. Single Source of Truth (SSOT) via Septum MCP
8
+
9
+ Sebelum menulis atau mengedit file backend/domain:
10
+ 1. **Dilarang Menebak Kontrak Domain:**
11
+ Gunakan tool MCP \`septum_get_domain_catalog(domain: "...")\` untuk memeriksa file, arketipe, dan signature publik yang sah di dalam domain tersebut.
12
+ 2. **Kunci Scope pada Fitur Aktif:**
13
+ Jika sedang mengerjakan fitur spesifik yang terdaftar di katalog Septum, panggil \`septum_get_feature_context(feature: "...")\`.
14
+ * **Wajib patuhi \`allowed_touchpoints\`:** Jangan menyentuh atau memodifikasi file di luar daftar yang diizinkan untuk fitur tersebut.
15
+ * **Wajib gunakan ulang \`reuse_symbols\`:** Jangan membuat fungsi atau helper duplikat jika simbol tersebut sudah terdaftar di \`reuse_symbols\`.
16
+
17
+ ---
18
+
19
+ ## 2. Pre-Flight Boundary Check Sebelum Menulis Kode
20
+
21
+ Sebelum mengeksekusi penulisan file (\`write_to_file\` atau \`replace_file_content\`):
22
+ 1. Panggil tool MCP:
23
+ \`\`\`json
24
+ septum_check_boundary({
25
+ "file_path": "path/ke/file.ts",
26
+ "proposed_imports": ["target_import_statement"],
27
+ "feature_key": "nama_fitur_opsional"
28
+ })
29
+ \`\`\`
30
+ 2. **Rejection Handling:** Jika Septum mengembalikan status \`rejected\` (\`forbidden_dependency\`, \`disallowed_dependency\`, atau \`touchpoint_violation\`), **BATALKAN** rencana penulisan tersebut dan cari rute arsitektur yang sah (misal melalui Interface/Shared-Kernel yang diizinkan).
31
+
32
+ ---
33
+
34
+ ## 3. Anti Local Scope Myopia
35
+
36
+ * Dilarang menghapus fungsi publik atau mengubah signature class hanya karena fungsi tersebut tidak terlihat dipanggil di file lokal yang sedang diedit.
37
+ * Pastikan seluruh dependensi inbound/outbound mematuhi batasan domain yang terdaftar di database Septum (\`.septum/septum.db\`).
38
+ `;
@@ -0,0 +1,66 @@
1
+ export const SEPTUM_MAP_SKILL_TEMPLATE = `---
2
+ name: septum-map
3
+ description: Framework-agnostic codebase discovery, architectural domain mapping, and bounded-context initialization for Septum. Uses AI reasoning to map unknown project topologies (JS/TS, PHP, Python, Go), auto-registers bounded contexts into Septum's SQLite catalog, and enforces deterministic architectural boundaries.
4
+ ---
5
+
6
+ # \`septum-map\` — Framework-Agnostic Semantic Discovery & Bounded-Context Initialization
7
+
8
+ > **Prinsip Utama:** AI Agent berperan sebagai *The Architect* untuk membedah topologi arsitektur proyek apa pun tanpa terikat pada framework tertentu, merumuskan batas bounded-context, dan mendaftarkannya langsung ke database deterministik (.septum/septum.db via septum_register_domain) agar AI coding berikutnya tidak mengalami context poisoning atau over-editing.
9
+
10
+ ---
11
+
12
+ ## 1. Landasan Riset & Filosofi Desain
13
+
14
+ Skill ini berakar langsung pada riset rekayasa perangkat lunak terkemuka:
15
+ 1. **Software Reflexion Models (Murphy, Notkin, & Sullivan, IEEE TSE):**
16
+ Membedakan model arsitektur tingkat tinggi yang dirancang (*high-level intention*) dengan implementasi fisik di kode sumber. Septum mendeteksi *Divergences* (pelanggaran baru) dan *Absences* sebelum kode di-merge.
17
+ 2. **Eliminasi Over-Editing (SWE-bench, RepoCoder - NeurIPS / ICLR):**
18
+ Akar kegagalan agen AI terbesar adalah mengedit file di luar lokus tugas (*local scope myopia*). \`septum-map\` menetapkan *bounded context* dan *allowed touchpoints* sejak awal.
19
+ 3. **Principle of Least Privilege & Object-Capabilities (Miller):**
20
+ Agen hanya diberikan kapabilitas membaca dan mengubah file dalam domain/fitur aktif, memangkas *blast radius* kesalahan hingga mendekati nol.
21
+ 4. **Hybrid Defense (SynCode / NeMo Guardrails):**
22
+ Probabilistic AI digunakan untuk penalaran awal (*discovery & blueprinting*), sementara eksekusi dan penegakannya 100% deterministik (*Tree-sitter WASM, SQLite WAL, Pre-Commit Hooks*).
23
+
24
+ ---
25
+
26
+ ## 2. Kapan Wajib Menggunakan (Trigger Moments)
27
+
28
+ Gunakan skill ini saat:
29
+ 1. Menghubungkan Septum pertama kali ke repositori baru atau codebase yang belum terpetakan.
30
+ 2. Memetakan ulang domain bisnis setelah terjadi refactoring arsitektur besar (*re-mapping*).
31
+ 3. Mengonfigurasi fitur baru (\`features\` session) untuk mengunci daftar file yang boleh dimodifikasi (*allowed_touchpoints*) dan simbol yang wajib dipakai ulang (*reuse_symbols*).
32
+ 4. Pengguna meminta inisialisasi cerdas berbasis pemahaman AI pada struktur proyek.
33
+
34
+ ---
35
+
36
+ ## 3. Alur Kerja 4 Langkah (The 4-Step Discovery Lifecycle)
37
+
38
+ ### Langkah 1: Topological Scan (Maksimal 2 Tool Call)
39
+ Analisis karakteristik proyek secara framework-agnostic:
40
+ 1. **Periksa Manifest & Config Root:**
41
+ * JS/TS: \`package.json\`, \`tsconfig.json\` (baca dependencies, path aliases).
42
+ * PHP: \`composer.json\` (baca \`autoload.psr-4\`, framework dependencies).
43
+ * Python: \`pyproject.toml\`, \`requirements.txt\`, \`setup.py\` (baca modules & framework).
44
+ * Go: \`go.mod\` (baca module path).
45
+ 2. **Petakan Direktori Tingkat Atas:**
46
+ Identifikasi apakah proyek menggunakan pola:
47
+ * *Domain-Driven / Vertical Slice:* \`src/domains/\`, \`app/Domain/\`, \`features/\`.
48
+ * *Layered / Clean Architecture:* \`core/\`, \`infrastructure/\`, \`application/\`, \`interfaces/\`.
49
+ * *Framework Conventional:* NestJS modules, Laravel app folders, FastAPI routers, Next.js route groups.
50
+
51
+ ### Langkah 2: Semantic Blueprint Drafting & Domain Boundary Formulation
52
+ Rumuskan spesifikasi domain dengan prinsip **Contract & Invariants (Bukan Prosedural)**:
53
+ * **\`root\`:** Direktori fisik domain (gunakan path kanonikal).
54
+ * **\`allowed_dependencies\`:** Domain mana saja yang sah diimpor (inbound whitelist).
55
+ * **\`forbidden_dependencies\`:** Domain yang dilarang keras diimpor (outbound blacklist).
56
+ * **\`archetypes\`:** Pola glob penamaan layer (\`service: "*Service.*"\`, \`model: "*Model.*"\`).
57
+
58
+ ### Langkah 3: Human-in-the-Loop Approval Gate
59
+ Tampilkan ringkasan domain boundary yang dirumuskan kepada pengguna / Tech Lead untuk disetujui atau disesuaikan sebelum didaftarkan.
60
+
61
+ ### Langkah 4: Deterministic Registration & Ingestion
62
+ Setelah disetujui:
63
+ 1. Daftarkan domain secara langsung via tool MCP \`septum_register_domain(domain_name, root, ...)\` dengan opsi \`ingest_now: true\`.
64
+ 2. Atau jalankan CLI \`septum ingest\` untuk mengekstrak seluruh simbol AST dan menyimpannya ke database SQLite \`.septum/septum.db\`.
65
+ 3. Pastikan hook pre-commit terpasang aktif di \`.git/hooks/pre-commit\`.
66
+ `;
@@ -0,0 +1,210 @@
1
+ import type { BoundaryViolation } from "../../types/index.ts";
2
+ import type { ValidatedSeptumConfig } from "../config/schema.ts";
3
+ import { ConfigLoader } from "../config/loader.ts";
4
+ import type { SeptumRepository } from "../database/repository.ts";
5
+ import { ModuleResolver } from "../resolver/module-resolver.ts";
6
+
7
+ export class BoundaryEvaluator {
8
+ private repo: SeptumRepository;
9
+ private resolver: ModuleResolver;
10
+
11
+ constructor(repo: SeptumRepository, resolver?: ModuleResolver) {
12
+ this.repo = repo;
13
+ this.resolver = resolver ?? new ModuleResolver(process.cwd(), repo);
14
+ }
15
+
16
+ public getResolver(): ModuleResolver {
17
+ return this.resolver;
18
+ }
19
+
20
+ public evaluate(
21
+ filePath: string,
22
+ proposedImports: string[] = [],
23
+ featureKey?: string,
24
+ config?: ValidatedSeptumConfig
25
+ ): BoundaryViolation[] {
26
+ const activeConfig = config ?? ConfigLoader.loadFromDatabaseOrDefaults();
27
+ const violations: BoundaryViolation[] = [];
28
+
29
+ if (featureKey) {
30
+ violations.push(...this.checkFeatureTouchpoints(featureKey, filePath, activeConfig));
31
+ }
32
+
33
+ if (proposedImports.length > 0) {
34
+ violations.push(...this.checkProposedChanges(filePath, proposedImports, activeConfig));
35
+ }
36
+
37
+ return violations;
38
+ }
39
+
40
+ public auditCodebase(config: ValidatedSeptumConfig): BoundaryViolation[] {
41
+ const violations: BoundaryViolation[] = [];
42
+ const allDependencies = this.repo.getAllDependenciesWithDomains();
43
+
44
+ for (const dep of allDependencies) {
45
+ const targetDomain = this.resolveTargetDomain(dep.target, config, dep.source_file);
46
+ if (!targetDomain || targetDomain === dep.source_domain) {
47
+ continue;
48
+ }
49
+
50
+ const sourceDomainConfig = config.domains[dep.source_domain];
51
+ if (!sourceDomainConfig) {
52
+ continue;
53
+ }
54
+
55
+ const forbidden = sourceDomainConfig.forbidden_dependencies ?? [];
56
+ const allowed = sourceDomainConfig.allowed_dependencies ?? [];
57
+
58
+ // 1. Explicit forbidden check
59
+ if (forbidden.includes(targetDomain)) {
60
+ violations.push({
61
+ file: dep.source_file,
62
+ line: dep.line_number,
63
+ source_domain: dep.source_domain,
64
+ target_domain: targetDomain,
65
+ imported_target: dep.target,
66
+ rule: "forbidden_dependency",
67
+ message: `Forbidden cross-domain dependency detected: '${dep.source_domain}' is strictly prohibited from importing domain '${targetDomain}'.`,
68
+ });
69
+ continue;
70
+ }
71
+
72
+ // 2. Strict enforcement allowed check
73
+ if (config.settings.enforcement === "strict" && !allowed.includes(targetDomain)) {
74
+ violations.push({
75
+ file: dep.source_file,
76
+ line: dep.line_number,
77
+ source_domain: dep.source_domain,
78
+ target_domain: targetDomain,
79
+ imported_target: dep.target,
80
+ rule: "disallowed_dependency",
81
+ message: `Disallowed dependency: '${targetDomain}' is not declared in allowed_dependencies for domain '${dep.source_domain}'.`,
82
+ });
83
+ }
84
+ }
85
+
86
+ return violations;
87
+ }
88
+
89
+ public checkProposedChanges(
90
+ sourceFilePath: string,
91
+ proposedImports: string[],
92
+ config: ValidatedSeptumConfig
93
+ ): BoundaryViolation[] {
94
+ const violations: BoundaryViolation[] = [];
95
+ const sourceDomain = this.resolveSourceDomain(sourceFilePath, config);
96
+
97
+ if (!sourceDomain) {
98
+ return violations;
99
+ }
100
+
101
+ const sourceDomainConfig = config.domains[sourceDomain];
102
+ if (!sourceDomainConfig) {
103
+ return violations;
104
+ }
105
+
106
+ const forbidden = sourceDomainConfig.forbidden_dependencies ?? [];
107
+ const allowed = sourceDomainConfig.allowed_dependencies ?? [];
108
+
109
+ for (const statement of proposedImports) {
110
+ const targetDomain = this.resolveTargetDomain(statement, config, sourceFilePath);
111
+ if (!targetDomain || targetDomain === sourceDomain) {
112
+ continue;
113
+ }
114
+
115
+ if (forbidden.includes(targetDomain)) {
116
+ violations.push({
117
+ file: sourceFilePath,
118
+ line: 1,
119
+ source_domain: sourceDomain,
120
+ target_domain: targetDomain,
121
+ imported_target: statement,
122
+ rule: "forbidden_dependency",
123
+ message: `Illegal import blocked: Domain '${sourceDomain}' is strictly forbidden from importing domain '${targetDomain}'.`,
124
+ });
125
+ } else if (config.settings.enforcement === "strict" && !allowed.includes(targetDomain)) {
126
+ violations.push({
127
+ file: sourceFilePath,
128
+ line: 1,
129
+ source_domain: sourceDomain,
130
+ target_domain: targetDomain,
131
+ imported_target: statement,
132
+ rule: "disallowed_dependency",
133
+ message: `Unlisted domain dependency: '${targetDomain}' must be explicitly added to allowed_dependencies before domain '${sourceDomain}' can import it.`,
134
+ });
135
+ }
136
+ }
137
+
138
+ return violations;
139
+ }
140
+
141
+ public resolveSourceDomain(filePath: string, config: ValidatedSeptumConfig): string | null {
142
+ return this.resolver.resolveSourceDomain(filePath, config);
143
+ }
144
+
145
+ public resolveTargetDomain(
146
+ targetStatement: string,
147
+ config: ValidatedSeptumConfig,
148
+ sourceFilePath?: string
149
+ ): string | null {
150
+ return this.resolver.resolveTargetDomain(targetStatement, sourceFilePath, config);
151
+ }
152
+
153
+ public checkFeatureTouchpoints(
154
+ featureKey: string,
155
+ targetFilePath: string,
156
+ config: ValidatedSeptumConfig
157
+ ): BoundaryViolation[] {
158
+ const violations: BoundaryViolation[] = [];
159
+ const features = config.features ?? {};
160
+ const featureConfig = features[featureKey];
161
+
162
+ if (!featureConfig) {
163
+ violations.push({
164
+ file: targetFilePath,
165
+ line: 1,
166
+ source_domain: "unknown",
167
+ target_domain: "unknown",
168
+ imported_target: targetFilePath,
169
+ rule: "unknown_feature",
170
+ message: `Feature '${featureKey}' is not defined in septum configuration.`,
171
+ });
172
+ return violations;
173
+ }
174
+
175
+ const normalizedTarget = targetFilePath.replace(/\\/g, "/").replace(/^\.\//, "");
176
+ const allowedTouchpoints = featureConfig.allowed_touchpoints.map((tp) =>
177
+ tp.replace(/\\/g, "/").replace(/^\.\//, "")
178
+ );
179
+
180
+ const isAllowed = allowedTouchpoints.some((pattern) => {
181
+ if (pattern.endsWith("/**")) {
182
+ const prefix = pattern.slice(0, -3);
183
+ return normalizedTarget.startsWith(prefix);
184
+ }
185
+ if (pattern.endsWith("/*")) {
186
+ const prefix = pattern.slice(0, -2);
187
+ return normalizedTarget.startsWith(prefix);
188
+ }
189
+ return (
190
+ normalizedTarget === pattern ||
191
+ normalizedTarget.endsWith("/" + pattern) ||
192
+ pattern.endsWith("/" + normalizedTarget)
193
+ );
194
+ });
195
+
196
+ if (!isAllowed) {
197
+ violations.push({
198
+ file: targetFilePath,
199
+ line: 1,
200
+ source_domain: featureConfig.domain,
201
+ target_domain: "unknown",
202
+ imported_target: targetFilePath,
203
+ rule: "touchpoint_violation",
204
+ message: `Scope violation: File '${targetFilePath}' is outside the declared allowed_touchpoints for feature '${featureKey}'. Permitted: [${allowedTouchpoints.join(", ")}].`,
205
+ });
206
+ }
207
+
208
+ return violations;
209
+ }
210
+ }
@@ -0,0 +1,84 @@
1
+ import { existsSync } from "node:fs";
2
+ import { Database } from "bun:sqlite";
3
+ import type { ValidatedSeptumConfig } from "./schema.ts";
4
+ import { TopologyDetector } from "../discovery/topology-detector.ts";
5
+ import { SeptumDatabase } from "../database/client.ts";
6
+ import { SeptumRepository } from "../database/repository.ts";
7
+
8
+ export class ConfigLoader {
9
+ /**
10
+ * Loads configuration with SQLite as the Single Source of Truth (SSOT).
11
+ * Automatically executes Zero-Config TopologyDetector if the database has no domains.
12
+ */
13
+ public static load(dbPath: string = ".septum/septum.db"): ValidatedSeptumConfig {
14
+ // 1. If database exists and has registered domains, load directly from SQLite SSOT
15
+ if (existsSync(dbPath)) {
16
+ const dbConfig = ConfigLoader.loadFromDatabaseOrDefaults(dbPath);
17
+ if (Object.keys(dbConfig.domains).length > 0) {
18
+ return dbConfig;
19
+ }
20
+ }
21
+
22
+ // 2. Zero-Config Auto-Discovery: Discover project topology and persist to SQLite
23
+ const septumDb = new SeptumDatabase(dbPath);
24
+ const repo = new SeptumRepository(septumDb.raw);
25
+ TopologyDetector.discoverAndPersist(repo, process.cwd());
26
+ septumDb.close();
27
+
28
+ return ConfigLoader.loadFromDatabaseOrDefaults(dbPath);
29
+ }
30
+
31
+ public static getDefaultConfig(): ValidatedSeptumConfig {
32
+ return {
33
+ version: "1.0",
34
+ settings: {
35
+ enforcement: "warn",
36
+ db_path: ".septum/septum.db",
37
+ ignore_patterns: [
38
+ "node_modules/**",
39
+ "vendor/**",
40
+ "dist/**",
41
+ "build/**",
42
+ "tests/**",
43
+ ".git/**",
44
+ ".septum/**",
45
+ ],
46
+ },
47
+ domains: {},
48
+ features: {},
49
+ };
50
+ }
51
+
52
+ public static loadFromDatabaseOrDefaults(dbPath: string = ".septum/septum.db"): ValidatedSeptumConfig {
53
+ const config = ConfigLoader.getDefaultConfig();
54
+ config.settings.db_path = dbPath;
55
+
56
+ if (existsSync(dbPath)) {
57
+ try {
58
+ const db = new Database(dbPath, { readonly: true });
59
+ const tables = db
60
+ .query<{ name: string }, [string]>(
61
+ "SELECT name FROM sqlite_master WHERE type='table' AND name = ?"
62
+ )
63
+ .get("domains");
64
+
65
+ if (tables) {
66
+ const rows = db.query<any, []>("SELECT * FROM domains").all();
67
+ for (const r of rows) {
68
+ config.domains[r.name] = {
69
+ root: r.root_path,
70
+ allowed_dependencies: JSON.parse(r.allowed_deps_json || "[]"),
71
+ forbidden_dependencies: JSON.parse(r.forbidden_deps_json || "[]"),
72
+ archetypes: JSON.parse(r.archetypes_json || "{}"),
73
+ };
74
+ }
75
+ }
76
+ db.close();
77
+ } catch {
78
+ // Fallback gracefully to default config
79
+ }
80
+ }
81
+
82
+ return config;
83
+ }
84
+ }
@@ -0,0 +1,39 @@
1
+ import { z } from "zod";
2
+
3
+ export const DomainConfigSchema = z.object({
4
+ root: z.string().min(1, "root path cannot be empty"),
5
+ description: z.string().optional(),
6
+ allowed_dependencies: z.array(z.string()).default([]),
7
+ forbidden_dependencies: z.array(z.string()).default([]),
8
+ archetypes: z.record(z.string()).optional().default({}),
9
+ });
10
+
11
+ export const FeatureConfigSchema = z.object({
12
+ domain: z.string().min(1, "domain reference is required"),
13
+ description: z.string().optional(),
14
+ allowed_touchpoints: z.array(z.string()).min(1, "allowed_touchpoints must contain at least one file/path"),
15
+ reuse_symbols: z.array(z.string()).optional().default([]),
16
+ input_contract: z.record(z.unknown()).optional().default({}),
17
+ output_contract: z.record(z.unknown()).optional().default({}),
18
+ });
19
+
20
+ export const SeptumConfigSchema = z.object({
21
+ version: z.string().default("1.0"),
22
+ settings: z
23
+ .object({
24
+ enforcement: z.enum(["strict", "warn"]).default("strict"),
25
+ db_path: z.string().default(".septum/septum.db"),
26
+ ignore_patterns: z
27
+ .array(z.string())
28
+ .default(["node_modules/**", "vendor/**", "dist/**", "build/**", "tests/**", ".git/**"]),
29
+ })
30
+ .default({
31
+ enforcement: "strict",
32
+ db_path: ".septum/septum.db",
33
+ ignore_patterns: ["node_modules/**", "vendor/**", "dist/**", "build/**", "tests/**", ".git/**"],
34
+ }),
35
+ domains: z.record(DomainConfigSchema).default({}),
36
+ features: z.record(FeatureConfigSchema).optional().default({}),
37
+ });
38
+
39
+ export type ValidatedSeptumConfig = z.infer<typeof SeptumConfigSchema>;
@@ -0,0 +1,132 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+
5
+ export class SeptumDatabase {
6
+ private db: Database;
7
+ private dbPath: string;
8
+
9
+ constructor(dbPath: string = ".septum/septum.db") {
10
+ this.dbPath = dbPath;
11
+ const dir = dirname(dbPath);
12
+ if (!existsSync(dir)) {
13
+ mkdirSync(dir, { recursive: true });
14
+ }
15
+
16
+ this.db = new Database(dbPath, { create: true });
17
+ this.configurePragmas();
18
+ this.initSchema();
19
+ }
20
+
21
+ private configurePragmas(): void {
22
+ this.db.run("PRAGMA journal_mode = WAL;");
23
+ this.db.run("PRAGMA foreign_keys = ON;");
24
+ this.db.run("PRAGMA synchronous = NORMAL;");
25
+ }
26
+
27
+ private initSchema(): void {
28
+ const schemaFile = join(import.meta.dir, "schema.sql");
29
+ if (existsSync(schemaFile)) {
30
+ const sql = readFileSync(schemaFile, "utf-8");
31
+ this.db.run(sql);
32
+ } else {
33
+ // Fallback inline schema if schema.sql is not found in compiled bundle
34
+ this.db.run(`
35
+ CREATE TABLE IF NOT EXISTS domains (
36
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
37
+ name TEXT NOT NULL UNIQUE,
38
+ root_path TEXT NOT NULL,
39
+ allowed_deps_json TEXT NOT NULL DEFAULT '[]',
40
+ forbidden_deps_json TEXT NOT NULL DEFAULT '[]',
41
+ archetypes_json TEXT NOT NULL DEFAULT '{}',
42
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
43
+ updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
44
+ );
45
+ CREATE TABLE IF NOT EXISTS files (
46
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
47
+ domain_id INTEGER NOT NULL,
48
+ path TEXT NOT NULL UNIQUE,
49
+ archetype TEXT NOT NULL DEFAULT 'unknown',
50
+ content_hash TEXT NOT NULL,
51
+ line_count INTEGER NOT NULL DEFAULT 0,
52
+ last_scanned_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
53
+ FOREIGN KEY (domain_id) REFERENCES domains(id) ON DELETE CASCADE
54
+ );
55
+ CREATE TABLE IF NOT EXISTS symbols (
56
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
57
+ file_id INTEGER NOT NULL,
58
+ name TEXT NOT NULL,
59
+ kind TEXT NOT NULL,
60
+ signature TEXT NOT NULL,
61
+ visibility TEXT NOT NULL DEFAULT 'public',
62
+ line_start INTEGER NOT NULL DEFAULT 0,
63
+ line_end INTEGER NOT NULL DEFAULT 0,
64
+ FOREIGN KEY (file_id) REFERENCES files(id) ON DELETE CASCADE
65
+ );
66
+ CREATE TABLE IF NOT EXISTS dependencies (
67
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
68
+ source_file_id INTEGER NOT NULL,
69
+ target_symbol_or_path TEXT NOT NULL,
70
+ import_statement TEXT NOT NULL,
71
+ line_number INTEGER NOT NULL DEFAULT 0,
72
+ is_external BOOLEAN NOT NULL DEFAULT 0,
73
+ FOREIGN KEY (source_file_id) REFERENCES files(id) ON DELETE CASCADE
74
+ );
75
+ CREATE TABLE IF NOT EXISTS vertical_slices (
76
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
77
+ domain_id INTEGER,
78
+ feature_key TEXT,
79
+ http_method TEXT NOT NULL,
80
+ route_uri TEXT NOT NULL,
81
+ route_name TEXT,
82
+ controller_class TEXT NOT NULL,
83
+ action_name TEXT NOT NULL,
84
+ controller_file TEXT,
85
+ controller_line INTEGER DEFAULT 0,
86
+ architecture_style TEXT NOT NULL DEFAULT 'clean',
87
+ entry_kind TEXT NOT NULL DEFAULT 'http_route',
88
+ execution_chain_json TEXT NOT NULL,
89
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
90
+ FOREIGN KEY (domain_id) REFERENCES domains(id) ON DELETE CASCADE
91
+ );
92
+ CREATE INDEX IF NOT EXISTS idx_slices_route_uri ON vertical_slices(route_uri);
93
+ CREATE INDEX IF NOT EXISTS idx_slices_route_name ON vertical_slices(route_name);
94
+ CREATE INDEX IF NOT EXISTS idx_slices_controller ON vertical_slices(controller_class, action_name);
95
+ CREATE TABLE IF NOT EXISTS repo_meta (
96
+ key TEXT PRIMARY KEY,
97
+ value TEXT NOT NULL,
98
+ updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
99
+ );
100
+ `);
101
+ }
102
+ this.migrateSchema();
103
+ }
104
+
105
+ private migrateSchema(): void {
106
+ try {
107
+ const tableInfo = this.db.query<{ name: string }, []>("PRAGMA table_info(vertical_slices)").all();
108
+ const existingCols = new Set(tableInfo.map((c) => c.name));
109
+
110
+ if (tableInfo.length > 0) {
111
+ if (existingCols.has("form_request_class") || !existingCols.has("execution_chain_json")) {
112
+ this.db.run("DROP TABLE IF EXISTS vertical_slices;");
113
+ this.initSchema();
114
+ }
115
+ }
116
+ } catch (_) {
117
+ // Ignore migration errors
118
+ }
119
+ }
120
+
121
+ public get raw(): Database {
122
+ return this.db;
123
+ }
124
+
125
+ public close(): void {
126
+ this.db.close();
127
+ }
128
+
129
+ public transaction<T>(fn: () => T): T {
130
+ return this.db.transaction(fn)();
131
+ }
132
+ }