cyclo over 15

quietlight
May 4, 2026, 9:19 PM
JAT3DXOLENZZGXE2NYFF3TVQAQIXMMNYO234ETKQGC2CRHJVZERQC

Dependencies

  • [2] I4CMOMXF dot files
  • [3] SMWSHUOW cyclo over 15
  • [4] LBWQJEDH minor refactor and more tests for utils/
  • [5] QVIGQOQZ more work on utils/ with glm
  • [6] BZ6KQRYD added complexity lint test
  • [7] KLUEQ6X5 cyclo 21+
  • [8] GPQSOVBP cyclo complexity over 25
  • [9] NS4TDPLN cyclomatic complexity
  • [10] LQLC7S3A trying gemini: Inconsistent Standards in @utils/ refactoring
  • [11] KZKLAINJ run out of space on nest, cleaned out
  • [12] T2WZBTVF cyclo 22
  • [13] 54GPBNIX added +_ for tui to select segments with no calltype
  • [14] 2P27XV3D fixed cyclo over 30
  • [*] YE6BZJUK tidy up lat lng timezone api for calls clip cmd

Change contents

  • replacement in utils/data_file.go at line 304
    [4.164598][4.164598:164656]()
    if filter != "" && label.Filter != filter {
    continue
    [4.164598]
    [4.164656]
    if labelMatchesFilters(label, filter, species, callType, certainty) {
    return true
  • replacement in utils/data_file.go at line 307
    [4.164660][4.164660:164721]()
    if species != "" && label.Species != species {
    continue
    [4.164660]
    [4.164721]
    }
    return false
    }
    // labelMatchesFilters checks if a single label matches all filter criteria.
    func labelMatchesFilters(label *Label, filter, species, callType string, certainty int) bool {
    if filter != "" && label.Filter != filter {
    return false
    }
    if species != "" && label.Species != species {
    return false
    }
    if callType == CallTypeNone {
    if label.CallType != "" {
    return false
  • replacement in utils/data_file.go at line 323
    [4.164725][4.199:337](),[4.337][4.164777:164877](),[4.164777][4.164777:164877]()
    if callType == CallTypeNone {
    if label.CallType != "" {
    continue
    }
    } else if callType != "" && label.CallType != callType {
    continue
    }
    if certainty >= 0 && label.Certainty != certainty {
    continue
    }
    return true
    [4.164725]
    [4.164877]
    } else if callType != "" && label.CallType != callType {
    return false
    }
    if certainty >= 0 && label.Certainty != certainty {
    return false
  • replacement in utils/data_file.go at line 329
    [4.164880][4.164880:164894]()
    return false
    [4.164880]
    [4.164894]
    return true
  • edit in tui/classify.go at line 555
    [4.239974][4.239974:240356]()
    }
    // Get WAV path
    wavPath := strings.TrimSuffix(df.FilePath, ".data")
    // Get basename without path and extension
    basename := wavPath[strings.LastIndex(wavPath, "/")+1:]
    basename = strings.TrimSuffix(basename, ".wav")
    // Calculate integer times for filename
    startInt := int(seg.StartTime)
    endInt := int(seg.EndTime)
    if seg.EndTime > float64(endInt) {
    endInt++ // ceil
  • replacement in tui/classify.go at line 557
    [4.240360][4.240360:240435]()
    // Build output paths (current working directory)
    cwd, err := os.Getwd()
    [4.240360]
    [4.240435]
    pngPath, wavOutPath, err := buildClipPaths(df, seg, prefix)
  • replacement in tui/classify.go at line 559
    [4.240452][4.240452:240940]()
    return fmt.Errorf("failed to get working directory: %w", err)
    }
    baseName := fmt.Sprintf("%s_%s_%d_%d", prefix, basename, startInt, endInt)
    pngPath := filepath.Join(cwd, baseName+".png")
    wavOutPath := filepath.Join(cwd, baseName+".wav")
    // Check if files already exist
    if _, err := os.Stat(pngPath); err == nil {
    return fmt.Errorf("file already exists: %s", pngPath)
    }
    if _, err := os.Stat(wavOutPath); err == nil {
    return fmt.Errorf("file already exists: %s", wavOutPath)
    [4.240452]
    [4.240940]
    return err
  • edit in tui/classify.go at line 563
    [4.240965]
    [4.240965]
    wavPath := strings.TrimSuffix(df.FilePath, ".data")
  • replacement in tui/classify.go at line 582
    [4.241565][4.241565:241806]()
    // Generate spectrogram (224px, color)
    config := utils.DefaultSpectrogramConfig(outputSampleRate)
    spectrogram := utils.GenerateSpectrogram(segSamples, config)
    if spectrogram == nil {
    return fmt.Errorf("failed to generate spectrogram")
    [4.241565]
    [4.241806]
    // Generate spectrogram image
    resized, err := generateClipSpectrogram(segSamples, outputSampleRate)
    if err != nil {
    return err
  • replacement in tui/classify.go at line 588
    [4.241810][4.241810:241962]()
    colorData := utils.ApplyL4Colormap(spectrogram)
    img := utils.CreateRGBImage(colorData)
    if img == nil {
    return fmt.Errorf("failed to create image")
    [4.241810]
    [4.241962]
    // Write output files
    if err := writeClipPNG(resized, pngPath); err != nil {
    return err
    }
    if err := utils.WriteWAVFile(wavOutPath, segSamples, outputSampleRate); err != nil {
    return fmt.Errorf("failed to write WAV: %w", err)
  • replacement in tui/classify.go at line 596
    [4.241966][4.241966:242011]()
    resized := utils.ResizeImage(img, 224, 224)
    [4.241966]
    [4.242011]
    return nil
    }
  • replacement in tui/classify.go at line 599
    [4.242012][4.242012:242062]()
    // Write PNG
    pngFile, err := os.Create(pngPath)
    [4.242012]
    [4.242062]
    // writeClipPNG writes a spectrogram image to a PNG file with proper cleanup.
    func writeClipPNG(img image.Image, path string) error {
    pngFile, err := os.Create(path)
  • replacement in tui/classify.go at line 605
    [4.242135][4.242135:242193]()
    if err := utils.WritePNG(resized, pngFile); err != nil {
    [4.242135]
    [4.242193]
    if err := utils.WritePNG(img, pngFile); err != nil {
  • edit in tui/classify.go at line 611
    [4.242363]
    [4.242363]
    }
    return nil
    }
    // buildClipPaths constructs output file paths for a clip and checks they don't already exist.
    func buildClipPaths(df *utils.DataFile, seg *utils.Segment, prefix string) (pngPath, wavOutPath string, err error) {
    wavPath := strings.TrimSuffix(df.FilePath, ".data")
    basename := wavPath[strings.LastIndex(wavPath, "/")+1:]
    basename = strings.TrimSuffix(basename, ".wav")
    startInt := int(seg.StartTime)
    endInt := int(seg.EndTime)
    if seg.EndTime > float64(endInt) {
    endInt++
    }
    cwd, err := os.Getwd()
    if err != nil {
    return "", "", fmt.Errorf("failed to get working directory: %w", err)
  • replacement in tui/classify.go at line 632
    [4.242367][4.242367:242519]()
    // Write WAV
    if err := utils.WriteWAVFile(wavOutPath, segSamples, outputSampleRate); err != nil {
    return fmt.Errorf("failed to write WAV: %w", err)
    [4.242367]
    [4.242519]
    baseName := fmt.Sprintf("%s_%s_%d_%d", prefix, basename, startInt, endInt)
    pngPath = filepath.Join(cwd, baseName+".png")
    wavOutPath = filepath.Join(cwd, baseName+".wav")
    if _, err := os.Stat(pngPath); err == nil {
    return "", "", fmt.Errorf("file already exists: %s", pngPath)
    }
    if _, err := os.Stat(wavOutPath); err == nil {
    return "", "", fmt.Errorf("file already exists: %s", wavOutPath)
    }
    return pngPath, wavOutPath, nil
    }
    // generateClipSpectrogram generates a 224px color spectrogram image from audio samples.
    func generateClipSpectrogram(segSamples []float64, sampleRate int) (image.Image, error) {
    config := utils.DefaultSpectrogramConfig(sampleRate)
    spectrogram := utils.GenerateSpectrogram(segSamples, config)
    if spectrogram == nil {
    return nil, fmt.Errorf("failed to generate spectrogram")
    }
    colorData := utils.ApplyL4Colormap(spectrogram)
    img := utils.CreateRGBImage(colorData)
    if img == nil {
    return nil, fmt.Errorf("failed to create image")
  • replacement in tui/classify.go at line 660
    [4.242523][4.242523:242535]()
    return nil
    [4.242523]
    [4.242535]
    return utils.ResizeImage(img, 224, 224), nil
  • edit in tools/pattern.go at line 35
    [4.281650][4.281650:281762]()
    func createPattern(ctx context.Context, input PatternInput) (PatternOutput, error) {
    var output PatternOutput
  • replacement in tools/pattern.go at line 36
    [4.281763][4.281763:281803]()
    // Validate required fields for create
    [4.281763]
    [4.281803]
    // validateCreatePatternInput validates required fields for pattern creation.
    func validateCreatePatternInput(input PatternInput) error {
  • replacement in tools/pattern.go at line 39
    [4.281836][4.281836:281918]()
    return output, fmt.Errorf("record_seconds is required when creating a pattern")
    [4.281836]
    [4.281918]
    return fmt.Errorf("record_seconds is required when creating a pattern")
  • replacement in tools/pattern.go at line 42
    [4.281953][4.281953:282034]()
    return output, fmt.Errorf("sleep_seconds is required when creating a pattern")
    [4.281953]
    [4.282034]
    return fmt.Errorf("sleep_seconds is required when creating a pattern")
  • replacement in tools/pattern.go at line 45
    [4.282125][4.282125:282146]()
    return output, err
    [4.282125]
    [4.282146]
    return err
    }
    return utils.ValidatePositive(*input.SleepSeconds, "sleep_seconds")
    }
    // findExistingPattern checks if an active pattern with the given record/sleep times exists.
    func findExistingPattern(ctx context.Context, tx *db.LoggedTx, recordSeconds, sleepSeconds int) (db.CyclicRecordingPattern, bool, error) {
    var existingID string
    err := tx.QueryRowContext(ctx,
    "SELECT id FROM cyclic_recording_pattern WHERE record_s = ? AND sleep_s = ? AND active = true",
    recordSeconds, sleepSeconds,
    ).Scan(&existingID)
    if err == sql.ErrNoRows {
    return db.CyclicRecordingPattern{}, false, nil
    }
    if err != nil {
    return db.CyclicRecordingPattern{}, false, err
    }
    var pattern db.CyclicRecordingPattern
    err = tx.QueryRowContext(ctx,
    "SELECT id, record_s, sleep_s, created_at, last_modified, active FROM cyclic_recording_pattern WHERE id = ?",
    existingID,
    ).Scan(&pattern.ID, &pattern.RecordS, &pattern.SleepS, &pattern.CreatedAt, &pattern.LastModified, &pattern.Active)
    if err != nil {
    return db.CyclicRecordingPattern{}, false, fmt.Errorf("failed to fetch existing pattern: %w", err)
    }
    return pattern, true, nil
    }
    // validateUpdatePatternInput validates fields for pattern update.
    func validateUpdatePatternInput(input PatternInput) error {
    if err := utils.ValidateShortID(*input.ID, "pattern_id"); err != nil {
    return err
    }
    if input.RecordSeconds != nil {
    if err := utils.ValidatePositive(*input.RecordSeconds, "record_seconds"); err != nil {
    return err
    }
  • replacement in tools/pattern.go at line 86
    [4.282149][4.282149:282235]()
    if err := utils.ValidatePositive(*input.SleepSeconds, "sleep_seconds"); err != nil {
    [4.282149]
    [4.282235]
    if input.SleepSeconds != nil {
    if err := utils.ValidateNonNegative(*input.SleepSeconds, "sleep_seconds"); err != nil {
    return err
    }
    }
    return nil
    }
    func createPattern(ctx context.Context, input PatternInput) (PatternOutput, error) {
    var output PatternOutput
    if err := validateCreatePatternInput(input); err != nil {
  • replacement in tools/pattern.go at line 120
    [4.282768][4.282768:283487]()
    var existingID string
    err = tx.QueryRowContext(ctx,
    "SELECT id FROM cyclic_recording_pattern WHERE record_s = ? AND sleep_s = ? AND active = true",
    *input.RecordSeconds, *input.SleepSeconds,
    ).Scan(&existingID)
    if err == nil {
    // Pattern already exists, return it instead of creating duplicate
    var pattern db.CyclicRecordingPattern
    err = tx.QueryRowContext(ctx,
    "SELECT id, record_s, sleep_s, created_at, last_modified, active FROM cyclic_recording_pattern WHERE id = ?",
    existingID,
    ).Scan(&pattern.ID, &pattern.RecordS, &pattern.SleepS, &pattern.CreatedAt, &pattern.LastModified, &pattern.Active)
    if err != nil {
    return output, fmt.Errorf("failed to fetch existing pattern: %w", err)
    }
    [4.282768]
    [4.283487]
    if existing, found, err := findExistingPattern(ctx, tx, *input.RecordSeconds, *input.SleepSeconds); err != nil {
    return output, fmt.Errorf("failed to check for existing pattern: %w", err)
    } else if found {
  • replacement in tools/pattern.go at line 126
    [4.283598][4.283598:283626]()
    output.Pattern = pattern
    [4.283598]
    [4.283626]
    output.Pattern = existing
  • replacement in tools/pattern.go at line 128
    [4.283747][4.283747:283796]()
    pattern.ID, pattern.RecordS, pattern.SleepS)
    [4.283747]
    [4.283796]
    existing.ID, existing.RecordS, existing.SleepS)
  • edit in tools/pattern.go at line 130
    [4.283817][4.283817:283928]()
    } else if err != sql.ErrNoRows {
    return output, fmt.Errorf("failed to check for existing pattern: %w", err)
  • edit in tools/pattern.go at line 170
    [4.285274][4.285274:285298]()
    patternID := *input.ID
  • replacement in tools/pattern.go at line 171
    [4.285299][4.285299:285394]()
    // Validate ID format
    if err := utils.ValidateShortID(patternID, "pattern_id"); err != nil {
    [4.285299]
    [4.285394]
    if err := validateUpdatePatternInput(input); err != nil {
  • edit in tools/pattern.go at line 175
    [4.285419][4.285419:285754]()
    // Validate fields if provided
    if input.RecordSeconds != nil {
    if err := utils.ValidatePositive(*input.RecordSeconds, "record_seconds"); err != nil {
    return output, err
    }
    }
    if input.SleepSeconds != nil {
    if err := utils.ValidateNonNegative(*input.SleepSeconds, "sleep_seconds"); err != nil {
    return output, err
    }
    }
  • replacement in tools/pattern.go at line 186
    [4.286185][4.286185:286209]()
    patternID, patternID,
    [4.286185]
    [4.286209]
    *input.ID, *input.ID,
  • replacement in tools/pattern.go at line 192
    [4.286333][4.286333:286397]()
    return output, fmt.Errorf("pattern not found: %s", patternID)
    [4.286333]
    [4.286397]
    return output, fmt.Errorf("pattern not found: %s", *input.ID)
  • replacement in tools/pattern.go at line 195
    [4.286414][4.286414:286517]()
    return output, fmt.Errorf("pattern '%s' is not active (cannot update inactive patterns)", patternID)
    [4.286414]
    [4.286517]
    return output, fmt.Errorf("pattern '%s' is not active (cannot update inactive patterns)", *input.ID)
  • replacement in tools/pattern.go at line 217
    [4.287011][4.287011:287043]()
    args = append(args, patternID)
    [4.287011]
    [4.287043]
    args = append(args, *input.ID)
  • replacement in tools/pattern.go at line 241
    [4.287733][4.287733:287746]()
    patternID,
    [4.287733]
    [4.287746]
    *input.ID,
  • file addition: parallel_aggregate.go (----------)
    [4.248737]
    package tools
    import (
    "fmt"
    "os"
    "path/filepath"
    "sort"
    "sync/atomic"
    )
    // parallelResult is the common interface for birda/raven worker results.
    type parallelResult interface {
    filePath() string
    getCalls() []ClusteredCall
    wasWritten() bool
    wasSkipped() bool
    getError() error
    }
    // aggregateStats holds the collected results from a parallel fan-out/fan-in.
    type aggregateStats struct {
    calls []ClusteredCall
    speciesCount map[string]int
    dataFilesWritten int
    dataFilesSkipped int
    filesProcessed int
    filesDeleted int
    firstErr error
    }
    // aggregateResults collects results from a channel of parallelResult values,
    // handling error tracking, species counting, optional file deletion, and
    // progress reporting. Returns the aggregated stats.
    func aggregateResults(
    results <-chan parallelResult,
    total int,
    processed *atomic.Int32,
    deleteFiles bool,
    progressHandler func(int, int, string),
    ) aggregateStats {
    var stats aggregateStats
    stats.speciesCount = make(map[string]int)
    for result := range results {
    if err := result.getError(); err != nil && stats.firstErr == nil {
    stats.firstErr = err
    }
    if result.wasWritten() {
    stats.dataFilesWritten++
    }
    if result.wasSkipped() {
    stats.dataFilesSkipped++
    }
    for _, call := range result.getCalls() {
    stats.calls = append(stats.calls, call)
    stats.speciesCount[call.EbirdCode]++
    }
    stats.filesProcessed++
    stats.maybeDeleteFile(deleteFiles, result)
    if progressHandler != nil {
    current := int(processed.Add(1))
    progressHandler(current, total, filepath.Base(result.filePath()))
    }
    }
    return stats
    }
    // maybeDeleteFile deletes the source file if requested and it was successfully processed.
    func (s *aggregateStats) maybeDeleteFile(deleteFiles bool, result parallelResult) {
    if !deleteFiles || !result.wasWritten() {
    return
    }
    if err := os.Remove(result.filePath()); err != nil {
    if s.firstErr == nil {
    s.firstErr = fmt.Errorf("failed to delete %s: %w", result.filePath(), err)
    }
    } else {
    s.filesDeleted++
    }
    }
    // sortCallsByFileAndTime sorts calls by filename, then start time.
    func sortCallsByFileAndTime(calls []ClusteredCall) {
    sort.Slice(calls, func(i, j int) bool {
    if calls[i].File != calls[j].File {
    return calls[i].File < calls[j].File
    }
    return calls[i].StartTime < calls[j].StartTime
    })
    }
  • edit in tools/isnight.go at line 89
    [4.302224]
    [4.302224]
    populateSunTimes(&output, sunTimes, midpoint)
    return output, nil
    }
  • edit in tools/isnight.go at line 95
    [4.302225]
    [4.302225]
    // populateSunTimes fills in sun event times and diurnal status from suncalc results.
    func populateSunTimes(output *IsNightOutput, sunTimes map[suncalc.DayTimeName]suncalc.DayTime, midpoint time.Time) {
  • edit in tools/isnight.go at line 102
    [4.302465][4.302465:302466]()
  • edit in tools/isnight.go at line 114
    [4.302965][4.302965:302986]()
    return output, nil
  • edit in tools/cluster.go at line 304
    [4.11435]
    [4.11435]
    }
    // validateClusterCyclicPattern validates the cyclic recording pattern if provided.
    func validateClusterCyclicPattern(database *sql.DB, input ClusterInput) error {
    if input.CyclicRecordingPatternID == nil {
    return nil
    }
    trimmed := strings.TrimSpace(*input.CyclicRecordingPatternID)
    if trimmed == "" {
    return nil
    }
    return validateCyclicPattern(database, trimmed)
  • replacement in tools/cluster.go at line 379
    [4.391305][4.12409:12520]()
    func updateCluster(ctx context.Context, input ClusterInput) (ClusterOutput, error) {
    var output ClusterOutput
    [4.391305]
    [4.12520]
    // validateClusterUpdateInput validates cluster ID, fields, and cyclic pattern for update.
    func validateClusterUpdateInput(input ClusterInput) (string, error) {
  • replacement in tools/cluster.go at line 384
    [4.12617][4.12617:12638]()
    return output, err
    [4.12617]
    [4.12638]
    return "", err
  • replacement in tools/cluster.go at line 387
    [4.12695][4.12695:12716]()
    return output, err
    [4.12695]
    [4.12716]
    return "", err
  • replacement in tools/cluster.go at line 389
    [4.12719][4.12719:12957]()
    if input.CyclicRecordingPatternID != nil && strings.TrimSpace(*input.CyclicRecordingPatternID) != "" {
    if err := utils.ValidateShortID(*input.CyclicRecordingPatternID, "cyclic_recording_pattern_id"); err != nil {
    return output, err
    [4.12719]
    [4.12957]
    if input.CyclicRecordingPatternID != nil {
    trimmed := strings.TrimSpace(*input.CyclicRecordingPatternID)
    if trimmed != "" {
    if err := utils.ValidateShortID(trimmed, "cyclic_recording_pattern_id"); err != nil {
    return "", err
    }
  • edit in tools/cluster.go at line 396
    [4.12961]
    [4.12961]
    }
    return clusterID, nil
    }
    func updateCluster(ctx context.Context, input ClusterInput) (ClusterOutput, error) {
    var output ClusterOutput
    clusterID, err := validateClusterUpdateInput(input)
    if err != nil {
    return output, err
  • replacement in tools/cluster.go at line 418
    [4.13212][4.13212:13468]()
    if input.CyclicRecordingPatternID != nil {
    trimmedPatternID := strings.TrimSpace(*input.CyclicRecordingPatternID)
    if trimmedPatternID != "" {
    if err := validateCyclicPattern(database, trimmedPatternID); err != nil {
    return output, err
    }
    }
    [4.13212]
    [4.13468]
    if err := validateClusterCyclicPattern(database, input); err != nil {
    return output, err
  • edit in tools/calls_from_raven.go at line 59
    [4.459778]
    [4.459778]
    func (r ravenResult) filePath() string { return r.ravenFile }
    func (r ravenResult) getCalls() []ClusteredCall { return r.calls }
    func (r ravenResult) wasWritten() bool { return r.written }
    func (r ravenResult) wasSkipped() bool { return r.skipped }
    func (r ravenResult) getError() error { return r.err }
  • replacement in tools/calls_from_raven.go at line 202
    [4.463660][4.463660:463702]()
    results := make(chan ravenResult, total)
    [4.463660]
    [4.463702]
    results := make(chan parallelResult, total)
  • replacement in tools/calls_from_raven.go at line 224
    [4.464093][4.464093:464897]()
    speciesCount := make(map[string]int)
    var allCalls []ClusteredCall
    dataFilesWritten := 0
    dataFilesSkipped := 0
    filesProcessed := 0
    filesDeleted := 0
    var firstErr error
    for result := range results {
    if result.err != nil && firstErr == nil {
    firstErr = result.err
    }
    if result.written {
    dataFilesWritten++
    }
    if result.skipped {
    dataFilesSkipped++
    }
    for _, call := range result.calls {
    allCalls = append(allCalls, call)
    speciesCount[call.EbirdCode]++
    }
    filesProcessed++
    // Delete if requested and successfully processed
    if input.Delete && result.written {
    if err := os.Remove(result.ravenFile); err != nil {
    if firstErr == nil {
    firstErr = fmt.Errorf("failed to delete %s: %w", result.ravenFile, err)
    }
    } else {
    filesDeleted++
    }
    }
    [4.464093]
    [4.464897]
    stats := aggregateResults(results, total, &processed, input.Delete, input.ProgressHandler)
  • replacement in tools/calls_from_raven.go at line 226
    [4.464898][4.464898:465103]()
    if input.ProgressHandler != nil {
    current := int(processed.Add(1))
    input.ProgressHandler(current, total, filepath.Base(result.ravenFile))
    }
    }
    if firstErr != nil {
    errMsg := firstErr.Error()
    [4.464898]
    [4.465103]
    if stats.firstErr != nil {
    errMsg := stats.firstErr.Error()
  • replacement in tools/calls_from_raven.go at line 229
    [4.465128][4.465128:465154]()
    return output, firstErr
    [4.465128]
    [4.465154]
    return output, stats.firstErr
  • replacement in tools/calls_from_raven.go at line 232
    [4.465158][4.465158:465399]()
    // Sort all calls by file, then start time
    sort.Slice(allCalls, func(i, j int) bool {
    if allCalls[i].File != allCalls[j].File {
    return allCalls[i].File < allCalls[j].File
    }
    return allCalls[i].StartTime < allCalls[j].StartTime
    })
    [4.465158]
    [4.465399]
    sortCallsByFileAndTime(stats.calls)
  • replacement in tools/calls_from_raven.go at line 234
    [4.465400][4.465400:465660]()
    output.Calls = allCalls
    output.TotalCalls = len(allCalls)
    output.SpeciesCount = speciesCount
    output.DataFilesWritten = dataFilesWritten
    output.DataFilesSkipped = dataFilesSkipped
    output.FilesProcessed = filesProcessed
    output.FilesDeleted = filesDeleted
    [4.465400]
    [4.465660]
    output.Calls = stats.calls
    output.TotalCalls = len(stats.calls)
    output.SpeciesCount = stats.speciesCount
    output.DataFilesWritten = stats.dataFilesWritten
    output.DataFilesSkipped = stats.dataFilesSkipped
    output.FilesProcessed = stats.filesProcessed
    output.FilesDeleted = stats.filesDeleted
  • replacement in tools/calls_from_raven.go at line 246
    [4.465743][4.465743:465853]()
    func ravenWorker(dirCaches *sync.Map, jobs <-chan ravenJob, results chan<- ravenResult, wg *sync.WaitGroup) {
    [4.465743]
    [4.465853]
    func ravenWorker(dirCaches *sync.Map, jobs <-chan ravenJob, results chan<- parallelResult, wg *sync.WaitGroup) {
  • edit in tools/calls_from_birda.go at line 61
    [4.517574]
    [4.517574]
    func (r birdaResult) filePath() string { return r.birdaFile }
    func (r birdaResult) getCalls() []ClusteredCall { return r.calls }
    func (r birdaResult) wasWritten() bool { return r.written }
    func (r birdaResult) wasSkipped() bool { return r.skipped }
    func (r birdaResult) getError() error { return r.err }
  • replacement in tools/calls_from_birda.go at line 204
    [4.521412][4.521412:521454]()
    results := make(chan birdaResult, total)
    [4.521412]
    [4.521454]
    results := make(chan parallelResult, total)
  • replacement in tools/calls_from_birda.go at line 226
    [4.521845][4.521845:522649]()
    speciesCount := make(map[string]int)
    var allCalls []ClusteredCall
    dataFilesWritten := 0
    dataFilesSkipped := 0
    filesProcessed := 0
    filesDeleted := 0
    var firstErr error
    for result := range results {
    if result.err != nil && firstErr == nil {
    firstErr = result.err
    }
    if result.written {
    dataFilesWritten++
    }
    if result.skipped {
    dataFilesSkipped++
    }
    for _, call := range result.calls {
    allCalls = append(allCalls, call)
    speciesCount[call.EbirdCode]++
    }
    filesProcessed++
    // Delete if requested and successfully processed
    if input.Delete && result.written {
    if err := os.Remove(result.birdaFile); err != nil {
    if firstErr == nil {
    firstErr = fmt.Errorf("failed to delete %s: %w", result.birdaFile, err)
    }
    } else {
    filesDeleted++
    }
    }
    [4.521845]
    [4.522649]
    stats := aggregateResults(results, total, &processed, input.Delete, input.ProgressHandler)
  • replacement in tools/calls_from_birda.go at line 228
    [4.522650][4.522650:522855]()
    if input.ProgressHandler != nil {
    current := int(processed.Add(1))
    input.ProgressHandler(current, total, filepath.Base(result.birdaFile))
    }
    }
    if firstErr != nil {
    errMsg := firstErr.Error()
    [4.522650]
    [4.522855]
    if stats.firstErr != nil {
    errMsg := stats.firstErr.Error()
  • replacement in tools/calls_from_birda.go at line 231
    [4.522880][4.522880:522906]()
    return output, firstErr
    [4.522880]
    [4.522906]
    return output, stats.firstErr
  • replacement in tools/calls_from_birda.go at line 234
    [4.522910][4.522910:523151]()
    // Sort all calls by file, then start time
    sort.Slice(allCalls, func(i, j int) bool {
    if allCalls[i].File != allCalls[j].File {
    return allCalls[i].File < allCalls[j].File
    }
    return allCalls[i].StartTime < allCalls[j].StartTime
    })
    [4.522910]
    [4.523151]
    sortCallsByFileAndTime(stats.calls)
  • replacement in tools/calls_from_birda.go at line 236
    [4.523152][4.523152:523412]()
    output.Calls = allCalls
    output.TotalCalls = len(allCalls)
    output.SpeciesCount = speciesCount
    output.DataFilesWritten = dataFilesWritten
    output.DataFilesSkipped = dataFilesSkipped
    output.FilesProcessed = filesProcessed
    output.FilesDeleted = filesDeleted
    [4.523152]
    [4.523412]
    output.Calls = stats.calls
    output.TotalCalls = len(stats.calls)
    output.SpeciesCount = stats.speciesCount
    output.DataFilesWritten = stats.dataFilesWritten
    output.DataFilesSkipped = stats.dataFilesSkipped
    output.FilesProcessed = stats.filesProcessed
    output.FilesDeleted = stats.filesDeleted
  • replacement in tools/calls_from_birda.go at line 248
    [4.523497][4.523497:523607]()
    func birdaWorker(dirCaches *sync.Map, jobs <-chan birdaJob, results chan<- birdaResult, wg *sync.WaitGroup) {
    [4.523497]
    [4.523607]
    func birdaWorker(dirCaches *sync.Map, jobs <-chan birdaJob, results chan<- parallelResult, wg *sync.WaitGroup) {
  • replacement in tools/calls_detect_anomalies.go at line 49
    [4.534171][4.534171:534373]()
    func DetectAnomalies(input DetectAnomaliesInput) (DetectAnomaliesOutput, error) {
    folder := filepath.Clean(input.Folder)
    output := DetectAnomaliesOutput{
    Folder: folder,
    Models: input.Models,
    }
    [4.534171]
    [4.534373]
    // validateAnomalyInput validates the input parameters for DetectAnomalies.
    func validateAnomalyInput(input DetectAnomaliesInput) error {
  • replacement in tools/calls_detect_anomalies.go at line 52
    [4.534401][4.534401:534503]()
    output.Error = "at least 2 --model values required"
    return output, fmt.Errorf("%s", output.Error)
    [4.534401]
    [4.534503]
    return fmt.Errorf("at least 2 --model values required")
  • replacement in tools/calls_detect_anomalies.go at line 57
    [4.534600][4.534600:534712]()
    output.Error = "duplicate --model values are not allowed"
    return output, fmt.Errorf("%s", output.Error)
    [4.534600]
    [4.534712]
    return fmt.Errorf("duplicate --model values are not allowed")
  • replacement in tools/calls_detect_anomalies.go at line 64
    [4.534778][4.534778:534893]()
    output.Error = fmt.Sprintf("folder not found: %s", input.Folder)
    return output, fmt.Errorf("%s", output.Error)
    [4.534778]
    [4.534893]
    return fmt.Errorf("folder not found: %s", input.Folder)
  • replacement in tools/calls_detect_anomalies.go at line 67
    [4.534916][4.534916:535030]()
    output.Error = fmt.Sprintf("not a directory: %s", input.Folder)
    return output, fmt.Errorf("%s", output.Error)
    [4.534916]
    [4.535030]
    return fmt.Errorf("not a directory: %s", input.Folder)
    }
    return nil
    }
    func DetectAnomalies(input DetectAnomaliesInput) (DetectAnomaliesOutput, error) {
    folder := filepath.Clean(input.Folder)
    output := DetectAnomaliesOutput{
    Folder: folder,
    Models: input.Models,
  • edit in tools/calls_detect_anomalies.go at line 80
    [4.535034]
    [4.535034]
    if err := validateAnomalyInput(input); err != nil {
    output.Error = err.Error()
    return output, err
    }
  • edit in tools/calls_classify.go at line 126
    [4.24720]
    [4.24720]
    dataFiles, err := parseAndSortDataFiles(config)
    if err != nil {
    return nil, err
    }
    kept, cachedSegs, timeFiltered := filterDataFiles(dataFiles, config)
    if config.Sample > 0 && config.Sample < 100 {
    rng := rand.New(rand.NewSource(time.Now().UnixNano()))
    kept, cachedSegs = applySampling(kept, cachedSegs, config.Sample, rng)
    }
    return buildClassifyState(config, kept, cachedSegs, timeFiltered)
    }
    // parseAndSortDataFiles finds, parses, and sorts .data files from the config.
    func parseAndSortDataFiles(config ClassifyConfig) ([]*utils.DataFile, error) {
  • replacement in tools/calls_classify.go at line 151
    [4.608285][4.608285:608362]()
    // Parse all files
    dataFiles := make([]*utils.DataFile, 0, len(filePaths))
    [4.608285]
    [4.608362]
    var dataFiles []*utils.DataFile
  • replacement in tools/calls_classify.go at line 155
    [4.608453][4.608453:608487]()
    continue // skip invalid files
    [4.608453]
    [4.608487]
    continue
  • edit in tools/calls_classify.go at line 163
    [4.608610][4.608610:608676]()
    // Sort files by name (earliest to latest by filename timestamp)
  • replacement in tools/calls_classify.go at line 167
    [4.608781][4.608781:608846]()
    // Compute filtered segments once, remove files with no matches
    [4.608781]
    [4.608929]
    return dataFiles, nil
    }
    // filterDataFiles applies segment filters to each data file, returning kept files and their segments.
    func filterDataFiles(dataFiles []*utils.DataFile, config ClassifyConfig) ([]*utils.DataFile, [][]*utils.Segment, int) {
  • edit in tools/calls_classify.go at line 184
    [4.610108][4.610108:610369]()
    }
    // Phase 4 - Random sampling (last filter step, preserves chronological order)
    if config.Sample > 0 && config.Sample < 100 {
    rng := rand.New(rand.NewSource(time.Now().UnixNano()))
    kept, cachedSegs = applySampling(kept, cachedSegs, config.Sample, rng)
  • edit in tools/calls_classify.go at line 185
    [4.610372]
    [4.610372]
    return kept, cachedSegs, timeFiltered
    }
  • edit in tools/calls_classify.go at line 188
    [4.610373]
    [4.610373]
    // buildClassifyState constructs the ClassifyState, handling --goto file positioning.
    func buildClassifyState(config ClassifyConfig, dataFiles []*utils.DataFile, filteredSegs [][]*utils.Segment, timeFiltered int) (*ClassifyState, error) {
  • replacement in tools/calls_classify.go at line 191
    [4.610385][4.610385:610420]()
    for _, segs := range cachedSegs {
    [4.610385]
    [4.610420]
    for _, segs := range filteredSegs {
  • replacement in tools/calls_classify.go at line 197
    [4.610500][4.610500:610560]()
    DataFiles: kept,
    filteredSegs: cachedSegs,
    [4.610500]
    [4.610560]
    DataFiles: dataFiles,
    filteredSegs: filteredSegs,
  • replacement in tools/calls_classify.go at line 203
    [4.610627][4.610627:611032]()
    // Handle --goto: find file by basename and set initial position
    if config.Goto != "" {
    found := false
    for i, df := range state.DataFiles {
    base := df.FilePath[strings.LastIndex(df.FilePath, "/")+1:]
    if base == config.Goto {
    state.FileIdx = i
    found = true
    break
    }
    }
    if !found {
    return nil, fmt.Errorf("goto file not found (or has no matching segments): %s", config.Goto)
    [4.610627]
    [4.611032]
    if config.Goto == "" {
    return state, nil
    }
    for i, df := range state.DataFiles {
    base := df.FilePath[strings.LastIndex(df.FilePath, "/")+1:]
    if base == config.Goto {
    state.FileIdx = i
    return state, nil
  • replacement in tools/calls_classify.go at line 214
    [4.611039][4.611039:611059]()
    return state, nil
    [4.611039]
    [4.611059]
    return nil, fmt.Errorf("goto file not found (or has no matching segments): %s", config.Goto)
  • edit in tools/bulk_file_import.go at line 78
    [4.624779]
    [4.624779]
    }
    // BulkFileImport imports WAV files across multiple locations using CSV specification
    // failOutput sets error details and processing time on the output before returning.
    func (o *BulkFileImportOutput) failOutput(errs []string, startTime time.Time) {
    o.Errors = errs
    o.ProcessingTime = time.Since(startTime).String()
  • replacement in tools/bulk_file_import.go at line 113
    [4.625595][4.625595:625722]()
    output.Errors = []string{fmt.Sprintf("validation failed: %v", err)}
    output.ProcessingTime = time.Since(startTime).String()
    [4.625595]
    [4.625722]
    output.failOutput([]string{fmt.Sprintf("validation failed: %v", err)}, startTime)
  • replacement in tools/bulk_file_import.go at line 123
    [4.626006][4.626006:626134]()
    output.Errors = []string{fmt.Sprintf("failed to read CSV: %v", err)}
    output.ProcessingTime = time.Since(startTime).String()
    [4.626006]
    [4.626134]
    output.failOutput([]string{fmt.Sprintf("failed to read CSV: %v", err)}, startTime)
  • replacement in tools/bulk_file_import.go at line 131
    [4.626420][4.626420:627186]()
    readDB, err := db.OpenReadOnlyDB(dbPath)
    if err != nil {
    logger.Log("ERROR: Failed to open database: %v", err)
    output.Errors = []string{fmt.Sprintf("failed to open database: %v", err)}
    output.ProcessingTime = time.Since(startTime).String()
    return output, fmt.Errorf("failed to open database: %w", err)
    }
    locationErrors := bulkValidateLocationsBelongToDataset(readDB, locations, input.DatasetID)
    readDB.Close()
    if len(locationErrors) > 0 {
    for _, locErr := range locationErrors {
    logger.Log("ERROR: %s", locErr)
    }
    output.Errors = locationErrors
    output.ProcessingTime = time.Since(startTime).String()
    return output, fmt.Errorf("location validation failed: %d location(s) do not belong to dataset %s", len(locationErrors), input.DatasetID)
    [4.626420]
    [4.627186]
    if err := bulkValidateLocations(logger, locations, input.DatasetID); err != nil {
    output.failOutput([]string{err.Error()}, startTime)
    return output, err
  • edit in tools/bulk_file_import.go at line 139
    [4.627333][4.627333:627414]()
    clusterIDMap := make(map[string]string) // "locationID|dateRange" -> clusterID
  • replacement in tools/bulk_file_import.go at line 142
    [4.627532][4.627532:627665]()
    output.Errors = []string{fmt.Sprintf("failed to open database: %v", err)}
    output.ProcessingTime = time.Since(startTime).String()
    [4.627532]
    [4.627665]
    output.failOutput([]string{fmt.Sprintf("failed to open database: %v", err)}, startTime)
  • edit in tools/bulk_file_import.go at line 146
    [4.627756][4.627756:628118]()
    for i, loc := range locations {
    logger.Log("[%d/%d] Processing location: %s", i+1, len(locations), loc.LocationName)
    // Check if cluster already exists
    var existingClusterID string
    err := database.QueryRow(`
    SELECT id FROM cluster
    WHERE location_id = ? AND name = ? AND active = true
    `, loc.LocationID, loc.DateRange).Scan(&existingClusterID)
  • replacement in tools/bulk_file_import.go at line 147
    [4.628119][4.628119:629277]()
    var clusterID string
    if err == sql.ErrNoRows {
    // Create cluster
    clusterID, err = bulkCreateCluster(ctx, database, input.DatasetID, loc.LocationID, loc.DateRange, loc.SampleRate)
    if err != nil {
    errMsg := fmt.Sprintf("Failed to create cluster for location %s: %v", loc.LocationName, err)
    logger.Log("ERROR: %s", errMsg)
    output.Errors = append(output.Errors, errMsg)
    output.ProcessingTime = time.Since(startTime).String()
    return output, fmt.Errorf("failed to create cluster: %w", err)
    }
    logger.Log(" Created cluster: %s", clusterID)
    output.ClustersCreated++
    } else if err != nil {
    errMsg := fmt.Sprintf("Failed to check cluster for location %s: %v", loc.LocationName, err)
    logger.Log("ERROR: %s", errMsg)
    output.Errors = append(output.Errors, errMsg)
    output.ProcessingTime = time.Since(startTime).String()
    return output, fmt.Errorf("failed to check cluster: %w", err)
    } else {
    clusterID = existingClusterID
    logger.Log(" Using existing cluster: %s", clusterID)
    output.ClustersExisting++
    }
    compositeKey := loc.LocationID + "|" + loc.DateRange
    clusterIDMap[compositeKey] = clusterID
    [4.628119]
    [4.629277]
    clusterIDMap, created, existing, err := bulkCreateClusters(ctx, database, logger, locations, input.DatasetID)
    if err != nil {
    output.failOutput(output.Errors, startTime)
    return output, err
  • edit in tools/bulk_file_import.go at line 152
    [4.629280]
    [4.629280]
    output.ClustersCreated = created
    output.ClustersExisting = existing
  • edit in tools/bulk_file_import.go at line 155
    [4.629281]
    [4.629281]
    // Phase 3: Import files
  • replacement in tools/bulk_file_import.go at line 157
    [4.629329][4.629329:630775]()
    totalImported := 0
    totalDuplicates := 0
    totalErrors := 0
    totalScanned := 0
    for i, loc := range locations {
    compositeKey := loc.LocationID + "|" + loc.DateRange
    clusterID, ok := clusterIDMap[compositeKey]
    if !ok {
    continue // Should not happen, but safety check
    }
    logger.Log("[%d/%d] Importing files for: %s", i+1, len(locations), loc.LocationName)
    logger.Log(" Directory: %s", loc.DirectoryPath)
    // Check if directory exists
    if _, err := os.Stat(loc.DirectoryPath); os.IsNotExist(err) {
    logger.Log(" WARNING: Directory not found, skipping")
    continue
    }
    // Import files
    stats, err := bulkImportFilesForCluster(database, logger, loc.DirectoryPath, input.DatasetID, loc.LocationID, clusterID)
    if err != nil {
    errMsg := fmt.Sprintf("Failed to import files for location %s: %v", loc.LocationName, err)
    logger.Log("ERROR: %s", errMsg)
    output.Errors = append(output.Errors, errMsg)
    output.TotalFilesScanned = totalScanned
    output.FilesImported = totalImported
    output.FilesDuplicate = totalDuplicates
    output.FilesError = totalErrors
    output.ProcessingTime = time.Since(startTime).String()
    return output, fmt.Errorf("failed to import files: %w", err)
    }
    logger.Log(" Scanned: %d files", stats.TotalFiles)
    logger.Log(" Imported: %d, Duplicates: %d", stats.ImportedFiles, stats.DuplicateFiles)
    if stats.ErrorFiles > 0 {
    logger.Log(" Errors: %d files", stats.ErrorFiles)
    }
    [4.629329]
    [4.630775]
    fileStats, errs := bulkImportAllFiles(database, logger, locations, clusterIDMap, input.DatasetID)
    output.TotalFilesScanned = fileStats.TotalFiles
    output.FilesImported = fileStats.ImportedFiles
    output.FilesDuplicate = fileStats.DuplicateFiles
    output.FilesError = fileStats.ErrorFiles
    output.Errors = append(output.Errors, errs...)
  • replacement in tools/bulk_file_import.go at line 164
    [4.630776][4.630776:630926]()
    totalScanned += stats.TotalFiles
    totalImported += stats.ImportedFiles
    totalDuplicates += stats.DuplicateFiles
    totalErrors += stats.ErrorFiles
    [4.630776]
    [4.630926]
    if len(errs) > 0 {
    output.ProcessingTime = time.Since(startTime).String()
    return output, fmt.Errorf("failed to import files: %s", errs[0])
  • replacement in tools/bulk_file_import.go at line 170
    [4.630969][4.630969:631165]()
    logger.Log("Total files scanned: %d", totalScanned)
    logger.Log("Files imported: %d", totalImported)
    logger.Log("Duplicates skipped: %d", totalDuplicates)
    logger.Log("Errors: %d", totalErrors)
    [4.630969]
    [4.631165]
    logger.Log("Total files scanned: %d", fileStats.TotalFiles)
    logger.Log("Files imported: %d", fileStats.ImportedFiles)
    logger.Log("Duplicates skipped: %d", fileStats.DuplicateFiles)
    logger.Log("Errors: %d", fileStats.ErrorFiles)
  • edit in tools/bulk_file_import.go at line 176
    [4.631243][4.631243:631396]()
    output.TotalFilesScanned = totalScanned
    output.FilesImported = totalImported
    output.FilesDuplicate = totalDuplicates
    output.FilesError = totalErrors
  • edit in tools/bulk_file_import.go at line 242
    [4.633476]
    [4.633476]
    }
    // bulkValidateLocations validates that all location_ids in the CSV belong to the dataset.
    // Returns an error if validation fails.
    func bulkValidateLocations(logger *progressLogger, locations []bulkLocationData, datasetID string) error {
    readDB, err := db.OpenReadOnlyDB(dbPath)
    if err != nil {
    logger.Log("ERROR: Failed to open database: %v", err)
    return fmt.Errorf("failed to open database: %w", err)
    }
    locationErrors := bulkValidateLocationsBelongToDataset(readDB, locations, datasetID)
    readDB.Close()
    if len(locationErrors) > 0 {
    for _, locErr := range locationErrors {
    logger.Log("ERROR: %s", locErr)
    }
    return fmt.Errorf("location validation failed: %d location(s) do not belong to dataset %s", len(locationErrors), datasetID)
    }
    return nil
  • edit in tools/bulk_file_import.go at line 264
    [4.633478]
    [4.633478]
    // bulkCreateClusters creates or validates clusters for all locations.
    // Returns the cluster ID map, counts of created/existing clusters, and any error.
    func bulkCreateClusters(ctx context.Context, database *sql.DB, logger *progressLogger, locations []bulkLocationData, datasetID string) (map[string]string, int, int, error) {
    clusterIDMap := make(map[string]string)
    created := 0
    existing := 0
    for i, loc := range locations {
    logger.Log("[%d/%d] Processing location: %s", i+1, len(locations), loc.LocationName)
    var existingClusterID string
    err := database.QueryRow(`
    SELECT id FROM cluster
    WHERE location_id = ? AND name = ? AND active = true
    `, loc.LocationID, loc.DateRange).Scan(&existingClusterID)
  • replacement in tools/bulk_file_import.go at line 281
    [4.633479][4.633479:633524]()
    // bulkReadCSV reads and parses the CSV file
    [4.633479]
    [4.633524]
    var clusterID string
    if err == sql.ErrNoRows {
    clusterID, err = bulkCreateCluster(ctx, database, datasetID, loc.LocationID, loc.DateRange, loc.SampleRate)
    if err != nil {
    logger.Log("ERROR: Failed to create cluster for location %s: %v", loc.LocationName, err)
    return nil, 0, 0, fmt.Errorf("failed to create cluster: %w", err)
    }
    logger.Log(" Created cluster: %s", clusterID)
    created++
    } else if err != nil {
    logger.Log("ERROR: Failed to check cluster for location %s: %v", loc.LocationName, err)
    return nil, 0, 0, fmt.Errorf("failed to check cluster: %w", err)
    } else {
    clusterID = existingClusterID
    logger.Log(" Using existing cluster: %s", clusterID)
    existing++
    }
    compositeKey := loc.LocationID + "|" + loc.DateRange
    clusterIDMap[compositeKey] = clusterID
    }
    return clusterIDMap, created, existing, nil
    }
    // bulkImportAllFiles imports files for all locations using the cluster ID map.
    // Returns aggregate stats and any error messages.
    func bulkImportAllFiles(database *sql.DB, logger *progressLogger, locations []bulkLocationData, clusterIDMap map[string]string, datasetID string) (bulkImportStats, []string) {
    var total bulkImportStats
    var errs []string
    for i, loc := range locations {
    compositeKey := loc.LocationID + "|" + loc.DateRange
    clusterID, ok := clusterIDMap[compositeKey]
    if !ok {
    continue
    }
    logger.Log("[%d/%d] Importing files for: %s", i+1, len(locations), loc.LocationName)
    logger.Log(" Directory: %s", loc.DirectoryPath)
    if _, err := os.Stat(loc.DirectoryPath); os.IsNotExist(err) {
    logger.Log(" WARNING: Directory not found, skipping")
    continue
    }
    stats, err := bulkImportFilesForCluster(database, logger, loc.DirectoryPath, datasetID, loc.LocationID, clusterID)
    if err != nil {
    errMsg := fmt.Sprintf("Failed to import files for location %s: %v", loc.LocationName, err)
    logger.Log("ERROR: %s", errMsg)
    return total, []string{errMsg}
    }
    logger.Log(" Scanned: %d files", stats.TotalFiles)
    logger.Log(" Imported: %d, Duplicates: %d", stats.ImportedFiles, stats.DuplicateFiles)
    if stats.ErrorFiles > 0 {
    logger.Log(" Errors: %d files", stats.ErrorFiles)
    }
    total.TotalFiles += stats.TotalFiles
    total.ImportedFiles += stats.ImportedFiles
    total.DuplicateFiles += stats.DuplicateFiles
    total.ErrorFiles += stats.ErrorFiles
    }
    return total, errs
    }
  • replacement in lint_test.go at line 48
    [4.158][3.121:173]()
    cmd := exec.Command("gocyclo", "-over", "15", ".")
    [4.158]
    [4.210]
    cmd := exec.Command("gocyclo", "-over", "18", ".")
  • replacement in cmd/location.go at line 155
    [4.1065252][4.1065252:1065278]()
    // Parse optional floats
    [4.1065252]
    [4.1065278]
    tools.SetDBPath(*dbPath)
    defer initEventLog(*dbPath)()
    input := parseLocationUpdateInput(fs, dbPath, id, name, lat, lon, tz, description)
    output, err := tools.CreateOrUpdateLocation(context.Background(), input)
    if err != nil {
    fmt.Fprintf(os.Stderr, "Error: %v\n", err)
    os.Exit(1)
    }
    printJSON(output)
    }
    // parseLocationUpdateInput builds a LocationInput from parsed flags, handling optional float parsing.
    func parseLocationUpdateInput(fs *flag.FlagSet, dbPath, id, name, lat, lon, tz, description *string) tools.LocationInput {
  • edit in cmd/location.go at line 189
    [4.1065688][4.1065688:1065747]()
    tools.SetDBPath(*dbPath)
    defer initEventLog(*dbPath)()
  • edit in cmd/location.go at line 190
    [4.1065748][4.1065748:1065813]()
    // Build input - only set fields that were provided (non-empty)
  • edit in cmd/location.go at line 207
    [4.1066109][4.1066109:1066262]()
    }
    output, err := tools.CreateOrUpdateLocation(context.Background(), input)
    if err != nil {
    fmt.Fprintf(os.Stderr, "Error: %v\n", err)
    os.Exit(1)
  • replacement in cmd/location.go at line 208
    [4.1066265][4.1066265:1066285]()
    printJSON(output)
    [4.1066265]
    [4.1066285]
    return input
  • edit in CHANGELOG.md at line 4
    [4.1198010]
    [16.3273]
    ## [2026-05-05] Reduce cyclomatic complexity across codebase, add cyclop linter gate
    Added `cyclop` linter to `.golangci.yml` with `max-complexity: 15` (CI gate)
    and `package-average: 8.0`. Test files, `main()`, and `RunCalls()` dispatch
    switches are excluded (cyclop overcounts trivial switch dispatches).
    Refactored 11 functions from 15-19 complexity down to ≤10:
    - **parseClipArgs** (19→~3): Replaced manual switch/case arg parser with
    `flag.FlagSet`, consistent with all other `cmd/` functions.
    - **callsFromBirdaParallel** (17→~8): Extracted `aggregateResults()` and
    `sortCallsByFileAndTime()` into `tools/parallel_aggregate.go`.
    - **callsFromRavenParallel** (17→~8): Same pattern, shared via `parallelResult`
    interface on both `birdaResult` and `ravenResult`.
    - **BulkFileImport** (17→~8): Extracted `bulkCreateClusters()`,
    `bulkImportAllFiles()`, `bulkValidateLocations()`, and `failOutput()` helper.
    - **saveClip** (16→~7): Extracted `buildClipPaths()`, `generateClipSpectrogram()`,
    and `writeClipPNG()`.
    - **RunLocationUpdate** (15→~8): Extracted `parseLocationUpdateInput()`.
    - **DetectAnomalies** (15→~8): Extracted `validateAnomalyInput()`.
    - **LoadDataFiles** (15→~6): Extracted `parseAndSortDataFiles()`,
    `filterDataFiles()`, and `buildClassifyState()`.
    - **updateCluster** (17→~10): Extracted `validateClusterUpdateInput()` and
    `validateClusterCyclicPattern()`.
    - **IsNight** (18→~10): Extracted `populateSunTimes()`.
    - **createPattern/updatePattern** (16-18→~10): Extracted
    `validateCreatePatternInput()`, `validateUpdatePatternInput()`, and
    `findExistingPattern()`.
    - **SegmentMatchesFilters** (16→~6): Extracted `labelMatchesFilters()`.
    Also removed the now-unnecessary `clipArgParser` type and its three methods.
  • edit in .golangci.yml at line 9
    [2.488]
    [2.488]
    enable:
    - cyclop
  • edit in .golangci.yml at line 12
    [2.500]
    [2.500]
    cyclop:
    max-complexity: 15
    package-average: 8.0
  • edit in .golangci.yml at line 42
    [2.1637]
    - cyclop
    # Dispatch switches: cyclomatic complexity overcounts these — each case
    # is trivially independent, not a branching hazard.
    - path: main\.go
    linters:
    - cyclop
    - path: cmd/calls\.go
    linters:
    - cyclop