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.
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.psm1Function:
Update-MyBookHashCacheIssue: The hash cache invalidates on every run, failing to bypass unchanged files. Furthermore, a
MethodInvocationExceptioncrashes the script during cache lookups.Root Cause: 1.
ConvertFrom-Jsonin PowerShell 5.1 outputs aPSCustomObject, 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.LastWriteTimeto the deserialized date fails.Resolution:
Convert the
PSCustomObjectto a hashtable during ingestion and enforce ISO 8601 string formatting ('o') for reliable date comparisons.2. Duplicate Resolution Sorting Failure
File:
src/MyBookTools.psm1Function:
Resolve-MyBookDuplicatesIssue: Duplicates are deleted at random rather than keeping the most recently modified file.
Root Cause:
Get-FileHashstrips the originalFileInfoobject, returning onlyAlgorithm,Hash, andPath. BecauseLastWriteTimeno longer exists in the pipeline,Sort-Object LastWriteTime -Descendingfails silently.Resolution:
Map the hashed paths back to full
FileInfoobjects usingGet-Item -LiteralPathbefore applying the sort.3. Infinite Categorization Nesting
File:
src/MyBookTools.psm1Function:
Invoke-MyBookCategorizeIssue: Running categorization multiple times causes files to nest infinitely (e.g., moving
Projects/file.txtintoProjects/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.
4. Recursive Archive Compression
File:
src/MyBookTools.psm1Function:
Invoke-MyBookCleanupIssue: Exponential file bloat during archive cleanup.
Root Cause: The script generates the new
.zipfile directly inside theArchivesfolder 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.5. Scheduled Task Syntax Error
File:
src/MyBookTools.psm1Function:
Register-MyBookMaintenanceTaskIssue: Task registration fails with a syntax exception.
Root Cause:
-At 3:00AMthrows an error because PowerShell evaluates3as an integer, which cannot natively parse theAMstring suffix without encapsulation.Resolution:
Wrap the time argument in string quotes.
6. UI Status Bar Scope Isolation
File:
tools/MyBookTools.GUI.ps1Issue: 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 isolatedpowershell.exeworker processes. Module variables like$Script:MyBook_Statusupdated in the background job do not serialize back to the main UI thread's memory space.Resolution:
Replace
Start-Jobwith the built-inRun-InBackgroundrunspace 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.jsonfile that the UI timer polls.