ZDZDASRTTRPJRIBAMNO3TB533XFELVYJQQGAMA3WOQYY35SVAQUQC DNLR36BZQ45JTKJIYXGH7VWZSI6ISEXQMQWK3WAXMAX4M2MQ4HXAC ZKLAOPURUGKKG4KC7C5NEQ5WSZSFTZM7SCV7PIYJMWN4UKI7UI3QC ZCCQ4P5T2AMJAPBDWZVHXIUKLI5U2E5GNDXRCWXEOJQRWPSJJFEQC NS4TDPLNAWJYJN37PZDYXMG6OJSAWZCMTPSPKX73JCLZZAMY25BAC KZKLAINJJWZ64T5MUZT34LJVQIKBTKZ6EJGD7C7TTSSDGCHEDPMAC 3DVPQOKB6BX63XSBIYYCPWBL2RBG3LXZS3XPQBANJP2FWVRAOVZQC HYCZTLSZ5WVJFMP4EPVHAVWRYSYNCIBJ62LLBNO4IRVEBY7WJI6QC BZ6KQRYDMP4PWYJRL62XXIUXLTBKEASIKSAJIQPZS6DKDSYKA76QC GPQSOVBPY7VTPHD75R6VWSNITPOL3AECF4DHJB32MF5Z72NV7YMQC QFPEKXL5OUKLT4WECMATSOHWYM24QPHKS6WZAAI5BAEQSAGAK6CQC DD3LCTLZFDIPVXXSG7RDINZCQ7NGKVG3X52OFVGVZIISD5VPF35QC 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}
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)}}
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},},},}
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)})}
}}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)}})
// 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}
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})}
// 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
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,})
// 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)
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,}
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)}
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)
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)
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) {
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()
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"])
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")}
}// 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
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)
for _, call := range calls {allCalls = append(allCalls, call)speciesCount[call.EbirdCode]++}
allCalls = append(allCalls, res.calls...)dataFilesWritten += res.dataFilesWrittendataFilesSkipped += res.dataFilesSkippedfilesDeleted += res.filesDeleted
// 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]++
// 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}
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}
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
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)
}// 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
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})
// 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) {
// 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}
}// 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}
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" {
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)
func runCallsDetectAnomalies(args []string) error {var folder stringvar models []stringvar species []string
//// detectAnomaliesArgs holds the parsed arguments.type detectAnomaliesArgs struct {folder stringmodels []stringspecies []string}
// 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 {
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
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" {
// RunCallsClassify handles the "calls classify" subcommandfunc RunCallsClassify(args []string) error {a := parseClassifyArgs(args)if err := a.validate(); err != nil {return err}
// 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) {
## [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)