cyclo fails
Dependencies
- [2]
GSLFNY4Pcilint cyclo and gocyclo now both agree at max 12 - [3]
XVJ2RNWUchangelog - [4]
V2HX6HEBclaude going nuts all over the place - [5]
JMDW37LVnew import tests - [6]
WVNOO43Bsome complex tests require changes to lint rules - [7]
E27ZWCDPcyclo over 18 - [8]
I4CMOMXFdot files - [9]
BZ6KQRYDadded complexity lint test - [10]
ZDZDASRTcomplexity over 12 now gone, but have some lint fails - [11]
XU7FTYK3third phase of utils refactor, wav/ - [12]
RUVJ3V4Ncyclo to 14 now - [13]
2P27XV3Dfixed cyclo over 30 - [14]
DD3LCTLZtidy up lat lng timezone api for calls classify and push certainty - [15]
JAT3DXOLcyclo over 15 - [16]
ZCCQ4P5Treduce complexity to under 14, gocyclo but cilint test still has 3 functions over - [17]
LHZQOX64complexity in tools calls and import - [18]
QFPEKXL5ck 6 - [19]
HYCZTLSZfixed tests with cyclo over 15 - [20]
DNLR36BZminor comments - [21]
NS4TDPLNcyclomatic complexity - [22]
SMWSHUOWcyclo over 15 - [23]
KZKLAINJrun out of space on nest, cleaned out - [24]
T2WZBTVFcyclo 22 - [25]
LBWQJEDHminor refactor and more tests for utils/
Change contents
- edit in wav/wav_metadata.go at line 308
}// handleFmtChunk processes the fmt chunk and updates info.func handleFmtChunk(file *os.File, chunkSize int64, info *wavChunkInfo) error {fmtData := make([]byte, chunkSize)if _, err := io.ReadFull(file, fmtData); err != nil {return fmt.Errorf("failed to read fmt chunk: %w", err)}if len(fmtData) >= 16 {info.channels = int(binary.LittleEndian.Uint16(fmtData[2:4]))info.sampleRate = int(binary.LittleEndian.Uint32(fmtData[4:8]))info.bitsPerSample = int(binary.LittleEndian.Uint16(fmtData[14:16]))}return nil - edit in wav/wav_metadata.go at line 324
// skipUnknownChunk skips an unknown chunk and its padding.func skipUnknownChunk(file *os.File, chunkSize int64) error {if _, err := file.Seek(chunkSize, io.SeekCurrent); err != nil {return fmt.Errorf("failed to skip chunk: %w", err)}if chunkSize%2 != 0 {if _, err := file.Seek(1, io.SeekCurrent); err != nil {return fmt.Errorf("failed to skip padding: %w", err)}}return nil} - replacement in wav/wav_metadata.go at line 355
fmtData := make([]byte, chunkSize)if _, err := io.ReadFull(file, fmtData); err != nil {return info, fmt.Errorf("failed to read fmt chunk: %w", err)if err := handleFmtChunk(file, chunkSize, &info); err != nil {return info, err - edit in wav/wav_metadata.go at line 358
if len(fmtData) >= 16 {info.channels = int(binary.LittleEndian.Uint16(fmtData[2:4]))info.sampleRate = int(binary.LittleEndian.Uint32(fmtData[4:8]))info.bitsPerSample = int(binary.LittleEndian.Uint16(fmtData[14:16]))} - replacement in wav/wav_metadata.go at line 363
if _, err := file.Seek(chunkSize, io.SeekCurrent); err != nil {return info, fmt.Errorf("failed to skip chunk: %w", err)if err := skipUnknownChunk(file, chunkSize); err != nil {return info, err - edit in wav/wav_metadata.go at line 366
continue - edit in wav/wav_metadata.go at line 369
// Skip padding for fmt chunk (data chunk returns above) - replacement in wav/filename_parser.go at line 122
// detectDateFormat analyzes filenames to determine the date formatfunc detectDateFormat(filenames []string) (DateFormat, error) {// Extract all date parts from filenamesvar parts []datePartsvar has8Digit bool// datePartsResult holds the result of collecting date parts from filenamestype datePartsResult struct {parts []datePartshas8Digit bool} - edit in wav/filename_parser.go at line 128
// collectDateParts extracts date parts from filenames for format detection.func collectDateParts(filenames []string) datePartsResult {var result datePartsResult - edit in wav/filename_parser.go at line 139
// Check for 8-digit format (YYYYMMDD) - replacement in wav/filename_parser.go at line 140
has8Digit = trueresult.has8Digit = true - edit in wav/filename_parser.go at line 144
// Parse 6-digit format - replacement in wav/filename_parser.go at line 148
parts = append(parts, dateParts{x1: x1, m: m, x2: x2})result.parts = append(result.parts, dateParts{x1: x1, m: m, x2: x2}) - edit in wav/filename_parser.go at line 151
return result} - edit in wav/filename_parser.go at line 154
// determineFormatFromParts determines the date format from collected parts.func determineFormatFromParts(result datePartsResult) (DateFormat, error) { - replacement in wav/filename_parser.go at line 157
if has8Digit && len(parts) == 0 {if result.has8Digit && len(result.parts) == 0 { - replacement in wav/filename_parser.go at line 162
if has8Digit && len(parts) > 0 {if result.has8Digit && len(result.parts) > 0 { - replacement in wav/filename_parser.go at line 167
if len(parts) == 0 {if len(result.parts) == 0 { - replacement in wav/filename_parser.go at line 172
if len(parts) == 1 {if len(result.parts) == 1 { - replacement in wav/filename_parser.go at line 177
// Compare uniqueness of x1 (first 2 digits) vs x2 (last 2 digits)// Day values vary more than year values across recordingsuniqueX1 := countUnique(parts, func(p dateParts) int { return p.x1 })uniqueX2 := countUnique(parts, func(p dateParts) int { return p.x2 })uniqueX1 := countUnique(result.parts, func(p dateParts) int { return p.x1 })uniqueX2 := countUnique(result.parts, func(p dateParts) int { return p.x2 }) - edit in wav/filename_parser.go at line 181
// x2 has more variance → likely day values → YYMMDD format - edit in wav/filename_parser.go at line 182
} else {// x1 has more variance → likely day values → DDMMYY formatreturn Format6DDMMYY, nil - edit in wav/filename_parser.go at line 183
return Format6DDMMYY, nil}// detectDateFormat analyzes filenames to determine the date formatfunc detectDateFormat(filenames []string) (DateFormat, error) {result := collectDateParts(filenames)return determineFormatFromParts(result) - edit in utils/fs.go at line 35
}// shouldSkipHidden checks if a path should be skipped due to hidden file rules.func shouldSkipHidden(name, path, rootPath string, info os.FileInfo, skipHidden bool) (bool, error) {if !skipHidden || !strings.HasPrefix(name, ".") || path == rootPath {return false, nil}if info.IsDir() {return true, filepath.SkipDir}return true, nil - edit in utils/fs.go at line 48
// shouldSkipDir checks if a directory should be skipped based on prefixes.func shouldSkipDir(name, path, rootPath string, skipPrefixes []string) bool {if path == rootPath {return false}for _, prefix := range skipPrefixes {if strings.HasPrefix(name, prefix) {return true}}return false} - replacement in utils/fs.go at line 70[7.612]→[7.612:736](∅→∅),[7.736]→[7.4464:4469](∅→∅),[7.4464]→[7.4464:4469](∅→∅),[7.4469]→[7.737:751](∅→∅)
if opts.SkipHidden && strings.HasPrefix(name, ".") && path != rootPath {if info.IsDir() {return filepath.SkipDir}return nilif skip, skipErr := shouldSkipHidden(name, path, rootPath, info, opts.SkipHidden); skip {return skipErr - replacement in utils/fs.go at line 75
if info.IsDir() && path != rootPath {for _, prefix := range opts.SkipPrefixes {if strings.HasPrefix(name, prefix) {return filepath.SkipDir}if info.IsDir() {if shouldSkipDir(name, path, rootPath, opts.SkipPrefixes) {return filepath.SkipDir - replacement in utils/fs.go at line 83
if !info.IsDir() {if fileMatches(name, info, opts) {*results = append(*results, path)}if fileMatches(name, info, opts) {*results = append(*results, path) - replacement in tools/import/import_unstructured_test.go at line 11
func TestImportUnstructured(t *testing.T) {func TestImportUnstructured_HappyPath(t *testing.T) { - edit in tools/import/import_unstructured_test.go at line 13
dbPath := setupFileBasedTestDB(t) - replacement in tools/import/import_unstructured_test.go at line 15
t.Run("happy path - import single WAV file", func(t *testing.T) {dbPath := setupFileBasedTestDB(t)// Create temp folder with a WAV filetmpDir := t.TempDir()wavPath := filepath.Join(tmpDir, "test_recording.wav")hash := createTestWAV(t, wavPath) - replacement in tools/import/import_unstructured_test.go at line 20
// Create temp folder with a WAV filetmpDir := t.TempDir()wavPath := filepath.Join(tmpDir, "test_recording.wav")hash := createTestWAV(t, wavPath)// Import to unstructured datasetoutput, err := ImportUnstructured(ctx, ImportUnstructuredInput{DBPath: dbPath,DatasetID: "dstest000002", // unstructured datasetFolderPath: tmpDir,Recursive: new(true),})if err != nil {t.Fatalf("ImportUnstructured failed: %v", err)} - replacement in tools/import/import_unstructured_test.go at line 31
// Import to unstructured datasetoutput, err := ImportUnstructured(ctx, ImportUnstructuredInput{DBPath: dbPath,DatasetID: "dstest000002", // unstructured datasetFolderPath: tmpDir,Recursive: new(true),})if err != nil {t.Fatalf("ImportUnstructured failed: %v", err)}// Verify outputif output.TotalFiles != 1 {t.Errorf("expected 1 total file, got %d", output.TotalFiles)}if output.ImportedFiles != 1 {t.Errorf("expected 1 imported file, got %d", output.ImportedFiles)}if output.SkippedFiles != 0 {t.Errorf("expected 0 skipped files, got %d", output.SkippedFiles)}if len(output.Errors) != 0 {t.Errorf("unexpected errors: %v", output.Errors)} - replacement in tools/import/import_unstructured_test.go at line 45
// Verify outputif output.TotalFiles != 1 {t.Errorf("expected 1 total file, got %d", output.TotalFiles)}if output.ImportedFiles != 1 {t.Errorf("expected 1 imported file, got %d", output.ImportedFiles)}if output.SkippedFiles != 0 {t.Errorf("expected 0 skipped files, got %d", output.SkippedFiles)}if len(output.Errors) != 0 {t.Errorf("unexpected errors: %v", output.Errors)}// Verify file was inserted into databasedatabase, err := sql.Open("duckdb", dbPath)if err != nil {t.Fatalf("failed to open database for verification: %v", err)}defer database.Close() - replacement in tools/import/import_unstructured_test.go at line 52
// Verify file was inserted into databasedatabase, err := sql.Open("duckdb", dbPath)if err != nil {t.Fatalf("failed to open database for verification: %v", err)}defer database.Close()var fileCount interr = database.QueryRow("SELECT COUNT(*) FROM file WHERE xxh64_hash = ? AND active = true", hash).Scan(&fileCount)if err != nil {t.Fatalf("failed to query file: %v", err)}if fileCount != 1 {t.Errorf("expected 1 file in database, got %d", fileCount)} - replacement in tools/import/import_unstructured_test.go at line 61
var fileCount interr = database.QueryRow("SELECT COUNT(*) FROM file WHERE xxh64_hash = ? AND active = true", hash).Scan(&fileCount)if err != nil {t.Fatalf("failed to query file: %v", err)}if fileCount != 1 {t.Errorf("expected 1 file in database, got %d", fileCount)}// Verify file_dataset linkvar linkCount interr = database.QueryRow(`SELECT COUNT(*) FROM file_dataset fdJOIN file f ON fd.file_id = f.idWHERE f.xxh64_hash = ? AND fd.dataset_id = 'dstest000002'`, hash).Scan(&linkCount)if err != nil {t.Fatalf("failed to query file_dataset: %v", err)}if linkCount != 1 {t.Errorf("expected 1 file_dataset link, got %d", linkCount)} - replacement in tools/import/import_unstructured_test.go at line 75
// Verify file_dataset linkvar linkCount interr = database.QueryRow(`SELECT COUNT(*) FROM file_dataset fdJOIN file f ON fd.file_id = f.idWHERE f.xxh64_hash = ? AND fd.dataset_id = 'dstest000002'`, hash).Scan(&linkCount)if err != nil {t.Fatalf("failed to query file_dataset: %v", err)}if linkCount != 1 {t.Errorf("expected 1 file_dataset link, got %d", linkCount)}// Verify location_id and cluster_id are NULL for unstructuredvar locID, clID sql.NullStringerr = database.QueryRow("SELECT location_id, cluster_id FROM file WHERE xxh64_hash = ?", hash).Scan(&locID, &clID)if err != nil {t.Fatalf("failed to query file: %v", err)}if locID.Valid {t.Errorf("expected NULL location_id for unstructured file, got %s", locID.String)}if clID.Valid {t.Errorf("expected NULL cluster_id for unstructured file, got %s", clID.String)}} - replacement in tools/import/import_unstructured_test.go at line 89
// Verify location_id and cluster_id are NULL for unstructuredvar locID, clID sql.NullStringerr = database.QueryRow("SELECT location_id, cluster_id FROM file WHERE xxh64_hash = ?", hash).Scan(&locID, &clID)if err != nil {t.Fatalf("failed to query file: %v", err)}if locID.Valid {t.Errorf("expected NULL location_id for unstructured file, got %s", locID.String)}if clID.Valid {t.Errorf("expected NULL cluster_id for unstructured file, got %s", clID.String)}})func TestImportUnstructured_DuplicateHandling(t *testing.T) {ctx := context.Background()dbPath := setupFileBasedTestDB(t) - replacement in tools/import/import_unstructured_test.go at line 93
t.Run("duplicate handling - skip file with existing hash", func(t *testing.T) {dbPath := setupFileBasedTestDB(t)// Create temp folder with a WAV filetmpDir := t.TempDir()wavPath := filepath.Join(tmpDir, "test_recording.wav")hash := createTestWAV(t, wavPath) - replacement in tools/import/import_unstructured_test.go at line 98
// Create temp folder with a WAV filetmpDir := t.TempDir()wavPath := filepath.Join(tmpDir, "test_recording.wav")hash := createTestWAV(t, wavPath)// First import_, err := ImportUnstructured(ctx, ImportUnstructuredInput{DBPath: dbPath,DatasetID: "dstest000002",FolderPath: tmpDir,Recursive: new(true),})if err != nil {t.Fatalf("first import failed: %v", err)} - replacement in tools/import/import_unstructured_test.go at line 109
// First import_, err := ImportUnstructured(ctx, ImportUnstructuredInput{DBPath: dbPath,DatasetID: "dstest000002",FolderPath: tmpDir,Recursive: new(true),})if err != nil {t.Fatalf("first import failed: %v", err)}// Second import of same file (should be skipped as duplicate)output, err := ImportUnstructured(ctx, ImportUnstructuredInput{DBPath: dbPath,DatasetID: "dstest000002",FolderPath: tmpDir,Recursive: new(true),})if err != nil {t.Fatalf("second import failed: %v", err)} - replacement in tools/import/import_unstructured_test.go at line 120
// Second import of same file (should be skipped as duplicate)output, err := ImportUnstructured(ctx, ImportUnstructuredInput{DBPath: dbPath,DatasetID: "dstest000002",FolderPath: tmpDir,Recursive: new(true),})if err != nil {t.Fatalf("second import failed: %v", err)}// Verify outputif output.TotalFiles != 1 {t.Errorf("expected 1 total file, got %d", output.TotalFiles)}if output.ImportedFiles != 0 {t.Errorf("expected 0 imported files (duplicate), got %d", output.ImportedFiles)}if output.SkippedFiles != 1 {t.Errorf("expected 1 skipped file (duplicate), got %d", output.SkippedFiles)} - replacement in tools/import/import_unstructured_test.go at line 131
// Verify outputif output.TotalFiles != 1 {t.Errorf("expected 1 total file, got %d", output.TotalFiles)}if output.ImportedFiles != 0 {t.Errorf("expected 0 imported files (duplicate), got %d", output.ImportedFiles)}if output.SkippedFiles != 1 {t.Errorf("expected 1 skipped file (duplicate), got %d", output.SkippedFiles)}// Verify only one file in database (not duplicated)database, err := sql.Open("duckdb", dbPath)if err != nil {t.Fatalf("failed to open database for verification: %v", err)}defer database.Close() - replacement in tools/import/import_unstructured_test.go at line 138
// Verify only one file in database (not duplicated)database, err := sql.Open("duckdb", dbPath)if err != nil {t.Fatalf("failed to open database for verification: %v", err)}defer database.Close()var fileCount interr = database.QueryRow("SELECT COUNT(*) FROM file WHERE xxh64_hash = ? AND active = true", hash).Scan(&fileCount)if err != nil {t.Fatalf("failed to query file: %v", err)}if fileCount != 1 {t.Errorf("expected 1 file in database (not duplicated), got %d", fileCount)}} - replacement in tools/import/import_unstructured_test.go at line 148
var fileCount interr = database.QueryRow("SELECT COUNT(*) FROM file WHERE xxh64_hash = ? AND active = true", hash).Scan(&fileCount)if err != nil {t.Fatalf("failed to query file: %v", err)}if fileCount != 1 {t.Errorf("expected 1 file in database (not duplicated), got %d", fileCount)}})func TestImportUnstructured_EdgeCases(t *testing.T) {ctx := context.Background() - replacement in tools/import/import_files_test.go at line 12
func TestImportAudioFiles(t *testing.T) {func TestImportAudioFiles_HappyPath(t *testing.T) { - edit in tools/import/import_files_test.go at line 14
dbPath := setupFileBasedTestDB(t) - replacement in tools/import/import_files_test.go at line 16
t.Run("happy path - import single WAV file", func(t *testing.T) {dbPath := setupFileBasedTestDB(t)// Don't keep database open - ImportAudioFiles manages its own connections// Create temp folder with a WAV filetmpDir := t.TempDir()wavPath := filepath.Join(tmpDir, "test_recording.wav")hash := createTestWAV(t, wavPath) - replacement in tools/import/import_files_test.go at line 21
// Create temp folder with a WAV filetmpDir := t.TempDir()wavPath := filepath.Join(tmpDir, "test_recording.wav")hash := createTestWAV(t, wavPath)// Importoutput, err := ImportAudioFiles(ctx, ImportAudioFilesInput{DBPath: dbPath,FolderPath: tmpDir,DatasetID: "dstest000001",LocationID: "loctest00001",ClusterID: "cltest000001",Recursive: new(true),})if err != nil {t.Fatalf("ImportAudioFiles failed: %v", err)} - replacement in tools/import/import_files_test.go at line 34
// Importoutput, err := ImportAudioFiles(ctx, ImportAudioFilesInput{DBPath: dbPath,FolderPath: tmpDir,DatasetID: "dstest000001",LocationID: "loctest00001",ClusterID: "cltest000001",Recursive: new(true),})if err != nil {t.Fatalf("ImportAudioFiles failed: %v", err)}// Verify outputif output.Summary.TotalFiles != 1 {t.Errorf("expected 1 total file, got %d", output.Summary.TotalFiles)}if output.Summary.ImportedFiles != 1 {t.Errorf("expected 1 imported file, got %d", output.Summary.ImportedFiles)}if output.Summary.SkippedFiles != 0 {t.Errorf("expected 0 skipped files, got %d", output.Summary.SkippedFiles)}if len(output.Errors) != 0 {t.Errorf("unexpected errors: %v", output.Errors)} - replacement in tools/import/import_files_test.go at line 48
// Verify outputif output.Summary.TotalFiles != 1 {t.Errorf("expected 1 total file, got %d", output.Summary.TotalFiles)}if output.Summary.ImportedFiles != 1 {t.Errorf("expected 1 imported file, got %d", output.Summary.ImportedFiles)}if output.Summary.SkippedFiles != 0 {t.Errorf("expected 0 skipped files, got %d", output.Summary.SkippedFiles)}if len(output.Errors) != 0 {t.Errorf("unexpected errors: %v", output.Errors)}// Verify file was inserted into database - open new connectiondatabase, err := sql.Open("duckdb", dbPath)if err != nil {t.Fatalf("failed to open database for verification: %v", err)}defer database.Close() - replacement in tools/import/import_files_test.go at line 55
// Verify file was inserted into database - open new connectiondatabase, err := sql.Open("duckdb", dbPath)if err != nil {t.Fatalf("failed to open database for verification: %v", err)}defer database.Close()var fileCount interr = database.QueryRow("SELECT COUNT(*) FROM file WHERE xxh64_hash = ? AND active = true", hash).Scan(&fileCount)if err != nil {t.Fatalf("failed to query file: %v", err)}if fileCount != 1 {t.Errorf("expected 1 file in database, got %d", fileCount)} - replacement in tools/import/import_files_test.go at line 64
var fileCount interr = database.QueryRow("SELECT COUNT(*) FROM file WHERE xxh64_hash = ? AND active = true", hash).Scan(&fileCount)if err != nil {t.Fatalf("failed to query file: %v", err)}if fileCount != 1 {t.Errorf("expected 1 file in database, got %d", fileCount)}// Verify file_dataset linkvar linkCount interr = database.QueryRow(`SELECT COUNT(*) FROM file_dataset fdJOIN file f ON fd.file_id = f.idWHERE f.xxh64_hash = ? AND fd.dataset_id = 'dstest000001'`, hash).Scan(&linkCount)if err != nil {t.Fatalf("failed to query file_dataset: %v", err)}if linkCount != 1 {t.Errorf("expected 1 file_dataset link, got %d", linkCount)}} - replacement in tools/import/import_files_test.go at line 79
// Verify file_dataset linkvar linkCount interr = database.QueryRow(`SELECT COUNT(*) FROM file_dataset fdJOIN file f ON fd.file_id = f.idWHERE f.xxh64_hash = ? AND fd.dataset_id = 'dstest000001'`, hash).Scan(&linkCount)if err != nil {t.Fatalf("failed to query file_dataset: %v", err)}if linkCount != 1 {t.Errorf("expected 1 file_dataset link, got %d", linkCount)}})func TestImportAudioFiles_DuplicateHandling(t *testing.T) {ctx := context.Background()dbPath := setupFileBasedTestDB(t) - replacement in tools/import/import_files_test.go at line 83
t.Run("duplicate handling - skip file with existing hash", func(t *testing.T) {dbPath := setupFileBasedTestDB(t)// Don't keep database open - ImportAudioFiles manages its own connections// Create temp folder with a WAV filetmpDir := t.TempDir()wavPath := filepath.Join(tmpDir, "test_recording.wav")hash := createTestWAV(t, wavPath) - replacement in tools/import/import_files_test.go at line 88
// Create temp folder with a WAV filetmpDir := t.TempDir()wavPath := filepath.Join(tmpDir, "test_recording.wav")hash := createTestWAV(t, wavPath)// First import_, err := ImportAudioFiles(ctx, ImportAudioFilesInput{DBPath: dbPath,FolderPath: tmpDir,DatasetID: "dstest000001",LocationID: "loctest00001",ClusterID: "cltest000001",Recursive: new(true),})if err != nil {t.Fatalf("first import failed: %v", err)} - replacement in tools/import/import_files_test.go at line 101
// First import_, err := ImportAudioFiles(ctx, ImportAudioFilesInput{DBPath: dbPath,FolderPath: tmpDir,DatasetID: "dstest000001",LocationID: "loctest00001",ClusterID: "cltest000001",Recursive: new(true),})if err != nil {t.Fatalf("first import failed: %v", err)}// Second import of same file (should be skipped as duplicate)output, err := ImportAudioFiles(ctx, ImportAudioFilesInput{DBPath: dbPath,FolderPath: tmpDir,DatasetID: "dstest000001",LocationID: "loctest00001",ClusterID: "cltest000001",Recursive: new(true),})if err != nil {t.Fatalf("second import failed: %v", err)} - replacement in tools/import/import_files_test.go at line 114
// Second import of same file (should be skipped as duplicate)output, err := ImportAudioFiles(ctx, ImportAudioFilesInput{DBPath: dbPath,FolderPath: tmpDir,DatasetID: "dstest000001",LocationID: "loctest00001",ClusterID: "cltest000001",Recursive: new(true),})if err != nil {t.Fatalf("second import failed: %v", err)}// Verify outputif output.Summary.TotalFiles != 1 {t.Errorf("expected 1 total file, got %d", output.Summary.TotalFiles)}if output.Summary.ImportedFiles != 0 {t.Errorf("expected 0 imported files (duplicate), got %d", output.Summary.ImportedFiles)}if output.Summary.SkippedFiles != 1 {t.Errorf("expected 1 skipped file (duplicate), got %d", output.Summary.SkippedFiles)} - replacement in tools/import/import_files_test.go at line 125
// Verify outputif output.Summary.TotalFiles != 1 {t.Errorf("expected 1 total file, got %d", output.Summary.TotalFiles)}if output.Summary.ImportedFiles != 0 {t.Errorf("expected 0 imported files (duplicate), got %d", output.Summary.ImportedFiles)}if output.Summary.SkippedFiles != 1 {t.Errorf("expected 1 skipped file (duplicate), got %d", output.Summary.SkippedFiles)}// Verify only one file in database (not duplicated) - open new connectiondatabase, err := sql.Open("duckdb", dbPath)if err != nil {t.Fatalf("failed to open database for verification: %v", err)}defer database.Close() - replacement in tools/import/import_files_test.go at line 132
// Verify only one file in database (not duplicated) - open new connectiondatabase, err := sql.Open("duckdb", dbPath)if err != nil {t.Fatalf("failed to open database for verification: %v", err)}defer database.Close()var fileCount interr = database.QueryRow("SELECT COUNT(*) FROM file WHERE xxh64_hash = ? AND active = true", hash).Scan(&fileCount)if err != nil {t.Fatalf("failed to query file: %v", err)}if fileCount != 1 {t.Errorf("expected 1 file in database (not duplicated), got %d", fileCount)}} - replacement in tools/import/import_files_test.go at line 142
var fileCount interr = database.QueryRow("SELECT COUNT(*) FROM file WHERE xxh64_hash = ? AND active = true", hash).Scan(&fileCount)if err != nil {t.Fatalf("failed to query file: %v", err)}if fileCount != 1 {t.Errorf("expected 1 file in database (not duplicated), got %d", fileCount)}})func TestImportAudioFiles_ErrorCases(t *testing.T) {ctx := context.Background() - replacement in lint_test.go at line 48
cmd := exec.Command("gocyclo", "-over", "12", "-ignore", "_test\\.go$", ".")cmd := exec.Command("gocyclo", "-over", "11", "-ignore", "_test\\.go$", ".") - replacement in lint_test.go at line 52
t.Errorf("cyclometric complexity is above 12:\n%s", out)t.Errorf("cyclometric complexity is above 11:\n%s", out) - replacement in db/schema.go at line 227
// topologicalSort orders tables so dependencies come first (Kahn's algorithm).// Tables in cycles are appended at the end.func topologicalSort(tables map[string]bool, dependsOnMe map[string][]string) []string {// countDependencies counts FK dependencies for each table.func countDependencies(tables map[string]bool, dependsOnMe map[string][]string) map[string]int { - edit in db/schema.go at line 238
return fkCount} - edit in db/schema.go at line 241
// buildInitialQueue builds the initial queue of tables with no dependencies.func buildInitialQueue(tables map[string]bool, fkCount map[string]int) []string { - edit in db/schema.go at line 247
}}return queue}// appendCyclicTables adds tables that are part of cycles to the result.func appendCyclicTables(result []string, tables map[string]bool) []string {if len(result) == len(tables) {return result}for table := range tables {if !slices.Contains(result, table) {result = append(result, table) - edit in db/schema.go at line 262
return result}// topologicalSort orders tables so dependencies come first (Kahn's algorithm).// Tables in cycles are appended at the end.func topologicalSort(tables map[string]bool, dependsOnMe map[string][]string) []string {fkCount := countDependencies(tables, dependsOnMe)queue := buildInitialQueue(tables, fkCount) - replacement in db/schema.go at line 285[7.1006735]→[7.9369:9412](∅→∅),[7.9412]→[7.1006793:1006826](∅→∅),[7.1006793]→[7.1006793:1006826](∅→∅),[7.1006869]→[7.1006869:1006899](∅→∅),[7.1006899]→[7.9413:9453](∅→∅),[7.9453]→[7.1006957:1007004](∅→∅),[7.1006957]→[7.1006957:1007004](∅→∅),[7.1007004]→[7.9454:9469](∅→∅)
// Handle cycles: append remaining tablesif len(result) != len(tables) {for table := range tables {if !slices.Contains(result, table) {result = append(result, table)}}}return resultreturn appendCyclicTables(result, tables) - replacement in cmd/calls_push_certainty.go at line 52[7.1106231]→[7.1106231:1106265](∅→∅),[7.1106265]→[7.3493:3539](∅→∅),[7.3539]→[7.1106408:1106425](∅→∅),[7.24861]→[7.1106408:1106425](∅→∅),[7.1106408]→[7.1106408:1106425](∅→∅),[7.1106425]→[7.3540:3582](∅→∅),[7.3582]→[7.1106564:1106583](∅→∅),[7.24902]→[7.1106564:1106583](∅→∅),[7.1106564]→[7.1106564:1106583](∅→∅),[7.1106583]→[7.3583:3629](∅→∅),[7.3629]→[7.1106726:1106746](∅→∅),[7.24945]→[7.1106726:1106746](∅→∅),[7.1106726]→[7.1106726:1106746](∅→∅),[7.1106746]→[7.3630:3678](∅→∅),[7.3678]→[7.1106891:1106909](∅→∅),[7.24989]→[7.1106891:1106909](∅→∅),[7.1106891]→[7.1106891:1106909](∅→∅),[7.1106909]→[7.24990:25008](∅→∅),[7.25008]→[7.1106925:1106932](∅→∅),[7.1106925]→[7.1106925:1106932](∅→∅),[7.1106933]→[7.1106933:1106949](∅→∅),[7.1106949]→[7.25009:25025](∅→∅),[7.25025]→[7.1106963:1106970](∅→∅),[7.1106963]→[7.1106963:1106970](∅→∅),[7.1106970]→[7.1037:1180](∅→∅),[7.1180]→[7.3679:3729](∅→∅),[7.1224]→[7.1107759:1107825](∅→∅),[7.3729]→[7.1107759:1107825](∅→∅),[7.25188]→[7.1107759:1107825](∅→∅),[7.1107759]→[7.1107759:1107825](∅→∅),[7.1107826]→[7.1107826:1107837](∅→∅)
switch arg {case "--folder":f.folder = mustValue(args, &i, "--folder")case "--file":f.file = mustValue(args, &i, "--file")case "--filter":f.filter = mustValue(args, &i, "--filter")case "--species":f.species = mustValue(args, &i, "--species")case "--night":f.night = truei++case "--day":f.day = truei++case "--location":if f.location != "" {fmt.Fprintf(os.Stderr, "Error: --location can only be specified once\n")os.Exit(1)}f.location = mustValue(args, &i, "--location")case "--help", "-h":printPushCertaintyUsage()os.Exit(0)default:if !parsePushCertaintyFlag(arg, args, &i, &f) { - edit in cmd/calls_push_certainty.go at line 59
}// parsePushCertaintyFlag handles a single flag. Returns false if unknown flag.func parsePushCertaintyFlag(arg string, args []string, i *int, f *pushCertaintyFlags) bool {switch arg {case "--folder":f.folder = mustValue(args, i, "--folder")case "--file":f.file = mustValue(args, i, "--file")case "--filter":f.filter = mustValue(args, i, "--filter")case "--species":f.species = mustValue(args, i, "--species")case "--night":f.night = true*i++case "--day":f.day = true*i++case "--location":if f.location != "" {fmt.Fprintf(os.Stderr, "Error: --location can only be specified once\n")os.Exit(1)}f.location = mustValue(args, i, "--location")case "--help", "-h":printPushCertaintyUsage()os.Exit(0)default:return false}return true - edit in CHANGELOG.md at line 4
## [2026-05-19] tools/calls and tools/import refactoring complete — all 5 phases doneRefactored `tools/calls/` and `tools/import/` to improve code quality, test coverage, and interface consistency.### Phase 0: Tests for tools/import Critical Paths- Created `tools/import/test_helpers.go` with shared test utilities- Added tests for `ImportAudioFiles`, `ImportUnstructured`, `ImportSegments`, `writeIDsToDataFiles`, `BulkFileImport`- **Coverage increased from 20.7% to 71.9%**### Phase 1: Reduce Cyclomatic Complexity- Extracted `resolveLabelIDs`, `insertLabel`, `insertLabelMetadata` from `importSingleLabel`- Extracted `detectBirdaColumns`, `validateBirdaColumns` from `parseBirdaCSVHeader`- Extracted `validateAndParseShowImagesInput`, `resolveImageSize`, `resolveGraphicsProtocol`, `displaySegmentSpectrograms` from `CallsShowImages`- Fixed `scanWavFiles`, `CallsRemove`, `findMatchingLabels` complexity issues- **All functions now under complexity 10**### Phase 2: Split import_segments.go- Created `import_segments_prepare.go` (429 lines) — validation, scanning, ID lookup- Created `import_segments_db.go` (422 lines) — DB write, writeback- Reduced `import_segments.go` to 155 lines — orchestrator + types### Phase 3: Fix *sql.DB Usage- **3A (tools/):** Changed `validateClusterActive`, `validateClusterCyclicPattern`, `executeSQLQuery` to use `db.Querier`- **3A (tools/export.go):** Defined local `writer` interface; replaced `sql.Open` with `db.OpenWriteableDB`- **3A (db/schema.go):** Changed `GetFKOrder`, `buildFKDependencyGraph`, `collectAllTables` to `db.Querier`- **3B (tools/import/):** Changed all bulk functions to accept `Mutator` interface- Updated `ImportSegments`, `ImportFile` to use `WithReadDB`/`WithWriteTx` patterns- Raw `*sql.DB` now only appears in callback signatures and type assertions### Phase 4: Move Classify TUI-Adjacent Types to Dedicated Files- Created `classify_bindings.go` (123 lines) — `KeyBinding`, `BindingResult`, binding methods- Created `classify_format.go` (32 lines) — `FormatLabels`- Created `classify_io.go` (106 lines) — `LoadFilteredSegment`, `SaveClip`, `PlaySegmentAtSpeed`- Created `classify_bookmarks.go` (135 lines) — bookmark navigation methods- Created `classify_labels.go` (87 lines) — label manipulation methods- Added documentation for TUI-only fields in `ClassifyConfig`- **`calls_classify.go` reduced from 802 to 375 lines**### Phase 5: Consolidate Shared Types into from_common.go- Moved `ClusteredCall`, `DirCache`, `ParseFilterFromFilename`, `clusterStartTimes`, `clusterDetections`, `predFileSpeciesKey` to `calls_from_common.go`- **`calls_from_preds.go` reduced from 717 to 546 lines (24% reduction)**- **`calls_from_common.go` grew from 287 to 460 lines** - edit in CHANGELOG.md at line 48
### Summary| Metric | Before | After ||--------|--------|-------|| tools/import test coverage | 20.7% | 71.9% || Max cyclomatic complexity | 12+ | <10 || import_segments.go lines | 944 | 155 || calls_classify.go lines | 802 | 375 || calls_from_preds.go lines | 717 | 546 | - replacement in .golangci.yml at line 14
max-complexity: 12max-complexity: 11