@soulbatical/tetra-dev-toolkit 1.16.1 → 1.16.3

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/README.md CHANGED
@@ -58,7 +58,7 @@ LAYER 8: DB HELPERS adminDB/userDB/publicDB/systemDB enforce correct
58
58
  | `tetra-audit` | Run quality/security/hygiene checks |
59
59
  | `tetra-audit quick` | Quick critical checks (pre-commit) |
60
60
  | `tetra-audit security` | Full security suite (12 checks) |
61
- | `tetra-audit stability` | Stability suite (3 checks) |
61
+ | `tetra-audit stability` | Stability suite (16 checks) |
62
62
  | `tetra-audit codeQuality` | Code quality suite (4 checks) |
63
63
  | `tetra-audit supabase` | Supabase suite (3 checks) |
64
64
  | `tetra-audit hygiene` | Repo hygiene suite (2 checks) |
@@ -119,13 +119,26 @@ Exit codes: `0` = passed, `1` = failed (CRITICAL/HIGH), `2` = error. No middle g
119
119
  | `rpc-param-mismatch` | critical | TypeScript `.rpc()` calls with wrong parameter names vs SQL |
120
120
  | `rpc-generator-origin` | high | RPC functions not generated by Tetra SQL Generator |
121
121
 
122
- ### Stability (3 checks)
122
+ ### Stability (16 checks)
123
123
 
124
124
  | Check | Severity | What it catches |
125
125
  |-------|----------|-----------------|
126
126
  | `husky-hooks` | medium | Missing pre-commit/pre-push hooks |
127
127
  | `ci-pipeline` | medium | Missing or incomplete CI config |
128
128
  | `npm-audit` | high | Known vulnerabilities in dependencies |
129
+ | `tests` | high | Missing test framework, test files, or test scripts |
130
+ | `eslint-security` | high | Missing ESLint config, security plugin, or SonarJS plugin |
131
+ | `typescript-strict` | medium | TypeScript strict mode not enabled |
132
+ | `coverage-thresholds` | medium | No coverage thresholds configured |
133
+ | `knip` | medium | Dead code: unused files, dependencies, exports |
134
+ | `dependency-cruiser` | medium | Circular dependencies, architecture violations |
135
+ | `dependency-automation` | medium | No Dependabot or Renovate configured |
136
+ | `prettier` | low | No code formatter configured |
137
+ | `conventional-commits` | low | No commit message convention enforced |
138
+ | `bundle-size` | low | No bundle size monitoring |
139
+ | `sast` | medium | No static application security testing |
140
+ | `license-audit` | low | No license compliance checking |
141
+ | `security-layers` | high | Missing security layers (auth, rate limiting, CORS, etc.) |
129
142
 
130
143
  ### Code Quality (4 checks)
131
144
 
@@ -220,6 +233,18 @@ If any step fails, fix it before writing code. No exceptions.
220
233
 
221
234
  ## Changelog
222
235
 
236
+ ### 1.16.0
237
+
238
+ **New: Full Stability Suite (16 checks)**
239
+ - Stability suite expanded from 3 → 16 checks via health check adapter
240
+ - New checks: tests, eslint-security, typescript-strict, coverage-thresholds, knip, dependency-cruiser, dependency-automation, prettier, conventional-commits, bundle-size, sast, license-audit, security-layers
241
+ - `tetra-audit stability` now catches missing test infrastructure, dead code, formatting, and security layer gaps
242
+ - Health checks (score-based) automatically adapted to runner format (pass/fail)
243
+ - RPC generator: SECURITY DEFINER → SECURITY INVOKER for all data RPCs
244
+ - Migration lint: expanded whitelist for legitimate DEFINER functions
245
+ - Mixed DB checker: fixed regex that matched `superadminDB` as `adminDB`
246
+ - Route config checker: support `@tetra-audit-ignore` directive
247
+
223
248
  ### 1.15.0
224
249
 
225
250
  **New: Migration Lint + DB Push Guard**
@@ -184,6 +184,43 @@ function parseMigrations(projectRoot) {
184
184
  })
185
185
  }
186
186
 
