Private/Test-OrgStanceClaim.ps1

# Copyright (c) 2026 Jeffrey Snover. All rights reserved.
# Licensed under the MIT License. See LICENSE file in the project root.

function Test-OrgStanceClaim {
    <#
    .SYNOPSIS
        Post-parse boundary validator for a single extracted stance claim (t/1553#12).
    .DESCRIPTION
        The Stage 1 UsageID lost its responseSchema after Gemini's structured-output
        validator rejected it live (400 across all fallback backends). CL condition:
        the cmdlet must validate each claim itself since the schema no longer guards
        the boundary. Rules:
 
          - polarity in { asserts, opposes }
          - extraction_confidence a number in [0.0, 1.0]
          - canonical_proposition non-empty and ≤ 60 words (prompt says ≤30 words,
            60 is a lenient upper bound for the drop threshold — cutoffs below
            leave room for near-miss compressions that CL can still audit)
          - text non-empty
 
        Returns { Ok, Reason } — caller drops the claim and logs the Reason when
        Ok=$false.
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory)]
        [object]$Claim
    )

    Set-StrictMode -Version Latest

    if ($null -eq $Claim) {
        return [PSCustomObject]@{ Ok = $false; Reason = 'null claim' }
    }
    if (-not $Claim.PSObject.Properties['polarity']) {
        return [PSCustomObject]@{ Ok = $false; Reason = 'missing polarity' }
    }
    $pol = [string]$Claim.polarity
    if ($pol -notin @('asserts', 'opposes')) {
        return [PSCustomObject]@{ Ok = $false; Reason = "polarity '$pol' not in {asserts, opposes}" }
    }

    if (-not $Claim.PSObject.Properties['extraction_confidence']) {
        return [PSCustomObject]@{ Ok = $false; Reason = 'missing extraction_confidence' }
    }
    $conf = 0.0
    try { $conf = [double]$Claim.extraction_confidence } catch {
        return [PSCustomObject]@{ Ok = $false; Reason = "extraction_confidence not numeric: '$($Claim.extraction_confidence)'" }
    }
    if ($conf -lt 0.0 -or $conf -gt 1.0) {
        return [PSCustomObject]@{ Ok = $false; Reason = "extraction_confidence $conf out of [0,1]" }
    }

    if (-not $Claim.PSObject.Properties['canonical_proposition']) {
        return [PSCustomObject]@{ Ok = $false; Reason = 'missing canonical_proposition' }
    }
    $prop = [string]$Claim.canonical_proposition
    if ([string]::IsNullOrWhiteSpace($prop)) {
        return [PSCustomObject]@{ Ok = $false; Reason = 'canonical_proposition empty' }
    }
    $wordCount = @($prop -split '\s+' | Where-Object { $_ }).Count
    if ($wordCount -gt 60) {
        return [PSCustomObject]@{ Ok = $false; Reason = "canonical_proposition too long: $wordCount words (> 60)" }
    }

    if (-not $Claim.PSObject.Properties['text']) {
        return [PSCustomObject]@{ Ok = $false; Reason = 'missing text' }
    }
    if ([string]::IsNullOrWhiteSpace([string]$Claim.text)) {
        return [PSCustomObject]@{ Ok = $false; Reason = 'text empty' }
    }

    [PSCustomObject]@{ Ok = $true; Reason = $null }
}