Public/Find-Conflict.ps1
|
# Copyright (c) 2026 Jeffrey Snover. All rights reserved. # Licensed under the MIT License. See LICENSE file in the project root. function Find-Conflict { <# .SYNOPSIS Factual conflict detection and deduplication. .DESCRIPTION Called by Invoke-BatchSummary after each summary is generated. Groups conflicts by Claim ID to prevent duplicate entries. Logic: 1. Read the newly generated summary JSON. 2. For each factual_claim in the summary: a. Check if a conflict file with that claim_id already exists in conflicts/. b. If YES: append a new instance entry to the existing file. c. If NO: create a new conflict file with a generated claim_id. 3. Never delete or overwrite conflict files — append only. .PARAMETER DocId The document ID whose summary should be checked for conflicts. .EXAMPLE Find-Conflict -DocId 'some-document-id' .OUTPUTS PSCustomObject with PSTypeName 'AITriad.ConflictResult' #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$DocId ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $ConflictsDir = Get-ConflictsDir $SummariesDir = Get-SummariesDir # -- Load summary -------------------------------------------------------- $SummaryPath = Join-Path $SummariesDir "$DocId.json" if (-not (Test-Path $SummaryPath)) { Write-Warn "Summary not found: $SummaryPath" return } $summaryObject = Get-Content $SummaryPath -Raw | ConvertFrom-Json -AsHashtable # -- Ensure conflicts/ exists -------------------------------------------- if (-not (Test-Path $ConflictsDir)) { [void](New-Item -Path $ConflictsDir -ItemType Directory -Force) Write-Info "Created conflicts directory" } # -- Counters ------------------------------------------------------------ $today = Get-Date -Format "yyyy-MM-dd" $created = 0 $appended = 0 $skipped = 0 $claims = $summaryObject["factual_claims"] if (-not $claims -or $claims.Count -eq 0) { Write-Info "No factual claims to process for $DocId." return [PSCustomObject]@{ PSTypeName = 'AITriad.ConflictResult' DocId = $DocId ClaimsProcessed = 0 Appended = 0 Created = 0 Skipped = 0 } } $writeErrors = 0 foreach ($claim in $claims) { try { $claimText = $claim["claim"] $claimLabel = $claim["claim_label"] $docPosition = $claim["doc_position"] $hintId = $claim["potential_conflict_id"] $linkedNodes = if ($claim.Contains("linked_taxonomy_nodes") -and $claim["linked_taxonomy_nodes"]) { @($claim["linked_taxonomy_nodes"]) } else { @() } # Normalize stance value $stance = if ($docPosition -in @('supports','disputes','neutral','qualifies')) { $docPosition } else { 'neutral' } $newInstance = [ordered]@{ doc_id = $DocId stance = $stance assertion = $claimText date_flagged = $today } if ($hintId) { # --- Hint ID provided by the model ---------------------------------- $existingPath = Join-Path $ConflictsDir "$hintId.json" if (Test-Path $existingPath) { $conflictData = Get-Content $existingPath -Raw | ConvertFrom-Json -AsHashtable $alreadyLogged = $conflictData["instances"] | Where-Object { $_["doc_id"] -eq $DocId } if ($alreadyLogged) { Write-Info " SKIP duplicate conflict instance: $hintId (doc already logged)" $skipped++ } else { $conflictData["instances"] += $newInstance # Merge in any new linked taxonomy nodes if ($linkedNodes.Count -gt 0) { $existing = @($conflictData["linked_taxonomy_nodes"]) $merged = @(($existing + $linkedNodes) | Select-Object -Unique) $conflictData["linked_taxonomy_nodes"] = $merged } Set-Content -Path $existingPath -Value ($conflictData | ConvertTo-Json -Depth 10) -Encoding UTF8 Write-OK " Appended to existing conflict: $hintId" $appended++ } } else { Write-Warn " Suggested conflict '$hintId' not found — creating new file" $newConflict = [ordered]@{ claim_id = $hintId claim_label = if ($claimLabel) { $claimLabel } else { $claimText.Substring(0, [Math]::Min(80, $claimText.Length)) } description = $claimText status = "open" linked_taxonomy_nodes = $linkedNodes instances = @($newInstance) human_notes = @() } Set-Content -Path $existingPath -Value ($newConflict | ConvertTo-Json -Depth 10) -Encoding UTF8 Write-OK " Created new conflict file: $hintId.json" $created++ } } else { # --- No hint — generate ID from claim text -------------------------- $slug = $claimText.ToLower() -replace '[^\w\s]', '' -replace '\s+', '-' $slug = $slug.Substring(0, [Math]::Min(40, $slug.Length)).TrimEnd('-') $newId = "conflict-$slug-$($DocId.Substring(0,[Math]::Min(8,$DocId.Length)))" $existingMatch = Get-ChildItem $ConflictsDir -Filter "*.json" | Where-Object { $_.BaseName -like "*$($slug.Substring(0,[Math]::Min(20,$slug.Length)))*" } | Select-Object -First 1 if ($existingMatch) { $conflictData = Get-Content $existingMatch.FullName -Raw | ConvertFrom-Json -AsHashtable $alreadyLogged = $conflictData["instances"] | Where-Object { $_["doc_id"] -eq $DocId } if ($alreadyLogged) { Write-Info " SKIP duplicate (fuzzy match): $($existingMatch.BaseName)" $skipped++ } else { $conflictData["instances"] += $newInstance # Merge in any new linked taxonomy nodes if ($linkedNodes.Count -gt 0) { $existing = @($conflictData["linked_taxonomy_nodes"]) $merged = @(($existing + $linkedNodes) | Select-Object -Unique) $conflictData["linked_taxonomy_nodes"] = $merged } Set-Content -Path $existingMatch.FullName -Value ($conflictData | ConvertTo-Json -Depth 10) -Encoding UTF8 Write-OK " Appended to fuzzy-matched conflict: $($existingMatch.BaseName)" $appended++ } } else { $newConflictPath = Join-Path $ConflictsDir "$newId.json" $newConflict = [ordered]@{ claim_id = $newId claim_label = if ($claimLabel) { $claimLabel } else { $claimText.Substring(0, [Math]::Min(80, $claimText.Length)) } description = $claimText status = "open" linked_taxonomy_nodes = $linkedNodes instances = @($newInstance) human_notes = @() } Set-Content -Path $newConflictPath -Value ($newConflict | ConvertTo-Json -Depth 10) -Encoding UTF8 Write-OK " Created new conflict file: $newId.json" $created++ } } } catch { $writeErrors++ Write-Warn " Failed to write conflict file for claim — $($_.Exception.Message)" } } if ($writeErrors -gt 0) { Write-Warn "$writeErrors conflict file write(s) failed. Check disk space and permissions." } # -- Return result object ------------------------------------------------ $processed = $created + $appended + $skipped Write-Info "Conflict detection complete: $processed claims — $created created, $appended appended, $skipped skipped" [PSCustomObject]@{ PSTypeName = 'AITriad.ConflictResult' DocId = $DocId ClaimsProcessed = $processed Appended = $appended Created = $created Skipped = $skipped } } |