187
+ // Find PL/pgSQL loop-based CREATE POLICY patterns (DO blocks with EXECUTE format)
188
+ // Handles two patterns:
189
+ // Pattern A: FOR t IN SELECT unnest(ARRAY['table1','table2']) LOOP ... EXECUTE format('CREATE POLICY ...')
190
+ // Pattern B: tables TEXT[] := ARRAY['table1','table2']; FOREACH tbl IN ARRAY tables LOOP ... EXECUTE format('CREATE POLICY ...')
191
+ if (/DO\s+\$/.test(content) && /EXECUTE\s+format\s*\(\s*'CREATE\s+POLICY/i.test(content)) {
192
+ // Try unnest pattern first, then FOREACH/variable pattern
193
+ const arrayMatch = content.match(/unnest\s*\(\s*ARRAY\s*\[\s*'([^[\]]+)'\s*\]/i)
194
+ || content.match(/(?:TEXT\[\]|text\[\])\s*:=\s*ARRAY\s*\[\s*'([^[\]]+)'\s*\]/i)
195
+ if (arrayMatch) {
196
+ const loopTables = arrayMatch[1].split(/'\s*,\s*'/).map(t => t.trim())
197
+ const execLines = [...content.matchAll(/EXECUTE\s+format\s*\(\s*'(CREATE\s+POLICY\s+.*?)'\s*,/gi)]
198
+ for (const exec of execLines) {
199
+ const stmt = exec[1]
200
+ const forOp = stmt.match(/FOR\s+(SELECT|INSERT|UPDATE|DELETE|ALL)/i)
201
+ const operation = forOp ? forOp[1].toUpperCase() : 'ALL'
202
+ const hasUsing = /\bUSING\b/i.test(stmt)
203
+ const hasWithCheck = /WITH\s+CHECK/i.test(stmt)
204
+ const condMatch = stmt.match(/(?:USING|WITH\s+CHECK)\s*\(\s*(.*?)\s*\)/i)
205
+ const condition = condMatch ? condMatch[1] : ''
206
+
207
+ for (const table of loopTables) {
208
+ if (!tables.has(table)) tables.set(table, { rlsEnabled: false, policies: [], rpcFunctions: new Map() })
209
+ const policyName = `${table}_${operation.toLowerCase()}_org`
210
+ if (!tables.get(table).policies.find(p => p.name === policyName)) {
211
+ tables.get(table).policies.push({
212
+ name: policyName,
213
+ operation,
214
+ using: hasUsing ? condition : '',
215
+ withCheck: hasWithCheck ? condition : '',
216
+ file: relFile
217
+ })
218
+ }
219
+ }
220
+ }
221
+ }
222
+ }
223
+
187
224
  // Find RPC functions and their security mode
188
225
  const funcRegex = /CREATE\s+(?:OR\s+REPLACE\s+)?FUNCTION\s+(?:public\.)?(\w+)\s*\(([\s\S]*?)\)\s*RETURNS\s+([\s\S]*?)(?:LANGUAGE|AS)/gi
189
226
  for (const m of content.matchAll(funcRegex)) {
@@ -204,6 +241,7 @@ function parseMigrations(projectRoot) {
204
241
  }
205
242
  }
206
243
  }
244
+
207
245
  }
208
246
 
209
247
  return tables
@@ -221,8 +259,10 @@ function findFiles(projectRoot, pattern) {
221
259
  * Check if a USING clause enforces org isolation
222
260
  */
223
261
  function isOrgIsolation(using) {
224
- return /auth_org_id\(\)|auth_admin_organizations\(\)/i.test(using) &&
225
- /organization_id/i.test(using)
262
+ const hasOrgFunction = /auth_org_id\(\)|auth_admin_organizations\(\)/i.test(using)
263
+ // Legacy pattern: auth.jwt() -> 'app_metadata' ->> 'organization_id'
264
+ const hasLegacyJwtOrg = /auth\.jwt\(\)\s*->\s*'app_metadata'\s*->>\s*'organization_id'/i.test(using)
265
+ return (hasOrgFunction || hasLegacyJwtOrg) && /organization_id/i.test(using)
226
266
  }
227
267
 
228
268
  /**
@@ -287,6 +327,7 @@ export async function run(config, projectRoot) {
287
327
  const updatePolicies = policies.filter(p => p.operation === 'UPDATE' || p.operation === 'ALL')
288
328
  const deletePolicies = policies.filter(p => p.operation === 'DELETE' || p.operation === 'ALL')
289
329
 
330
+
290
331
  // CHECK 2: Must have policies for all operations
291
332
  const missingOps = []
292
333
  if (selectPolicies.length === 0) missingOps.push('SELECT')
@@ -135,50 +135,53 @@ export async function run(config, projectRoot) {
135
135
 
136
136
  if (whitelistMatch) {
137
137
  const rawLines = whitelistMatch[1].split('\n')
138
- let lastComment = null
138
+ let groupComment = null
139
139
 
140
140
  for (let i = 0; i < rawLines.length; i++) {
141
141
  const line = rawLines[i].trim()
142
142
 
143
- // Track comments — group comments (// ...) or inline comments
144
- const commentMatch = line.match(/^\s*\/\/\s*(.+)/)
145
- if (commentMatch) {
146
- lastComment = commentMatch[1].trim()
143
+ // Track group comments — a comment line that is NOT inline with an entry
144
+ // Group comments apply to ALL entries below them until the next group comment
145
+ if (/^\s*\/\//.test(line) && !line.match(/['"][^'"]+['"]/)) {
146
+ const commentText = line.replace(/^\s*\/\/\s*/, '').trim()
147
+ if (commentText.length > 0) {
148
+ groupComment = commentText
149
+ }
147
150
  continue
148
151
  }
