cyclo to 14 now

quietlight
May 4, 2026, 9:56 PM
RUVJ3V4N5V4Z3HSH2YYESKQF5G7RIHBFB5TLV2IPDWXSGJDRD54AC

Dependencies

  • [2] 2HAQZPV3 more refactoring with glm
  • [3] VYNOHQJW tidied up CLAUDE.md
  • [4] HYCZTLSZ fixed tests with cyclo over 15
  • [5] SMWSHUOW cyclo over 15
  • [6] JAT3DXOL cyclo over 15
  • [7] NS4TDPLN cyclomatic complexity
  • [8] GPQSOVBP cyclo complexity over 25
  • [9] BZ6KQRYD added complexity lint test
  • [10] LQLC7S3A trying gemini: Inconsistent Standards in @utils/ refactoring
  • [11] LBWQJEDH minor refactor and more tests for utils/
  • [12] KZKLAINJ run out of space on nest, cleaned out
  • [13] QVIGQOQZ more work on utils/ with glm

Change contents

  • replacement in utils/wav_metadata.go at line 404
    [5.1650][5.1650:1932]()
    // 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) {
    [5.1650]
    [5.1932]
    // 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
    [5.1981][5.1981:2041]()
    return nil, 0, fmt.Errorf("failed to open file: %w", err)
    [5.1981]
    [5.2041]
    return nil, wavChunkInfo{}, fmt.Errorf("failed to open file: %w", err)
  • edit in utils/wav_metadata.go at line 411
    [5.2044][5.2044:2081]()
    defer func() { _ = file.Close() }()
  • replacement in utils/wav_metadata.go at line 414
    [5.2170][5.2170:2232]()
    return nil, 0, fmt.Errorf("failed to read header: %w", err)
    [5.2170]
    [5.2232]
    file.Close()
    return nil, wavChunkInfo{}, fmt.Errorf("failed to read header: %w", err)
  • replacement in utils/wav_metadata.go at line 418
    [5.2311][5.2311:2363]()
    return nil, 0, fmt.Errorf("not a valid WAV file")
    [5.2311]
    [5.2363]
    file.Close()
    return nil, wavChunkInfo{}, fmt.Errorf("not a valid WAV file")
  • replacement in utils/wav_metadata.go at line 423
    [5.2412][5.2412:2467]()
    return nil, 0, fmt.Errorf("failed to seek: %w", err)
    [5.2412]
    [5.2467]
    file.Close()
    return nil, wavChunkInfo{}, fmt.Errorf("failed to seek: %w", err)
  • replacement in utils/wav_metadata.go at line 429
    [5.2523][5.2523:2544]()
    return nil, 0, err
    [5.2523]
    [5.3874]
    file.Close()
    return nil, wavChunkInfo{}, err
  • replacement in utils/wav_metadata.go at line 433
    [5.2621][5.2621:2681]()
    return nil, 0, fmt.Errorf("missing or invalid fmt chunk")
    [5.2621]
    [5.2681]
    file.Close()
    return nil, wavChunkInfo{}, fmt.Errorf("missing or invalid fmt chunk")
  • replacement in utils/wav_metadata.go at line 437
    [5.3878][5.2685:2752]()
    startOffset, readSize := calcWAVReadRange(startSec, endSec, info)
    [5.3878]
    [5.3878]
    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
    [5.3898][5.2753:2796]()
    return []float64{}, info.sampleRate, nil
    [5.3898]
    [5.3936]
    return nil, nil
  • replacement in utils/wav_metadata.go at line 447
    [5.2878][5.4016:4087](),[5.4016][5.4016:4087]()
    return nil, 0, fmt.Errorf("failed to seek to data segment: %w", err)
    [5.2878]
    [5.4087]
    return nil, fmt.Errorf("failed to seek to data segment: %w", err)
  • replacement in utils/wav_metadata.go at line 453
    [5.4238][5.4238:4305]()
    return nil, 0, fmt.Errorf("failed to read audio data: %w", err)
    [5.4238]
    [5.4305]
    return nil, fmt.Errorf("failed to read audio data: %w", err)
  • edit in utils/wav_metadata.go at line 456
    [5.39465]
    [5.39562]
    return audioData, nil
    }
  • edit in utils/wav_metadata.go at line 459
    [5.39563]
    [5.2879]
    // 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
    [5.60333][5.60333:60565]()
    // 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 {
    [5.60333]
    [5.60565]
    // resizeScale holds precomputed scale factors for nearest-neighbor resizing.
    type resizeScale struct {
    srcWidth, srcHeight int
    scaleX, scaleY float64
    }
    func newResizeScale(img image.Image, newWidth, newHeight int) resizeScale {
  • replacement in utils/terminal_image.go at line 156
    [5.60589][5.60589:60640]()
    srcWidth := bounds.Dx()
    srcHeight := bounds.Dy()
    [5.60589]
    [5.60640]
    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
    [5.60641][5.60641:60741]()
    scaleX := float64(srcWidth) / float64(newWidth)
    scaleY := float64(srcHeight) / float64(newHeight)
    [5.60641]
    [5.60741]
    // 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
    [5.60742][5.60742:61223]()
    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.Stride
    srcRowOff := srcY * srcGray.Stride
    for x := range newWidth {
    srcX := int(float64(x) * scaleX)
    if srcX >= srcWidth {
    srcX = srcWidth - 1
    }
    result.Pix[dstOff+x] = srcGray.Pix[srcRowOff+srcX]
    }
    [5.60742]
    [5.61223]
    // 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.Stride
    for 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
    [5.61227][5.61227:61243]()
    return result
  • edit in utils/terminal_image.go at line 189
    [5.61246]
    [5.61246]
    return result
    }
  • replacement in utils/terminal_image.go at line 192
    [5.61247][5.61247:61885]()
    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.Stride
    srcRowOff := srcY * srcRGBA.Stride
    for x := range newWidth {
    srcX := int(float64(x) * scaleX)
    if srcX >= srcWidth {
    srcX = srcWidth - 1
    }
    si := srcRowOff + srcX*4
    di := dstOff + x*4
    result.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]
    }
    [5.61247]
    [5.61885]
    // 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.Stride
    for x := range newWidth {
    srcX, _ := s.srcCoord(x, 0)
    si := srcRowOff + srcX*4
    di := dstOff + x*4
    result.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
    [5.61889][5.61889:61905]()
    return result
  • edit in utils/terminal_image.go at line 209
    [5.61908]
    [5.61908]
    return result
    }
  • replacement in utils/terminal_image.go at line 212
    [5.61909][5.61909:61944]()
    // Fallback for other image types
    [5.61909]
    [5.61944]
    // 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
    [5.62036][5.62036:62124]()
    srcY := int(float64(y) * scaleY)
    if srcY >= srcHeight {
    srcY = srcHeight - 1
    }
  • replacement in utils/terminal_image.go at line 218
    [5.62152][5.62152:62242]()
    srcX := int(float64(x) * scaleX)
    if srcX >= srcWidth {
    srcX = srcWidth - 1
    }
    [5.62152]
    [5.62242]
    srcX, srcY := s.srcCoord(x, y)
  • edit in utils/terminal_image.go at line 230
    [5.62463]
    [5.62463]
    }
    // 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
    [5.178446]
    [5.178446]
    // wavInfo holds WAV metadata and hash for a single file during batch processing
    type wavInfo struct {
    path string
    metadata *WAVMetadata
    hash string
    err 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 []FileImportError
    result := 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
    [5.178447]
    [5.178447]
    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
    [5.178738][5.178738:178842]()
    type wavInfo struct {
    path string
    metadata *WAVMetadata
    hash string
    err error
    }
  • edit in utils/cluster_import.go at line 308
    [5.179393][5.179393:179442]()
    // Check if file has timestamp filename format
  • replacement in utils/cluster_import.go at line 315
    [5.179672][5.179672:179754]()
    filenameTimestampMap := make(map[int]time.Time) // Maps file index to timestamp
    [5.179672]
    [5.179754]
    filenameTimestampMap := make(map[int]time.Time)
  • replacement in utils/cluster_import.go at line 317
    [5.179789][5.179789:180745]()
    filenameTimestamps, err := ParseFilenameTimestamps(filenamesForParsing)
    if err != nil {
    // If batch parsing fails, record error for all files
    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",
    })
    }
    } else {
    // Apply timezone offset
    adjustedTimestamps, 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 timestamp
    for j, idx := range filenameIndices {
    filenameTimestampMap[idx] = adjustedTimestamps[j]
    }
    }
    }
    [5.179789]
    [5.180745]
    tsMap, tsErrors := parseFilenameTimestampsBatch(wavInfos, filenameIndices, filenamesForParsing, location.TimezoneID)
    errors = append(errors, tsErrors...)
    filenameTimestampMap = tsMap
  • replacement in utils/cluster_import.go at line 325
    [5.180835][5.180835:180873]()
    continue // Already recorded error
    [5.180835]
    [5.181700]
    continue
  • replacement in utils/cluster_import.go at line 333
    [5.182017][5.11872:12021]()
    // Resolve timestamp using common function
    tsResult, err := ResolveTimestamp(info.metadata, info.path, location.TimezoneID, true, preParsedTime)
    [5.182017]
    [5.12021]
    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 data
    astroData := CalculateAstronomicalData(
    tsResult.Timestamp.UTC(),
    info.metadata.Duration,
    location.Latitude,
    location.Longitude,
    )
    // Add to results
    filesData = 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,
    })
    [5.182322]
    [5.182857]
    filesData = append(filesData, fd)
  • replacement in utils/cluster_import.go at line 348
    [5.182890][5.182890:183040]()
    // insertClusterFiles inserts all file data into database in a single transaction
    func insertClusterFiles(
    database *sql.DB,
    filesData []*fileData,
    [5.182890]
    [5.183040]
    // 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
    [5.183082][5.183082:183271]()
    ) (imported, skipped int, errors []FileImportError, err error) {
    // Begin logged transaction
    ctx := context.Background()
    tx, err := db.BeginLoggedTx(ctx, database, "import_audio_files")
    [5.183082]
    [5.183271]
    ) (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 ID
    fileID, 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
    [5.183288][5.183288:183359]()
    return 0, 0, nil, fmt.Errorf("failed to begin transaction: %w", err)
    [5.183288]
    [5.183359]
    return false, fmt.Errorf("file_dataset insert failed: %v", err)
  • replacement in utils/cluster_import.go at line 387
    [5.183362][5.183362:183412]()
    defer tx.Rollback() // Rollback if not committed
    [5.183362]
    [5.183412]
    // If AudioMoth, insert moth_metadata
    if 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.LoggedStmt
    datasetStmt *db.LoggedStmt
    mothStmt *db.LoggedStmt
    }
  • replacement in utils/cluster_import.go at line 413
    [5.183413][5.183413:183436]()
    // Prepare statements
    [5.183413]
    [5.183436]
    // 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
    [5.183771][5.183771:183847]()
    return 0, 0, nil, fmt.Errorf("failed to prepare file statement: %w", err)
    [5.183771]
    [5.183847]
    return nil, fmt.Errorf("failed to prepare file statement: %w", err)
  • edit in utils/cluster_import.go at line 425
    [5.183850][5.183850:183874]()
    defer fileStmt.Close()
  • replacement in utils/cluster_import.go at line 431
    [5.184048][5.184048:184127]()
    return 0, 0, nil, fmt.Errorf("failed to prepare dataset statement: %w", err)
    [5.184048]
    [5.184127]
    fileStmt.Close()
    return nil, fmt.Errorf("failed to prepare dataset statement: %w", err)
  • edit in utils/cluster_import.go at line 434
    [5.184130][5.184130:184157]()
    defer datasetStmt.Close()
  • replacement in utils/cluster_import.go at line 442
    [5.184400][5.184400:184476]()
    return 0, 0, nil, fmt.Errorf("failed to prepare moth statement: %w", err)
    [5.184400]
    [5.184476]
    fileStmt.Close()
    datasetStmt.Close()
    return nil, fmt.Errorf("failed to prepare moth statement: %w", err)
  • edit in utils/cluster_import.go at line 446
    [5.184479][5.184479:184503]()
    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 file
    for _, 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
    }
    [5.184504]
    [5.184926]
    return &clusterStmts{fileStmt: fileStmt, datasetStmt: datasetStmt, mothStmt: mothStmt}, nil
    }
  • replacement in utils/cluster_import.go at line 450
    [5.184927][2.825:844](),[2.844][5.184941:184970](),[5.184941][5.184941:184970]()
    if isDuplicate {
    skipped++
    continue
    }
    [5.184927]
    [5.184970]
    // 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
    [5.184971][5.184971:185222]()
    // Generate file ID
    fileID, err := GenerateLongID()
    if err != nil {
    errors = append(errors, FileImportError{
    FileName: fd.FileName,
    Error: fmt.Sprintf("ID generation failed: %v", err),
    Stage: "insert",
    })
    continue
    }
    [5.184971]
    [5.185222]
    // insertClusterFiles inserts all file data into database in a single transaction
    func 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
    [5.185223][5.185223:185664]()
    // 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
    }
    [5.185223]
    [5.185664]
    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
    [5.185665][5.185665:185785]()
    // Insert file_dataset junction (ALWAYS)
    _, err = datasetStmt.ExecContext(ctx, fileID, datasetID)
    if err != nil {
    [5.185665]
    [5.185785]
    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
    [5.185856][5.185856:185922]()
    Error: fmt.Sprintf("file_dataset insert failed: %v", err),
    [5.185856]
    [5.185922]
    Error: insertErr.Error(),
  • replacement in utils/cluster_import.go at line 486
    [5.185968][5.185968:186448]()
    // If AudioMoth, insert moth_metadata
    if 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
    }
    [5.185968]
    [5.186448]
    if wasImported {
    imported++
    } else {
    skipped++
  • edit in utils/cluster_import.go at line 491
    [5.186452][5.186452:186466]()
    imported++
  • replacement in utils/cluster_import.go at line 493
    [5.186470][5.186470:186529]()
    // Commit transaction
    err = tx.Commit()
    if err != nil {
    [5.186470]
    [5.186529]
    if err := tx.Commit(); err != nil {
  • replacement in utils/astronomical_test.go at line 27
    [5.216724][5.216724:217023]()
    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 minute
    [5.216724]
    [5.217023]
    tests := []struct {
    name string
    timestamp string
    duration float64
    lat, lon float64
    wantNoSNight bool // if true, assert SolarNight=false
    wantNoCNight 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
    [5.217024][5.217024:217140]()
    result := CalculateAstronomicalData(winterMidnight, duration, testLocationAuckland.lat, testLocationAuckland.lon)
    [5.217024]
    [5.217140]
    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
    [5.217141][5.217141:221226]()
    // Check types exist
    if 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 minute
    result := CalculateAstronomicalData(summerMidday, duration, testLocationAuckland.lat, testLocationAuckland.lon)
    // During summer midday, should NOT be solar night
    if 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 seconds
    longDuration := 3600.0 // 1 hour
    shortResult := CalculateAstronomicalData(timestamp, shortDuration, testLocationAuckland.lat, testLocationAuckland.lon)
    longResult := CalculateAstronomicalData(timestamp, longDuration, testLocationAuckland.lat, testLocationAuckland.lon)
    // Both should have valid results
    if 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 time
    startTime := 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 noon
    duration := 60.0
    aucklandResult := 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.0
    result := 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 seconds
    result := 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 hours
    result := 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)
    }
    })
    [5.217141]
    [5.221226]
    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
    [5.320618]
    [5.320618]
    // segmentValidation holds the results of pre-import validation (phases B+C).
    type segmentValidation struct {
    scannedFiles []scannedDataFile
    filterIDMap map[string]string
    speciesIDMap map[string]string
    calltypeIDMap map[string]map[string]string
    fileIDMap 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 values
    scannedFiles, parseErrors, uniqueFilters, uniqueSpecies, uniqueCalltypes := scanAllDataFiles(dataFiles, input.Folder)
    if len(scannedFiles) == 0 {
    return nil, parseErrors, nil
    }
    // Validate dataset/location/cluster hierarchy
    if err := validateSegmentHierarchy(database, input.DatasetID, input.LocationID, input.ClusterID); err != nil {
    return nil, parseErrors, err
    }
    // Validate all filters exist
    filterIDMap, 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 DB
    validationResult, 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 maps
    speciesIDMap, 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 labels
    fileIDMap, 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
    [5.321540][5.321540:321903]()
    }
    // Phase B: Parse all .data files and collect unique values
    scannedFiles, 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
    [5.321907][5.321907:321942]()
    // Phase C: Pre-Import Validation
    [5.321907]
    [5.321942]
    // Phase B+C: Parse data files and validate against DB
  • edit in tools/import_segments.go at line 178
    [5.322095][5.322095:322855]()
    // Validate dataset/location/cluster hierarchy
    if err := validateSegmentHierarchy(database, input.DatasetID, input.LocationID, input.ClusterID); err != nil {
    return output, err
    }
    // Validate all filters exist
    filterIDMap, 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 DB
    validationResult, 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
    [5.322856][5.322856:323005]()
    // Load species and calltype ID maps
    speciesIDMap, calltypeIDMap, err := loadSpeciesCalltypeIDs(database, mapping, uniqueSpecies, uniqueCalltypes)
    [5.322856]
    [5.323005]
    val, valErrors, err := validateAndPrepareSegments(database, input, mapping, dataFiles)
    output.Errors = append(output.Errors, valErrors...)
  • replacement in tools/import_segments.go at line 182
    [5.323022][5.323022:323098]()
    return output, fmt.Errorf("failed to load species/calltype IDs: %w", err)
    [5.323022]
    [5.323098]
    return output, err
  • replacement in tools/import_segments.go at line 184
    [5.323101][5.323101:323383]()
    // Validate files: hash exists, linked to dataset, no existing labels
    fileIDMap, hashErrors := validateAndMapFiles(database, scannedFiles, input.ClusterID, input.DatasetID)
    output.Errors = append(output.Errors, hashErrors...)
    if len(fileIDMap) == 0 && len(scannedFiles) > 0 {
    [5.323101]
    [5.323383]
    if val == nil || len(val.fileIDMap) == 0 {
  • replacement in tools/import_segments.go at line 191
    [5.323619][5.323619:323752]()
    ctx, database, fileIDMap, scannedFiles, mapping, filterIDMap, speciesIDMap, calltypeIDMap, input.DatasetID, input.ProgressHandler,
    [5.323619]
    [5.323752]
    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
    [5.323811][5.323811:323838]()
    // Build output segments
  • replacement in tools/import_segments.go at line 202
    [5.324083][5.324083:324197]()
    output.Summary.DataFilesProcessed = len(fileIDMap)
    output.Summary.TotalSegments = countTotalSegments(fileIDMap)
    [5.324083]
    [5.324197]
    output.Summary.DataFilesProcessed = len(val.fileIDMap)
    output.Summary.TotalSegments = countTotalSegments(val.fileIDMap)
  • replacement in lint_test.go at line 48
    [5.158][4.11917:11969]()
    cmd := exec.Command("gocyclo", "-over", "15", ".")
    [5.158]
    [5.210]
    cmd := exec.Command("gocyclo", "-over", "14", ".")
  • edit in CLAUDE.md at line 80
    [3.632]
    [3.632]
    ### Cyclomatic Complexity
    Please work to reduce cyclomatic complexity.
  • edit in CLAUDE.md at line 85
    [3.633]
    [3.633]
    Endeavour to keep new code well under 10.
    ```bash
    gocyclo -over 10 .
    gocyclo <file>
    ```