@soulbatical/tetra-dev-toolkit 1.1.1 → 1.3.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,389 @@
1
+ /**
2
+ * Health Check: File Organization (docs, scripts, config & code dir cleanliness)
3
+ *
4
+ * Verifies that:
5
+ * 1. Documentation files (.md) live in /docs/ or repo root
6
+ * 2. Scripts (.sh) live in /scripts/ or allowed dirs
7
+ * 3. Config files (.yml/.yaml) live in allowed locations
8
+ * 4. No nested docs/ directories exist
9
+ * 5. Code directories (backend/, frontend/, etc.) contain only code — no clutter
10
+ *
11
+ * Score: 6pt max
12
+ * - 1pt: No stray .md files outside /docs and root
13
+ * - 1pt: No stray .sh scripts outside allowed dirs
14
+ * - 1pt: No stray .yml/.yaml config files outside allowed dirs
15
+ * - 1pt: No nested docs/ directories
16
+ * - 1pt: No clutter in code directories (no .txt, .log, .pdf, .csv, .env, .bak, .tmp, .orig)
17
+ * - 1pt: No clutter in repo root (no .txt, .log, .pdf, .csv, .png, .jpg, tmp/, logs/, data/, venv/)
18
+ */
19
+
20
+ import { readdirSync, readFileSync, existsSync } from 'fs'
21
+ import { join, relative, basename } from 'path'
22
+ import { createCheck } from './types.js'
23
+
24
+ const ALLOWED_ROOT_MD = new Set([
25
+ 'readme.md', 'claude.md', 'changelog.md', 'license.md', 'license',
26
+ 'prompt.md', 'plan.md', 'contributing.md', 'code_of_conduct.md'
27
+ ])
28
+
29
+ const IGNORED_DIRS = new Set([
30
+ 'node_modules', 'dist', 'build', '.git', '.next', '.cache', '.turbo',
31
+ 'coverage', '.nyc_output', '.playwright', 'test-results'
32
+ ])
33
+
34
+ const ALLOWED_SCRIPT_DIRS = new Set([
35
+ 'scripts', 'shell', 'hooks', '.husky', '.ralph', '.claude'
36
+ ])
37
+
38
+ /** Directories whose .md content is always allowed (tooling config) */
39
+ const ALLOWED_MD_DIRS = new Set([
40
+ 'docs', '.ralph', '.claude', '.agents', 'e2e'
41
+ ])
42
+
43
+ /** Directories where .yml/.yaml config files are allowed */
44
+ const ALLOWED_CONFIG_DIRS = new Set([
45
+ '.ralph', '.github', '.claude', '.circleci', '.gitlab'
46
+ ])
47
+
48
+ /** Code directories that should only contain code */
49
+ const CODE_DIRS = ['backend', 'frontend', 'backend-mcp', 'backend-pdf', 'mcp']
50
+
51
+ /** File extensions that are clutter in code directories */
52
+ const CLUTTER_EXTENSIONS = new Set([
53
+ '.txt', '.log', '.pdf', '.csv', '.bak', '.tmp', '.orig',
54
+ '.patch', '.diff', '.bk', '.old'
55
+ ])
56
+
57
+ /** File names that are clutter in code directories (regardless of extension) */
58
+ const CLUTTER_NAMES = new Set([
59
+ '.env', '.env.local', '.env.development', '.env.production', '.env.staging'
60
+ ])
61
+
62
+ /** Files allowed in code dirs despite having clutter extensions */
63
+ const ALLOWED_CLUTTER = new Set([
64
+ 'robots.txt', 'llms.txt', 'llms-full.txt', 'humans.txt',
65
+ 'security.txt', 'ads.txt', 'manifest.txt'
66
+ ])
67
+
68
+ /** Subdirectories within code dirs where clutter files are OK */
69
+ const ALLOWED_CLUTTER_SUBDIRS = new Set([
70
+ 'public', 'static', 'assets', 'fixtures', 'test-fixtures',
71
+ 'supabase', 'migrations', 'prisma'
72
+ ])
73
+
74
+ /** File extensions that are clutter in the repo root */
75
+ const ROOT_CLUTTER_EXTENSIONS = new Set([
76
+ '.txt', '.log', '.pdf', '.csv', '.png', '.jpg', '.jpeg', '.gif',
77
+ '.bak', '.tmp', '.orig', '.patch', '.diff', '.bk', '.old', '.py'
78
+ ])
79
+
80
+ /**
81
+ * Root files that are allowed despite having clutter extensions.
82
+ * Standard tooling/platform configs that belong in root.
83
+ */
84
+ const ALLOWED_ROOT_FILES = new Set([
85
+ // Platform configs
86
+ 'dockerfile', 'dockerfile.bot', 'dockerfile.dev', 'dockerfile.prod',
87
+ 'procfile', 'nixpacks.toml', 'netlify.toml', 'vercel.json', 'railway.json',
88
+ 'render.yaml', 'fly.toml',
89
+ // JS/TS tooling
90
+ 'components.json', 'ecosystem.config.js', 'ecosystem.config.cjs',
91
+ '.eslintrc.json', '.prettierrc', '.prettierrc.json',
92
+ 'vitest.setup.ts', 'vitest.setup.js', 'jest.setup.ts', 'jest.setup.js',
93
+ 'commitlint.config.js', 'lint-staged.config.js',
94
+ // Lock files
95
+ 'bun.lockb', 'yarn.lock', 'pnpm-lock.yaml', 'deno.lock',
96
+ ])
97
+
98
+ /** Root directories that are clutter (should not exist in repo root) */
99
+ const ROOT_CLUTTER_DIRS = new Set([
100
+ 'tmp', 'temp', 'logs', 'log', 'data', 'venv', '.venv',
101
+ 'reports', 'output', 'out', 'backup', 'backups'
102
+ ])
103
+
104
+ /**
105
+ * Recursively find files matching a predicate, respecting ignored dirs
106
+ */
107
+ function findFiles(dir, predicate, maxDepth = 5, currentDepth = 0) {
108
+ const results = []
109
+ if (currentDepth >= maxDepth) return results
110
+
111
+ let entries
112
+ try { entries = readdirSync(dir, { withFileTypes: true }) } catch { return results }
113
+
114
+ for (const entry of entries) {
115
+ const name = entry.name
116
+ if (IGNORED_DIRS.has(name)) continue
117
+
118
+ const fullPath = join(dir, name)
119
+ if (entry.isDirectory()) {
120
+ results.push(...findFiles(fullPath, predicate, maxDepth, currentDepth + 1))
121
+ } else if (entry.isFile() && predicate(name, fullPath)) {
122
+ results.push(fullPath)
123
+ }
124
+ }
125
+ return results
126
+ }
127
+
128
+ /** Directories that may contain a docs/ subdirectory (allowed, not flagged) */
129
+ const ALLOWED_NESTED_DOCS_PARENTS = new Set(['.ralph'])
130
+
131
+ /**
132
+ * Find directories named "docs" that are not at the repo root,
133
+ * excluding allowed parents like .ralph/docs
134
+ */
135
+ function findNestedDocsDirs(rootPath, currentDir, maxDepth = 4, currentDepth = 0) {
136
+ const results = []
137
+ if (currentDepth >= maxDepth) return results
138
+
139
+ let entries
140
+ try { entries = readdirSync(currentDir, { withFileTypes: true }) } catch { return results }
141
+
142
+ for (const entry of entries) {
143
+ const name = entry.name
144
+ if (IGNORED_DIRS.has(name)) continue
145
+ if (!entry.isDirectory()) continue
146
+
147
+ const fullPath = join(currentDir, name)
148
+ const relFromRoot = relative(rootPath, fullPath)
149
+ const topDir = relFromRoot.split('/')[0]
150
+
151
+ if (name === 'docs' && currentDepth > 0) {
152
+ // Skip if parent is an allowed directory (e.g., .ralph/docs)
153
+ if (!ALLOWED_NESTED_DOCS_PARENTS.has(topDir)) {
154
+ results.push(fullPath)
155
+ }
156
+ } else {
157
+ results.push(...findNestedDocsDirs(rootPath, fullPath, maxDepth, currentDepth + 1))
158
+ }
159
+ }
160
+ return results
161
+ }
162
+
163
+ export async function check(projectPath) {
164
+ const result = createCheck('file-organization', 6, {
165
+ strayMdFiles: [],
166
+ strayScripts: [],
167
+ strayConfigs: [],
168
+ clutterFiles: [],
169
+ rootClutter: [],
170
+ nestedDocsDirs: [],
171
+ totalStrayMd: 0,
172
+ totalStrayScripts: 0,
173
+ totalStrayConfigs: 0,
174
+ totalClutter: 0,
175
+ totalRootClutter: 0,
176
+ totalNestedDocs: 0
177
+ })
178
+
179
+ // Load .gitignore entries for root clutter check
180
+ const gitignorePath = join(projectPath, '.gitignore')
181
+ const gitignoredDirs = new Set()
182
+ if (existsSync(gitignorePath)) {
183
+ try {
184
+ const lines = readFileSync(gitignorePath, 'utf-8').split('\n')
185
+ .map(l => l.trim()).filter(l => l && !l.startsWith('#'))
186
+ for (const line of lines) {
187
+ // Match dir entries like "tmp/" or "tmp"
188
+ const cleaned = line.replace(/\/$/, '')
189
+ if (cleaned) gitignoredDirs.add(cleaned)
190
+ }
191
+ } catch { /* ignore */ }
192
+ }
193
+
194
+ // --- Check 1: Stray .md files ---
195
+ const allMdFiles = findFiles(projectPath, (name) => name.endsWith('.md'))
196
+ const strayMd = []
197
+
198
+ for (const file of allMdFiles) {
199
+ const rel = relative(projectPath, file)
200
+ const parts = rel.split('/')
201
+
202
+ // README.md and CLAUDE.md are always allowed, anywhere
203
+ const nameLc = basename(file).toLowerCase()
204
+ if (nameLc === 'readme.md' || nameLc === 'claude.md') continue
205
+
206
+ // Root .md files: only allowed ones
207
+ if (parts.length === 1) {
208
+ if (ALLOWED_ROOT_MD.has(basename(file).toLowerCase())) continue
209
+ strayMd.push(rel)
210
+ continue
211
+ }
212
+
213
+ // Inside allowed top-level directories: always OK
214
+ if (ALLOWED_MD_DIRS.has(parts[0])) continue
215
+
216
+ // Inside shell/templates: OK (Ralph templates)
217
+ if (parts[0] === 'shell' && parts[1] === 'templates') continue
218
+
219
+ // Everything else is stray
220
+ strayMd.push(rel)
221
+ }
222
+
223
+ result.details.strayMdFiles = strayMd.slice(0, 20) // Cap at 20 for readability
224
+ result.details.totalStrayMd = strayMd.length
225
+
226
+ // --- Check 2: Stray scripts ---
227
+ const allScripts = findFiles(projectPath, (name) => name.endsWith('.sh'))
228
+ const strayScripts = []
229
+
230
+ for (const file of allScripts) {
231
+ const rel = relative(projectPath, file)
232
+ const parts = rel.split('/')
233
+ const topDir = parts[0]
234
+
235
+ // Root-level scripts: OK
236
+ if (parts.length === 1) continue
237
+
238
+ // Inside allowed script directories: OK
239
+ if (ALLOWED_SCRIPT_DIRS.has(topDir)) continue
240
+
241
+ // Everything else is stray
242
+ strayScripts.push(rel)
243
+ }
244
+
245
+ result.details.strayScripts = strayScripts.slice(0, 20)
246
+ result.details.totalStrayScripts = strayScripts.length
247
+
248
+ // --- Check 3: Stray config files (.yml/.yaml) ---
249
+ const allConfigs = findFiles(projectPath, (name) => name.endsWith('.yml') || name.endsWith('.yaml'))
250
+ const strayConfigs = []
251
+
252
+ for (const file of allConfigs) {
253
+ const rel = relative(projectPath, file)
254
+ const parts = rel.split('/')
255
+
256
+ // Root-level configs: always OK (doppler.yaml, .coderabbit.yaml, etc.)
257
+ if (parts.length === 1) continue
258
+
259
+ // Inside allowed config directories: OK
260
+ if (ALLOWED_CONFIG_DIRS.has(parts[0])) continue
261
+
262
+ // Everything else is stray
263
+ strayConfigs.push(rel)
264
+ }
265
+
266
+ result.details.strayConfigs = strayConfigs.slice(0, 20)
267
+ result.details.totalStrayConfigs = strayConfigs.length
268
+
269
+ // --- Check 4: Clutter in code directories ---
270
+ const clutterFiles = []
271
+
272
+ for (const codeDir of CODE_DIRS) {
273
+ const codePath = join(projectPath, codeDir)
274
+ let dirExists
275
+ try { readdirSync(codePath); dirExists = true } catch { dirExists = false }
276
+ if (!dirExists) continue
277
+
278
+ const allFiles = findFiles(codePath, () => true)
279
+ for (const file of allFiles) {
280
+ const name = basename(file).toLowerCase()
281
+ const rel = relative(projectPath, file)
282
+ const relFromCodeDir = relative(codePath, file)
283
+ const subParts = relFromCodeDir.split('/')
284
+
285
+ // Check if in an allowed subdirectory (public/, supabase/migrations/, etc.)
286
+ if (subParts.length > 1 && ALLOWED_CLUTTER_SUBDIRS.has(subParts[0])) continue
287
+
288
+ // Check for .env files (secrets!)
289
+ if (CLUTTER_NAMES.has(name)) {
290
+ clutterFiles.push(rel)
291
+ continue
292
+ }
293
+
294
+ // Check for allowed files (robots.txt in public, etc.)
295
+ if (ALLOWED_CLUTTER.has(name)) continue
296
+
297
+ // Check extension
298
+ const ext = name.includes('.') ? '.' + name.split('.').pop() : ''
299
+ if (CLUTTER_EXTENSIONS.has(ext)) {
300
+ // .env.example and .env.test are OK (templates)
301
+ if (name.endsWith('.example') || name.endsWith('.test')) continue
302
+ clutterFiles.push(rel)
303
+ }
304
+ }
305
+ }
306
+
307
+ result.details.clutterFiles = clutterFiles.slice(0, 20)
308
+ result.details.totalClutter = clutterFiles.length
309
+
310
+ // --- Check 5: Root clutter (files & directories) ---
311
+ const rootClutter = []
312
+
313
+ let rootEntries
314
+ try { rootEntries = readdirSync(projectPath, { withFileTypes: true }) } catch { rootEntries = [] }
315
+
316
+ for (const entry of rootEntries) {
317
+ const name = entry.name
318
+ const nameLower = name.toLowerCase()
319
+
320
+ if (entry.isDirectory()) {
321
+ // Check for clutter directories in root (skip if already gitignored)
322
+ if (ROOT_CLUTTER_DIRS.has(nameLower) && !gitignoredDirs.has(name) && !gitignoredDirs.has(nameLower)) {
323
+ rootClutter.push(name + '/')
324
+ }
325
+ continue
326
+ }
327
+
328
+ if (!entry.isFile()) continue
329
+
330
+ // Skip dotfiles that are standard config (.gitignore, .eslintrc, .env.example, etc.)
331
+ if (name.startsWith('.')) continue
332
+
333
+ // Skip known allowed root files
334
+ if (ALLOWED_ROOT_FILES.has(nameLower)) continue
335
+
336
+ // Skip package manifests, lockfiles, configs (handled by other checks or standard)
337
+ if (nameLower.endsWith('.json') || nameLower.endsWith('.mjs') || nameLower.endsWith('.cjs')) continue
338
+ if (nameLower.endsWith('.toml') || nameLower.endsWith('.lock')) continue
339
+ if (nameLower.endsWith('.md')) continue // Handled by check 1
340
+ if (nameLower.endsWith('.sh')) continue // Handled by check 2
341
+ if (nameLower.endsWith('.yml') || nameLower.endsWith('.yaml')) continue // Handled by check 3
342
+ if (nameLower.endsWith('.ts') || nameLower.endsWith('.js')) continue // Config files like vitest.setup.ts
343
+
344
+ // Check for clutter extensions
345
+ const ext = nameLower.includes('.') ? '.' + nameLower.split('.').pop() : ''
346
+ if (ROOT_CLUTTER_EXTENSIONS.has(ext)) {
347
+ rootClutter.push(name)
348
+ }
349
+ }
350
+
351
+ result.details.rootClutter = rootClutter.slice(0, 20)
352
+ result.details.totalRootClutter = rootClutter.length
353
+
354
+ // --- Check 6: Nested docs/ directories ---
355
+ const nestedDocs = findNestedDocsDirs(projectPath, projectPath)
356
+ const nestedDocsRel = nestedDocs.map(d => relative(projectPath, d))
357
+
358
+ result.details.nestedDocsDirs = nestedDocsRel
359
+ result.details.totalNestedDocs = nestedDocsRel.length
360
+
361
+ // --- Scoring ---
362
+ let score = 6
363
+
364
+ if (strayMd.length > 0) score -= 1
365
+ if (strayScripts.length > 0) score -= 1
366
+ if (strayConfigs.length > 0) score -= 1
367
+ if (clutterFiles.length > 0) score -= 1
368
+ if (rootClutter.length > 0) score -= 1
369
+ if (nestedDocsRel.length > 0) score -= 1
370
+
371
+ result.score = score
372
+
373
+ if (score === 6) {
374
+ result.status = 'ok'
375
+ result.details.message = 'Clean repo: docs in /docs, scripts in /scripts, root & code dirs clean'
376
+ } else {
377
+ result.status = score >= 5 ? 'warning' : 'error'
378
+ const issues = []
379
+ if (strayMd.length > 0) issues.push(`${strayMd.length} stray .md`)
380
+ if (strayScripts.length > 0) issues.push(`${strayScripts.length} stray .sh`)
381
+ if (strayConfigs.length > 0) issues.push(`${strayConfigs.length} stray .yml`)
382
+ if (clutterFiles.length > 0) issues.push(`${clutterFiles.length} clutter in code dirs`)
383
+ if (rootClutter.length > 0) issues.push(`${rootClutter.length} root clutter`)
384
+ if (nestedDocsRel.length > 0) issues.push(`${nestedDocsRel.length} nested docs/`)
385
+ result.details.message = issues.join(', ')
386
+ }
387
+
388
+ return result
389
+ }
@@ -24,3 +24,4 @@ export { check as checkStellaIntegration } from './stella-integration.js'
24
24
  export { check as checkClaudeMd } from './claude-md.js'