149
152
 
153
+ // Skip empty lines (don't reset group comment)
154
+ if (!line || line === ',') continue
155
+
150
156
  const entryMatch = line.match(/['"]([^'"]+)['"]/)
151
- if (!entryMatch) { continue }
157
+ if (!entryMatch) continue
152
158
 
153
159
  const entry = entryMatch[1]
154
160
  whitelist.add(entry)
155
161
 
156
162
  // Check for inline comment: 'entry', // reason
157
163
  const inlineCommentMatch = line.match(/['"][^'"]+['"]\s*,?\s*\/\/\s*(.+)/)
158
- const reason = inlineCommentMatch ? inlineCommentMatch[1].trim() : lastComment
164
+ const inlineReason = inlineCommentMatch ? inlineCommentMatch[1].trim() : null
165
+
166
+ // Entry is justified if it has an inline comment OR falls under a group comment
167
+ const hasJustification = inlineReason || groupComment
159
168
 
160
- // Find line number in original file
161
- const entryLineInFile = systemDbContent.substring(0, systemDbContent.indexOf(entry)).split('\n').length
169
+ if (!hasJustification) {
170
+ // Find line number in original file
171
+ const entryLineInFile = systemDbContent.substring(0, systemDbContent.indexOf(entry)).split('\n').length
162
172
 
163
- if (!reason) {
164
173
  results.findings.push({
165
174
  file: systemDbPath.replace(projectRoot + '/', ''),
166
175
  line: entryLineInFile,
167
176
  type: 'whitelist-no-justification',
168
177
  severity: 'high',
169
- message: `systemDB whitelist entry '${entry}' has NO comment explaining WHY it needs service role key access. Every whitelist entry MUST have a comment above or inline.`,
178
+ message: `systemDB whitelist entry '${entry}' has NO comment explaining WHY it needs service role key access. Add a group comment above or an inline comment.`,
170
179
  fix: `Add a comment explaining why '${entry}' cannot use adminDB/userDB. Example:\n // OAuth callback — browser redirect, no JWT in header\n '${entry}',`
171
180
  })
172
181
  results.summary.high++
173
182
  results.summary.total++
174
183
  results.passed = false
175
184
  }
176
-
177
- // Reset lastComment after consuming it for a non-dynamic entry
178
- // (don't reset for "// Dynamic:" comments which apply to patterns, not specific entries)
179
- if (lastComment && !lastComment.toLowerCase().startsWith('dynamic')) {
180
- lastComment = null
181
- }
182
185
  }
183
186
  }
184
187
 
package/lib/runner.js CHANGED
@@ -32,6 +32,56 @@ import * as rpcGeneratorOrigin from './checks/supabase/rpc-generator-origin.js'
32
32
  import * as fileOrganization from './checks/hygiene/file-organization.js'
33
33
  import * as stellaCompliance from './checks/hygiene/stella-compliance.js'
34
34
 
