<#
.SYNOPSIS
  Corrections required by the third referee report: paired citation analysis, notice-level sensitivity,
  Mantel-Haenszel risk-ratio interval and heterogeneity accounting.
.DESCRIPTION
  1. Citation analysis restricted to the 1:1 matched pairs, after de-duplicating retrieved DOIs, with a
     selection flow table and standardised mean differences for balance.
  2. Notice-level sensitivity that removes the two genuinely multi-article notices individually and
     together, replacing the arbitrary "ten largest notices" analysis, plus the median-per-notice
     aggregation reported separately from the one-record-per-notice variant.
  3. Greenland-Robins confidence interval for the Mantel-Haenszel risk ratio, and an explicit count of
     the strata contributing to the Cochran heterogeneity test.
.EXAMPLE
  .\Analyze-Fixes.ps1 -Csv ..\data\retraction_watch_gitlab.csv -RunDir ..\runs\ai-retractions-v3
#>
[CmdletBinding()]
param(
    [Parameter(Mandatory = $true)][string]$Csv,
    [Parameter(Mandatory = $true)][string]$RunDir,
    [string]$ReasonPattern = 'Computer-Aided Content|Computer-Generated Content',
    [int]$SnapshotYear = 2026
)
$ErrorActionPreference = 'Stop'
$inv = [Globalization.CultureInfo]::InvariantCulture
$tab = Join-Path $RunDir 'tables'

