Private/Merge-CruxExternalEvidence.ps1
|
# Copyright (c) 2026 Jeffrey Snover. All rights reserved. # Licensed under the MIT License. See LICENSE file in the project root. <# .SYNOPSIS Preserve reviewer-entered external_evidence entries across crux regeneration (t/1535, t/1540). .DESCRIPTION Reviewers may attach external_evidence (URL + note + added_by + added_at) to a crux via the Taxonomy Editor. aggregated-cruxes.json is regenerated wholesale by Export-AggregatedCruxes.ps1 — without this helper, the next regeneration would silently destroy that reviewer input. Unlike question_form (regenerable via UsageID), external_evidence is IRREPLACEABLE — a human curator's citation cannot be reconstructed after loss. So this helper is preserve-only: no generation path. Preservation requires BOTH id AND statement (Trim-normalized, case-sensitive -ceq) to match the prior entry. Mismatched statements fall through — the dedup clustering may have reassigned an id to a materially different crux, and riding the old evidence along would silently misattach a citation. On read/parse failure of the previous file, emits a warning and returns zero-preservation stats — crux regeneration MUST NOT be blocked by preservation logic (AC #3, fail-open). Mutates the crux array in place. Returns { Preserved; Dropped }. - Preserved: number of cruxes where non-empty external_evidence carried forward. - Dropped: number of cruxes whose id matched a prior evidence-bearing entry but whose statement had changed — the evidence was intentionally NOT preserved (visible cost of the statement-guard, so callers can tell "no evidence found" from "found but dropped"). #> function Merge-CruxExternalEvidence { [CmdletBinding()] param( [Parameter(Mandatory)] [object]$Cruxes, [Parameter(Mandatory)] [string]$PreviousPath ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' # Build map: id -> @{ Statement (Trim'd); Evidence (array) }. # Only entries with a non-empty external_evidence array are indexed — # nothing else is worth preserving. $ExistingEv = @{} if (Test-Path $PreviousPath) { try { $Existing = Get-Content -Raw $PreviousPath | ConvertFrom-Json if ($Existing.PSObject.Properties['cruxes'] -and $Existing.cruxes) { foreach ($EC in @($Existing.cruxes)) { if (-not $EC.PSObject.Properties['id']) { continue } if (-not $EC.PSObject.Properties['statement']) { continue } if (-not $EC.PSObject.Properties['external_evidence']) { continue } $Ev = @($EC.external_evidence) if (@($Ev).Count -eq 0) { continue } $ExistingEv[[string]$EC.id] = [PSCustomObject]@{ Statement = ([string]$EC.statement).Trim() Evidence = $Ev } } } } catch { # Fail-open: warn and proceed. AC #3 — regeneration must not be blocked # by preservation failure. Write-Warning "Merge-CruxExternalEvidence: could not read $PreviousPath for preservation: $($_.Exception.Message)" return [PSCustomObject]@{ Preserved = 0; Dropped = 0 } } } $Preserved = 0 $Dropped = 0 foreach ($Crux in $Cruxes) { $CId = [string]$Crux.id if (-not $ExistingEv.ContainsKey($CId)) { continue } $Prev = $ExistingEv[$CId] $CurStmt = ([string]$Crux.statement).Trim() if ($Prev.Statement -ceq $CurStmt) { $Crux['external_evidence'] = $Prev.Evidence $Preserved++ } else { Write-Verbose "Merge-CruxExternalEvidence: id ${CId} statement changed — evidence dropped to avoid misattachment" $Dropped++ } } [PSCustomObject]@{ Preserved = $Preserved Dropped = $Dropped } } |