complexity over 12 now gone, but have some lint fails
Dependencies
- [2]
DNLR36BZminor comments - [3]
ZKLAOPURfix event logging - [4]
ZCCQ4P5Treduce complexity to under 14, gocyclo but cilint test still has 3 functions over - [5]
RUVJ3V4Ncyclo to 14 now - [6]
DD3LCTLZtidy up lat lng timezone api for calls classify and push certainty - [7]
SMWSHUOWcyclo over 15 - [8]
JAT3DXOLcyclo over 15 - [9]
BZ6KQRYDadded complexity lint test - [10]
3DVPQOKBbig tidy up of tools/ - [11]
NS4TDPLNcyclomatic complexity - [12]
KZKLAINJrun out of space on nest, cleaned out - [13]
GPQSOVBPcyclo complexity over 25 - [14]
HYCZTLSZfixed tests with cyclo over 15 - [15]
QFPEKXL5ck 6
Change contents
- replacement in utils/filename_parser_test.go at line 57
func TestParseFilenameTimestamps(t *testing.T) {t.Run("should parse YYMMDD format (test case a)", func(t *testing.T) {filenames := []string{"201012_123456.wav","201014_123456.WAV","201217_123456.wav","211122_123456.WAV",}// parseTestCase defines a table-driven test case for ParseFilenameTimestamps.type parseTestCase struct {name stringfiles []stringexpected map[int]expectedTS // index → expected timestamp} - replacement in utils/filename_parser_test.go at line 64
results, err := ParseFilenameTimestamps(filenames)if err != nil {t.Fatalf("Failed to parse filenames: %v", err)}func runParseTestCase(t *testing.T, tc parseTestCase) {t.Helper()results, err := ParseFilenameTimestamps(tc.files)if err != nil {t.Fatalf("Failed to parse filenames: %v", err)}if len(results) != len(tc.files) {t.Fatalf("Expected %d results, got %d", len(tc.files), len(results))}for idx, want := range tc.expected {assertTimestamp(t, results[idx].Timestamp, want)}} - replacement in utils/filename_parser_test.go at line 78[5.99397]→[5.99397:99552](∅→∅),[5.99552]→[5.2676:2838](∅→∅),[5.2838]→[5.100784:100986](∅→∅),[5.100784]→[5.100784:100986](∅→∅)
if len(results) != 4 {t.Fatalf("Expected 4 results, got %d", len(results))}// Year 20 should be interpreted as 2020 (less variance than days)assertTimestamp(t, results[0].Timestamp, expectedTS{2020, 10, 12, 12, 34, 56})assertTimestamp(t, results[3].Timestamp, expectedTS{2021, 11, 22, 12, 34, 56})})t.Run("should parse DDMMYY format (test case b)", func(t *testing.T) {filenames := []string{"121020_123456.WAV","141020_123456.wav","171220_123456.WAV","221121_123456.wav",}func TestParseFilenameTimestamps(t *testing.T) {cases := []parseTestCase{{name: "YYMMDD format (test case a)",files: []string{"201012_123456.wav", "201014_123456.WAV", "201217_123456.wav", "211122_123456.WAV"},expected: map[int]expectedTS{0: {2020, 10, 12, 12, 34, 56}, // Year 20 → 20203: {2021, 11, 22, 12, 34, 56},},},{name: "DDMMYY format (test case b)",files: []string{"121020_123456.WAV", "141020_123456.wav", "171220_123456.WAV", "221121_123456.wav"},expected: map[int]expectedTS{0: {2020, 10, 12, 12, 34, 56},2: {2020, 12, 17, 12, 34, 56},},},{name: "YYYYMMDD format (test case c)",files: []string{"20230609_103000.WAV", "20241109_201504.wav"},expected: map[int]expectedTS{0: {2023, 6, 9, 10, 30, 0},1: {2024, 11, 9, 20, 15, 4},},},{name: "6-digit with variance detection (test case d)",files: []string{"120119_003002.wav", "180120_231502.wav", "170122_010005.wav", "010419_234502.WAV", "310320_231502.wav", "220824_231502.WAV", "240123_231502.wav"},expected: map[int]expectedTS{0: {2019, 1, 12, 0, 30, 2}, // DDMMYY4: {2020, 3, 31, 23, 15, 2},},},{name: "prefixes (test case e)",files: []string{"XYZ123_7689_20230609_103000.WAV", "string 20241109_201504.wav"},expected: map[int]expectedTS{0: {2023, 6, 9, 10, 30, 0},1: {2024, 11, 9, 20, 15, 4},},},{name: "complex prefixes (test case f)",files: []string{"abcdefg__1234_180120_231502.wav", "string 120119_003002.wav", "ABCD EFG___170122_010005.wav", "BHD_1234 010419_234502.WAV", "cill xyz 310320_231502.wav", "220824_231502.WAV", "240123_231502.wav"},expected: map[int]expectedTS{0: {2020, 1, 18, 23, 15, 2},1: {2019, 1, 12, 0, 30, 2},4: {2020, 3, 31, 23, 15, 2},},},} - replacement in utils/filename_parser_test.go at line 131[5.100987]→[5.100987:101343](∅→∅),[5.101343]→[5.2839:3001](∅→∅),[5.3001]→[5.102167:102540](∅→∅),[5.102167]→[5.102167:102540](∅→∅),[5.102540]→[5.3002:3159](∅→∅),[5.3159]→[5.103410:104080](∅→∅),[5.103410]→[5.103410:104080](∅→∅),[5.104080]→[5.3160:3317](∅→∅),[5.3317]→[5.105032:105037](∅→∅),[5.105032]→[5.105032:105037](∅→∅),[5.105285]→[5.105285:105680](∅→∅),[5.105680]→[5.3318:3475](∅→∅),[5.3475]→[5.107175:107782](∅→∅),[5.107175]→[5.107175:107782](∅→∅),[5.107782]→[5.3476:3716](∅→∅)
results, err := ParseFilenameTimestamps(filenames)if err != nil {t.Fatalf("Failed to parse filenames: %v", err)}if len(results) != 4 {t.Fatalf("Expected 4 results, got %d", len(results))}// More variance in first two digits (12,14,17,22) than last two (20,20,20,21)// So DDMMYY format: day=first, month=middle, year=last+2000assertTimestamp(t, results[0].Timestamp, expectedTS{2020, 10, 12, 12, 34, 56})assertTimestamp(t, results[2].Timestamp, expectedTS{2020, 12, 17, 12, 34, 56})})t.Run("should parse YYYYMMDD format (test case c)", func(t *testing.T) {filenames := []string{"20230609_103000.WAV","20241109_201504.wav",}results, err := ParseFilenameTimestamps(filenames)if err != nil {t.Fatalf("Failed to parse filenames: %v", err)}if len(results) != 2 {t.Fatalf("Expected 2 results, got %d", len(results))}assertTimestamp(t, results[0].Timestamp, expectedTS{2023, 6, 9, 10, 30, 0})assertTimestamp(t, results[1].Timestamp, expectedTS{2024, 11, 9, 20, 15, 4})})t.Run("should parse mixed 6-digit dates with variance detection (test case d)", func(t *testing.T) {filenames := []string{"120119_003002.wav","180120_231502.wav","170122_010005.wav","010419_234502.WAV","310320_231502.wav","220824_231502.WAV","240123_231502.wav",}results, err := ParseFilenameTimestamps(filenames)if err != nil {t.Fatalf("Failed to parse filenames: %v", err)}if len(results) != 7 {t.Fatalf("Expected 7 results, got %d", len(results))}// First two digits: 12,18,17,01,31,22,24 (variance = high)// Last two digits: 19,20,22,19,20,24,23 (variance = lower)// Should be DDMMYY formatassertTimestamp(t, results[0].Timestamp, expectedTS{2019, 1, 12, 0, 30, 2})assertTimestamp(t, results[4].Timestamp, expectedTS{2020, 3, 31, 23, 15, 2})})t.Run("should parse filenames with prefixes (test case e)", func(t *testing.T) {filenames := []string{"XYZ123_7689_20230609_103000.WAV","string 20241109_201504.wav",}results, err := ParseFilenameTimestamps(filenames)if err != nil {t.Fatalf("Failed to parse filenames: %v", err)}if len(results) != 2 {t.Fatalf("Expected 2 results, got %d", len(results))}assertTimestamp(t, results[0].Timestamp, expectedTS{2023, 6, 9, 10, 30, 0})assertTimestamp(t, results[1].Timestamp, expectedTS{2024, 11, 9, 20, 15, 4})})t.Run("should parse filenames with complex prefixes (test case f)", func(t *testing.T) {filenames := []string{"abcdefg__1234_180120_231502.wav","string 120119_003002.wav","ABCD EFG___170122_010005.wav","BHD_1234 010419_234502.WAV","cill xyz 310320_231502.wav","220824_231502.WAV","240123_231502.wav",}results, err := ParseFilenameTimestamps(filenames)if err != nil {t.Fatalf("Failed to parse filenames: %v", err)}if len(results) != 7 {t.Fatalf("Expected 7 results, got %d", len(results))}// Same pattern as test case d - should be DDMMYYassertTimestamp(t, results[0].Timestamp, expectedTS{2020, 1, 18, 23, 15, 2})assertTimestamp(t, results[1].Timestamp, expectedTS{2019, 1, 12, 0, 30, 2})assertTimestamp(t, results[4].Timestamp, expectedTS{2020, 3, 31, 23, 15, 2})})for _, tc := range cases {t.Run(tc.name, func(t *testing.T) {runParseTestCase(t, tc)})} - edit in tools/prepend_test.go at line 187
}}func TestProcessPrependFile(t *testing.T) {tmpDir, err := os.MkdirTemp("", "prepend_test")if err != nil {t.Fatalf("Failed to create temp dir: %v", err)}defer os.RemoveAll(tmpDir)tests := []struct {name stringfilename stringdryRun boolwantRename intwantSkip intwantErr int}{{"datestring wav - dry run", "20250920_011509.wav", true, 1, 0, 0},{"datestring wav - real", "20250920_011509.wav", false, 1, 0, 0},{"no datestring wav", "recording.wav", false, 0, 1, 0},{"log.txt", "log.txt", false, 1, 0, 0},{"non-target file", "readme.txt", false, 0, 0, 0},}for _, tt := range tests {t.Run(tt.name, func(t *testing.T) {oldPath := filepath.Join(tmpDir, tt.filename)if err := os.WriteFile(oldPath, []byte{}, 0644); err != nil {t.Fatalf("create file: %v", err)}defer os.Remove(oldPath)output := &PrependOutput{Renamed: []PrependResult{},Skipped: []PrependSkipped{},Errors: []PrependError{},}processPrependFile(tmpDir, tt.filename, "LOC", tt.dryRun, output)if len(output.Renamed) != tt.wantRename {t.Errorf("renamed = %d, want %d", len(output.Renamed), tt.wantRename)}if len(output.Skipped) != tt.wantSkip {t.Errorf("skipped = %d, want %d", len(output.Skipped), tt.wantSkip)}if len(output.Errors) != tt.wantErr {t.Errorf("errors = %d, want %d", len(output.Errors), tt.wantErr)}}) - edit in tools/prepend.go at line 50
// prependFolders collects the folders to process (root + immediate subdirs if recursive).func prependFolders(input PrependInput) ([]string, error) {folders := []string{input.Folder}if !input.Recursive {return folders, nil}entries, err := os.ReadDir(input.Folder)if err != nil {return nil, fmt.Errorf("failed to read folder: %w", err)}for _, entry := range entries {if entry.IsDir() {folders = append(folders, filepath.Join(input.Folder, entry.Name()))}}return folders, nil}// processPrependFile handles a single file within a folder rename pass.func processPrependFile(folder, filename, prefix string, dryRun bool, output *PrependOutput) {oldPath := filepath.Join(folder, filename)shouldRename, skipReason := shouldPrependFile(filename, prefix)if !shouldRename {if skipReason != "" {output.Skipped = append(output.Skipped, PrependSkipped{File: oldPath, Reason: skipReason})}return} - edit in tools/prepend.go at line 81
newPath := filepath.Join(folder, prefix+"_"+filename)if dryRun {output.Renamed = append(output.Renamed, PrependResult{Old: oldPath, New: newPath})return}if err := os.Rename(oldPath, newPath); err != nil {output.Errors = append(output.Errors, PrependError{File: oldPath, Error: err.Error()})return}output.Renamed = append(output.Renamed, PrependResult{Old: oldPath, New: newPath})} - replacement in tools/prepend.go at line 108
// Collect folders to processfolders := []string{input.Folder}if input.Recursive {entries, err := os.ReadDir(input.Folder)if err != nil {return nil, fmt.Errorf("failed to read folder: %w", err)}for _, entry := range entries {if entry.IsDir() {folders = append(folders, filepath.Join(input.Folder, entry.Name()))}}folders, err := prependFolders(input)if err != nil {return nil, err - edit in tools/prepend.go at line 113
// Process each folder - edit in tools/prepend.go at line 118
- edit in tools/prepend.go at line 120
continue}filename := entry.Name()oldPath := filepath.Join(folder, filename)shouldRename, skipReason := shouldPrependFile(filename, input.Prefix)if !shouldRename {if skipReason != "" {output.Skipped = append(output.Skipped, PrependSkipped{File: oldPath,Reason: skipReason,})}continue}newFilename := input.Prefix + "_" + filenamenewPath := filepath.Join(folder, newFilename)if input.DryRun {output.Renamed = append(output.Renamed, PrependResult{Old: oldPath,New: newPath,}) - replacement in tools/prepend.go at line 122
// Perform the renameif err := os.Rename(oldPath, newPath); err != nil {output.Errors = append(output.Errors, PrependError{File: oldPath,Error: err.Error(),})continue}output.Renamed = append(output.Renamed, PrependResult{Old: oldPath,New: newPath,})processPrependFile(folder, entry.Name(), input.Prefix, input.DryRun, output) - edit in tools/pattern_test.go at line 9
func testExistingPattern(t *testing.T, ctx context.Context, testDB string) {t.Helper()record := 60sleep := 1740input := PatternInput{DBPath: testDB,RecordSeconds: &record,SleepSeconds: &sleep,}output, err := CreateOrUpdatePattern(ctx, input)if err != nil {t.Fatalf("Expected no error, got: %v", err)}if output.Pattern.ID != "IBv_KxDGsNQs" {t.Errorf("Expected existing pattern ID 'IBv_KxDGsNQs', got '%s'", output.Pattern.ID)}if output.Pattern.RecordS != 60 {t.Errorf("Expected record_s 60, got %d", output.Pattern.RecordS)}if output.Pattern.SleepS != 1740 {t.Errorf("Expected sleep_s 1740, got %d", output.Pattern.SleepS)}if output.Message == "" {t.Error("Expected non-empty message")}t.Logf("Message: %s", output.Message)}func testUniquePattern(t *testing.T, ctx context.Context, testDB string) {t.Helper()record := 999sleep := 888input := PatternInput{DBPath: testDB,RecordSeconds: &record,SleepSeconds: &sleep,} - edit in tools/pattern_test.go at line 50
output, err := CreateOrUpdatePattern(ctx, input)if err != nil {t.Fatalf("Expected no error, got: %v", err)}firstID := output.Pattern.IDif firstID == "" {t.Fatal("Expected non-empty ID")}if output.Pattern.RecordS != 999 {t.Errorf("Expected record_s 999, got %d", output.Pattern.RecordS)}if output.Pattern.SleepS != 888 {t.Errorf("Expected sleep_s 888, got %d", output.Pattern.SleepS)}t.Logf("Created pattern ID: %s", firstID)testIdempotentCreate(t, ctx, testDB, record, sleep, firstID)}func testIdempotentCreate(t *testing.T, ctx context.Context, testDB string, record, sleep int, expectedID string) {t.Helper()input2 := PatternInput{DBPath: testDB,RecordSeconds: &record,SleepSeconds: &sleep,}output2, err2 := CreateOrUpdatePattern(ctx, input2)if err2 != nil {t.Fatalf("Expected no error on duplicate, got: %v", err2)}if output2.Pattern.ID != expectedID {t.Errorf("Expected same pattern ID '%s', got '%s'", expectedID, output2.Pattern.ID)}t.Logf("Idempotent test passed - returned same ID: %s", output2.Pattern.ID)} - edit in tools/pattern_test.go at line 88
// Setup: Use test database - edit in tools/pattern_test.go at line 94
// Test 1: Try to create duplicate of existing pattern (60s/1740s)// Should return existing pattern IBv_KxDGsNQs - replacement in tools/pattern_test.go at line 95[5.276994]→[5.276994:277050](∅→∅),[5.277050]→[5.313114:313140](∅→∅),[5.313140]→[5.277050:277778](∅→∅),[5.277050]→[5.277050:277778](∅→∅)
record := 60sleep := 1740input := PatternInput{DBPath: testDB,RecordSeconds: &record,SleepSeconds: &sleep,}output, err := CreateOrUpdatePattern(ctx, input)if err != nil {t.Fatalf("Expected no error, got: %v", err)}// Should return existing patternif output.Pattern.ID != "IBv_KxDGsNQs" {t.Errorf("Expected existing pattern ID 'IBv_KxDGsNQs', got '%s'", output.Pattern.ID)}if output.Pattern.RecordS != 60 {t.Errorf("Expected record_s 60, got %d", output.Pattern.RecordS)}if output.Pattern.SleepS != 1740 {t.Errorf("Expected sleep_s 1740, got %d", output.Pattern.SleepS)}// Check message indicates existing patternif output.Message == "" {t.Error("Expected non-empty message")}t.Logf("Message: %s", output.Message)testExistingPattern(t, ctx, testDB) - edit in tools/pattern_test.go at line 97
// Test 2: Create new unique pattern - replacement in tools/pattern_test.go at line 98[5.277872]→[5.277872:277928](∅→∅),[5.277928]→[5.313141:313167](∅→∅),[5.313167]→[5.277928:278573](∅→∅),[5.277928]→[5.277928:278573](∅→∅),[5.278573]→[5.313168:313332](∅→∅),[5.313332]→[5.278626:278946](∅→∅),[5.278626]→[5.278626:278946](∅→∅)
record := 999sleep := 888input := PatternInput{DBPath: testDB,RecordSeconds: &record,SleepSeconds: &sleep,}output, err := CreateOrUpdatePattern(ctx, input)if err != nil {t.Fatalf("Expected no error, got: %v", err)}// Should create new patternfirstID := output.Pattern.IDif firstID == "" {t.Fatal("Expected non-empty ID")}if output.Pattern.RecordS != 999 {t.Errorf("Expected record_s 999, got %d", output.Pattern.RecordS)}if output.Pattern.SleepS != 888 {t.Errorf("Expected sleep_s 888, got %d", output.Pattern.SleepS)}t.Logf("Created pattern ID: %s", firstID)// Test 3: Try to create duplicate of the pattern we just created (idempotent)input2 := PatternInput{DBPath: testDB,RecordSeconds: &record,SleepSeconds: &sleep,}output2, err2 := CreateOrUpdatePattern(ctx, input2)if err2 != nil {t.Fatalf("Expected no error on duplicate, got: %v", err2)}// Should return same patternif output2.Pattern.ID != firstID {t.Errorf("Expected same pattern ID '%s', got '%s'", firstID, output2.Pattern.ID)}t.Logf("Idempotent test passed - returned same ID: %s", output2.Pattern.ID)testUniquePattern(t, ctx, testDB) - replacement in tools/integration_test.go at line 48
func testCreateClusterWithPattern(t *testing.T, ctx context.Context, testDB string) {// lookupActiveDatasetAndLocation finds an active dataset and location for integration tests.func lookupActiveDatasetAndLocation(t *testing.T, ctx context.Context, testDB string) (datasetID, locationID string) { - edit in tools/integration_test.go at line 51
// Find a valid dataset - replacement in tools/integration_test.go at line 58
datasetID := datasetOutput.Rows[0]["id"].(string)datasetID = datasetOutput.Rows[0]["id"].(string) - edit in tools/integration_test.go at line 60
// Find a valid location - replacement in tools/integration_test.go at line 68
locationID := locationOutput.Rows[0]["id"].(string)locationID = locationOutput.Rows[0]["id"].(string) - edit in tools/integration_test.go at line 71
return datasetID, locationID} - replacement in tools/integration_test.go at line 74[5.306999]→[5.5460:5538](∅→∅),[5.5538]→[5.313935:313971](∅→∅),[5.313971]→[5.5538:5939](∅→∅),[5.5538]→[5.5538:5939](∅→∅),[5.5939]→[5.307194:307195](∅→∅),[5.307194]→[5.307194:307195](∅→∅),[5.307195]→[5.5940:5989](∅→∅)
sampleRate := 16000output, err := CreateOrUpdateCluster(ctx, ClusterInput{DBPath: testDB,DatasetID: &datasetID,LocationID: &locationID,Name: new("Integration Test Cluster"),SampleRate: &sampleRate,CyclicRecordingPatternID: new("IBv_KxDGsNQs"),})if err != nil {t.Fatalf("Failed to create cluster: %v", err)}clusterID := output.Cluster.IDt.Logf("Created cluster: %s with pattern reference", clusterID)// Verify the cluster has the pattern reference// verifyClusterPattern verifies a cluster has the expected pattern reference.func verifyClusterPattern(t *testing.T, ctx context.Context, testDB, clusterID, expectedPatternID string) {t.Helper() - replacement in tools/integration_test.go at line 92
if row["cyclic_recording_pattern_id"] != "IBv_KxDGsNQs" {t.Errorf("Expected pattern ID 'IBv_KxDGsNQs', got '%v'", row["cyclic_recording_pattern_id"])}if row["cyclic_recording_pattern_id"] == nil || row["cyclic_recording_pattern_id"] == "" {t.Error("Pattern ID is empty")if row["cyclic_recording_pattern_id"] != expectedPatternID {t.Errorf("Expected pattern ID '%s', got '%v'", expectedPatternID, row["cyclic_recording_pattern_id"]) - edit in tools/integration_test.go at line 102[5.307631]
func testCreateClusterWithPattern(t *testing.T, ctx context.Context, testDB string) {t.Helper()datasetID, locationID := lookupActiveDatasetAndLocation(t, ctx, testDB)sampleRate := 16000output, err := CreateOrUpdateCluster(ctx, ClusterInput{DBPath: testDB,DatasetID: &datasetID,LocationID: &locationID,Name: new("Integration Test Cluster"),SampleRate: &sampleRate,CyclicRecordingPatternID: new("IBv_KxDGsNQs"),})if err != nil {t.Fatalf("Failed to create cluster: %v", err)}t.Logf("Created cluster: %s with pattern reference", output.Cluster.ID)verifyClusterPattern(t, ctx, testDB, output.Cluster.ID, "IBv_KxDGsNQs")} - edit in tools/calls/calls_from_common.go at line 79
}// sequentialFileResult holds the outcome of processing a single file sequentially.type sequentialFileResult struct {calls []ClusteredCalldataFilesWritten intdataFilesSkipped intfilesDeleted int}// processSequentialFile handles one source file in the sequential path.func processSequentialFile(src CallSource, file string, dirCaches map[string]*DirCache, shouldDelete bool) (sequentialFileResult, error) {var res sequentialFileResultdir := filepath.Dir(file)cache := dirCaches[dir]if cache == nil {cache = NewDirCache(dir)dirCaches[dir] = cache}calls, written, skipped, err := src.ProcessFile(file, cache)if err != nil {return res, fmt.Errorf("Error processing %s: %v", file, err)}if written {res.dataFilesWritten = 1}if skipped {res.dataFilesSkipped = 1}res.calls = callsif shouldDelete && written {if err := os.Remove(file); err != nil {return res, fmt.Errorf("Failed to delete %s: %v", file, err)}res.filesDeleted = 1}return res, nil - edit in tools/calls/calls_from_common.go at line 134
speciesCount := make(map[string]int) - edit in tools/calls/calls_from_common.go at line 135
speciesCount := make(map[string]int) - replacement in tools/calls/calls_from_common.go at line 142
dir := filepath.Dir(file)cache := dirCaches[dir]if cache == nil {cache = NewDirCache(dir)dirCaches[dir] = cache}calls, written, skipped, err := src.ProcessFile(file, cache)res, err := processSequentialFile(src, file, dirCaches, input.Delete) - replacement in tools/calls/calls_from_common.go at line 144
errMsg := fmt.Sprintf("Error processing %s: %v", file, err)errMsg := err.Error() - replacement in tools/calls/calls_from_common.go at line 146
return output, fmt.Errorf("%s", errMsg)}if written {dataFilesWritten++}if skipped {dataFilesSkipped++return output, err - replacement in tools/calls/calls_from_common.go at line 149
for _, call := range calls {allCalls = append(allCalls, call)speciesCount[call.EbirdCode]++}allCalls = append(allCalls, res.calls...)dataFilesWritten += res.dataFilesWrittendataFilesSkipped += res.dataFilesSkippedfilesDeleted += res.filesDeleted - replacement in tools/calls/calls_from_common.go at line 155
// Delete if requested and successfully processedif input.Delete && written {if err := os.Remove(file); err != nil {errMsg := fmt.Sprintf("Failed to delete %s: %v", file, err)output.Error = &errMsgreturn output, fmt.Errorf("%s", errMsg)}filesDeleted++for _, call := range res.calls {speciesCount[call.EbirdCode]++ - edit in tools/calls/calls_from_common.go at line 164
// Sort all calls by file, then start time - edit in tools/calls/calls_clip_labels.go at line 113
// collectSpeciesFromDataFile parses a .data file, validates it, and returns// its DataFile and the set of species names seen (filtered by filter).func collectSpeciesFromDataFile(path, filter string) (*utils.DataFile, map[string]bool, error) {df, err := utils.ParseDataFile(path)if err != nil {return nil, nil, fmt.Errorf("parse %s: %w", path, err)}if df.Meta == nil || df.Meta.Duration <= 0 {return nil, nil, fmt.Errorf("missing or non-positive Duration in %s (cannot generate clips)", path)}speciesSeen := map[string]bool{}for _, seg := range df.Segments {for _, lbl := range seg.Labels {if filter != "" && lbl.Filter != filter {continue}speciesSeen[lbl.Species] = true}}return df, speciesSeen, nil} - replacement in tools/calls/calls_clip_labels.go at line 147
df, err := utils.ParseDataFile(p)df, species, err := collectSpeciesFromDataFile(p, filter) - replacement in tools/calls/calls_clip_labels.go at line 149
return nil, fmt.Errorf("parse %s: %w", p, err)}if df.Meta == nil || df.Meta.Duration <= 0 {return nil, fmt.Errorf("missing or non-positive Duration in %s (cannot generate clips)", p)return nil, err - replacement in tools/calls/calls_clip_labels.go at line 151
for _, seg := range df.Segments {for _, lbl := range seg.Labels {if filter != "" && lbl.Filter != filter {continue}speciesSeen[lbl.Species] = true}for s := range species {speciesSeen[s] = true - replacement in tools/calls/calls_clip_labels.go at line 179
func CallsClipLabels(input CallsClipLabelsInput) (CallsClipLabelsOutput, error) {out := CallsClipLabelsOutput{Folder: input.Folder,OutputPath: input.OutputPath,PerClassTrueCount: map[string]int{},}// clipLabelsContext holds the initialized state needed to process clip labels.type clipLabelsContext struct {mapping utils.MappingFileclasses []stringclassIdx map[string]intparsed []parsedClipFileexisting map[rowKey]boolappendMode boolexpectedHeader []stringcwd stringfolderAbs stringfinalClipMode utils.FinalClipMode} - edit in tools/calls/calls_clip_labels.go at line 193
// initClipLabelsContext performs all initialization steps for CallsClipLabels.func initClipLabelsContext(input CallsClipLabelsInput, out *CallsClipLabelsOutput) (*clipLabelsContext, error) { - replacement in tools/calls/calls_clip_labels.go at line 197
return out, errreturn nil, err - replacement in tools/calls/calls_clip_labels.go at line 202
return out, fmt.Errorf("load mapping %s: %w", input.MappingPath, err)return nil, fmt.Errorf("load mapping %s: %w", input.MappingPath, err) - replacement in tools/calls/calls_clip_labels.go at line 207
return out, fmt.Errorf("mapping.json has no real (non-sentinel) classes")return nil, fmt.Errorf("mapping.json has no real (non-sentinel) classes") - replacement in tools/calls/calls_clip_labels.go at line 218
return out, errreturn nil, err - replacement in tools/calls/calls_clip_labels.go at line 225
return out, errreturn nil, err - replacement in tools/calls/calls_clip_labels.go at line 232
return out, fmt.Errorf("getwd: %w", err)return nil, fmt.Errorf("getwd: %w", err) - replacement in tools/calls/calls_clip_labels.go at line 236
return out, fmt.Errorf("abs %s: %w", input.Folder, err)return nil, fmt.Errorf("abs %s: %w", input.Folder, err)}return &clipLabelsContext{mapping: mapping,classes: classes,classIdx: classIdx,parsed: parsed,existing: existing,appendMode: appendMode,expectedHeader: expectedHeader,cwd: cwd,folderAbs: folderAbs,finalClipMode: finalClipMode,}, nil}func CallsClipLabels(input CallsClipLabelsInput) (CallsClipLabelsOutput, error) {out := CallsClipLabelsOutput{Folder: input.Folder,OutputPath: input.OutputPath,PerClassTrueCount: map[string]int{},}ctx, err := initClipLabelsContext(input, &out)if err != nil {return out, err - replacement in tools/calls/calls_clip_labels.go at line 266
for _, pf := range parsed {fileRows, err := processClipLabelsFile(pf.path, pf.df, mapping, classIdx, classes, input, finalClipMode, cwd, folderAbs, &out)for _, pf := range ctx.parsed {fileRows, err := processClipLabelsFile(pf.path, pf.df, ctx.mapping, ctx.classIdx, ctx.classes, input, ctx.finalClipMode, ctx.cwd, ctx.folderAbs, &out) - replacement in tools/calls/calls_clip_labels.go at line 274
if err := dedupClipLabelsRows(rows, existing); err != nil {if err := dedupClipLabelsRows(rows, ctx.existing); err != nil { - replacement in tools/calls/calls_clip_labels.go at line 278
if err := writeRows(input.OutputPath, expectedHeader, rows, appendMode); err != nil {if err := writeRows(input.OutputPath, ctx.expectedHeader, rows, ctx.appendMode); err != nil { - edit in tools/calls/calls_clip_labels.go at line 321
}// resolveLabel attempts to classify a single label, returning a resolvedSeg if valid.// Returns (seg, isIgnored, ok). When ok is false the label should be skipped.func resolveLabel(lbl *utils.Label, seg *utils.Segment, filter string, mapping utils.MappingFile, classIdx map[string]int) (resolvedSeg, bool, bool) {if filter != "" && lbl.Filter != filter {return resolvedSeg{}, false, false}canon, kind, ok := mapping.Classify(lbl.Species)if !ok {return resolvedSeg{}, false, false}switch kind {case utils.MappingIgn:return resolvedSeg{start: seg.StartTime, end: seg.EndTime, kind: kind}, true, truecase utils.MappingNeg:return resolvedSeg{start: seg.StartTime, end: seg.EndTime, kind: kind}, false, truecase utils.MappingReal:idx, present := classIdx[canon]if !present {return resolvedSeg{}, false, false}return resolvedSeg{start: seg.StartTime, end: seg.EndTime, kind: kind, classIdx: idx}, false, true}return resolvedSeg{}, false, false - replacement in tools/calls/calls_clip_labels.go at line 363
if filter != "" && lbl.Filter != filter {continue}canon, kind, ok := mapping.Classify(lbl.Species)rs, isIgnored, ok := resolveLabel(lbl, seg, filter, mapping, classIdx) - replacement in tools/calls/calls_clip_labels.go at line 367
switch kind {case utils.MappingIgn:if isIgnored { - edit in tools/calls/calls_clip_labels.go at line 369
segs = append(segs, resolvedSeg{start: seg.StartTime, end: seg.EndTime, kind: kind})case utils.MappingNeg:segs = append(segs, resolvedSeg{start: seg.StartTime, end: seg.EndTime, kind: kind})case utils.MappingReal:idx, present := classIdx[canon]if !present {continue}segs = append(segs, resolvedSeg{start: seg.StartTime, end: seg.EndTime, kind: kind, classIdx: idx}) - edit in tools/calls/calls_clip_labels.go at line 370
segs = append(segs, rs) - replacement in tools/calls/calls_clip_labels.go at line 462
// loadExistingRows reads an existing output CSV and returns its row keys// (for deduplication) and whether we're in append mode.func loadExistingRows(outputPath string, expectedHeader []string) (map[rowKey]bool, bool, error) {fi, err := os.Stat(outputPath)if err != nil {if os.IsNotExist(err) {return nil, false, nil}return nil, false, fmt.Errorf("stat %s: %w", outputPath, err)}if fi.Size() == 0 {return nil, false, nil}f, err := os.Open(outputPath)if err != nil {return nil, false, fmt.Errorf("open existing %s: %w", outputPath, err)}defer func() { _ = f.Close() }()// readExistingCSV reads an existing CSV file, validates its header, and returns row keys.func readExistingCSV(f *os.File, outputPath string, expectedHeader []string) (map[rowKey]bool, error) { - replacement in tools/calls/calls_clip_labels.go at line 469
return nil, false, fmt.Errorf("read header of existing %s: %w", outputPath, err)return nil, fmt.Errorf("read header of existing %s: %w", outputPath, err) - replacement in tools/calls/calls_clip_labels.go at line 472
return nil, false, fmt.Errorf("column-set mismatch in existing %s\n existing: %s\n new: %s",return nil, fmt.Errorf("column-set mismatch in existing %s\n existing: %s\n new: %s", - replacement in tools/calls/calls_clip_labels.go at line 483
return nil, false, fmt.Errorf("read row of existing %s: %w", outputPath, err)return nil, fmt.Errorf("read row of existing %s: %w", outputPath, err) - replacement in tools/calls/calls_clip_labels.go at line 486
return nil, false, fmt.Errorf("malformed row in existing %s: %v", outputPath, rec)return nil, fmt.Errorf("malformed row in existing %s: %v", outputPath, rec) - edit in tools/calls/calls_clip_labels.go at line 490
return existing, nil} - edit in tools/calls/calls_clip_labels.go at line 493
// loadExistingRows reads an existing output CSV and returns its row keys// (for deduplication) and whether we're in append mode.func loadExistingRows(outputPath string, expectedHeader []string) (map[rowKey]bool, bool, error) {fi, err := os.Stat(outputPath)if err != nil {if os.IsNotExist(err) {return nil, false, nil}return nil, false, fmt.Errorf("stat %s: %w", outputPath, err)}if fi.Size() == 0 {return nil, false, nil}f, err := os.Open(outputPath)if err != nil {return nil, false, fmt.Errorf("open existing %s: %w", outputPath, err)}defer func() { _ = f.Close() }()existing, err := readExistingCSV(f, outputPath, expectedHeader)if err != nil {return nil, false, err} - replacement in lint_test.go at line 48
cmd := exec.Command("gocyclo", "-over", "13", ".")cmd := exec.Command("gocyclo", "-over", "12", ".") - replacement in lint_test.go at line 52
t.Errorf("cyclometric complexity is above 14:\n%s", out)t.Errorf("cyclometric complexity is above 12:\n%s", out) - edit in cmd/calls_modify.go at line 60
}// modifyFlagHandler returns a function that sets a flag value on modifyArgs,// or nil if the flag is unknown. Returns (handler, isBool, handled).// isBool indicates the flag takes no value.func modifyFlagHandler(arg string, ma *modifyArgs) (func(string), bool, bool) {switch arg {case "--file":return func(v string) { ma.file = v }, false, truecase "--reviewer":return func(v string) { ma.reviewer = v }, false, truecase "--filter":return func(v string) { ma.filter = v }, false, truecase "--segment":return func(v string) { ma.segment = v }, false, truecase "--species":return func(v string) { ma.species = v }, false, truecase "--certainty":return func(v string) {n, err := strconv.Atoi(v)if err != nil {fmt.Fprintf(os.Stderr, "Error: --certainty must be an integer\n")os.Exit(1)}ma.certainty = nma.certaintySet = true}, false, truecase "--bookmark":return func(_ string) { ma.bookmark = true }, true, truecase "--comment":return func(v string) { ma.comment = v }, false, truedefault:return nil, false, false} - edit in cmd/calls_modify.go at line 97
// Uses mustValue for flag-value extraction (exits on missing value). - replacement in cmd/calls_modify.go at line 102[5.1121025]→[5.1121025:1121057](∅→∅),[5.1121057]→[5.5091:5134](∅→∅),[5.5134]→[5.1121196:1121217](∅→∅),[5.1121196]→[5.1121196:1121217](∅→∅),[5.1121217]→[5.5135:5186](∅→∅),[5.5186]→[5.1121364:1121383](∅→∅),[5.1121364]→[5.1121364:1121383](∅→∅),[5.1121383]→[5.5187:5234](∅→∅),[5.5234]→[5.1121526:1121546](∅→∅),[5.1121526]→[5.1121526:1121546](∅→∅),[5.1121546]→[5.5235:5284](∅→∅),[5.5284]→[5.1121691:1121711](∅→∅),[5.1121691]→[5.1121691:1121711](∅→∅),[5.1121711]→[5.5285:5334](∅→∅),[5.5334]→[5.1121856:1121878](∅→∅),[5.1121856]→[5.1121856:1121878](∅→∅),[5.1121878]→[5.5335:5380](∅→∅),[5.5380]→[5.26022:26053](∅→∅),[5.26022]→[5.26022:26053](∅→∅),[5.26053]→[5.1122028:1122137](∅→∅),[5.1122028]→[5.1122028:1122137](∅→∅),[5.1122137]→[5.26054:26100](∅→∅),[5.1122188]→[5.1122188:1122209](∅→∅),[5.1122209]→[5.26101:26123](∅→∅),[5.26123]→[5.1122228:1122235](∅→∅),[5.1122228]→[5.1122228:1122235](∅→∅),[5.1122236]→[5.1122236:1122256](∅→∅),[5.1122256]→[5.5381:5430](∅→∅),[5.5430]→[5.1122401:1122424](∅→∅),[5.1122401]→[5.1122401:1122424](∅→∅)
switch arg {case "--file":ma.file = mustValue(args, &i, "--file")case "--reviewer":ma.reviewer = mustValue(args, &i, "--reviewer")case "--filter":ma.filter = mustValue(args, &i, "--filter")case "--segment":ma.segment = mustValue(args, &i, "--segment")case "--species":ma.species = mustValue(args, &i, "--species")case "--certainty":val := mustValue(args, &i, "--certainty")v, err := strconv.Atoi(val)if err != nil {fmt.Fprintf(os.Stderr, "Error: --certainty must be an integer\n")os.Exit(1)}ma.certainty = vma.certaintySet = truecase "--bookmark":ma.bookmark = truei++case "--comment":ma.comment = mustValue(args, &i, "--comment")case "-h", "--help":if arg == "-h" || arg == "--help" { - replacement in cmd/calls_modify.go at line 105[5.1122461]→[5.1122461:1122472](∅→∅),[5.1122502]→[5.1122502:1122539](∅→∅),[5.1122602]→[5.1122602:1122625](∅→∅),[5.1122625]→[5.5431:5492](∅→∅),[5.5492]→[5.1122625:1122645](∅→∅),[5.1122625]→[5.1122625:1122645](∅→∅)
default:if strings.HasPrefix(arg, "--") {printModifyUsage()fmt.Fprintf(os.Stderr, "Error: unknown flag: %s\n", arg)os.Exit(1)}}handler, isBool, handled := modifyFlagHandler(arg, &ma)if !handled {handleUnknownFlag(arg) - edit in cmd/calls_modify.go at line 110
continue - edit in cmd/calls_modify.go at line 112
if isBool {i++} else {handler(mustValue(args, &i, arg))} - edit in cmd/calls_modify.go at line 119
}// handleUnknownFlag prints usage and exits for an unknown flag.func handleUnknownFlag(arg string) {if strings.HasPrefix(arg, "--") {printModifyUsage()fmt.Fprintf(os.Stderr, "Error: unknown flag: %s\n", arg)os.Exit(1)} - replacement in cmd/calls_detect_anomalies.go at line 60[5.1126708]→[5.5848:5900](∅→∅),[5.5900]→[5.1126754:1126816](∅→∅),[5.1126754]→[5.1126754:1126816](∅→∅)
func runCallsDetectAnomalies(args []string) error {var folder stringvar models []stringvar species []string//// detectAnomaliesArgs holds the parsed arguments.type detectAnomaliesArgs struct {folder stringmodels []stringspecies []string} - edit in cmd/calls_detect_anomalies.go at line 68
// parseDetectAnomaliesArgs parses CLI arguments for the detect-anomalies command.func parseDetectAnomaliesArgs(args []string) (detectAnomaliesArgs, error) {var da detectAnomaliesArgs - replacement in cmd/calls_detect_anomalies.go at line 77
return fmt.Errorf("--folder requires a value")return da, fmt.Errorf("--folder requires a value") - replacement in cmd/calls_detect_anomalies.go at line 79
folder = args[i+1]da.folder = args[i+1] - edit in cmd/calls_detect_anomalies.go at line 81
- replacement in cmd/calls_detect_anomalies.go at line 83
return fmt.Errorf("--model requires a value")return da, fmt.Errorf("--model requires a value") - replacement in cmd/calls_detect_anomalies.go at line 85
models = append(models, args[i+1])da.models = append(da.models, args[i+1]) - edit in cmd/calls_detect_anomalies.go at line 87
- replacement in cmd/calls_detect_anomalies.go at line 89
return fmt.Errorf("--species requires a value")return da, fmt.Errorf("--species requires a value") - replacement in cmd/calls_detect_anomalies.go at line 91
species = append(species, args[i+1])da.species = append(da.species, args[i+1]) - edit in cmd/calls_detect_anomalies.go at line 93
- replacement in cmd/calls_detect_anomalies.go at line 95[5.1127452]→[5.6057:6071](∅→∅),[5.6071]→[5.1127466:1127467](∅→∅),[5.1127466]→[5.1127466:1127467](∅→∅)
return nilreturn da, nil - replacement in cmd/calls_detect_anomalies.go at line 99
return fmt.Errorf("unknown flag: %s", arg)return da, fmt.Errorf("unknown flag: %s", arg) - edit in cmd/calls_detect_anomalies.go at line 102
return da, nil} - replacement in cmd/calls_detect_anomalies.go at line 105
if folder == "" {// validateDetectAnomaliesArgs checks required flags.func validateDetectAnomaliesArgs(da detectAnomaliesArgs) error {if da.folder == "" { - replacement in cmd/calls_detect_anomalies.go at line 111
if len(models) < 2 {if len(da.models) < 2 { - edit in cmd/calls_detect_anomalies.go at line 115
return nil} - edit in cmd/calls_detect_anomalies.go at line 118
func runCallsDetectAnomalies(args []string) error {da, err := parseDetectAnomaliesArgs(args)if err != nil {return err}if err := validateDetectAnomaliesArgs(da); err != nil {return err} - replacement in cmd/calls_detect_anomalies.go at line 128
Folder: folder,Models: models,Species: species,Folder: da.folder,Models: da.models,Species: da.species, - edit in cmd/calls_classify.go at line 6
"strconv" - edit in cmd/calls_classify.go at line 78
}// classifyFlagSet maps a flag name to a setter function.// The setter receives the value string and the current args state, returning the updated state.// isBool flags take no value argument.type classifyFlag struct {set func(val string, a *classifyArgs)isBool bool - replacement in cmd/calls_classify.go at line 88
// mustUniqueValue is like mustValue but exits if the flag was already set.func mustUniqueValue(args []string, i *int, flag, current string) string {// classifyFlags defines the flag dispatch table for parseClassifyArgs.var classifyFlags = map[string]classifyFlag{"--folder": {func(v string, a *classifyArgs) { a.folder = v }, false},"--file": {func(v string, a *classifyArgs) { a.file = v }, false},"--filter": {func(v string, a *classifyArgs) { a.filter = classifyUniqueSet(v, "--filter", a.filter) }, false},"--species": {func(v string, a *classifyArgs) { a.species = classifyUniqueSet(v, "--species", a.species) }, false},"--certainty": {func(v string, a *classifyArgs) { a.certainty = classifyIntValue(v, "--certainty", 0, 100) }, false},"--sample": {func(v string, a *classifyArgs) { a.sample = classifyIntValue(v, "--sample", 1, 100) }, false},"--goto": {func(v string, a *classifyArgs) { a.gotoFile = v }, false},"--location": {func(v string, a *classifyArgs) { a.location = classifyUniqueSet(v, "--location", a.location) }, false},"--night": {func(_ string, a *classifyArgs) { a.night = true }, true},"--day": {func(_ string, a *classifyArgs) { a.day = true }, true},}// classifyUniqueSet sets a string field, exiting if already set.func classifyUniqueSet(val, flag, current string) string { - replacement in cmd/calls_classify.go at line 108
return mustValue(args, i, flag)return val}// classifyIntValue parses and validates an integer flag value.func classifyIntValue(val, flag string, lo, hi int) int {v, err := strconv.Atoi(val)if err != nil {fmt.Fprintf(os.Stderr, "Error: %s must be an integer\n", flag)os.Exit(1)}if v < lo || v > hi {fmt.Fprintf(os.Stderr, "Error: %s must be between %d and %d\n", flag, lo, hi)os.Exit(1)}return v - edit in cmd/calls_classify.go at line 126
// Uses mustValue for flag-value extraction (exits on missing value). - replacement in cmd/calls_classify.go at line 132[5.1145577]→[5.1145577:1145612](∅→∅),[5.1145612]→[5.7577:7623](∅→∅),[5.7623]→[5.1145755:1145772](∅→∅),[5.1145755]→[5.1145755:1145772](∅→∅),[5.1145772]→[5.7624:7666](∅→∅),[5.7666]→[5.1145911:1145930](∅→∅),[5.1145911]→[5.1145911:1145930](∅→∅),[5.1145930]→[5.7667:7729](∅→∅),[5.7729]→[5.1146189:1146209](∅→∅),[5.1146189]→[5.1146189:1146209](∅→∅),[5.1146209]→[5.7730:7795](∅→∅),[5.7795]→[5.1146472:1146494](∅→∅),[5.1146472]→[5.1146472:1146494](∅→∅),[5.1146494]→[5.7796:7859](∅→∅),[5.7859]→[5.8732:8751](∅→∅),[5.1146902]→[5.8732:8751](∅→∅),[5.8751]→[5.7860:7917](∅→∅),[5.7917]→[5.8813:8830](∅→∅),[5.1147292]→[5.8813:8830](∅→∅),[5.8830]→[5.7918:7964](∅→∅),[5.7964]→[5.2328:2349](∅→∅),[5.9034]→[5.2328:2349](∅→∅),[5.2349]→[5.7965:8033](∅→∅),[5.8033]→[5.9114:9196](∅→∅),[5.1147770]→[5.9114:9196](∅→∅),[5.9196]→[5.1147771:1147794](∅→∅),[5.1147771]→[5.1147771:1147794](∅→∅)
switch arg {case "--folder":a.folder = mustValue(args, &i, "--folder")case "--file":a.file = mustValue(args, &i, "--file")case "--filter":a.filter = mustUniqueValue(args, &i, "--filter", a.filter)case "--species":a.species = mustUniqueValue(args, &i, "--species", a.species)case "--certainty":a.certainty = mustIntValue(args, &i, "--certainty", 0, 100)case "--sample":a.sample = mustIntValue(args, &i, "--sample", 1, 100)case "--goto":a.gotoFile = mustValue(args, &i, "--goto")case "--location":a.location = mustUniqueValue(args, &i, "--location", a.location)case "--night":a.night = truei++case "--day":a.day = truei++case "--help", "-h":if arg == "--help" || arg == "-h" { - replacement in cmd/calls_classify.go at line 135
default:}fl, ok := classifyFlags[arg]if !ok { - edit in cmd/calls_classify.go at line 142
if fl.isBool {fl.set("", &a)i++} else {fl.set(mustValue(args, &i, arg), &a)} - edit in cmd/calls_classify.go at line 155
if err := a.validateSampleCertainty(); err != nil {return err}if err := a.validateSource(); err != nil {return err}return a.validateDayNight()}func (a classifyArgs) validateSampleCertainty() error { - edit in cmd/calls_classify.go at line 168
return nil}func (a classifyArgs) validateSource() error { - edit in cmd/calls_classify.go at line 176
return nil}func (a classifyArgs) validateDayNight() error { - edit in cmd/calls_classify.go at line 232[5.11423]→[5.11423:11484](∅→∅),[5.11484]→[5.9126:9171](∅→∅),[5.9171]→[5.11523:11553](∅→∅),[5.11523]→[5.11523:11553](∅→∅),[5.11553]→[5.9172:9226](∅→∅)
// RunCallsClassify handles the "calls classify" subcommandfunc RunCallsClassify(args []string) error {a := parseClassifyArgs(args)if err := a.validate(); err != nil {return err} - replacement in cmd/calls_classify.go at line 233
// Load reviewer, bindings, and display flags from ~/.skraak/config.json.// loadClassifyConfig loads and validates the classify config from disk.func loadClassifyConfig() (utils.Config, string, []calls.KeyBinding, error) { - replacement in cmd/calls_classify.go at line 238
return fmt.Errorf("loading config: %w", err)return cfg, cfgPath, nil, fmt.Errorf("loading config: %w", err) - edit in cmd/calls_classify.go at line 241
// Validate config contents - replacement in cmd/calls_classify.go at line 242
return fmt.Errorf("%s is missing \"classify.reviewer\"", cfgPath)return cfg, cfgPath, nil, fmt.Errorf("%s is missing \"classify.reviewer\"", cfgPath) - replacement in cmd/calls_classify.go at line 245
return fmt.Errorf("%s is missing \"classify.bindings\" (need at least one key)", cfgPath)return cfg, cfgPath, nil, fmt.Errorf("%s is missing \"classify.bindings\" (need at least one key)", cfgPath) - replacement in cmd/calls_classify.go at line 250
return errreturn cfg, cfgPath, nil, err - edit in cmd/calls_classify.go at line 252
return cfg, cfgPath, bindings, nil} - replacement in cmd/calls_classify.go at line 255
// Parse species+calltype// buildClassifyConfig constructs the ClassifyConfig from parsed args and loaded config.func buildClassifyConfig(a classifyArgs, cfg utils.Config, bindings []calls.KeyBinding) (calls.ClassifyConfig, error) { - edit in cmd/calls_classify.go at line 259
// Parse location into lat/lng/timezone - edit in cmd/calls_classify.go at line 262
var err error - replacement in cmd/calls_classify.go at line 265
return fmt.Errorf("parsing location: %w", err)return calls.ClassifyConfig{}, fmt.Errorf("parsing location: %w", err) - replacement in cmd/calls_classify.go at line 269[5.2810]→[5.1151628:1151645](∅→∅),[5.1151628]→[5.1151628:1151645](∅→∅),[5.1151645]→[5.315524:315557](∅→∅)
// Build configconfig := calls.ClassifyConfig{return calls.ClassifyConfig{ - edit in cmd/calls_classify.go at line 290
}, nil}// RunCallsClassify handles the "calls classify" subcommandfunc RunCallsClassify(args []string) error {a := parseClassifyArgs(args)if err := a.validate(); err != nil {return err}cfg, _, bindings, err := loadClassifyConfig()if err != nil {return err - edit in cmd/calls_classify.go at line 305
config, err := buildClassifyConfig(a, cfg, bindings)if err != nil {return err} - edit in CHANGELOG.md at line 4
## [2026-05-14] Reduce cyclomatic complexity of 10 functions below threshold of 10Reduced complexity of all 10 functions previously scoring 13 on gocyclo.Refactoring focused on extracting helpers, introducing table-driven tests,and grouping initialization logic into context structs.### Changed- `utils/filename_parser_test.go`: `TestParseFilenameTimestamps` → table-driven with `runParseTestCase` helper (13→4)- `tools/prepend.go`: `Prepend` → extracted `prependFolders` and `processPrependFile` (13→6)- `tools/pattern_test.go`: `TestCreateOrUpdatePattern_CreateDuplicate` → extracted `testExistingPattern`, `testUniquePattern`, `testIdempotentCreate` (13→2)- `tools/integration_test.go`: `testCreateClusterWithPattern` → extracted `lookupActiveDatasetAndLocation` and `verifyClusterPattern` (13→2)- `tools/calls/calls_from_common.go`: `callsFromSourceSequential` → extracted `processSequentialFile` (13→7)- `tools/calls/calls_clip_labels.go`: `CallsClipLabels` → extracted `initClipLabelsContext` and `clipLabelsContext` struct (13→6); also reduced `parseClipLabelsDataFiles` (12→7), `loadExistingRows` (11→6), `resolveSegments` (11→6) via helper extraction- `cmd/calls_modify.go`: `parseModifyArgs` → extracted `modifyFlagHandler` and `handleUnknownFlag` (13→6)- `cmd/calls_detect_anomalies.go`: `runCallsDetectAnomalies` → extracted `parseDetectAnomaliesArgs` and `validateDetectAnomaliesArgs` (13→5)- `cmd/calls_classify.go`: `RunCallsClassify` → extracted `loadClassifyConfig` and `buildClassifyConfig` (13→9); `parseClassifyArgs` → map-based dispatch with `classifyFlags` table (13→6); `validate` → split into `validateSampleCertainty`, `validateSource`, `validateDayNight` (11→3) - edit in CHANGELOG.md at line 22
### Added- `tools/prepend_test.go`: `TestProcessPrependFile` - table-driven unit test for extracted `processPrependFile`