function Get-Q { param([double[]]$S, [double]$P)
    if ($S.Length -eq 0) { return 0.0 }
    $i = ($S.Length - 1) * $P
    $lo = [int][Math]::Floor($i); $hi = [int][Math]::Ceiling($i)
    if ($lo -eq $hi) { return $S[$lo] }
    return $S[$lo] + ($i - $lo) * ($S[$hi] - $S[$lo])
}
function Get-Med { param([double[]]$V) $a = $V.Clone(); [Array]::Sort($a); return (Get-Q $a 0.5) }
function Get-NormalCdf { param([double]$Z)
    if ($Z -lt -8.5) { return 0.0 }; if ($Z -gt 8.5) { return 1.0 }
    $t = 1.0 / (1.0 + 0.2316419 * [Math]::Abs($Z))
    $d = 0.3989422804014327 * [Math]::Exp(-$Z * $Z / 2.0)
    $p = $d * $t * (0.319381530 + $t * (-0.356563782 + $t * (1.781477937 + $t * (-1.821255978 + $t * 1.330274429))))
    if ($Z -ge 0) { return 1.0 - $p }
    return $p
}
function Format-P { param([double]$P)
    if ($P -le 0) { return '< 1e-15' }
    if ($P -lt 0.001) { return ('{0:0.0e+0}' -f $P) }
    return ('{0:0.000}' -f $P)
}
function Get-PZ { param([double]$Z) $a = [Math]::Abs($Z); if ($a -gt 8.2) { return 0.0 }; return 2.0 * (1.0 - (Get-NormalCdf $a)) }
function Get-Wilson { param([int]$K, [int]$N)
    if ($N -eq 0) { return [pscustomobject]@{ P = 0; Lo = 0; Hi = 0 } }
    $z = 1.959963985; $p = [double]$K / $N; $den = 1.0 + $z * $z / $N
    $c = ($p + $z * $z / (2.0 * $N)) / $den
    $h = ($z * [Math]::Sqrt($p * (1 - $p) / $N + $z * $z / (4.0 * $N * $N))) / $den
    return [pscustomobject]@{ P = [Math]::Round(100 * $p, 2); Lo = [Math]::Round(100 * ($c - $h), 2); Hi = [Math]::Round(100 * ($c + $h), 2) }
}
function Test-MW { param([double[]]$A, [double[]]$B)
    $n1 = $A.Length; $n2 = $B.Length; $N = $n1 + $n2
    $v = New-Object 'double[]' $N; $g = New-Object 'int[]' $N
    [Array]::Copy($A, 0, $v, 0, $n1); [Array]::Copy($B, 0, $v, $n1, $n2)
    for ($i = 0; $i -lt $n1; $i++) { $g[$i] = 1 }
    for ($i = $n1; $i -lt $N; $i++) { $g[$i] = 2 }
    [Array]::Sort($v, $g)
    $r1 = 0.0; $tie = 0.0; $i = 0
    while ($i -lt $N) {
        $j = $i
        while (($j + 1) -lt $N -and $v[$j + 1] -eq $v[$i]) { $j++ }
        $t = $j - $i + 1; $avg = ($i + 1 + $j + 1) / 2.0
        if ($t -gt 1) { $tie += ([double]$t * $t * $t - $t) }
        for ($k = $i; $k -le $j; $k++) { if ($g[$k] -eq 1) { $r1 += $avg } }
        $i = $j + 1
    }
    $u1 = $r1 - ($n1 * ($n1 + 1.0) / 2.0)
    $sd = [Math]::Sqrt(($n1 * $n2 / 12.0) * (($N + 1.0) - $tie / ([double]$N * ($N - 1.0))))
    $z = ($u1 - $n1 * $n2 / 2.0) / $sd
    return [pscustomobject]@{ Z = [Math]::Round($z, 3); Pstr = Format-P (Get-PZ $z); Delta = [Math]::Round(2.0 * $u1 / ($n1 * $n2) - 1.0, 4) }
}
function Test-BF { param([double[]]$A, [double[]]$B)
    $ma = Get-Med $A; $mb = Get-Med $B
    $n1 = $A.Length; $n2 = $B.Length
    $s1 = 0.0; foreach ($x in $A) { $s1 += [Math]::Abs($x - $ma) }; $m1 = $s1 / $n1
    $s2 = 0.0; foreach ($x in $B) { $s2 += [Math]::Abs($x - $mb) }; $m2 = $s2 / $n2
    $v1 = 0.0; foreach ($x in $A) { $d = [Math]::Abs($x - $ma) - $m1; $v1 += $d * $d }; $v1 = $v1 / ($n1 - 1)
    $v2 = 0.0; foreach ($x in $B) { $d = [Math]::Abs($x - $mb) - $m2; $v2 += $d * $d }; $v2 = $v2 / ($n2 - 1)
    $se = [Math]::Sqrt($v1 / $n1 + $v2 / $n2)
    $t = ($m1 - $m2) / $se
    return [pscustomobject]@{ Mad1 = [Math]::Round($m1, 1); Mad2 = [Math]::Round($m2, 1); Ratio = [Math]::Round($m1 / $m2, 3); T = [Math]::Round($t, 3); Pstr = Format-P (Get-PZ $t) }
}
function Test-Prop { param([int]$K1, [int]$N1, [int]$K2, [int]$N2)
    $p1 = [double]$K1 / $N1; $p2 = [double]$K2 / $N2
    $pp = [double]($K1 + $K2) / ($N1 + $N2)
    $z = ($p1 - $p2) / [Math]::Sqrt($pp * (1 - $pp) * (1.0 / $N1 + 1.0 / $N2))
    $seD = [Math]::Sqrt($p1 * (1 - $p1) / $N1 + $p2 * (1 - $p2) / $N2)
    return [pscustomobject]@{ Pct1 = [Math]::Round(100 * $p1, 2); Pct2 = [Math]::Round(100 * $p2, 2)
        DiffPP = [Math]::Round(100 * ($p1 - $p2), 2)
        Lo = [Math]::Round(100 * (($p1 - $p2) - 1.959963985 * $seD), 2)
        Hi = [Math]::Round(100 * (($p1 - $p2) + 1.959963985 * $seD), 2)
        Z = [Math]::Round($z, 2); Pstr = Format-P (Get-PZ $z) }
}
function Get-Smd { param([double[]]$A, [double[]]$B)
    $m1 = ($A | Measure-Object -Average).Average; $m2 = ($B | Measure-Object -Average).Average
    $v1 = 0.0; foreach ($x in $A) { $v1 += ($x - $m1) * ($x - $m1) }; $v1 = $v1 / [Math]::Max(1, $A.Length - 1)
    $v2 = 0.0; foreach ($x in $B) { $v2 += ($x - $m2) * ($x - $m2) }; $v2 = $v2 / [Math]::Max(1, $B.Length - 1)
    $pooled = [Math]::Sqrt(($v1 + $v2) / 2.0)
    $smd = 0.0; if ($pooled -gt 0) { $smd = ($m1 - $m2) / $pooled }
    return [pscustomobject]@{ Mean1 = [Math]::Round($m1, 2); Mean2 = [Math]::Round($m2, 2); Smd = [Math]::Round($smd, 3) }
}

