Skip to content

Architecture Notes & Bug Fixes (v2.0) #1

Description

@aj1126

Architecture Notes & Bug Fixes (v2.0)

This document outlines critical logic, type-coercion, and scope isolation bugs identified during the v2.0 module audit, along with their implemented resolutions.

1. Hash Cache Deserialization & Date Precision

File: src/MyBookTools.psm1
Function: Update-MyBookHashCache

Issue: The hash cache invalidates on every run, failing to bypass unchanged files. Furthermore, a MethodInvocationException crashes the script during cache lookups.
Root Cause: 1. ConvertFrom-Json in PowerShell 5.1 outputs a PSCustomObject, not a standard hashtable. Calling .ContainsKey() on it triggers a fatal error.
2. [datetime] objects lose tick precision during JSON serialization. Direct comparison of a file's $meta.LastWriteTime to the deserialized date fails.

Resolution:
Convert the PSCustomObject to a hashtable during ingestion and enforce ISO 8601 string formatting ('o') for reliable date comparisons.

    $cache = @{}
    if (Test-Path $CachePath) {
        $json = Get-Content -Path $CachePath -Raw | ConvertFrom-Json
        if ($json) {
            foreach ($prop in $json.psobject.properties) {
                $cache[$prop.Name] = $prop.Value
            }
        }
    }

    $result = @{}

    Get-ChildItem -Path $RootPath -Recurse -File -ErrorAction SilentlyContinue |
        ForEach-Object {
            $key = $_.FullName
            $meta = @{
                Length        = $_.Length
                LastWriteTime = $_.LastWriteTime.ToString('o') # Format securely for string comparison
            }

            if ($null -ne $cache[$key] -and
                $cache[$key].Length -eq $meta.Length -and
                $cache[$key].LastWriteTime -eq $meta.LastWriteTime) {

                $hash = $cache[$key].Hash
            } else {
                try { $hash = (Get-FileHash -LiteralPath $_.FullName).Hash } catch { $hash = $null }
            }

            $result[$key] = @{
                Length        = $meta.Length
                LastWriteTime = $meta.LastWriteTime
                Hash          = $hash
            }
        }

2. Duplicate Resolution Sorting Failure

File: src/MyBookTools.psm1

Function: Resolve-MyBookDuplicates

Issue: Duplicates are deleted at random rather than keeping the most recently modified file.

Root Cause: Get-FileHash strips the original FileInfo object, returning only Algorithm, Hash, and Path. Because LastWriteTime no longer exists in the pipeline, Sort-Object LastWriteTime -Descending fails silently.

Resolution:
Map the hashed paths back to full FileInfo objects using Get-Item -LiteralPath before applying the sort.

    $hashGroups = $files | Get-FileHash | Group-Object Hash | Where-Object Count -gt 1

    foreach ($group in $hashGroups) {
        # Map back to FileInfo objects to retrieve LastWriteTime
        $duplicateFiles = $group.Group | ForEach-Object { Get-Item -LiteralPath $_.Path }
        $sorted = $duplicateFiles | Sort-Object LastWriteTime -Descending
        
        $keep   = $sorted[0]
        $remove = $sorted[1..($sorted.Count - 1)]

        Write-MyBookLog -Message "Keeping newest duplicate: $($keep.FullName)"

        foreach ($f in $remove) {
            if ($DryRun) {
                Write-MyBookLog -Message "[DryRun] Would delete: $($f.FullName)"
            } else {
                if ($PSCmdlet.ShouldProcess($f.FullName, "Delete duplicate")) {
                    Remove-Item -LiteralPath $f.FullName -Force
                    Write-MyBookLog -Message "Deleted duplicate: $($f.FullName)"
                }
            }
        }
    }

3. Infinite Categorization Nesting

File: src/MyBookTools.psm1

Function: Invoke-MyBookCategorize

Issue: Running categorization multiple times causes files to nest infinitely (e.g., moving Projects/file.txt into Projects/Projects/file.txt).

Root Cause: The logic computes relative paths from the root without checking if the file is already seated within a valid category destination.

Resolution:
Implement a guard clause to bypass files that reside within any computed destination directory.

    foreach ($file in $files) {
        # Skip if the file is already seated in a destination category directory
        $alreadyCategorized = $false
        foreach ($dest in $destinations.Values) {
            if ($file.FullName.StartsWith($dest, [System.StringComparison]::InvariantCultureIgnoreCase)) {
                $alreadyCategorized = $true
                break
            }
        }
        if ($alreadyCategorized) { continue }

        $targetCategory = $null
        # ... [rest of existing matching logic] ...

4. Recursive Archive Compression

File: src/MyBookTools.psm1

Function: Invoke-MyBookCleanup

Issue: Exponential file bloat during archive cleanup.

Root Cause: The script generates the new .zip file directly inside the Archives folder it is actively compressing, causing it to swallow the previous iterations of itself.

Resolution:
Target the $RootPath (outside the scope of the archive directory) for the output .zip.

    if ($CompressArchives) {
        $archiveRoot = Join-Path $RootPath 'Archives'
        if (Test-Path $archiveRoot) {
            # Target the root path, not the archive folder itself
            $zipPath = Join-Path $RootPath ("Archives_{0:yyyyMMdd_HHmmss}.zip" -f (Get-Date))
            Compress-Archive -Path (Join-Path $archiveRoot '*') -DestinationPath $zipPath -Force
            Write-MyBookLog -Message "Compressed archives → $zipPath"
        }
    }

5. Scheduled Task Syntax Error

File: src/MyBookTools.psm1

Function: Register-MyBookMaintenanceTask

Issue: Task registration fails with a syntax exception.

Root Cause: -At 3:00AM throws an error because PowerShell evaluates 3 as an integer, which cannot natively parse the AM string suffix without encapsulation.

Resolution:
Wrap the time argument in string quotes.

    switch ($Schedule) {
        'Daily' {
            $trigger = New-ScheduledTaskTrigger -Daily -At "3:00 AM"
        }
        'Hourly' {
            $trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(5)
            $trigger.RepetitionInterval = 'PT1H'
            $trigger.RepetitionDuration = 'P1D'
        }
        default {
            $trigger = New-ScheduledTaskTrigger -Daily -At "3:00 AM"
        }
    }

6. UI Status Bar Scope Isolation

File: tools/MyBookTools.GUI.ps1

Issue: The WPF GUI status bar permanently reads "Idle" even when long-running tasks are active.

Root Cause: The UI action buttons utilize Start-Job, which spawns isolated powershell.exe worker processes. Module variables like $Script:MyBook_Status updated in the background job do not serialize back to the main UI thread's memory space.

Resolution:
Replace Start-Job with the built-in Run-InBackground runspace factory implementation and inject a synchronized hashtable ([hashtable]::Synchronized(@{})) to safely marshal status variables between the main UI thread and the background worker space, or dump the status payload to a local temporary .json file that the UI timer polls.

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions