@soulbatical/tetra-dev-toolkit 1.8.1 → 1.8.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.
@@ -42,6 +42,46 @@ function scanDatabaseNaming(projectPath, options = {}) {
42
42
 
43
43
  if (sqlFiles.length === 0) return { totalFields: 0, compliant: 0, violations: [], compliancePercent: 100 }
44
44
 
45
+ // Sort migration files chronologically so we can track renames
46
+ sqlFiles.sort()
47
+
48
+ // First pass: collect all renames and late-added columns from ALL migration files.
49
+ // These are used to suppress violations from earlier migrations.
50
+ const renamedTables = new Map() // "old_table" → "new_table"
51
+ const renamedColumns = new Map() // "table.old_col" → "new_col"
52
+ const renamedIndexes = new Map() // "old_index" → "new_index"
53
+ const lateAddedColumns = new Map() // "table.col" → true (columns added via ALTER TABLE ADD COLUMN)
54
+
55
+ for (const filePath of sqlFiles) {
56
+ let content
57
+ try { content = readFileSync(filePath, 'utf-8') } catch { continue }
58
+
59
+ // Track RENAME TABLE: ALTER TABLE old RENAME TO new
60
+ const renameTableRegex = /ALTER\s+TABLE\s+(?:IF\s+EXISTS\s+)?(?:public\.)?["']?(\w+)["']?\s+RENAME\s+TO\s+["']?(\w+)["']?/gi
61
+ let m
62
+ while ((m = renameTableRegex.exec(content)) !== null) {
63
+ renamedTables.set(m[1], m[2])
64
+ }
65
+
66
+ // Track RENAME COLUMN: ALTER TABLE x RENAME COLUMN old TO new
67
+ const renameColRegex = /ALTER\s+TABLE\s+(?:IF\s+EXISTS\s+)?(?:public\.)?["']?(\w+)["']?\s+RENAME\s+COLUMN\s+["']?(\w+)["']?\s+TO\s+["']?(\w+)["']?/gi
68
+ while ((m = renameColRegex.exec(content)) !== null) {
69
+ renamedColumns.set(`${m[1]}.${m[2]}`, m[3])
70
+ }
71
+
72
+ // Track RENAME INDEX: ALTER INDEX old RENAME TO new
73
+ const renameIdxRegex = /ALTER\s+INDEX\s+(?:IF\s+EXISTS\s+)?["']?(\w+)["']?\s+RENAME\s+TO\s+["']?(\w+)["']?/gi
74
+ while ((m = renameIdxRegex.exec(content)) !== null) {
75
+ renamedIndexes.set(m[1], m[2])
76
+ }
77
+
78
+ // Track ADD COLUMN (timestamps added later fix "missing timestamps" violations)
79
+ const addColRegex = /ALTER\s+TABLE\s+(?:IF\s+EXISTS\s+)?(?:public\.)?["']?(\w+)["']?\s+ADD\s+(?:COLUMN\s+)?(?:IF\s+NOT\s+EXISTS\s+)?["']?(\w+)["']?\s+\w+/gi
80
+ while ((m = addColRegex.exec(content)) !== null) {
81
+ lateAddedColumns.set(`${m[1]}.${m[2]}`, true)
82
+ }
83
+ }
84
+
45
85
  const globalColumns = {}
46
86
 
47
87
  const checkColumn = (tableName, colName, colType, body, fileName) => {
@@ -140,6 +180,85 @@ function scanDatabaseNaming(projectPath, options = {}) {
140
180
  }
141
181
  }
142
182
 
183
+ // ── Filter violations resolved by later migrations ──
184
+
185
+ // Resolve a table name through renames: la_reflections → stella_reflections
186
+ const resolveTable = (table) => {
187
+ return renamedTables.get(table) || table
188
+ }
189
+
190
+ // Helper: check if a column was renamed (on original or renamed table)
191
+ const isColumnRenamed = (table, col) => {
192
+ if (renamedColumns.has(`${table}.${col}`)) return true
193
+ const resolved = resolveTable(table)
194
+ if (resolved !== table && renamedColumns.has(`${resolved}.${col}`)) return true
195
+ return false
196
+ }
197
+
198
+ // Helper: check if an index was renamed
199
+ const isIndexRenamed = (indexName) => {
200
+ return renamedIndexes.has(indexName)
201
+ }
202
+
203
+ // Helper: check if a missing timestamp was added (on original or renamed table)
204
+ const isTimestampAddedLater = (table, col) => {
205
+ if (lateAddedColumns.has(`${table}.${col}`)) return true
206
+ const resolved = resolveTable(table)
207
+ if (resolved !== table && lateAddedColumns.has(`${resolved}.${col}`)) return true
208
+ return false
209
+ }
210
+
211
+ // Helper: check if a table was renamed (violations on old name are resolved)
212
+ const isTableRenamed = (table) => {
213
+ return renamedTables.has(table)
214
+ }
215
+
216
+ // Filter out resolved violations
217
+ const unresolvedViolations = violations.filter(v => {
218
+ // Extract table name from any violation that mentions "table.col" or Table "name"
219
+ const tableColMatch = v.match(/"(\w+)\.(\w+)"/)
220
+ const tableOnlyMatch = v.match(/Table\s+"(\w+)"/)
221
+ const table = tableColMatch ? tableColMatch[1] : (tableOnlyMatch ? tableOnlyMatch[1] : null)
222
+
223
+ // If the table itself was renamed, ALL violations on the old name are resolved
224
+ // (the new table name will be checked separately if it appears in a CREATE TABLE)
225
+ if (table && isTableRenamed(table)) {
226
+ compliant++
227
+ return false
228
+ }
229
+
230
+ // Column violations: "Boolean/JSON/Column/PK/FK "table.col" ..."
231
+ if (tableColMatch && isColumnRenamed(tableColMatch[1], tableColMatch[2])) {
232
+ compliant++
233
+ return false
234
+ }
235
+
236
+ // Index violations: 'Index "name" should use idx_...'
237
+ const idxMatch = v.match(/Index\s+"(\w+)"\s+should/)
238
+ if (idxMatch && isIndexRenamed(idxMatch[1])) {
239
+ compliant++
240
+ return false
241
+ }
242
+
243
+ // Missing timestamp violations: 'Table "x" missing created_at, updated_at'
244
+ const tsMatch = v.match(/Table\s+"(\w+)"\s+missing\s+(.+?)(?:\s+\()/)
245
+ if (tsMatch) {
246
+ const tbl = tsMatch[1]
247
+ const missingCols = tsMatch[2].split(/,\s*/)
248
+ const allFixed = missingCols.every(col => isTimestampAddedLater(tbl, col.trim()))
249
+ if (allFixed) {
250
+ compliant++
251
+ return false
252
+ }
253
+ }
254
+
255
+ return true
256
+ })
257
+
258
+ // Replace violations array with filtered version
259
+ violations.length = 0
260
+ violations.push(...unresolvedViolations)
261
+
143
262
  // Consistency checks
144
263
  const consistencyGroups = [
145
264
  { concept: 'creation timestamp', preferred: 'created_at', alternatives: ['created_on', 'creation_date', 'inserted_at', 'date_created'] },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soulbatical/tetra-dev-toolkit",
3
- "version": "1.8.1",
3
+ "version": "1.8.3",
4
4
  "publishConfig": {
5
5
  "access": "restricted"
6
6
  },