# ================ PART 1: paired citation analysis ================
Write-Host "Part 1: paired citation analysis ..." -ForegroundColor Cyan
$rowsRaw = @(Import-Csv (Join-Path $tab 't19_citations_full.csv'))
$pairs = @(Import-Csv (Join-Path $tab 't18_matched_pairs.csv'))
# de-duplicate retrieved records by DOI, keeping the first occurrence
$seen = @{}; $rows = New-Object System.Collections.ArrayList
foreach ($r in $rowsRaw) { $d = $r.Doi.ToLower(); if ($seen.ContainsKey($d)) { continue }; $seen[$d] = $true; [void]$rows.Add($r) }
$dupRemoved = $rowsRaw.Count - $rows.Count
$byDoi = @{}
foreach ($r in $rows) { $byDoi[$r.Doi.ToLower()] = $r }

$complete = New-Object System.Collections.ArrayList
foreach ($p in $pairs) {
    $m = $p.MachineDoi.ToLower(); $c = $p.MatchDoi.ToLower()
    if ($byDoi.ContainsKey($m) -and $byDoi.ContainsKey($c)) {
        [void]$complete.Add([pscustomobject]@{ M = $byDoi[$m]; C = $byDoi[$c]; Tier = $p.MatchTier })
    }
}
$flow = @(
    [pscustomobject]@{ Step = 'Machine-attributed records with usable DOI and dates'; Machine = 9142; Controls = '' }
    [pscustomobject]@{ Step = 'Successfully paired 1:1 (without replacement)'; Machine = $pairs.Count; Controls = $pairs.Count }
    [pscustomobject]@{ Step = 'Could not be paired'; Machine = (9142 - $pairs.Count); Controls = '' }
    [pscustomobject]@{ Step = 'Duplicate retrieved records removed'; Machine = $dupRemoved; Controls = '' }
    [pscustomobject]@{ Step = 'Pairs with BOTH members retrieved in OpenAlex (analysed)'; Machine = $complete.Count; Controls = $complete.Count }
)
$flow | Export-Csv (Join-Path $tab 't27_citation_flow.csv') -NoTypeInformation -Encoding UTF8

$mSet = @($complete | ForEach-Object { $_.M })
$cSet = @($complete | ForEach-Object { $_.C })
# balance
$bal = @(
    (Get-Smd ([double[]]@($mSet | ForEach-Object { [double]$_.UpToRetraction })) ([double[]]@($cSet | ForEach-Object { [double]$_.UpToRetraction }))) | Select-Object @{n = 'Covariate'; e = { 'Pre-retraction citations' } }, Mean1, Mean2, Smd
    (Get-Smd ([double[]]@($mSet | ForEach-Object { [double]$_.PubYear })) ([double[]]@($cSet | ForEach-Object { [double]$_.PubYear }))) | Select-Object @{n = 'Covariate'; e = { 'Publication year' } }, Mean1, Mean2, Smd
    (Get-Smd ([double[]]@($mSet | ForEach-Object { [double]$_.RetractionYear })) ([double[]]@($cSet | ForEach-Object { [double]$_.RetractionYear }))) | Select-Object @{n = 'Covariate'; e = { 'Retraction year' } }, Mean1, Mean2, Smd
    (Get-Smd ([double[]]@($mSet | ForEach-Object { [double]$_.ExposureYears })) ([double[]]@($cSet | ForEach-Object { [double]$_.ExposureYears }))) | Select-Object @{n = 'Covariate'; e = { 'Exposure years' } }, Mean1, Mean2, Smd
)
$tier1 = @($complete | Where-Object { $_.Tier -eq '1' }).Count
$bal += [pscustomobject]@{ Covariate = 'Publisher matched exactly (proportion)'; Mean1 = [Math]::Round($tier1 / [double]$complete.Count, 3); Mean2 = [Math]::Round($tier1 / [double]$complete.Count, 3); Smd = 0 }
$bal | Export-Csv (Join-Path $tab 't28_citation_balance.csv') -NoTypeInformation -Encoding UTF8

function Get-CitStats { param($Set, [string]$Label)
    $n = $Set.Count
    $we = @($Set | Where-Object { [int]$_.ExposureYears -ge 1 })
    $rate = [double[]]@($we | ForEach-Object { [double]$_.AfterRetraction / [double]$_.ExposureYears }); [Array]::Sort($rate)
    $tot = [double[]]@($Set | ForEach-Object { [double]$_.TotalCitations }); [Array]::Sort($tot)
    $anyPost = @($Set | Where-Object { [int]$_.AfterRetraction -gt 0 }).Count
    $e1 = @($Set | Where-Object { [int]$_.ExposureYears -ge 1 }); $c1 = @($e1 | Where-Object { [int]$_.CitesY1 -gt 0 }).Count
    $e2 = @($Set | Where-Object { [int]$_.ExposureYears -ge 2 }); $c2 = @($e2 | Where-Object { ([int]$_.CitesY1 + [int]$_.CitesY2) -gt 0 }).Count
    $e3 = @($Set | Where-Object { [int]$_.ExposureYears -ge 3 }); $c3 = @($e3 | Where-Object { ([int]$_.CitesY1 + [int]$_.CitesY2 + [int]$_.CitesY3) -gt 0 }).Count
    $flag = @($Set | Where-Object { $_.FlaggedRetOA -eq 'True' -or $_.FlaggedRetOA -eq $true }).Count
    return [pscustomobject]@{ Label = $Label; N = $n; NRate = $we.Count
        TotalMed = [Math]::Round((Get-Q $tot 0.5), 1); TotalQ1 = [Math]::Round((Get-Q $tot 0.25), 1); TotalQ3 = [Math]::Round((Get-Q $tot 0.75), 1)
        RateMed = [Math]::Round((Get-Q $rate 0.5), 3); RateQ1 = [Math]::Round((Get-Q $rate 0.25), 3); RateQ3 = [Math]::Round((Get-Q $rate 0.75), 3)
        RateMean = [Math]::Round((($rate | Measure-Object -Average).Average), 3)
        AnyK = $anyPost; AnyN = $n; W1K = $c1; W1N = $e1.Count; W2K = $c2; W2N = $e2.Count; W3K = $c3; W3N = $e3.Count
        FlagK = $flag; RateArr = $rate }
}
$sM = Get-CitStats $mSet 'Machine-attributed'
$sC = Get-CitStats $cSet 'Matched controls'
$mwRate = Test-MW -A $sM.RateArr -B $sC.RateArr
$pAny = Test-Prop -K1 $sM.AnyK -N1 $sM.AnyN -K2 $sC.AnyK -N2 $sC.AnyN
$p1 = Test-Prop -K1 $sM.W1K -N1 $sM.W1N -K2 $sC.W1K -N2 $sC.W1N
$p2 = Test-Prop -K1 $sM.W2K -N1 $sM.W2N -K2 $sC.W2K -N2 $sC.W2N
$p3 = Test-Prop -K1 $sM.W3K -N1 $sM.W3N -K2 $sC.W3K -N2 $sC.W3N
$pFlag = Test-Prop -K1 $sM.FlagK -N1 $sM.N -K2 $sC.FlagK -N2 $sC.N
@($sM, $sC) | Select-Object * -ExcludeProperty RateArr | Export-Csv (Join-Path $tab 't29_citation_paired_summary.csv') -NoTypeInformation -Encoding UTF8

# ================ PART 2: notice-level sensitivity ================
Write-Host "Part 2: notice-level sensitivity ..." -ForegroundColor Cyan
$ret = @(Import-Csv $Csv | Where-Object { $_.RetractionNature -eq 'Retraction' })
foreach ($r in $ret) {
    $od = $null; $rd = $null
    try { $od = [datetime]::Parse($r.OriginalPaperDate, $inv) } catch { }
    try { $rd = [datetime]::Parse($r.RetractionDate, $inv) } catch { }
    $lag = $null; if ($od -and $rd -and $rd -ge $od) { $lag = ($rd - $od).TotalDays }
    $n = $r.RetractionDOI; if (-not $n -or $n -match '^(unavailable|Unavailable)$') { $n = 'NA-' + $r.'Record ID' }
    Add-Member -InputObject $r -NotePropertyName LagDays -NotePropertyValue $lag -Force
    Add-Member -InputObject $r -NotePropertyName Notice -NotePropertyValue $n -Force
    Add-Member -InputObject $r -NotePropertyName IsMachine -NotePropertyValue ($r.Reason -match $ReasonPattern) -Force
    Add-Member -InputObject $r -NotePropertyName IsMill -NotePropertyValue ($r.Reason -match 'Paper Mill') -Force
}
$ai = @($ret | Where-Object { $_.IsMachine })
$other = @($ret | Where-Object { -not $_.IsMachine })
$lagOt = [double[]]@($other | Where-Object { $null -ne $_.LagDays } | ForEach-Object { [double]$_.LagDays })
$grp = @($ai | Group-Object Notice)
$big = @($grp | Where-Object { $_.Count -gt 1 } | Sort-Object Count -Descending)
$bigNames = @($big | ForEach-Object { $_.Name })
$sens = New-Object System.Collections.ArrayList
$defs = @(
    @{ L = 'Full machine cohort'; S = $ai }
    @{ L = ("Excluding notice A (" + $big[0].Count + " articles)"); S = @($ai | Where-Object { $_.Notice -ne $big[0].Name }) }
    @{ L = ("Excluding notice B (" + $big[1].Count + " articles)"); S = @($ai | Where-Object { $_.Notice -ne $big[1].Name }) }
    @{ L = 'Excluding both multi-article notices'; S = @($ai | Where-Object { $bigNames -notcontains $_.Notice }) }
)
foreach ($d in $defs) {
    $lg = [double[]]@($d.S | Where-Object { $null -ne $_.LagDays } | ForEach-Object { [double]$_.LagDays })
    $mw = Test-MW -A $lg -B $lagOt; $bf = Test-BF -A $lg -B $lagOt
    $mill = @($d.S | Where-Object { $_.IsMill }).Count
    $w = Get-Wilson $mill $d.S.Count
    [void]$sens.Add([pscustomobject]@{ Analysis = $d.L; N = $d.S.Count; Median = [Math]::Round((Get-Med $lg), 1)
            MeanAbsDev = $bf.Mad1; LocDelta = $mw.Delta; LocP = $mw.Pstr; DispRatio = $bf.Ratio; DispP = $bf.Pstr
            MillPct = $w.P; MillLo = $w.Lo; MillHi = $w.Hi })
}
# median-per-notice aggregation, reported separately
function Get-NoticeMedians { param($Set)
    $o = New-Object 'System.Collections.Generic.List[double]'
    foreach ($g in @($Set | Group-Object Notice)) {
        $l = [double[]]@($g.Group | Where-Object { $null -ne $_.LagDays } | ForEach-Object { [double]$_.LagDays })
        if ($l.Length -gt 0) { $o.Add((Get-Med $l)) }
    }
    return $o.ToArray()
}
$nm = Get-NoticeMedians $ai; $no = Get-NoticeMedians $other
$mwN = Test-MW -A $nm -B $no; $bfN = Test-BF -A $nm -B $no
[void]$sens.Add([pscustomobject]@{ Analysis = 'Notice level, median lag per notice'; N = $nm.Length; Median = [Math]::Round((Get-Med $nm), 1)
        MeanAbsDev = $bfN.Mad1; LocDelta = $mwN.Delta; LocP = $mwN.Pstr; DispRatio = $bfN.Ratio; DispP = $bfN.Pstr
        MillPct = ''; MillLo = ''; MillHi = '' })
$sens | Export-Csv (Join-Path $tab 't30_notice_sensitivity.csv') -NoTypeInformation -Encoding UTF8

# ================ PART 3: MH risk-ratio interval and heterogeneity accounting ================
Write-Host "Part 3: Mantel-Haenszel interval ..." -ForegroundColor Cyan
$bigPubs = @(@($ai | Where-Object { $_.Publisher } | Group-Object Publisher | Sort-Object Count -Descending | Select-Object -First 6) | ForEach-Object { $_.Name })
$strata = @{}
foreach ($r in $ret) {
    $ry = $null; try { $ry = [datetime]::Parse($r.RetractionDate, $inv).Year } catch { continue }
    $pg = 'Other publishers'; if ($bigPubs -contains $r.Publisher) { $pg = $r.Publisher }
    $k = "$ry|$pg"
    if (-not $strata.ContainsKey($k)) { $strata[$k] = [pscustomobject]@{ A = 0; B = 0; C = 0; D = 0 } }
    $s = $strata[$k]
    if ($r.IsMachine) { if ($r.IsMill) { $s.A++ } else { $s.B++ } } else { if ($r.IsMill) { $s.C++ } else { $s.D++ } }
}
$rNum = 0.0; $rDen = 0.0; $pSum = 0.0; $used = 0; $qEligible = 0
foreach ($k in $strata.Keys) {
    $s = $strata[$k]; $n = $s.A + $s.B + $s.C + $s.D
    if ($n -eq 0) { continue }
    $n1 = $s.A + $s.B; $n0 = $s.C + $s.D
    if ($n1 -eq 0 -or $n0 -eq 0) { continue }
    if (($s.A + $s.C) -eq 0) { continue }
    $used++
    $rNum += ($s.A * $n0) / $n
    $rDen += ($s.C * $n1) / $n
    $pSum += (($n1 * $n0 * ($s.A + $s.C)) - ($s.A * $s.C * $n)) / ([double]$n * $n)
    if ($s.A -gt 0 -and $s.B -gt 0 -and $s.C -gt 0 -and $s.D -gt 0) { $qEligible++ }
}
$rrMh = $rNum / $rDen
$varLn = $pSum / ($rNum * $rDen)
$seLn = [Math]::Sqrt($varLn)
$mhOut = [pscustomobject]@{
    MantelHaenszelRiskRatio = [Math]::Round($rrMh, 3)
    RrCiLo = [Math]::Round(($rrMh * [Math]::Exp(-1.959963985 * $seLn)), 3)
    RrCiHi = [Math]::Round(($rrMh * [Math]::Exp(1.959963985 * $seLn)), 3)
    VarianceMethod = 'Greenland-Robins'
    StrataContributingToPooledEstimate = $used
    StrataWithAllFourCellsNonZero = $qEligible
    CochranQDegreesOfFreedom = ($qEligible - 1)
    Note = 'The Cochran Q test uses only strata in which all four cells are non-zero, because the stratum-specific log odds ratio and its variance are otherwise undefined. This is why the degrees of freedom are fewer than the number of strata contributing to the pooled estimate.'
}
$mhOut | Export-Csv (Join-Path $tab 't31_mh_riskratio_ci.csv') -NoTypeInformation -Encoding UTF8

# ================ report ================
$sb = New-Object System.Text.StringBuilder
[void]$sb.AppendLine("# Third-round corrections")
[void]$sb.AppendLine("")
[void]$sb.AppendLine("Generated $(Get-Date -Format 'yyyy-MM-dd HH:mm').")
[void]$sb.AppendLine("")
[void]$sb.AppendLine("## Citation selection flow")
[void]$sb.AppendLine("| Step | Machine | Controls |"); [void]$sb.AppendLine("|---|---|---|")
foreach ($f in $flow) { [void]$sb.AppendLine("| $($f.Step) | $($f.Machine) | $($f.Controls) |") }
[void]$sb.AppendLine("")
[void]$sb.AppendLine("Duplicate retrieved records removed: $dupRemoved. Controls were drawn without replacement, so each control appears at most once.")
[void]$sb.AppendLine("")
[void]$sb.AppendLine("## Covariate balance (standardised mean differences)")
[void]$sb.AppendLine("| Covariate | Machine mean | Control mean | SMD |"); [void]$sb.AppendLine("|---|---|---|---|")
foreach ($b in $bal) { [void]$sb.AppendLine("| $($b.Covariate) | $($b.Mean1) | $($b.Mean2) | $($b.Smd) |") }
[void]$sb.AppendLine("")
[void]$sb.AppendLine("## Paired citation results (n = $($complete.Count) pairs)")
[void]$sb.AppendLine("| Measure | Machine | Controls | Difference (95% CI) | Test |"); [void]$sb.AppendLine("|---|---|---|---|---|")
[void]$sb.AppendLine("| Total citations, median (IQR) | $($sM.TotalMed) ($($sM.TotalQ1)-$($sM.TotalQ3)) | $($sC.TotalMed) ($($sC.TotalQ1)-$($sC.TotalQ3)) | | |")
[void]$sb.AppendLine("| Post-retraction citations per article-year, median (IQR) | $($sM.RateMed) ($($sM.RateQ1)-$($sM.RateQ3)) | $($sC.RateMed) ($($sC.RateQ1)-$($sC.RateQ3)) | | z = $($mwRate.Z), p $($mwRate.Pstr), delta $($mwRate.Delta) |")
[void]$sb.AppendLine("| Post-retraction citations per article-year, mean | $($sM.RateMean) | $($sC.RateMean) | | |")
[void]$sb.AppendLine("| Cited in any post-retraction calendar year | $($pAny.Pct1)% | $($pAny.Pct2)% | $($pAny.DiffPP) pp ($($pAny.Lo) to $($pAny.Hi)) | z = $($pAny.Z), p $($pAny.Pstr) |")
[void]$sb.AppendLine("| Cited in the first post-retraction calendar year | $($p1.Pct1)% (n = $($sM.W1N)) | $($p1.Pct2)% (n = $($sC.W1N)) | $($p1.DiffPP) pp ($($p1.Lo) to $($p1.Hi)) | p $($p1.Pstr) |")
[void]$sb.AppendLine("| Cited in the first two post-retraction calendar years | $($p2.Pct1)% (n = $($sM.W2N)) | $($p2.Pct2)% (n = $($sC.W2N)) | $($p2.DiffPP) pp ($($p2.Lo) to $($p2.Hi)) | p $($p2.Pstr) |")
[void]$sb.AppendLine("| Cited in the first three post-retraction calendar years | $($p3.Pct1)% (n = $($sM.W3N)) | $($p3.Pct2)% (n = $($sC.W3N)) | $($p3.DiffPP) pp ($($p3.Lo) to $($p3.Hi)) | p $($p3.Pstr) |")
[void]$sb.AppendLine("| Flagged as retracted in OpenAlex | $($pFlag.Pct1)% | $($pFlag.Pct2)% | $($pFlag.DiffPP) pp ($($pFlag.Lo) to $($pFlag.Hi)) | z = $($pFlag.Z), p $($pFlag.Pstr) |")
[void]$sb.AppendLine("")
[void]$sb.AppendLine("## Notice-level sensitivity (replacing the arbitrary ten-largest-notices analysis)")
[void]$sb.AppendLine("| Analysis | n | Median | Mean abs dev | Location delta | Location p | Dispersion ratio | Dispersion p | Paper mill % |")
[void]$sb.AppendLine("|---|---|---|---|---|---|---|---|---|")
foreach ($s in $sens) { [void]$sb.AppendLine("| $($s.Analysis) | $($s.N) | $($s.Median) | $($s.MeanAbsDev) | $($s.LocDelta) | $($s.LocP) | $($s.DispRatio) | $($s.DispP) | $($s.MillPct) |") }
[void]$sb.AppendLine("")
[void]$sb.AppendLine("## Mantel-Haenszel risk ratio with Greenland-Robins interval")
[void]$sb.AppendLine("Adjusted risk ratio $($mhOut.MantelHaenszelRiskRatio) (95% CI $($mhOut.RrCiLo) to $($mhOut.RrCiHi)).")
[void]$sb.AppendLine("Strata contributing to the pooled estimate: $($mhOut.StrataContributingToPooledEstimate). Strata with all four cells non-zero, and therefore contributing to the Cochran Q test: $($mhOut.StrataWithAllFourCellsNonZero), giving $($mhOut.CochranQDegreesOfFreedom) degrees of freedom.")
[void]$sb.AppendLine("")
[void]$sb.AppendLine($mhOut.Note)
Set-Content -Path (Join-Path $RunDir 'fixes-v4.md') -Value $sb.ToString() -Encoding UTF8
Write-Host "`nWrote fixes-v4.md and tables t27-t31" -ForegroundColor Green
$flow | Format-Table -AutoSize
$sens | Format-Table Analysis, N, Median, MeanAbsDev, LocDelta, LocP, DispRatio, DispP, MillPct -AutoSize
$mhOut | Format-List
