@soulbatical/tetra-dev-toolkit 1.7.0 → 1.8.1
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/lib/checks/codeQuality/file-size.js +48 -11
- package/lib/checks/codeQuality/naming-conventions.js +131 -0
- package/lib/checks/codeQuality/route-separation.js +233 -0
- package/lib/checks/health/naming-conventions.js +6 -3
- package/lib/config.js +19 -2
- package/lib/runner.js +5 -1
- package/package.json +1 -1
|
@@ -6,7 +6,13 @@
|
|
|
6
6
|
* - Indicate missing separation of concerns
|
|
7
7
|
* - Grow silently until they become unmovable
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* Supports differentiated limits per file type via config.codeQuality.fileSizeLimits:
|
|
10
|
+
* route: 200 (routes = wiring, no logic)
|
|
11
|
+
* controller: 400 (request/response + error handling)
|
|
12
|
+
* service: 600 (business logic, allowed to be complex)
|
|
13
|
+
* default: 400 (everything else)
|
|
14
|
+
*
|
|
15
|
+
* Falls back to config.codeQuality.maxFileLines (legacy, flat limit for all files).
|
|
10
16
|
* Scans backend + frontend source directories.
|
|
11
17
|
*/
|
|
12
18
|
|
|
@@ -18,18 +24,47 @@ export const meta = {
|
|
|
18
24
|
name: 'File Size / God File Detection',
|
|
19
25
|
category: 'codeQuality',
|
|
20
26
|
severity: 'high',
|
|
21
|
-
description: 'Detects source files exceeding
|
|
27
|
+
description: 'Detects source files exceeding size limits — god files that need splitting'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Determine file type and applicable line limit based on path/name patterns.
|
|
32
|
+
*/
|
|
33
|
+
function getFileTypeAndLimit(filePath, limits) {
|
|
34
|
+
// Route files: in routes/ directory or named *Routes.ts
|
|
35
|
+
if (/\/routes\//.test(filePath) || /Routes\.\w+$/.test(filePath)) {
|
|
36
|
+
return { type: 'route', limit: limits.route }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Controller files: named *Controller.ts
|
|
40
|
+
if (/Controller\.\w+$/.test(filePath)) {
|
|
41
|
+
return { type: 'controller', limit: limits.controller }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Service files: in services/ directory or named *Service.ts
|
|
45
|
+
if (/\/services\//.test(filePath) || /Service\.\w+$/.test(filePath)) {
|
|
46
|
+
return { type: 'service', limit: limits.service }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return { type: 'default', limit: limits.default }
|
|
22
50
|
}
|
|
23
51
|
|
|
24
52
|
export async function run(config, projectRoot) {
|
|
25
|
-
|
|
53
|
+
// Support new fileSizeLimits config, with maxFileLines as legacy fallback
|
|
54
|
+
const legacyMax = config.codeQuality?.maxFileLines || 500
|
|
55
|
+
const limits = {
|
|
56
|
+
route: config.codeQuality?.fileSizeLimits?.route ?? 200,
|
|
57
|
+
controller: config.codeQuality?.fileSizeLimits?.controller ?? 400,
|
|
58
|
+
service: config.codeQuality?.fileSizeLimits?.service ?? 600,
|
|
59
|
+
default: config.codeQuality?.fileSizeLimits?.default ?? legacyMax,
|
|
60
|
+
}
|
|
26
61
|
|
|
27
62
|
const results = {
|
|
28
63
|
passed: true,
|
|
29
64
|
findings: [],
|
|
30
65
|
summary: { total: 0, critical: 0, high: 0, medium: 0, low: 0 },
|
|
31
66
|
info: {
|
|
32
|
-
|
|
67
|
+
fileSizeLimits: limits,
|
|
33
68
|
filesScanned: 0,
|
|
34
69
|
violations: 0,
|
|
35
70
|
largest: null
|
|
@@ -93,6 +128,7 @@ export async function run(config, projectRoot) {
|
|
|
93
128
|
results.info.filesScanned++
|
|
94
129
|
|
|
95
130
|
const lineCount = content.split('\n').length
|
|
131
|
+
const { type, limit } = getFileTypeAndLimit(file, limits)
|
|
96
132
|
|
|
97
133
|
// Track largest file
|
|
98
134
|
if (lineCount > largestLines) {
|
|
@@ -100,17 +136,17 @@ export async function run(config, projectRoot) {
|
|
|
100
136
|
largestFile = file
|
|
101
137
|
}
|
|
102
138
|
|
|
103
|
-
if (lineCount <=
|
|
139
|
+
if (lineCount <= limit) continue
|
|
104
140
|
|
|
105
141
|
// Determine severity based on how far over the limit
|
|
106
|
-
const ratio = lineCount /
|
|
142
|
+
const ratio = lineCount / limit
|
|
107
143
|
let severity
|
|
108
144
|
if (ratio >= 5) {
|
|
109
|
-
severity = 'critical' // 5x over limit
|
|
145
|
+
severity = 'critical' // 5x over limit
|
|
110
146
|
} else if (ratio >= 3) {
|
|
111
|
-
severity = 'high' // 3x over limit
|
|
147
|
+
severity = 'high' // 3x over limit
|
|
112
148
|
} else if (ratio >= 2) {
|
|
113
|
-
severity = 'medium' // 2x over limit
|
|
149
|
+
severity = 'medium' // 2x over limit
|
|
114
150
|
} else {
|
|
115
151
|
severity = 'low' // Just over limit
|
|
116
152
|
}
|
|
@@ -119,9 +155,10 @@ export async function run(config, projectRoot) {
|
|
|
119
155
|
file,
|
|
120
156
|
line: 1,
|
|
121
157
|
type: 'GOD_FILE',
|
|
158
|
+
fileType: type,
|
|
122
159
|
severity,
|
|
123
|
-
message: `${file} has ${lineCount} lines (max: ${
|
|
124
|
-
fix: `Break this file into focused modules of <${
|
|
160
|
+
message: `${file} has ${lineCount} lines (${type} max: ${limit}) — split into smaller modules`,
|
|
161
|
+
fix: `Break this ${type} file into focused modules of <${limit} lines each`
|
|
125
162
|
})
|
|
126
163
|
|
|
127
164
|
results.summary[severity]++
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code Quality Check: Naming Conventions (DB + Code)
|
|
3
|
+
*
|
|
4
|
+
* Wraps the health check naming-conventions as a tetra-audit check.
|
|
5
|
+
* Enforces snake_case in DB, camelCase/PascalCase in code, lowercase dirs.
|
|
6
|
+
*
|
|
7
|
+
* DB violations: table/column naming, PK/FK conventions, bool prefixes, JSON suffixes
|
|
8
|
+
* Code violations: variable/type naming, file naming, directory casing
|
|
9
|
+
*
|
|
10
|
+
* Severity: high — inconsistent naming creates confusion and bugs
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { check as healthCheck } from '../health/naming-conventions.js'
|
|
14
|
+
|
|
15
|
+
export const meta = {
|
|
16
|
+
id: 'naming-conventions',
|
|
17
|
+
name: 'Naming Conventions',
|
|
18
|
+
category: 'codeQuality',
|
|
19
|
+
severity: 'high',
|
|
20
|
+
description: 'Enforces snake_case in DB schema, camelCase/PascalCase in TypeScript, lowercase directories'
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function run(config, projectRoot) {
|
|
24
|
+
const result = {
|
|
25
|
+
passed: true,
|
|
26
|
+
findings: [],
|
|
27
|
+
summary: { total: 0, critical: 0, high: 0, medium: 0, low: 0 },
|
|
28
|
+
details: {}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const ignoreMigrations = config.codeQuality?.namingConventions?.ignoreMigrations || []
|
|
32
|
+
const health = await healthCheck(projectRoot, { ignoreMigrations })
|
|
33
|
+
result.details = health.details
|
|
34
|
+
|
|
35
|
+
const dbViolations = health.details.database?.violations || []
|
|
36
|
+
const codeViolations = health.details.code?.violations || []
|
|
37
|
+
const dbCompliance = health.details.database?.compliancePercent ?? 100
|
|
38
|
+
const codeCompliance = health.details.code?.compliancePercent ?? 100
|
|
39
|
+
|
|
40
|
+
// DB naming violations
|
|
41
|
+
if (dbViolations.length > 0) {
|
|
42
|
+
// Separate critical (PK/FK structural) from high (naming style)
|
|
43
|
+
const structural = dbViolations.filter(v =>
|
|
44
|
+
v.includes('should be named "id"') ||
|
|
45
|
+
v.includes('should use _id suffix') ||
|
|
46
|
+
v.includes('should be "deleted_at"') ||
|
|
47
|
+
v.includes('missing created_at') ||
|
|
48
|
+
v.includes('missing updated_at') ||
|
|
49
|
+
v.includes('Inconsistent') ||
|
|
50
|
+
v.includes('Non-standard')
|
|
51
|
+
)
|
|
52
|
+
const style = dbViolations.filter(v => !structural.includes(v))
|
|
53
|
+
|
|
54
|
+
if (structural.length > 0) {
|
|
55
|
+
result.findings.push({
|
|
56
|
+
type: 'DB structural naming violations',
|
|
57
|
+
severity: 'critical',
|
|
58
|
+
message: `${structural.length} structural naming issue(s) in DB schema (PK/FK/timestamps/consistency)`,
|
|
59
|
+
files: structural.slice(0, 20)
|
|
60
|
+
})
|
|
61
|
+
result.summary.critical++
|
|
62
|
+
result.summary.total++
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (style.length > 0) {
|
|
66
|
+
result.findings.push({
|
|
67
|
+
type: 'DB naming style violations',
|
|
68
|
+
severity: 'high',
|
|
69
|
+
message: `${style.length} naming style issue(s) in DB schema (snake_case, bool prefix, JSON suffix)`,
|
|
70
|
+
files: style.slice(0, 20)
|
|
71
|
+
})
|
|
72
|
+
result.summary.high++
|
|
73
|
+
result.summary.total++
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Code naming violations
|
|
78
|
+
if (codeViolations.length > 0) {
|
|
79
|
+
const dirViolations = codeViolations.filter(v => v.includes('should be lowercase'))
|
|
80
|
+
const fileViolations = codeViolations.filter(v => v.includes('naming issue'))
|
|
81
|
+
const varViolations = codeViolations.filter(v =>
|
|
82
|
+
v.includes('should be camelCase') ||
|
|
83
|
+
v.includes('should be PascalCase') ||
|
|
84
|
+
v.includes('should not use I-prefix')
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
if (dirViolations.length > 0) {
|
|
88
|
+
result.findings.push({
|
|
89
|
+
type: 'Directory naming violations',
|
|
90
|
+
severity: 'high',
|
|
91
|
+
message: `${dirViolations.length} director(ies) not lowercase`,
|
|
92
|
+
files: dirViolations.slice(0, 10)
|
|
93
|
+
})
|
|
94
|
+
result.summary.high++
|
|
95
|
+
result.summary.total++
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (fileViolations.length > 0) {
|
|
99
|
+
result.findings.push({
|
|
100
|
+
type: 'File naming violations',
|
|
101
|
+
severity: 'medium',
|
|
102
|
+
message: `${fileViolations.length} file(s) with naming issues`,
|
|
103
|
+
files: fileViolations.slice(0, 10)
|
|
104
|
+
})
|
|
105
|
+
result.summary.medium++
|
|
106
|
+
result.summary.total++
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (varViolations.length > 0) {
|
|
110
|
+
result.findings.push({
|
|
111
|
+
type: 'Variable/type naming violations',
|
|
112
|
+
severity: 'medium',
|
|
113
|
+
message: `${varViolations.length} variable(s) or type(s) with naming issues`,
|
|
114
|
+
files: varViolations.slice(0, 10)
|
|
115
|
+
})
|
|
116
|
+
result.summary.medium++
|
|
117
|
+
result.summary.total++
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Overall compliance summary
|
|
122
|
+
result.details.dbCompliance = dbCompliance
|
|
123
|
+
result.details.codeCompliance = codeCompliance
|
|
124
|
+
|
|
125
|
+
// Fail if DB compliance < 80% or code compliance < 70%, or any critical findings
|
|
126
|
+
result.passed = result.summary.critical === 0 &&
|
|
127
|
+
dbCompliance >= 80 &&
|
|
128
|
+
codeCompliance >= 70
|
|
129
|
+
|
|
130
|
+
return result
|
|
131
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check: Route Separation (ZERO LOGIC in routes)
|
|
3
|
+
*
|
|
4
|
+
* Detects business logic leaking into route files. Based on Express.js best practices
|
|
5
|
+
* and the "thin routes, fat services" principle.
|
|
6
|
+
*
|
|
7
|
+
* Routes should only do:
|
|
8
|
+
* - Wire HTTP methods to controller/handler functions
|
|
9
|
+
* - Apply middleware (auth, validation)
|
|
10
|
+
* - Define path parameters
|
|
11
|
+
*
|
|
12
|
+
* Routes should NOT contain:
|
|
13
|
+
* - Direct database queries (Supabase .from().select() etc.)
|
|
14
|
+
* - RPC calls (.rpc())
|
|
15
|
+
* - Helper function declarations with logic
|
|
16
|
+
* - Data transformation chains (.map + .filter/.reduce)
|
|
17
|
+
*
|
|
18
|
+
* Config:
|
|
19
|
+
* routeSeparation.enabled: true (default)
|
|
20
|
+
* routeSeparation.allowDbInRoutes: false (default, set true to not block on DB findings)
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { glob } from 'glob'
|
|
24
|
+
import { readFileSync } from 'fs'
|
|
25
|
+
|
|
26
|
+
export const meta = {
|
|
27
|
+
id: 'route-separation',
|
|
28
|
+
name: 'Route Separation (Zero Logic)',
|
|
29
|
+
category: 'codeQuality',
|
|
30
|
+
severity: 'high',
|
|
31
|
+
description: 'Detects business logic in route files — routes should delegate to controllers/services'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function run(config, projectRoot) {
|
|
35
|
+
const routeConfig = config.codeQuality?.routeSeparation || {}
|
|
36
|
+
|
|
37
|
+
if (routeConfig.enabled === false) {
|
|
38
|
+
return {
|
|
39
|
+
passed: true,
|
|
40
|
+
skipped: true,
|
|
41
|
+
skipReason: 'Route separation check disabled in config',
|
|
42
|
+
findings: [],
|
|
43
|
+
summary: { total: 0, critical: 0, high: 0, medium: 0, low: 0 }
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const allowDbInRoutes = routeConfig.allowDbInRoutes || false
|
|
48
|
+
|
|
49
|
+
const results = {
|
|
50
|
+
passed: true,
|
|
51
|
+
findings: [],
|
|
52
|
+
summary: { total: 0, critical: 0, high: 0, medium: 0, low: 0 },
|
|
53
|
+
info: {
|
|
54
|
+
filesScanned: 0,
|
|
55
|
+
violations: 0,
|
|
56
|
+
allowDbInRoutes
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Find route files (excl. generated)
|
|
61
|
+
const patterns = [
|
|
62
|
+
'backend/src/**/routes/**/*.ts',
|
|
63
|
+
'src/**/routes/**/*.ts',
|
|
64
|
+
'backend/src/**/*Routes.ts',
|
|
65
|
+
'src/**/*Routes.ts',
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
let files = []
|
|
69
|
+
for (const pattern of patterns) {
|
|
70
|
+
const found = await glob(pattern, {
|
|
71
|
+
cwd: projectRoot,
|
|
72
|
+
ignore: [
|
|
73
|
+
...(config.ignore || []),
|
|
74
|
+
'node_modules/**',
|
|
75
|
+
'**/node_modules/**',
|
|
76
|
+
'**/generated/**',
|
|
77
|
+
'**/*.test.*',
|
|
78
|
+
'**/*.spec.*',
|
|
79
|
+
'**/*.d.ts',
|
|
80
|
+
]
|
|
81
|
+
})
|
|
82
|
+
files.push(...found)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
files = [...new Set(files)]
|
|
86
|
+
|
|
87
|
+
if (files.length === 0) {
|
|
88
|
+
results.skipped = true
|
|
89
|
+
results.skipReason = 'No route files found'
|
|
90
|
+
return results
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
for (const file of files) {
|
|
94
|
+
const filePath = `${projectRoot}/${file}`
|
|
95
|
+
let content
|
|
96
|
+
try {
|
|
97
|
+
content = readFileSync(filePath, 'utf-8')
|
|
98
|
+
} catch {
|
|
99
|
+
continue
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
results.info.filesScanned++
|
|
103
|
+
const lines = content.split('\n')
|
|
104
|
+
|
|
105
|
+
// Signal 1: DB queries in routes — .from( followed by .select/.insert/.update/.delete
|
|
106
|
+
const dbQueryPattern = /\.from\s*\(/
|
|
107
|
+
const dbOpPattern = /\.(select|insert|update|delete|upsert)\s*\(/
|
|
108
|
+
let hasDbQuery = false
|
|
109
|
+
for (let i = 0; i < lines.length; i++) {
|
|
110
|
+
if (dbQueryPattern.test(lines[i]) && (
|
|
111
|
+
dbOpPattern.test(lines[i]) ||
|
|
112
|
+
(i + 1 < lines.length && dbOpPattern.test(lines[i + 1])) ||
|
|
113
|
+
(i + 2 < lines.length && dbOpPattern.test(lines[i + 2]))
|
|
114
|
+
)) {
|
|
115
|
+
hasDbQuery = true
|
|
116
|
+
results.findings.push({
|
|
117
|
+
file,
|
|
118
|
+
line: i + 1,
|
|
119
|
+
type: 'DB_QUERY_IN_ROUTE',
|
|
120
|
+
severity: 'high',
|
|
121
|
+
message: `DB query in route file (line ${i + 1}) — move to service layer`,
|
|
122
|
+
fix: 'Extract database queries into a service class'
|
|
123
|
+
})
|
|
124
|
+
results.summary.high++
|
|
125
|
+
results.summary.total++
|
|
126
|
+
results.info.violations++
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Signal 2: Supabase/DB client imports/usage in routes
|
|
131
|
+
const dbClientPatterns = [
|
|
132
|
+
/systemDB\s*\(/,
|
|
133
|
+
/adminDB\s*\(/,
|
|
134
|
+
/userDB\s*\(/,
|
|
135
|
+
/createClient\s*\(/,
|
|
136
|
+
/supabaseAdmin/,
|
|
137
|
+
]
|
|
138
|
+
for (let i = 0; i < lines.length; i++) {
|
|
139
|
+
// Skip import lines — we check usage, not imports
|
|
140
|
+
if (/^\s*(import|from)\b/.test(lines[i])) continue
|
|
141
|
+
for (const pattern of dbClientPatterns) {
|
|
142
|
+
if (pattern.test(lines[i])) {
|
|
143
|
+
results.findings.push({
|
|
144
|
+
file,
|
|
145
|
+
line: i + 1,
|
|
146
|
+
type: 'DB_CLIENT_IN_ROUTE',
|
|
147
|
+
severity: 'high',
|
|
148
|
+
message: `DB client usage in route file (line ${i + 1}) — routes should not create DB connections`,
|
|
149
|
+
fix: 'Pass DB client through controller/service, not in route definitions'
|
|
150
|
+
})
|
|
151
|
+
results.summary.high++
|
|
152
|
+
results.summary.total++
|
|
153
|
+
results.info.violations++
|
|
154
|
+
break
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Signal 3: RPC calls in routes
|
|
160
|
+
for (let i = 0; i < lines.length; i++) {
|
|
161
|
+
if (/^\s*(import|from)\b/.test(lines[i])) continue
|
|
162
|
+
if (/\.rpc\s*\(/.test(lines[i])) {
|
|
163
|
+
results.findings.push({
|
|
164
|
+
file,
|
|
165
|
+
line: i + 1,
|
|
166
|
+
type: 'RPC_IN_ROUTE',
|
|
167
|
+
severity: 'high',
|
|
168
|
+
message: `RPC call in route file (line ${i + 1}) — move to service layer`,
|
|
169
|
+
fix: 'Extract RPC calls into a service class'
|
|
170
|
+
})
|
|
171
|
+
results.summary.high++
|
|
172
|
+
results.summary.total++
|
|
173
|
+
results.info.violations++
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Signal 4: Helper function declarations (not router method)
|
|
178
|
+
for (let i = 0; i < lines.length; i++) {
|
|
179
|
+
const line = lines[i]
|
|
180
|
+
if (/^\s*(export\s+)?(async\s+)?function\s+\w+/.test(line) && !/router\./.test(line)) {
|
|
181
|
+
// Skip if it's a middleware function (common pattern)
|
|
182
|
+
if (/middleware/i.test(line)) continue
|
|
183
|
+
results.findings.push({
|
|
184
|
+
file,
|
|
185
|
+
line: i + 1,
|
|
186
|
+
type: 'HELPER_IN_ROUTE',
|
|
187
|
+
severity: 'medium',
|
|
188
|
+
message: `Helper function in route file (line ${i + 1}) — move to service or util`,
|
|
189
|
+
fix: 'Extract helper functions to a service or utility module'
|
|
190
|
+
})
|
|
191
|
+
results.summary.medium++
|
|
192
|
+
results.summary.total++
|
|
193
|
+
results.info.violations++
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Signal 5: Data transformation chains (.map + .filter or .reduce)
|
|
198
|
+
let hasMap = false
|
|
199
|
+
let hasFilterOrReduce = false
|
|
200
|
+
for (let i = 0; i < lines.length; i++) {
|
|
201
|
+
if (/^\s*(import|from)\b/.test(lines[i])) continue
|
|
202
|
+
if (/\.map\s*\(/.test(lines[i])) hasMap = true
|
|
203
|
+
if (/\.(filter|reduce)\s*\(/.test(lines[i])) hasFilterOrReduce = true
|
|
204
|
+
}
|
|
205
|
+
if (hasMap && hasFilterOrReduce) {
|
|
206
|
+
results.findings.push({
|
|
207
|
+
file,
|
|
208
|
+
line: 1,
|
|
209
|
+
type: 'TRANSFORMATION_IN_ROUTE',
|
|
210
|
+
severity: 'low',
|
|
211
|
+
message: `Data transformation chain in route file — move to service layer`,
|
|
212
|
+
fix: 'Extract data transformations into a service method'
|
|
213
|
+
})
|
|
214
|
+
results.summary.low++
|
|
215
|
+
results.summary.total++
|
|
216
|
+
results.info.violations++
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Fail on high findings, unless allowDbInRoutes is set
|
|
221
|
+
if (allowDbInRoutes) {
|
|
222
|
+
// Only fail on critical (none defined currently, but future-proof)
|
|
223
|
+
results.passed = results.findings.filter(f =>
|
|
224
|
+
f.severity === 'critical'
|
|
225
|
+
).length === 0
|
|
226
|
+
} else {
|
|
227
|
+
results.passed = results.findings.filter(f =>
|
|
228
|
+
f.severity === 'critical' || f.severity === 'high'
|
|
229
|
+
).length === 0
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return results
|
|
233
|
+
}
|
|
@@ -17,7 +17,7 @@ const BOOL_PREFIX = /^(is_|has_|can_|should_|allow_|enable_|use_|include_)/
|
|
|
17
17
|
const JSON_SUFFIX = /_(data|json|meta|config|settings|options|payload|context|params|attributes|properties|extra|raw)$/
|
|
18
18
|
const JSON_STANDALONE = /^(metadata|settings|config|configuration|options|preferences|tags|labels|permissions|context|payload|attributes|properties|extras|params|parameters)$/
|
|
19
19
|
|
|
20
|
-
function scanDatabaseNaming(projectPath) {
|
|
20
|
+
function scanDatabaseNaming(projectPath, options = {}) {
|
|
21
21
|
const violations = []
|
|
22
22
|
let totalFields = 0
|
|
23
23
|
let compliant = 0
|
|
@@ -27,11 +27,14 @@ function scanDatabaseNaming(projectPath) {
|
|
|
27
27
|
join(projectPath, 'backend', 'supabase', 'migrations')
|
|
28
28
|
]
|
|
29
29
|
|
|
30
|
+
const ignoreMigrations = options.ignoreMigrations || []
|
|
31
|
+
|
|
30
32
|
const sqlFiles = []
|
|
31
33
|
for (const dir of migrationDirs) {
|
|
32
34
|
if (!existsSync(dir)) continue
|
|
33
35
|
try {
|
|
34
36
|
for (const f of readdirSync(dir).filter(f => f.endsWith('.sql'))) {
|
|
37
|
+
if (ignoreMigrations.includes(f)) continue
|
|
35
38
|
sqlFiles.push(join(dir, f))
|
|
36
39
|
}
|
|
37
40
|
} catch { /* ignore */ }
|
|
@@ -265,14 +268,14 @@ function scanCodeNaming(projectPath) {
|
|
|
265
268
|
}
|
|
266
269
|
}
|
|
267
270
|
|
|
268
|
-
export async function check(projectPath) {
|
|
271
|
+
export async function check(projectPath, options = {}) {
|
|
269
272
|
const result = createCheck('naming-conventions', 3, {
|
|
270
273
|
database: { totalFields: 0, compliant: 0, violations: [], compliancePercent: 0 },
|
|
271
274
|
code: { totalChecked: 0, compliant: 0, violations: [], compliancePercent: 0 },
|
|
272
275
|
overallCompliancePercent: 0
|
|
273
276
|
})
|
|
274
277
|
|
|
275
|
-
const dbResult = scanDatabaseNaming(projectPath)
|
|
278
|
+
const dbResult = scanDatabaseNaming(projectPath, options)
|
|
276
279
|
result.details.database = dbResult
|
|
277
280
|
if (dbResult.totalFields > 0) {
|
|
278
281
|
if (dbResult.compliancePercent >= 80) result.score += 1
|
package/lib/config.js
CHANGED
|
@@ -78,10 +78,27 @@ export const DEFAULT_CONFIG = {
|
|
|
78
78
|
|
|
79
79
|
// Code quality checks
|
|
80
80
|
codeQuality: {
|
|
81
|
-
// Architecture
|
|
82
|
-
maxFileLines: 500,
|
|
81
|
+
// Architecture — file size limits per type (lines)
|
|
82
|
+
maxFileLines: 500, // legacy fallback
|
|
83
|
+
fileSizeLimits: {
|
|
84
|
+
route: 200, // routes = wiring, no logic
|
|
85
|
+
controller: 400, // request/response + error handling
|
|
86
|
+
service: 600, // business logic, allowed to be complex
|
|
87
|
+
default: 400, // everything else
|
|
88
|
+
},
|
|
83
89
|
checkCircularDeps: true,
|
|
84
90
|
|
|
91
|
+
// Route separation — detect business logic in route files
|
|
92
|
+
routeSeparation: {
|
|
93
|
+
enabled: true,
|
|
94
|
+
allowDbInRoutes: false,
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
// Naming conventions
|
|
98
|
+
namingConventions: {
|
|
99
|
+
ignoreMigrations: [], // migration filenames to skip (already applied, can't rename)
|
|
100
|
+
},
|
|
101
|
+
|
|
85
102
|
// Dead code
|
|
86
103
|
runKnip: true,
|
|
87
104
|
|
package/lib/runner.js
CHANGED
|
@@ -16,6 +16,8 @@ import * as ciPipeline from './checks/stability/ci-pipeline.js'
|
|
|
16
16
|
import * as npmAudit from './checks/stability/npm-audit.js'
|
|
17
17
|
import * as apiResponseFormat from './checks/codeQuality/api-response-format.js'
|
|
18
18
|
import * as fileSize from './checks/codeQuality/file-size.js'
|
|
19
|
+
import * as namingConventions from './checks/codeQuality/naming-conventions.js'
|
|
20
|
+
import * as routeSeparation from './checks/codeQuality/route-separation.js'
|
|
19
21
|
import * as gitignoreValidation from './checks/security/gitignore-validation.js'
|
|
20
22
|
import * as rlsPolicyAudit from './checks/supabase/rls-policy-audit.js'
|
|
21
23
|
import * as rpcParamMismatch from './checks/supabase/rpc-param-mismatch.js'
|
|
@@ -38,7 +40,9 @@ const ALL_CHECKS = {
|
|
|
38
40
|
],
|
|
39
41
|
codeQuality: [
|
|
40
42
|
apiResponseFormat,
|
|
41
|
-
fileSize
|
|
43
|
+
fileSize,
|
|
44
|
+
namingConventions,
|
|
45
|
+
routeSeparation
|
|
42
46
|
],
|
|
43
47
|
supabase: [
|
|
44
48
|
rlsPolicyAudit,
|