cyclo to 14 now
Dependencies
- [2]
2HAQZPV3more refactoring with glm - [3]
VYNOHQJWtidied up CLAUDE.md - [4]
HYCZTLSZfixed tests with cyclo over 15 - [5]
SMWSHUOWcyclo over 15 - [6]
JAT3DXOLcyclo over 15 - [7]
NS4TDPLNcyclomatic complexity - [8]
GPQSOVBPcyclo complexity over 25 - [9]
BZ6KQRYDadded complexity lint test - [10]
LQLC7S3Atrying gemini: Inconsistent Standards in @utils/ refactoring - [11]
LBWQJEDHminor refactor and more tests for utils/ - [12]
KZKLAINJrun out of space on nest, cleaned out - [13]
QVIGQOQZmore work on utils/ with glm
Change contents
- replacement in utils/wav_metadata.go at line 404
// ReadWAVSegmentSamples reads a specific time range of audio samples from a WAV file.// If startSec < 0, it starts from 0.// If endSec <= 0 or endSec > duration, it reads to the end.func ReadWAVSegmentSamples(filepath string, startSec, endSec float64) ([]float64, int, error) {// parseWAVInfo opens a WAV file, validates its header, and parses chunks.// Returns the parsed chunk info and the open file (caller must close).func parseWAVInfo(filepath string) (*os.File, wavChunkInfo, error) { - replacement in utils/wav_metadata.go at line 409
return nil, 0, fmt.Errorf("failed to open file: %w", err)return nil, wavChunkInfo{}, fmt.Errorf("failed to open file: %w", err) - edit in utils/wav_metadata.go at line 411
defer func() { _ = file.Close() }() - replacement in utils/wav_metadata.go at line 414
return nil, 0, fmt.Errorf("failed to read header: %w", err)file.Close()return nil, wavChunkInfo{}, fmt.Errorf("failed to read header: %w", err) - replacement in utils/wav_metadata.go at line 418
return nil, 0, fmt.Errorf("not a valid WAV file")file.Close()return nil, wavChunkInfo{}, fmt.Errorf("not a valid WAV file") - replacement in utils/wav_metadata.go at line 423
return nil, 0, fmt.Errorf("failed to seek: %w", err)file.Close()return nil, wavChunkInfo{}, fmt.Errorf("failed to seek: %w", err) - replacement in utils/wav_metadata.go at line 429
return nil, 0, errfile.Close()return nil, wavChunkInfo{}, err - replacement in utils/wav_metadata.go at line 433
return nil, 0, fmt.Errorf("missing or invalid fmt chunk")file.Close()return nil, wavChunkInfo{}, fmt.Errorf("missing or invalid fmt chunk") - replacement in utils/wav_metadata.go at line 437
startOffset, readSize := calcWAVReadRange(startSec, endSec, info)return file, info, nil}// readAudioSegment reads audio bytes from an already-parsed WAV file.func readAudioSegment(file *os.File, info wavChunkInfo, startOffset, readSize int64) ([]byte, error) { - replacement in utils/wav_metadata.go at line 443
return []float64{}, info.sampleRate, nilreturn nil, nil - replacement in utils/wav_metadata.go at line 447
return nil, 0, fmt.Errorf("failed to seek to data segment: %w", err)return nil, fmt.Errorf("failed to seek to data segment: %w", err) - replacement in utils/wav_metadata.go at line 453
return nil, 0, fmt.Errorf("failed to read audio data: %w", err)return nil, fmt.Errorf("failed to read audio data: %w", err) - edit in utils/wav_metadata.go at line 456
return audioData, nil} - edit in utils/wav_metadata.go at line 459
// ReadWAVSegmentSamples reads a specific time range of audio samples from a WAV file.// If startSec < 0, it starts from 0.// If endSec <= 0 or endSec > duration, it reads to the end.func ReadWAVSegmentSamples(filepath string, startSec, endSec float64) ([]float64, int, error) {file, info, err := parseWAVInfo(filepath)if err != nil {return nil, 0, err}defer func() { _ = file.Close() }()startOffset, readSize := calcWAVReadRange(startSec, endSec, info)audioData, err := readAudioSegment(file, info, startOffset, readSize)if err != nil {return nil, 0, err}if readSize == 0 {return []float64{}, info.sampleRate, nil} - replacement in utils/terminal_image.go at line 148
// ResizeImage resizes an image using nearest-neighbor interpolation.// For higher quality, use golang.org/x/image/draw, but this keeps dependencies minimal.func ResizeImage(img image.Image, newWidth, newHeight int) image.Image {// resizeScale holds precomputed scale factors for nearest-neighbor resizing.type resizeScale struct {srcWidth, srcHeight intscaleX, scaleY float64}func newResizeScale(img image.Image, newWidth, newHeight int) resizeScale { - replacement in utils/terminal_image.go at line 156
srcWidth := bounds.Dx()srcHeight := bounds.Dy()return resizeScale{srcWidth: bounds.Dx(),srcHeight: bounds.Dy(),scaleX: float64(bounds.Dx()) / float64(newWidth),scaleY: float64(bounds.Dy()) / float64(newHeight),}} - replacement in utils/terminal_image.go at line 164
scaleX := float64(srcWidth) / float64(newWidth)scaleY := float64(srcHeight) / float64(newHeight)// srcCoord maps a destination pixel coordinate to source coordinate, clamped to bounds.func (s resizeScale) srcCoord(dstX, dstY int) (srcX, srcY int) {srcX = int(float64(dstX) * s.scaleX)srcY = int(float64(dstY) * s.scaleY)if srcX >= s.srcWidth {srcX = s.srcWidth - 1}if srcY >= s.srcHeight {srcY = s.srcHeight - 1}return} - replacement in utils/terminal_image.go at line 177
if srcGray, ok := img.(*image.Gray); ok {result := image.NewGray(image.Rect(0, 0, newWidth, newHeight))for y := range newHeight {srcY := int(float64(y) * scaleY)if srcY >= srcHeight {srcY = srcHeight - 1}dstOff := y * result.StridesrcRowOff := srcY * srcGray.Stridefor x := range newWidth {srcX := int(float64(x) * scaleX)if srcX >= srcWidth {srcX = srcWidth - 1}result.Pix[dstOff+x] = srcGray.Pix[srcRowOff+srcX]}// resizeGray resizes a Gray image using nearest-neighbor interpolation.func resizeGray(src *image.Gray, s resizeScale, newWidth, newHeight int) *image.Gray {result := image.NewGray(image.Rect(0, 0, newWidth, newHeight))for y := range newHeight {dstOff := y * result.Stride_, srcY := s.srcCoord(0, y)srcRowOff := srcY * src.Stridefor x := range newWidth {srcX, _ := s.srcCoord(x, 0)result.Pix[dstOff+x] = src.Pix[srcRowOff+srcX] - edit in utils/terminal_image.go at line 188
return result - edit in utils/terminal_image.go at line 189
return result} - replacement in utils/terminal_image.go at line 192
if srcRGBA, ok := img.(*image.RGBA); ok {result := image.NewRGBA(image.Rect(0, 0, newWidth, newHeight))for y := range newHeight {srcY := int(float64(y) * scaleY)if srcY >= srcHeight {srcY = srcHeight - 1}dstOff := y * result.StridesrcRowOff := srcY * srcRGBA.Stridefor x := range newWidth {srcX := int(float64(x) * scaleX)if srcX >= srcWidth {srcX = srcWidth - 1}si := srcRowOff + srcX*4di := dstOff + x*4result.Pix[di] = srcRGBA.Pix[si]result.Pix[di+1] = srcRGBA.Pix[si+1]result.Pix[di+2] = srcRGBA.Pix[si+2]result.Pix[di+3] = srcRGBA.Pix[si+3]}// resizeRGBA resizes an RGBA image using nearest-neighbor interpolation.func resizeRGBA(src *image.RGBA, s resizeScale, newWidth, newHeight int) *image.RGBA {result := image.NewRGBA(image.Rect(0, 0, newWidth, newHeight))for y := range newHeight {dstOff := y * result.Stride_, srcY := s.srcCoord(0, y)srcRowOff := srcY * src.Stridefor x := range newWidth {srcX, _ := s.srcCoord(x, 0)si := srcRowOff + srcX*4di := dstOff + x*4result.Pix[di] = src.Pix[si]result.Pix[di+1] = src.Pix[si+1]result.Pix[di+2] = src.Pix[si+2]result.Pix[di+3] = src.Pix[si+3] - edit in utils/terminal_image.go at line 208
return result - edit in utils/terminal_image.go at line 209
return result} - replacement in utils/terminal_image.go at line 212
// Fallback for other image types// resizeGeneric resizes any image using nearest-neighbor interpolation (slow fallback).func resizeGeneric(img image.Image, s resizeScale, newWidth, newHeight int) *image.RGBA {bounds := img.Bounds() - edit in utils/terminal_image.go at line 217
srcY := int(float64(y) * scaleY)if srcY >= srcHeight {srcY = srcHeight - 1} - replacement in utils/terminal_image.go at line 218
srcX := int(float64(x) * scaleX)if srcX >= srcWidth {srcX = srcWidth - 1}srcX, srcY := s.srcCoord(x, y) - edit in utils/terminal_image.go at line 230
}// ResizeImage resizes an image using nearest-neighbor interpolation.// For higher quality, use golang.org/x/image/draw, but this keeps dependencies minimal.func ResizeImage(img image.Image, newWidth, newHeight int) image.Image {s := newResizeScale(img, newWidth, newHeight)if srcGray, ok := img.(*image.Gray); ok {return resizeGray(srcGray, s, newWidth, newHeight)}if srcRGBA, ok := img.(*image.RGBA); ok {return resizeRGBA(srcRGBA, s, newWidth, newHeight)}return resizeGeneric(img, s, newWidth, newHeight) - edit in utils/cluster_import.go at line 206
// wavInfo holds WAV metadata and hash for a single file during batch processingtype wavInfo struct {path stringmetadata *WAVMetadatahash stringerr error}// parseFilenameTimestampsBatch parses filename timestamps and applies timezone offsets.// Returns a map from wavInfos index to adjusted timestamp, and any errors.func parseFilenameTimestampsBatch(wavInfos []wavInfo,filenameIndices []int,filenames []string,timezoneID string,) (map[int]time.Time, []FileImportError) {var errors []FileImportErrorresult := make(map[int]time.Time)filenameTimestamps, err := ParseFilenameTimestamps(filenames)if err != nil {for _, idx := range filenameIndices {errors = append(errors, FileImportError{FileName: filepath.Base(wavInfos[idx].path),Error: fmt.Sprintf("filename timestamp parsing failed: %v", err),Stage: "parse",})}return result, errors} - edit in utils/cluster_import.go at line 238
adjustedTimestamps, err := ApplyTimezoneOffset(filenameTimestamps, timezoneID)if err != nil {for _, idx := range filenameIndices {errors = append(errors, FileImportError{FileName: filepath.Base(wavInfos[idx].path),Error: fmt.Sprintf("timezone offset failed: %v", err),Stage: "parse",})}return result, errors}for j, idx := range filenameIndices {result[idx] = adjustedTimestamps[j]}return result, errors}// resolveFileData resolves timestamp and calculates astronomical data for a single WAV file.func resolveFileData(info wavInfo, preParsedTime *time.Time, location *LocationData) (*fileData, error) {tsResult, err := ResolveTimestamp(info.metadata, info.path, location.TimezoneID, true, preParsedTime)if err != nil {return nil, err}astroData := CalculateAstronomicalData(tsResult.Timestamp.UTC(),info.metadata.Duration,location.Latitude,location.Longitude,)return &fileData{FileName: filepath.Base(info.path),Hash: info.hash,Duration: info.metadata.Duration,SampleRate: info.metadata.SampleRate,TimestampLocal: tsResult.Timestamp,IsAudioMoth: tsResult.IsAudioMoth,MothData: tsResult.MothData,AstroData: astroData,}, nil} - edit in utils/cluster_import.go at line 288
type wavInfo struct {path stringmetadata *WAVMetadatahash stringerr error} - edit in utils/cluster_import.go at line 308
// Check if file has timestamp filename format - replacement in utils/cluster_import.go at line 315
filenameTimestampMap := make(map[int]time.Time) // Maps file index to timestampfilenameTimestampMap := make(map[int]time.Time) - replacement in utils/cluster_import.go at line 317
filenameTimestamps, err := ParseFilenameTimestamps(filenamesForParsing)if err != nil {// If batch parsing fails, record error for all filesfor _, idx := range filenameIndices {errors = append(errors, FileImportError{FileName: filepath.Base(wavInfos[idx].path),Error: fmt.Sprintf("filename timestamp parsing failed: %v", err),Stage: "parse",})}} else {// Apply timezone offsetadjustedTimestamps, err := ApplyTimezoneOffset(filenameTimestamps, location.TimezoneID)if err != nil {for _, idx := range filenameIndices {errors = append(errors, FileImportError{FileName: filepath.Base(wavInfos[idx].path),Error: fmt.Sprintf("timezone offset failed: %v", err),Stage: "parse",})}} else {// Build map from file index to timestampfor j, idx := range filenameIndices {filenameTimestampMap[idx] = adjustedTimestamps[j]}}}tsMap, tsErrors := parseFilenameTimestampsBatch(wavInfos, filenameIndices, filenamesForParsing, location.TimezoneID)errors = append(errors, tsErrors...)filenameTimestampMap = tsMap - replacement in utils/cluster_import.go at line 325
continue // Already recorded errorcontinue - replacement in utils/cluster_import.go at line 333
// Resolve timestamp using common functiontsResult, err := ResolveTimestamp(info.metadata, info.path, location.TimezoneID, true, preParsedTime)fd, err := resolveFileData(info, preParsedTime, location) - replacement in utils/cluster_import.go at line 342[5.182322]→[5.182322:182398](∅→∅),[5.182398]→[5.12068:12097](∅→∅),[5.12097]→[5.182423:182726](∅→∅),[5.182423]→[5.182423:182726](∅→∅),[5.182726]→[5.12098:12216](∅→∅),[5.12216]→[5.182822:182857](∅→∅),[5.182822]→[5.182822:182857](∅→∅)
// Calculate astronomical dataastroData := CalculateAstronomicalData(tsResult.Timestamp.UTC(),info.metadata.Duration,location.Latitude,location.Longitude,)// Add to resultsfilesData = append(filesData, &fileData{FileName: filepath.Base(info.path),Hash: info.hash,Duration: info.metadata.Duration,SampleRate: info.metadata.SampleRate,TimestampLocal: tsResult.Timestamp,IsAudioMoth: tsResult.IsAudioMoth,MothData: tsResult.MothData,AstroData: astroData,})filesData = append(filesData, fd) - replacement in utils/cluster_import.go at line 348
// insertClusterFiles inserts all file data into database in a single transactionfunc insertClusterFiles(database *sql.DB,filesData []*fileData,// insertSingleFile inserts one file's data into the database within an existing transaction.// Returns (imported=true, nil) on success, (imported=false, nil) if skipped, or (false, error) on failure.func insertSingleFile(ctx context.Context,tx *db.LoggedTx,fd *fileData,fileStmt, datasetStmt, mothStmt *db.LoggedStmt, - replacement in utils/cluster_import.go at line 356
) (imported, skipped int, errors []FileImportError, err error) {// Begin logged transactionctx := context.Background()tx, err := db.BeginLoggedTx(ctx, database, "import_audio_files")) (bool, error) {// Check for duplicate hash_, isDuplicate, err := CheckDuplicateHash(tx, fd.Hash)if err != nil {return false, fmt.Errorf("duplicate check failed: %v", err)}if isDuplicate {return false, nil // skipped}// Generate file IDfileID, err := GenerateLongID()if err != nil {return false, fmt.Errorf("ID generation failed: %v", err)}// Insert file record_, err = fileStmt.ExecContext(ctx,fileID, fd.FileName, fd.Hash, locationID,fd.TimestampLocal, clusterID, fd.Duration, fd.SampleRate,fd.AstroData.SolarNight, fd.AstroData.CivilNight, fd.AstroData.MoonPhase,)if err != nil {return false, fmt.Errorf("file insert failed: %v", err)}// Insert file_dataset junction (ALWAYS)_, err = datasetStmt.ExecContext(ctx, fileID, datasetID) - replacement in utils/cluster_import.go at line 385
return 0, 0, nil, fmt.Errorf("failed to begin transaction: %w", err)return false, fmt.Errorf("file_dataset insert failed: %v", err) - replacement in utils/cluster_import.go at line 387
defer tx.Rollback() // Rollback if not committed// If AudioMoth, insert moth_metadataif fd.IsAudioMoth && fd.MothData != nil {_, err = mothStmt.ExecContext(ctx,fileID,fd.MothData.Timestamp,&fd.MothData.RecorderID,&fd.MothData.Gain,&fd.MothData.BatteryV,&fd.MothData.TempC,)if err != nil {return false, fmt.Errorf("moth_metadata insert failed: %v", err)}}return true, nil}// clusterStmts holds prepared statements for cluster file insertion.type clusterStmts struct {fileStmt *db.LoggedStmtdatasetStmt *db.LoggedStmtmothStmt *db.LoggedStmt} - replacement in utils/cluster_import.go at line 413
// Prepare statements// prepareClusterStmts creates prepared statements for cluster file insertion.func prepareClusterStmts(ctx context.Context, tx *db.LoggedTx) (*clusterStmts, error) { - replacement in utils/cluster_import.go at line 423
return 0, 0, nil, fmt.Errorf("failed to prepare file statement: %w", err)return nil, fmt.Errorf("failed to prepare file statement: %w", err) - edit in utils/cluster_import.go at line 425
defer fileStmt.Close() - replacement in utils/cluster_import.go at line 431
return 0, 0, nil, fmt.Errorf("failed to prepare dataset statement: %w", err)fileStmt.Close()return nil, fmt.Errorf("failed to prepare dataset statement: %w", err) - edit in utils/cluster_import.go at line 434
defer datasetStmt.Close() - replacement in utils/cluster_import.go at line 442
return 0, 0, nil, fmt.Errorf("failed to prepare moth statement: %w", err)fileStmt.Close()datasetStmt.Close()return nil, fmt.Errorf("failed to prepare moth statement: %w", err) - edit in utils/cluster_import.go at line 446
defer mothStmt.Close() - replacement in utils/cluster_import.go at line 447[5.184504]→[5.184504:184587](∅→∅),[5.184587]→[2.767:824](∅→∅),[2.824]→[5.184729:184926](∅→∅),[5.184729]→[5.184729:184926](∅→∅)
// Insert each filefor _, fd := range filesData {// Check for duplicate hash_, isDuplicate, err := CheckDuplicateHash(tx, fd.Hash)if err != nil {errors = append(errors, FileImportError{FileName: fd.FileName,Error: fmt.Sprintf("duplicate check failed: %v", err),Stage: "insert",})continue}return &clusterStmts{fileStmt: fileStmt, datasetStmt: datasetStmt, mothStmt: mothStmt}, nil} - replacement in utils/cluster_import.go at line 450
if isDuplicate {skipped++continue}// Close closes all prepared statements.func (s *clusterStmts) Close() {s.fileStmt.Close()s.datasetStmt.Close()s.mothStmt.Close()} - replacement in utils/cluster_import.go at line 457
// Generate file IDfileID, err := GenerateLongID()if err != nil {errors = append(errors, FileImportError{FileName: fd.FileName,Error: fmt.Sprintf("ID generation failed: %v", err),Stage: "insert",})continue}// insertClusterFiles inserts all file data into database in a single transactionfunc insertClusterFiles(database *sql.DB,filesData []*fileData,datasetID, clusterID, locationID string,) (imported, skipped int, errors []FileImportError, err error) {ctx := context.Background()tx, err := db.BeginLoggedTx(ctx, database, "import_audio_files")if err != nil {return 0, 0, nil, fmt.Errorf("failed to begin transaction: %w", err)}defer tx.Rollback() - replacement in utils/cluster_import.go at line 470
// Insert file record_, err = fileStmt.ExecContext(ctx,fileID, fd.FileName, fd.Hash, locationID,fd.TimestampLocal, clusterID, fd.Duration, fd.SampleRate,fd.AstroData.SolarNight, fd.AstroData.CivilNight, fd.AstroData.MoonPhase,)if err != nil {errors = append(errors, FileImportError{FileName: fd.FileName,Error: fmt.Sprintf("file insert failed: %v", err),Stage: "insert",})continue}stmts, err := prepareClusterStmts(ctx, tx)if err != nil {return 0, 0, nil, err}defer stmts.Close() - replacement in utils/cluster_import.go at line 476
// Insert file_dataset junction (ALWAYS)_, err = datasetStmt.ExecContext(ctx, fileID, datasetID)if err != nil {for _, fd := range filesData {wasImported, insertErr := insertSingleFile(ctx, tx, fd, stmts.fileStmt, stmts.datasetStmt, stmts.mothStmt, datasetID, clusterID, locationID)if insertErr != nil { - replacement in utils/cluster_import.go at line 481
Error: fmt.Sprintf("file_dataset insert failed: %v", err),Error: insertErr.Error(), - replacement in utils/cluster_import.go at line 486
// If AudioMoth, insert moth_metadataif fd.IsAudioMoth && fd.MothData != nil {_, err = mothStmt.ExecContext(ctx,fileID,fd.MothData.Timestamp,&fd.MothData.RecorderID,&fd.MothData.Gain,&fd.MothData.BatteryV,&fd.MothData.TempC,)if err != nil {errors = append(errors, FileImportError{FileName: fd.FileName,Error: fmt.Sprintf("moth_metadata insert failed: %v", err),Stage: "insert",})continue}if wasImported {imported++} else {skipped++ - edit in utils/cluster_import.go at line 491
imported++ - replacement in utils/cluster_import.go at line 493
// Commit transactionerr = tx.Commit()if err != nil {if err := tx.Commit(); err != nil { - replacement in utils/astronomical_test.go at line 27
t.Run("should return valid types for all fields", func(t *testing.T) {// Winter midnight in Auckland (should be solar night)winterMidnight := parseTime(t, "2024-06-15T12:00:00Z") // UTC midnight = noon in Auckland (winter)duration := 60.0 // 1 minutetests := []struct {name stringtimestamp stringduration float64lat, lon float64wantNoSNight bool // if true, assert SolarNight=falsewantNoCNight bool // if true, assert CivilNight=false}{{name: "valid moon phase range", timestamp: "2024-06-15T12:00:00Z", duration: 60.0, lat: testLocationAuckland.lat, lon: testLocationAuckland.lon},{name: "no solar night during daytime", timestamp: "2024-12-15T00:00:00Z", duration: 60.0, lat: testLocationAuckland.lat, lon: testLocationAuckland.lon, wantNoSNight: true, wantNoCNight: true},{name: "short duration", timestamp: "2024-06-15T10:00:00Z", duration: 30.0, lat: testLocationAuckland.lat, lon: testLocationAuckland.lon},{name: "long duration", timestamp: "2024-06-15T10:00:00Z", duration: 3600.0, lat: testLocationAuckland.lat, lon: testLocationAuckland.lon},{name: "midpoint calculation", timestamp: "2024-06-15T10:00:00Z", duration: 7200.0, lat: testLocationAuckland.lat, lon: testLocationAuckland.lon},{name: "different location", timestamp: "2024-06-15T12:00:00Z", duration: 60.0, lat: testLocationAuckland.lat, lon: testLocationAuckland.lon},{name: "very short duration", timestamp: "2024-06-15T12:00:00Z", duration: 0.1, lat: testLocationAuckland.lat, lon: testLocationAuckland.lon},{name: "very long duration", timestamp: "2024-06-15T12:00:00Z", duration: 86400.0, lat: testLocationAuckland.lat, lon: testLocationAuckland.lon},} - replacement in utils/astronomical_test.go at line 45
result := CalculateAstronomicalData(winterMidnight, duration, testLocationAuckland.lat, testLocationAuckland.lon)for _, tt := range tests {t.Run(tt.name, func(t *testing.T) {ts := parseTime(t, tt.timestamp)result := CalculateAstronomicalData(ts, tt.duration, tt.lat, tt.lon) - replacement in utils/astronomical_test.go at line 50
// Check types existif result.MoonPhase < 0 || result.MoonPhase > 1 {t.Errorf("MoonPhase out of range: got %f, want 0-1", result.MoonPhase)}})t.Run("should return false for solar night during daytime hours", func(t *testing.T) {// Summer midday in Auckland (should NOT be solar night)summerMidday := parseTime(t, "2024-12-15T00:00:00Z") // UTC midnight = noon in Auckland (summer)duration := 60.0 // 1 minuteresult := CalculateAstronomicalData(summerMidday, duration, testLocationAuckland.lat, testLocationAuckland.lon)// During summer midday, should NOT be solar nightif result.SolarNight {t.Error("Expected SolarNight to be false during daytime")}if result.CivilNight {t.Error("Expected CivilNight to be false during daytime")}})t.Run("should handle different durations correctly", func(t *testing.T) {timestamp := parseTime(t, "2024-06-15T10:00:00Z")shortDuration := 30.0 // 30 secondslongDuration := 3600.0 // 1 hourshortResult := CalculateAstronomicalData(timestamp, shortDuration, testLocationAuckland.lat, testLocationAuckland.lon)longResult := CalculateAstronomicalData(timestamp, longDuration, testLocationAuckland.lat, testLocationAuckland.lon)// Both should have valid resultsif shortResult.MoonPhase < 0 || shortResult.MoonPhase > 1 {t.Errorf("Short duration moon phase out of range: %f", shortResult.MoonPhase)}if longResult.MoonPhase < 0 || longResult.MoonPhase > 1 {t.Errorf("Long duration moon phase out of range: %f", longResult.MoonPhase)}})t.Run("should calculate midpoint time correctly", func(t *testing.T) {// Test that the calculation uses the midpoint, not the start timestartTime := parseTime(t, "2024-06-15T10:00:00Z")duration := 7200.0 // 2 hours (midpoint would be 1 hour later)result := CalculateAstronomicalData(startTime, duration, testLocationAuckland.lat, testLocationAuckland.lon)// Should calculate based on 11:00 UTC, not 10:00 UTC// Just verify we get valid boolean results_ = result.SolarNight_ = result.CivilNight})t.Run("should handle different geographical locations", func(t *testing.T) {timestamp := parseTime(t, "2024-06-15T12:00:00Z") // UTC noonduration := 60.0aucklandResult := CalculateAstronomicalData(timestamp, duration, testLocationAuckland.lat, testLocationAuckland.lon)londonResult := CalculateAstronomicalData(timestamp, duration, testLocationLondon.lat, testLocationLondon.lon)// Both should have valid boolean results (don't compare values, just that they're boolean)_ = aucklandResult.SolarNight_ = londonResult.SolarNight// Results might differ due to different timezones and seasons// Auckland: UTC noon = midnight local (winter) = likely night// London: UTC noon = 1pm local (summer) = likely day})t.Run("should return valid moon phase values", func(t *testing.T) {timestamp := parseTime(t, "2024-06-15T12:00:00Z")duration := 60.0result := CalculateAstronomicalData(timestamp, duration, testLocationAuckland.lat, testLocationAuckland.lon)if result.MoonPhase < 0 || result.MoonPhase > 1 {t.Errorf("MoonPhase out of range: got %f, want 0-1", result.MoonPhase)}})t.Run("should handle edge cases with very short durations", func(t *testing.T) {timestamp := parseTime(t, "2024-06-15T12:00:00Z")duration := 0.1 // 0.1 secondsresult := CalculateAstronomicalData(timestamp, duration, testLocationAuckland.lat, testLocationAuckland.lon)if result.MoonPhase < 0 || result.MoonPhase > 1 {t.Errorf("MoonPhase out of range: got %f, want 0-1", result.MoonPhase)}})t.Run("should handle edge cases with very long durations", func(t *testing.T) {timestamp := parseTime(t, "2024-06-15T12:00:00Z")duration := 86400.0 // 24 hoursresult := CalculateAstronomicalData(timestamp, duration, testLocationAuckland.lat, testLocationAuckland.lon)if result.MoonPhase < 0 || result.MoonPhase > 1 {t.Errorf("MoonPhase out of range: got %f, want 0-1", result.MoonPhase)}})if result.MoonPhase < 0 || result.MoonPhase > 1 {t.Errorf("MoonPhase out of range: got %f, want 0-1", result.MoonPhase)}if tt.wantNoSNight && result.SolarNight {t.Error("Expected SolarNight to be false")}if tt.wantNoCNight && result.CivilNight {t.Error("Expected CivilNight to be false")}})} - edit in tools/import_segments.go at line 82
// segmentValidation holds the results of pre-import validation (phases B+C).type segmentValidation struct {scannedFiles []scannedDataFilefilterIDMap map[string]stringspeciesIDMap map[string]stringcalltypeIDMap map[string]map[string]stringfileIDMap map[string]scannedDataFile}// validateAndPrepareSegments performs phases B+C: parse data files, validate DB state, and prepare ID maps.func validateAndPrepareSegments(database *sql.DB,input ImportSegmentsInput,mapping utils.MappingFile,dataFiles []string,) (*segmentValidation, []ImportSegmentError, error) {// Phase B: Parse all .data files and collect unique valuesscannedFiles, parseErrors, uniqueFilters, uniqueSpecies, uniqueCalltypes := scanAllDataFiles(dataFiles, input.Folder)if len(scannedFiles) == 0 {return nil, parseErrors, nil}// Validate dataset/location/cluster hierarchyif err := validateSegmentHierarchy(database, input.DatasetID, input.LocationID, input.ClusterID); err != nil {return nil, parseErrors, err}// Validate all filters existfilterIDMap, err := validateFiltersExist(database, uniqueFilters)if err != nil {return nil, parseErrors, fmt.Errorf("filter validation failed: %w", err)}// Validate mapping covers all species/calltypes and they exist in DBvalidationResult, err := utils.ValidateMappingAgainstDB(database, mapping, uniqueSpecies, uniqueCalltypes)if err != nil {return nil, parseErrors, fmt.Errorf("mapping validation failed: %w", err)}if validationResult.HasErrors() {return nil, parseErrors, fmt.Errorf("mapping validation failed: %s", validationResult.Error())}// Load species and calltype ID mapsspeciesIDMap, calltypeIDMap, err := loadSpeciesCalltypeIDs(database, mapping, uniqueSpecies, uniqueCalltypes)if err != nil {return nil, parseErrors, fmt.Errorf("failed to load species/calltype IDs: %w", err)}// Validate files: hash exists, linked to dataset, no existing labelsfileIDMap, hashErrors := validateAndMapFiles(database, scannedFiles, input.ClusterID, input.DatasetID)allErrors := append(parseErrors, hashErrors...)return &segmentValidation{scannedFiles: scannedFiles,filterIDMap: filterIDMap,speciesIDMap: speciesIDMap,calltypeIDMap: calltypeIDMap,fileIDMap: fileIDMap,}, allErrors, nil} - edit in tools/import_segments.go at line 170
}// Phase B: Parse all .data files and collect unique valuesscannedFiles, parseErrors, uniqueFilters, uniqueSpecies, uniqueCalltypes := scanAllDataFiles(dataFiles, input.Folder)output.Errors = append(output.Errors, parseErrors...)if len(scannedFiles) == 0 {output.Summary.ProcessingTimeMs = time.Since(startTime).Milliseconds()return output, nil - replacement in tools/import_segments.go at line 172
// Phase C: Pre-Import Validation// Phase B+C: Parse data files and validate against DB - edit in tools/import_segments.go at line 178
// Validate dataset/location/cluster hierarchyif err := validateSegmentHierarchy(database, input.DatasetID, input.LocationID, input.ClusterID); err != nil {return output, err}// Validate all filters existfilterIDMap, err := validateFiltersExist(database, uniqueFilters)if err != nil {return output, fmt.Errorf("filter validation failed: %w", err)}// Validate mapping covers all species/calltypes and they exist in DBvalidationResult, err := utils.ValidateMappingAgainstDB(database, mapping, uniqueSpecies, uniqueCalltypes)if err != nil {return output, fmt.Errorf("mapping validation failed: %w", err)}if validationResult.HasErrors() {return output, fmt.Errorf("mapping validation failed: %s", validationResult.Error())} - replacement in tools/import_segments.go at line 179
// Load species and calltype ID mapsspeciesIDMap, calltypeIDMap, err := loadSpeciesCalltypeIDs(database, mapping, uniqueSpecies, uniqueCalltypes)val, valErrors, err := validateAndPrepareSegments(database, input, mapping, dataFiles)output.Errors = append(output.Errors, valErrors...) - replacement in tools/import_segments.go at line 182
return output, fmt.Errorf("failed to load species/calltype IDs: %w", err)return output, err - replacement in tools/import_segments.go at line 184
// Validate files: hash exists, linked to dataset, no existing labelsfileIDMap, hashErrors := validateAndMapFiles(database, scannedFiles, input.ClusterID, input.DatasetID)output.Errors = append(output.Errors, hashErrors...)if len(fileIDMap) == 0 && len(scannedFiles) > 0 {if val == nil || len(val.fileIDMap) == 0 { - replacement in tools/import_segments.go at line 191
ctx, database, fileIDMap, scannedFiles, mapping, filterIDMap, speciesIDMap, calltypeIDMap, input.DatasetID, input.ProgressHandler,ctx, database, val.fileIDMap, val.scannedFiles, mapping, val.filterIDMap, val.speciesIDMap, val.calltypeIDMap, input.DatasetID, input.ProgressHandler, - edit in tools/import_segments.go at line 194
// Build output segments - replacement in tools/import_segments.go at line 202
output.Summary.DataFilesProcessed = len(fileIDMap)output.Summary.TotalSegments = countTotalSegments(fileIDMap)output.Summary.DataFilesProcessed = len(val.fileIDMap)output.Summary.TotalSegments = countTotalSegments(val.fileIDMap) - replacement in lint_test.go at line 48
cmd := exec.Command("gocyclo", "-over", "15", ".")cmd := exec.Command("gocyclo", "-over", "14", ".") - edit in CLAUDE.md at line 80
### Cyclomatic ComplexityPlease work to reduce cyclomatic complexity. - edit in CLAUDE.md at line 85
Endeavour to keep new code well under 10.```bashgocyclo -over 10 .gocyclo <file>```