35
+ // Health checks (score-based) — wrapped as runner checks via adapter
36
+ import { check as checkTests } from './checks/health/tests.js'
37
+ import { check as checkEslintSecurity } from './checks/health/eslint-security.js'
38
+ import { check as checkTypescriptStrict } from './checks/health/typescript-strict.js'
39
+ import { check as checkCoverageThresholds } from './checks/health/coverage-thresholds.js'
40
+ import { check as checkKnip } from './checks/health/knip.js'
41
+ import { check as checkDependencyCruiser } from './checks/health/dependency-cruiser.js'
42
+ import { check as checkDependencyAutomation } from './checks/health/dependency-automation.js'
43
+ import { check as checkPrettier } from './checks/health/prettier.js'
44
+ import { check as checkConventionalCommits } from './checks/health/conventional-commits.js'
45
+ import { check as checkBundleSize } from './checks/health/bundle-size.js'
46
+ import { check as checkSast } from './checks/health/sast.js'
47
+ import { check as checkLicenseAudit } from './checks/health/license-audit.js'
48
+ import { check as checkSecurityLayers } from './checks/health/security-layers.js'
49
+
50
+ /**
51
+ * Adapt a health check (score-based) to the runner format (meta + run).
52
+ * A health check passes if it scores > 0 (has at least some infrastructure).
53
+ */
54
+ function adaptHealthCheck(id, name, severity, healthCheckFn) {
55
+ return {
56
+ meta: { id, name, severity },
57
+ async run(config, projectRoot) {
58
+ const result = await healthCheckFn(projectRoot)
59
+ const passed = result.score > 0
60
+ const findings = []
61
+
62
+ if (!passed && result.details?.message) {
63
+ findings.push({
64
+ file: 'project',
65
+ line: 0,
66
+ severity,
67
+ message: result.details.message
68
+ })
69
+ }
70
+
71
+ return {
72
+ passed,
73
+ findings,
74
+ summary: {
75
+ total: 1,
76
+ [severity]: passed ? 0 : 1,
77
+ score: result.score,
78
+ maxScore: result.maxScore
79
+ }
80
+ }
81
+ }
82
+ }
83
+ }
84
+
35
85
  // Register all checks
36
86
  const ALL_CHECKS = {
37
87
  security: [
@@ -51,7 +101,20 @@ const ALL_CHECKS = {
51
101
  stability: [
52
102
  huskyHooks,
53
103
  ciPipeline,
54
- npmAudit
104
+ npmAudit,
105
+ adaptHealthCheck('tests', 'Test Infrastructure', 'high', checkTests),
106
+ adaptHealthCheck('eslint-security', 'ESLint Security Plugins', 'high', checkEslintSecurity),
107
+ adaptHealthCheck('typescript-strict', 'TypeScript Strictness', 'medium', checkTypescriptStrict),
108
+ adaptHealthCheck('coverage-thresholds', 'Test Coverage Thresholds', 'medium', checkCoverageThresholds),
109
+ adaptHealthCheck('knip', 'Dead Code Detection (Knip)', 'medium', checkKnip),
110
+ adaptHealthCheck('dependency-cruiser', 'Dependency Architecture', 'medium', checkDependencyCruiser),
111
+ adaptHealthCheck('dependency-automation', 'Dependency Updates (Dependabot/Renovate)', 'medium', checkDependencyAutomation),
112
+ adaptHealthCheck('prettier', 'Code Formatting (Prettier)', 'low', checkPrettier),
113
+ adaptHealthCheck('conventional-commits', 'Conventional Commits', 'low', checkConventionalCommits),
114
+ adaptHealthCheck('bundle-size', 'Bundle Size Monitoring', 'low', checkBundleSize),
115
+ adaptHealthCheck('sast', 'Static Application Security Testing', 'medium', checkSast),
116
+ adaptHealthCheck('license-audit', 'License Compliance', 'low', checkLicenseAudit),
117
+ adaptHealthCheck('security-layers', 'Security Layer Coverage', 'high', checkSecurityLayers)
55
118
  ],
56
119
  codeQuality: [
57
120
  apiResponseFormat,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soulbatical/tetra-dev-toolkit",
3
- "version": "1.16.1",
3
+ "version": "1.16.3",
4
4
  "publishConfig": {
5
5
  "access": "restricted"
6
6
  },