25
25
  export { check as checkDopplerCompliance } from './doppler-compliance.js'
26
26
  export { check as checkInfrastructureYml } from './infrastructure-yml.js'
27
+ export { check as checkFileOrganization } from './file-organization.js'
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Health Check: @vca/dev-toolkit Installation
2
+ * Health Check: @soulbatical/tetra-dev-toolkit Installation
3
3
  *
4
4
  * Checks if the quality toolkit is installed and CLI commands available.
5
5
  * Score: 0 = not installed, 1 = installed, 2 = all commands available
@@ -15,7 +15,7 @@ export async function check(projectPath, { getCachedCodeQuality } = {}) {
15
15
  const result = createCheck('quality-toolkit', 2, {
16
16
  installed: false,
17
17
  version: null,
18
- commands: { 'vca-audit': false, 'vca-setup': false, 'vca-dev-token': false }
18
+ commands: { 'tetra-audit': false, 'tetra-setup': false, 'tetra-dev-token': false }
19
19
  })
20
20
 
21
21
  const packageJsonPath = join(projectPath, 'package.json')
@@ -28,12 +28,12 @@ export async function check(projectPath, { getCachedCodeQuality } = {}) {
28
28
  try {
29
29
  const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8'))
30
30
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies }
31
- const toolkitDep = allDeps['@vca/dev-toolkit'] || allDeps['@vca/quality-toolkit']
31
+ const toolkitDep = allDeps['@soulbatical/tetra-dev-toolkit'] || allDeps['@vca/dev-toolkit'] || allDeps['@vca/quality-toolkit']
32
32
 
33
33
  if (!toolkitDep) {
34
34
  result.status = 'warning'
35
35
  result.details.message = 'Not installed'
36
- result.details.installCommand = 'npm install --save-dev /Users/albertbarth/projecten/vca-quality-toolkit'
36
+ result.details.installCommand = 'npm install --save-dev @soulbatical/tetra-dev-toolkit'
37
37
  return result
38
38
  }
39
39
 
@@ -42,8 +42,12 @@ export async function check(projectPath, { getCachedCodeQuality } = {}) {
42
42
  result.score = 1
43
43
 
44
44
  // Get installed version from node_modules
45
- for (const pkgName of ['dev-toolkit', 'quality-toolkit']) {
46
- const toolkitPackagePath = join(projectPath, 'node_modules', '@vca', pkgName, 'package.json')
45
+ const lookupPaths = [
46
+ join(projectPath, 'node_modules', '@soulbatical', 'tetra-dev-toolkit', 'package.json'),
47
+ join(projectPath, 'node_modules', '@vca', 'dev-toolkit', 'package.json'),
48
+ join(projectPath, 'node_modules', '@vca', 'quality-toolkit', 'package.json'),
49
+ ]
50
+ for (const toolkitPackagePath of lookupPaths) {
47
51
  if (existsSync(toolkitPackagePath)) {
48
52
  try {
49
53
  result.details.version = JSON.parse(readFileSync(toolkitPackagePath, 'utf-8')).version
@@ -54,9 +58,9 @@ export async function check(projectPath, { getCachedCodeQuality } = {}) {
54
58
  }
55
59
  }
56
60
 
57
- // Check CLI commands
61
+ // Check CLI commands (check new tetra-* names, fall back to legacy vca-*)
58
62
  const binPath = join(projectPath, 'node_modules', '.bin')
59
- const commands = ['vca-audit', 'vca-setup', 'vca-dev-token']
63
+ const commands = ['tetra-audit', 'tetra-setup', 'tetra-dev-token']
60
64
  for (const cmd of commands) {
61
65
  result.details.commands[cmd] = existsSync(join(binPath, cmd))
62
66
  }
@@ -43,7 +43,7 @@ export async function check(projectPath) {
43
43
 
44
44
  try {
45
45
  const response = await fetch(`https://api.github.com/repos/${owner}/${repo}`, {
46
- headers: { 'User-Agent': 'vca-health-check' },
46
+ headers: { 'User-Agent': 'tetra-health-check' },
47
47
  signal: AbortSignal.timeout(5000)
48
48
  })
49
49
 
@@ -20,6 +20,7 @@ import { check as checkStellaIntegration } from './stella-integration.js'
20
20
  import { check as checkClaudeMd } from './claude-md.js'
21
21
  import { check as checkDopplerCompliance } from './doppler-compliance.js'
22
22
  import { check as checkInfrastructureYml } from './infrastructure-yml.js'
23
+ import { check as checkFileOrganization } from './file-organization.js'
23
24
  import { calculateHealthStatus } from './types.js'
24
25
 
25
26
  /**
@@ -48,7 +49,8 @@ export async function scanProjectHealth(projectPath, projectName, options = {})
48
49
  checkStellaIntegration(projectPath),
49
50
  checkClaudeMd(projectPath),
50
51
  checkDopplerCompliance(projectPath),
51
- checkInfrastructureYml(projectPath)
52
+ checkInfrastructureYml(projectPath),
53
+ checkFileOrganization(projectPath)
52
54
  ])
53
55
 
54
56
  const totalScore = checks.reduce((sum, c) => sum + c.score, 0)
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Health Check: @ralph/stella Integration
2
+ * Health Check: @soulbatical/stella Integration
3
3
  *
4
- * Checks for @ralph/stella dependency and integration level in MCP server.
4
+ * Checks for @soulbatical/stella dependency and integration level in MCP server.
5
5
  * Score: 0 = not installed, 1 = basic, 2 = full integration
6
6
  */
7
7
 
@@ -14,7 +14,7 @@ export async function check(projectPath) {
14
14
  installed: false, version: null, integrationLevel: 'none', features: [], packageLocation: null
15
15
  })
16
16
 
17
- // Search for @ralph/stella in package.json files
17
+ // Search for @soulbatical/stella in package.json files
18
18
  const packageLocations = [
19
19
  { path: join(projectPath, 'package.json'), label: 'root' },
20
20
  { path: join(projectPath, 'mcp', 'package.json'), label: 'mcp/' },
@@ -29,8 +29,8 @@ export async function check(projectPath) {
29
29
  try {
30
30
  const pkg = JSON.parse(readFileSync(loc.path, 'utf-8'))
31
31
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies }
32
- if (allDeps['@ralph/stella']) {
33
- stellaDep = allDeps['@ralph/stella']
32
+ if (allDeps['@soulbatical/stella']) {
33
+ stellaDep = allDeps['@soulbatical/stella']
34
34
  stellaLocation = loc.label
35
35
  break
36
36
  }
@@ -50,8 +50,8 @@ export async function check(projectPath) {
50
50
 
51
51
  // Get installed version
52
52
  const nmPaths = [
53
- join(projectPath, 'node_modules', '@ralph', 'stella', 'package.json'),
54
- join(projectPath, stellaLocation || '', 'node_modules', '@ralph', 'stella', 'package.json'),
53
+ join(projectPath, 'node_modules', '@soulbatical', 'stella', 'package.json'),
54
+ join(projectPath, stellaLocation || '', 'node_modules', '@soulbatical', 'stella', 'package.json'),
55
55
  ]
56
56
  for (const nmPath of nmPaths) {
57
57
  if (!existsSync(nmPath)) continue
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  /**
8
- * @typedef {'plugins'|'mcps'|'git'|'tests'|'secrets'|'quality-toolkit'|'naming-conventions'|'rls-audit'|'gitignore'|'repo-visibility'|'vincifox-widget'|'stella-integration'|'claude-md'|'doppler-compliance'|'infrastructure-yml'} HealthCheckType
8
+ * @typedef {'plugins'|'mcps'|'git'|'tests'|'secrets'|'quality-toolkit'|'naming-conventions'|'rls-audit'|'gitignore'|'repo-visibility'|'vincifox-widget'|'stella-integration'|'claude-md'|'doppler-compliance'|'infrastructure-yml'|'file-organization'} HealthCheckType
9
9
  *
10
10
  * @typedef {'ok'|'warning'|'error'} HealthStatus
11
11
  *
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Hygiene Check: File Organization
3
+ *
4
+ * Wraps the health check file-organization as a tetra-audit check.
5
+ * Detects stray docs, scripts, clutter in code dirs, and root mess.
6
+ *
7
+ * Severity: high — messy repos ship messy code
8
+ */
9
+
10
+ import { check as healthCheck } from '../health/file-organization.js'
11
+
12
+ export const meta = {
13
+ id: 'file-organization',
14
+ name: 'File Organization',
15
+ category: 'hygiene',
16
+ severity: 'high',
17
+ description: 'Checks that docs are in /docs, scripts in /scripts, no clutter in code dirs or repo root'
18
+ }
19
+
20
+ export async function run(config, projectRoot) {
21
+ const result = {
22
+ passed: true,
23
+ findings: [],
24
+ summary: { total: 0, critical: 0, high: 0, medium: 0, low: 0 },
25
+ details: {}
26
+ }
27
+
28
+ const health = await healthCheck(projectRoot)
29
+ result.details = health.details
30
+
31
+ // Convert health check results to findings
32
+ if (health.details.totalStrayMd > 0) {
33
+ result.findings.push({
34
+ type: 'Stray documentation files',
35
+ severity: 'high',
36
+ message: `${health.details.totalStrayMd} .md file(s) outside /docs/ — move to docs/ or delete`,
37
+ files: health.details.strayMdFiles
38
+ })
39
+ result.summary.high++
40
+ result.summary.total++
41
+ }
42
+
43
+ if (health.details.totalStrayScripts > 0) {
44
+ result.findings.push({
45
+ type: 'Stray shell scripts',
46
+ severity: 'medium',
47
+ message: `${health.details.totalStrayScripts} .sh file(s) outside /scripts/ — move to scripts/`,
48
+ files: health.details.strayScripts
49
+ })
50
+ result.summary.medium++
51
+ result.summary.total++
52
+ }
53
+
54
+ if (health.details.totalStrayConfigs > 0) {
55
+ result.findings.push({
56
+ type: 'Stray config files',
57
+ severity: 'low',
58
+ message: `${health.details.totalStrayConfigs} .yml/.yaml file(s) outside allowed dirs`,
59
+ files: health.details.strayConfigs
60
+ })
61
+ result.summary.low++
62
+ result.summary.total++
63
+ }
64
+
65
+ if (health.details.totalClutter > 0) {
66
+ // Clutter in code dirs is high — often includes .env secrets
67
+ const hasEnv = health.details.clutterFiles.some(f => f.includes('.env'))
68
+ const severity = hasEnv ? 'critical' : 'high'
69
+ result.findings.push({
70
+ type: 'Clutter in code directories',
71
+ severity,
72
+ message: `${health.details.totalClutter} clutter file(s) in code dirs (${hasEnv ? 'includes .env!' : '.txt, .log, .bak, etc.'})`,
73
+ files: health.details.clutterFiles
74
+ })
75
+ result.summary[severity]++
76
+ result.summary.total++
77
+ }
78
+
79
+ if (health.details.totalRootClutter > 0) {
80
+ result.findings.push({
81
+ type: 'Root clutter',
82
+ severity: 'high',
83
+ message: `${health.details.totalRootClutter} clutter item(s) in repo root — ${health.details.rootClutter.join(', ')}`,
84
+ files: health.details.rootClutter
85
+ })
86
+ result.summary.high++
87
+ result.summary.total++
88
+ }
89
+
90
+ if (health.details.totalNestedDocs > 0) {
91
+ result.findings.push({
92
+ type: 'Nested docs/ directories',
93
+ severity: 'medium',
94
+ message: `${health.details.totalNestedDocs} nested docs/ dir(s) — consolidate into root /docs/`,
95
+ files: health.details.nestedDocsDirs
96
+ })
97
+ result.summary.medium++
98
+ result.summary.total++
99
+ }
100
+
101
+ // Fail if any high+ findings
102
+ result.passed = result.summary.critical === 0 && result.summary.high === 0
103
+
104
+ return result
105
+ }
@@ -13,5 +13,9 @@ export * as huskyHooks from './stability/husky-hooks.js'
13
13
  export * as ciPipeline from './stability/ci-pipeline.js'
14
14
  export * as npmAudit from './stability/npm-audit.js'
15
15
 
16
+ // Supabase checks
17
+ export * as rlsPolicyAudit from './supabase/rls-policy-audit.js'
18
+ export * as rpcParamMismatch from './supabase/rpc-param-mismatch.js'
19
+
16
20
  // Health checks (project ecosystem)
17
21
  export * as health from './health/index.js'
@@ -91,7 +91,7 @@ export async function run(config, projectRoot) {
91
91
  },
92
92
  {
93
93
  name: 'security-audit',
94
- patterns: ['npm audit', 'vca-audit', 'security-check', 'snyk', 'CodeQL'],
94
+ patterns: ['npm audit', 'tetra-audit', 'security-check', 'snyk', 'CodeQL'],
95
95
  severity: 'medium'
96
96
  }
97
97
  ]
@@ -56,7 +56,7 @@ export async function run(config, projectRoot) {
56
56
  { name: 'lint', patterns: ['lint', 'eslint'] },
57
57
  { name: 'type-check', patterns: ['tsc', 'typecheck', 'type-check'] },
58
58
  { name: 'test', patterns: ['test', 'jest', 'vitest'] },
59
- { name: 'security', patterns: ['security', 'audit', 'vca-'] }
59
+ { name: 'security', patterns: ['security', 'audit', 'tetra-', 'vca-'] }
60
60
  ]
61
61
 
62
62
  const missingChecks = []