@soulbatical/tetra-dev-toolkit 1.3.3 → 1.5.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.
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Health Check: Bundle Size Tracking
3
+ *
4
+ * Checks for bundle size monitoring and regression prevention.
5
+ * Score: up to 2 points:
6
+ * +1 for bundle analysis tool (bundlewatch, size-limit, @next/bundle-analyzer, webpack-bundle-analyzer)
7
+ * +1 for budget config or CI integration (lighthouse budget counts)
8
+ */
9
+
10
+ import { existsSync, readFileSync, readdirSync } from 'fs'
11
+ import { join } from 'path'
12
+ import { createCheck } from './types.js'
13
+
14
+ export async function check(projectPath) {
15
+ const result = createCheck('bundle-size', 2, {
16
+ tool: null,
17
+ toolInstalled: false,
18
+ hasConfig: false,
19
+ configPath: null,
20
+ hasBudget: false,
21
+ hasCiIntegration: false,
22
+ message: ''
23
+ })
24
+
25
+ // Bundle analysis tools
26
+ const bundleTools = {
27
+ 'bundlewatch': 'bundlewatch',
28
+ 'size-limit': 'size-limit',
29
+ '@size-limit/preset-small-lib': 'size-limit',
30
+ '@size-limit/preset-app': 'size-limit',
31
+ '@next/bundle-analyzer': 'next-bundle-analyzer',
32
+ 'webpack-bundle-analyzer': 'webpack-bundle-analyzer',
33
+ 'rollup-plugin-visualizer': 'rollup-visualizer',
34
+ 'source-map-explorer': 'source-map-explorer',
35
+ 'vite-bundle-visualizer': 'vite-bundle-visualizer'
36
+ }
37
+
38
+ // Check package.json files
39
+ const packageLocations = [
40
+ 'package.json',
41
+ 'backend/package.json',
42
+ 'frontend/package.json',
43
+ 'frontend-user/package.json'
44
+ ]
45
+
46
+ for (const loc of packageLocations) {
47
+ const pkgPath = join(projectPath, loc)
48
+ if (!existsSync(pkgPath)) continue
49
+
50
+ try {
51
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
52
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies }
53
+
54
+ // Check for bundle tools
55
+ for (const [dep, tool] of Object.entries(bundleTools)) {
56
+ if (allDeps[dep]) {
57
+ result.details.toolInstalled = true
58
+ result.details.tool = tool
59
+ result.score += 1
60
+ break
61
+ }
62
+ }
63
+
64
+ // Check for size-limit config in package.json
65
+ if (pkg['size-limit']) {
66
+ result.details.hasConfig = true
67
+ result.details.hasBudget = true
68
+ result.details.configPath = `${loc} (size-limit key)`
69
+ }
70
+
71
+ // Check for bundle analysis scripts
72
+ const scripts = pkg.scripts || {}
73
+ for (const [name, cmd] of Object.entries(scripts)) {
74
+ if (typeof cmd !== 'string') continue
75
+ if (name.includes('analyze') || name.includes('bundle') ||
76
+ cmd.includes('ANALYZE=true') || cmd.includes('bundle-analyzer') ||
77
+ cmd.includes('size-limit') || cmd.includes('bundlewatch')) {
78
+ result.details.hasConfig = true
79
+ result.details.configPath = result.details.configPath || `${loc} → ${name}`
80
+ break
81
+ }
82
+ }
83
+ } catch { /* ignore */ }
84
+
85
+ if (result.details.toolInstalled) break
86
+ }
87
+
88
+ // Check for bundlewatch config
89
+ const bundlewatchConfigs = [
90
+ '.bundlewatch.config.json',
91
+ 'bundlewatch.config.js'
92
+ ]
93
+ for (const config of bundlewatchConfigs) {
94
+ if (existsSync(join(projectPath, config))) {
95
+ result.details.hasConfig = true
96
+ result.details.hasBudget = true
97
+ result.details.configPath = config
98
+ if (!result.details.tool) result.details.tool = 'bundlewatch'
99
+ break
100
+ }
101
+ }
102
+
103
+ // Check for size-limit config
104
+ if (existsSync(join(projectPath, '.size-limit.json')) ||
105
+ existsSync(join(projectPath, '.size-limit.js'))) {
106
+ result.details.hasConfig = true
107
+ result.details.hasBudget = true
108
+ if (!result.details.tool) result.details.tool = 'size-limit'
109
+ }
110
+
111
+ // Check for Lighthouse budget (counts as budget)
112
+ if (existsSync(join(projectPath, 'lighthouse-budget.json')) ||
113
+ existsSync(join(projectPath, 'frontend-user/lighthouse-budget.json'))) {
114
+ result.details.hasBudget = true
115
+ }
116
+
117
+ // Check CI for bundle size checks
118
+ const workflowDir = join(projectPath, '.github/workflows')
119
+ if (existsSync(workflowDir)) {
120
+ try {
121
+ const files = readdirSync(workflowDir).filter(f => f.endsWith('.yml') || f.endsWith('.yaml'))
122
+ for (const file of files) {
123
+ try {
124
+ const content = readFileSync(join(workflowDir, file), 'utf-8')
125
+ if (content.includes('bundlewatch') || content.includes('size-limit') ||
126
+ content.includes('bundle-analyzer') || content.includes('lighthouse')) {
127
+ result.details.hasCiIntegration = true
128
+ break
129
+ }
130
+ } catch { /* ignore */ }
131
+ }
132
+ } catch { /* ignore */ }
133
+ }
134
+
135
+ // Budget or CI integration counts toward second point
136
+ if (result.details.hasBudget || result.details.hasCiIntegration) {
137
+ result.score += 1
138
+ }
139
+
140
+ result.score = Math.min(result.score, result.maxScore)
141
+
142
+ // Determine status
143
+ if (result.score === 0) {
144
+ result.status = 'warning'
145
+ result.details.message = 'No bundle size tracking — regressions go undetected'
146
+ } else if (result.score < 2) {
147
+ result.status = 'warning'
148
+ result.details.message = result.details.toolInstalled
149
+ ? `${result.details.tool} installed but no budget or CI enforcement`
150
+ : 'Budget defined but no analysis tool installed'
151
+ } else {
152
+ result.details.message = `Bundle size tracked via ${result.details.tool || 'lighthouse budgets'}`
153
+ }
154
+
155
+ return result
156
+ }
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Health Check: Conventional Commits
3
+ *
4
+ * Checks for commit message convention enforcement and changelog generation.
5
+ * Score: up to 2 points:
6
+ * +1 for commitlint config OR standard-version/semantic-release config
7
+ * +1 for commit-msg hook OR release script
8
+ */
9
+
10
+ import { existsSync, readFileSync } from 'fs'
11
+ import { join } from 'path'
12
+ import { createCheck } from './types.js'
13
+
14
+ export async function check(projectPath) {
15
+ const result = createCheck('conventional-commits', 2, {
16
+ hasCommitlint: false,
17
+ commitlintConfig: null,
18
+ hasVersionTool: false,
19
+ versionTool: null,
20
+ hasCommitMsgHook: false,
21
+ hasReleaseScript: false,
22
+ releaseScript: null,
23
+ hasChangelog: false,
24
+ message: ''
25
+ })
26
+
27
+ // Check for commitlint config
28
+ const commitlintConfigs = [
29
+ 'commitlint.config.js',
30
+ 'commitlint.config.cjs',
31
+ 'commitlint.config.mjs',
32
+ 'commitlint.config.ts',
33
+ '.commitlintrc',
34
+ '.commitlintrc.json',
35
+ '.commitlintrc.yml',
36
+ '.commitlintrc.js',
37
+ '.commitlintrc.cjs'
38
+ ]
39
+
40
+ for (const config of commitlintConfigs) {
41
+ if (existsSync(join(projectPath, config))) {
42
+ result.details.hasCommitlint = true
43
+ result.details.commitlintConfig = config
44
+ break
45
+ }
46
+ }
47
+
48
+ // Check package.json for commitlint key
49
+ const pkgPath = join(projectPath, 'package.json')
50
+ let rootPkg = null
51
+ if (existsSync(pkgPath)) {
52
+ try {
53
+ rootPkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
54
+ if (rootPkg.commitlint) {
55
+ result.details.hasCommitlint = true
56
+ result.details.commitlintConfig = 'package.json (commitlint key)'
57
+ }
58
+ } catch { /* ignore */ }
59
+ }
60
+
61
+ // Check for versioning tools
62
+ const versionConfigs = [
63
+ { file: '.versionrc', tool: 'standard-version' },
64
+ { file: '.versionrc.json', tool: 'standard-version' },
65
+ { file: '.versionrc.js', tool: 'standard-version' },
66
+ { file: 'backend/.versionrc.json', tool: 'standard-version' },
67
+ { file: '.releaserc', tool: 'semantic-release' },
68
+ { file: '.releaserc.json', tool: 'semantic-release' },
69
+ { file: '.releaserc.js', tool: 'semantic-release' },
70
+ { file: 'release.config.js', tool: 'semantic-release' },
71
+ { file: '.changeset/config.json', tool: 'changesets' }
72
+ ]
73
+
74
+ for (const { file, tool } of versionConfigs) {
75
+ if (existsSync(join(projectPath, file))) {
76
+ result.details.hasVersionTool = true
77
+ result.details.versionTool = tool
78
+ break
79
+ }
80
+ }
81
+
82
+ // Check dependencies for version tools
83
+ const packageLocations = ['package.json', 'backend/package.json']
84
+ for (const loc of packageLocations) {
85
+ const path = join(projectPath, loc)
86
+ if (!existsSync(path)) continue
87
+
88
+ try {
89
+ const pkg = JSON.parse(readFileSync(path, 'utf-8'))
90
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies }
91
+
92
+ if (allDeps['standard-version'] && !result.details.hasVersionTool) {
93
+ result.details.hasVersionTool = true
94
+ result.details.versionTool = 'standard-version'
95
+ }
96
+ if (allDeps['semantic-release'] && !result.details.hasVersionTool) {
97
+ result.details.hasVersionTool = true
98
+ result.details.versionTool = 'semantic-release'
99
+ }
100
+ if (allDeps['@changesets/cli'] && !result.details.hasVersionTool) {
101
+ result.details.hasVersionTool = true
102
+ result.details.versionTool = 'changesets'
103
+ }
104
+
105
+ // Check for release scripts
106
+ const scripts = pkg.scripts || {}
107
+ for (const [name, cmd] of Object.entries(scripts)) {
108
+ if (typeof cmd !== 'string') continue
109
+ if (name.includes('release') || cmd.includes('standard-version') ||
110
+ cmd.includes('semantic-release') || cmd.includes('changeset')) {
111
+ result.details.hasReleaseScript = true
112
+ result.details.releaseScript = `${loc} → ${name}`
113
+ break
114
+ }
115
+ }
116
+ } catch { /* ignore */ }
117
+ }
118
+
119
+ // Check for commit-msg hook
120
+ const commitMsgHooks = [
121
+ '.husky/commit-msg',
122
+ 'backend/.husky/commit-msg'
123
+ ]
124
+
125
+ for (const hook of commitMsgHooks) {
126
+ if (existsSync(join(projectPath, hook))) {
127
+ result.details.hasCommitMsgHook = true
128
+ break
129
+ }
130
+ }
131
+
132
+ // Check for CHANGELOG
133
+ if (existsSync(join(projectPath, 'CHANGELOG.md')) ||
134
+ existsSync(join(projectPath, 'backend/CHANGELOG.md'))) {
135
+ result.details.hasChangelog = true
136
+ }
137
+
138
+ // Scoring
139
+ if (result.details.hasCommitlint || result.details.hasVersionTool) {
140
+ result.score += 1
141
+ }
142
+
143
+ if (result.details.hasCommitMsgHook || result.details.hasReleaseScript) {
144
+ result.score += 1
145
+ }
146
+
147
+ result.score = Math.min(result.score, result.maxScore)
148
+
149
+ // Determine status
150
+ if (result.score === 0) {
151
+ result.status = 'warning'
152
+ result.details.message = 'No commit convention or release management configured'
153
+ } else if (result.score < 2) {
154
+ result.status = 'warning'
155
+ const tool = result.details.hasCommitlint ? 'commitlint' :
156
+ result.details.versionTool || 'commit tool'
157
+ result.details.message = `${tool} configured but no enforcement hook or release script`
158
+ } else {
159
+ const tool = result.details.versionTool || 'commitlint'
160
+ result.details.message = `Commit conventions enforced via ${tool}`
161
+ }
162
+
163
+ return result
164
+ }
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Health Check: Test Coverage Thresholds
3
+ *
4
+ * Checks that test coverage thresholds are configured and enforced.
5
+ * Score: up to 3 points:
6
+ * +1 for coverage config in test framework (jest/vitest)
7
+ * +1 for threshold values defined (statements/branches/lines)
8
+ * +1 for coverage script in package.json or CI enforcement
9
+ */
10
+
11
+ import { existsSync, readFileSync } from 'fs'
12
+ import { join } from 'path'
13
+ import { createCheck } from './types.js'
14
+
15
+ export async function check(projectPath) {
16
+ const result = createCheck('coverage-thresholds', 3, {
17
+ framework: null,
18
+ hasCoverageConfig: false,
19
+ hasThresholds: false,
20
+ thresholds: null,
21
+ hasCoverageScript: false,
22
+ coverageScript: null,
23
+ hasCiCoverage: false,
24
+ message: ''
25
+ })
26
+
27
+ // Check Jest configs
28
+ const jestConfigs = [
29
+ 'jest.config.js',
30
+ 'jest.config.ts',
31
+ 'jest.config.cjs',
32
+ 'jest.config.mjs',
33
+ 'backend/jest.config.js',
34
+ 'backend/jest.config.ts'
35
+ ]
36
+
37
+ for (const config of jestConfigs) {
38
+ const configPath = join(projectPath, config)
39
+ if (!existsSync(configPath)) continue
40
+
41
+ try {
42
+ const content = readFileSync(configPath, 'utf-8')
43
+ result.details.framework = 'jest'
44
+
45
+ // Check for coverage configuration
46
+ if (content.includes('collectCoverage') || content.includes('coverageDirectory') ||
47
+ content.includes('coverageReporters') || content.includes('collectCoverageFrom')) {
48
+ result.details.hasCoverageConfig = true
49
+ result.score += 1
50
+ }
51
+
52
+ // Check for thresholds
53
+ if (content.includes('coverageThreshold')) {
54
+ result.details.hasThresholds = true
55
+ result.score += 1
56
+
57
+ // Try to extract threshold values
58
+ const thresholdMatch = content.match(/coverageThreshold\s*:\s*\{[\s\S]*?global\s*:\s*\{([^}]+)\}/)
59
+ if (thresholdMatch) {
60
+ result.details.thresholds = thresholdMatch[1].trim()
61
+ }
62
+ }
63
+ } catch { /* ignore */ }
64
+ break
65
+ }
66
+
67
+ // Check Vitest configs
68
+ if (!result.details.framework) {
69
+ const vitestConfigs = [
70
+ 'vitest.config.ts',
71
+ 'vitest.config.js',
72
+ 'vitest.config.mjs',
73
+ 'vite.config.ts',
74
+ 'vite.config.js',
75
+ 'backend/vitest.config.ts',
76
+ 'backend/vite.config.ts'
77
+ ]
78
+
79
+ for (const config of vitestConfigs) {
80
+ const configPath = join(projectPath, config)
81
+ if (!existsSync(configPath)) continue
82
+
83
+ try {
84
+ const content = readFileSync(configPath, 'utf-8')
85
+
86
+ // Only count as vitest if it has test config
87
+ if (!content.includes('test')) continue
88
+
89
+ result.details.framework = 'vitest'
90
+
91
+ if (content.includes('coverage')) {
92
+ result.details.hasCoverageConfig = true
93
+ result.score += 1
94
+ }
95
+
96
+ if (content.includes('thresholds') || content.includes('threshold')) {
97
+ result.details.hasThresholds = true
98
+ result.score += 1
99
+ }
100
+ } catch { /* ignore */ }
101
+ break
102
+ }
103
+ }
104
+
105
+ // Also check package.json for jest coverage config
106
+ if (!result.details.hasCoverageConfig) {
107
+ const pkgPath = join(projectPath, 'package.json')
108
+ if (existsSync(pkgPath)) {
109
+ try {
110
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
111
+ if (pkg.jest) {
112
+ result.details.framework = result.details.framework || 'jest'
113
+ if (pkg.jest.collectCoverage || pkg.jest.coverageReporters) {
114
+ result.details.hasCoverageConfig = true
115
+ result.score += 1
116
+ }
117
+ if (pkg.jest.coverageThreshold) {
118
+ result.details.hasThresholds = true
119
+ result.score += 1
120
+ const global = pkg.jest.coverageThreshold.global
121
+ if (global) {
122
+ result.details.thresholds = global
123
+ }
124
+ }
125
+ }
126
+ } catch { /* ignore */ }
127
+ }
128
+ }
129
+
130
+ // Check for coverage script
131
+ const packageLocations = [
132
+ 'package.json',
133
+ 'backend/package.json',
134
+ 'frontend/package.json'
135
+ ]
136
+
137
+ for (const loc of packageLocations) {
138
+ const pkgPath = join(projectPath, loc)
139
+ if (!existsSync(pkgPath)) continue
140
+
141
+ try {
142
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
143
+ const scripts = pkg.scripts || {}
144
+
145
+ for (const [name, cmd] of Object.entries(scripts)) {
146
+ if (typeof cmd !== 'string') continue
147
+ if (name.includes('coverage') || cmd.includes('--coverage') ||
148
+ cmd.includes('--coverageThreshold')) {
149
+ result.details.hasCoverageScript = true
150
+ result.details.coverageScript = `${loc} → ${name}`
151
+ break
152
+ }
153
+ }
154
+ if (result.details.hasCoverageScript) break
155
+ } catch { /* ignore */ }
156
+ }
157
+
158
+ // Check CI workflows for coverage enforcement
159
+ const ciFiles = [
160
+ '.github/workflows/quality.yml',
161
+ '.github/workflows/test.yml',
162
+ '.github/workflows/ci.yml',
163
+ '.github/workflows/admin-mode-tests.yml'
164
+ ]
165
+
166
+ for (const ci of ciFiles) {
167
+ const ciPath = join(projectPath, ci)
168
+ if (!existsSync(ciPath)) continue
169
+
170
+ try {
171
+ const content = readFileSync(ciPath, 'utf-8')
172
+ if (content.includes('coverage') || content.includes('--coverageThreshold')) {
173
+ result.details.hasCiCoverage = true
174
+ break
175
+ }
176
+ } catch { /* ignore */ }
177
+ }
178
+
179
+ if (result.details.hasCoverageScript || result.details.hasCiCoverage) {
180
+ result.score += 1
181
+ }
182
+
183
+ result.score = Math.min(result.score, result.maxScore)
184
+
185
+ // Determine status
186
+ if (result.score === 0) {
187
+ result.status = result.details.framework ? 'warning' : 'error'
188
+ result.details.message = result.details.framework
189
+ ? `${result.details.framework} detected but no coverage configuration`
190
+ : 'No test framework or coverage configuration detected'
191
+ } else if (result.score < 2) {
192
+ result.status = 'warning'
193
+ result.details.message = 'Coverage partially configured — add thresholds for enforcement'
194
+ } else {
195
+ result.details.message = `Coverage well-configured (${result.details.framework})`
196
+ }
197
+
198
+ return result
199
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Health Check: Dependency Update Automation
3
+ *
4
+ * Checks for automated dependency updates via Renovate or Dependabot.
5
+ * Score: up to 2 points:
6
+ * +1 for Renovate or Dependabot config exists
7
+ * +1 for automerge or schedule configured (indicates active maintenance)
8
+ */
9
+
10
+ import { existsSync, readFileSync } from 'fs'
11
+ import { join } from 'path'
12
+ import { createCheck } from './types.js'
13
+
14
+ export async function check(projectPath) {
15
+ const result = createCheck('dependency-automation', 2, {
16
+ tool: null,
17
+ configFound: false,
18
+ configPath: null,
19
+ hasSchedule: false,
20
+ hasAutomerge: false,
21
+ message: ''
22
+ })
23
+
24
+ // Check for Renovate config
25
+ const renovateConfigs = [
26
+ 'renovate.json',
27
+ 'renovate.json5',
28
+ '.renovaterc',
29
+ '.renovaterc.json',
30
+ '.renovaterc.json5',
31
+ '.github/renovate.json',
32
+ '.github/renovate.json5'
33
+ ]
34
+
35
+ for (const config of renovateConfigs) {
36
+ const configPath = join(projectPath, config)
37
+ if (!existsSync(configPath)) continue
38
+
39
+ result.details.tool = 'renovate'
40
+ result.details.configFound = true
41
+ result.details.configPath = config
42
+ result.score += 1
43
+
44
+ try {
45
+ const content = readFileSync(configPath, 'utf-8')
46
+ if (content.includes('schedule')) result.details.hasSchedule = true
47
+ if (content.includes('automerge')) result.details.hasAutomerge = true
48
+ } catch { /* ignore */ }
49
+ break
50
+ }
51
+
52
+ // Also check package.json for renovate key
53
+ if (!result.details.configFound) {
54
+ const pkgPath = join(projectPath, 'package.json')
55
+ if (existsSync(pkgPath)) {
56
+ try {
57
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
58
+ if (pkg.renovate) {
59
+ result.details.tool = 'renovate'
60
+ result.details.configFound = true
61
+ result.details.configPath = 'package.json (renovate key)'
62
+ result.score += 1
63
+
64
+ const content = JSON.stringify(pkg.renovate)
65
+ if (content.includes('schedule')) result.details.hasSchedule = true
66
+ if (content.includes('automerge')) result.details.hasAutomerge = true
67
+ }
68
+ } catch { /* ignore */ }
69
+ }
70
+ }
71
+
72
+ // Check for Dependabot config
73
+ if (!result.details.configFound) {
74
+ const dependabotPath = join(projectPath, '.github/dependabot.yml')
75
+ const dependabotAlt = join(projectPath, '.github/dependabot.yaml')
76
+
77
+ const depPath = existsSync(dependabotPath) ? dependabotPath :
78
+ existsSync(dependabotAlt) ? dependabotAlt : null
79
+
80
+ if (depPath) {
81
+ result.details.tool = 'dependabot'
82
+ result.details.configFound = true
83
+ result.details.configPath = depPath.replace(projectPath + '/', '')
84
+ result.score += 1
85
+
86
+ try {
87
+ const content = readFileSync(depPath, 'utf-8')
88
+ if (content.includes('schedule')) result.details.hasSchedule = true
89
+ // Dependabot doesn't have automerge natively, but check for it
90
+ if (content.includes('auto-merge') || content.includes('automerge')) {
91
+ result.details.hasAutomerge = true
92
+ }
93
+ } catch { /* ignore */ }
94
+ }
95
+ }
96
+
97
+ // Bonus point for active config
98
+ if (result.details.hasSchedule || result.details.hasAutomerge) {
99
+ result.score += 1
100
+ }
101
+
102
+ result.score = Math.min(result.score, result.maxScore)
103
+
104
+ // Determine status
105
+ if (result.score === 0) {
106
+ result.status = 'warning'
107
+ result.details.message = 'No dependency update automation — outdated deps accumulate silently'
108
+ } else if (result.score < 2) {
109
+ result.status = 'warning'
110
+ result.details.message = `${result.details.tool} configured but no schedule or automerge`
111
+ } else {
112
+ result.details.message = `Dependencies auto-updated via ${result.details.tool}`
113
+ }
114
+
115
+ return result
116
+ }