<#
.SYNOPSIS
  Supplementary analyses with corrected cohort filtering, separated location and dispersion, notice-level clustering, regime definitions and stratified co-coding.
.DESCRIPTION
  Issue 4: regime analysis restricted to publication cohorts with at least 12, 18 and 24 months of
           possible follow-up, so the recent-cohort result cannot be driven by differential follow-up.
  Issue 5: expanded, word-boundary search for generative-AI terminology, with every match listed.
  Issue 6: Mantel-Haenszel analysis of paper-mill co-coding adjusted for retraction year and publisher,
           with a Cochran heterogeneity test across strata.
  Issue 7: notice-level aggregation computed properly - per-notice median, minimum and maximum lag and
           proportion carrying the paper-mill flag - rather than selecting one arbitrary record.
.EXAMPLE
  .\Analyze-Supplement.ps1 -Csv ..\data\retraction_watch_gitlab.csv -OutDir ..\runs\ai-retractions-v3
#>
[CmdletBinding()]
param(
    [Parameter(Mandatory = $true)][string]$Csv,
    [Parameter(Mandatory = $true)][string]$OutDir,
    [string]$ReasonPattern = 'Computer-Aided Content|Computer-Generated Content',
    [string]$SnapshotDate = '2026-07-29'
)
$ErrorActionPreference = 'Stop'
New-Item -ItemType Directory -Force -Path (Join-Path $OutDir 'tables') | Out-Null
$inv = [Globalization.CultureInfo]::InvariantCulture
$snap = [datetime]::Parse($SnapshotDate, $inv)

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-PFromZ { param([double]$Z)
    $az = [Math]::Abs($Z); if ($az -gt 8.2) { return 0.0 }
    return 2.0 * (1.0 - (Get-NormalCdf $az))
}
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
    if ($n1 -lt 2 -or $n2 -lt 2) { return $null }
    $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-PFromZ $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-PFromZ $t) }
}

# ---------------- load ----------------
Write-Host "Loading $Csv ..." -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 }
    $notice = $r.RetractionDOI
    if (-not $notice -or $notice -match '^(unavailable|Unavailable)$') { $notice = 'NA-' + $r.'Record ID' }
    Add-Member -InputObject $r -NotePropertyName OrigDate -NotePropertyValue $od -Force
    Add-Member -InputObject $r -NotePropertyName RetYear -NotePropertyValue $(if ($rd) { $rd.Year } else { $null }) -Force
    Add-Member -InputObject $r -NotePropertyName LagDays -NotePropertyValue $lag -Force
    Add-Member -InputObject $r -NotePropertyName Notice -NotePropertyValue $notice -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 })
Write-Host ("  retractions {0} | machine {1} | comparison {2}" -f $ret.Count, $ai.Count, $other.Count)

# ================ ISSUE 4: follow-up-restricted regime analysis ================
Write-Host "Issue 4: follow-up-restricted regime analysis ..." -ForegroundColor Cyan
$cuts = @('2022-11-30', '2023-07-01', '2024-01-01', '2024-07-01')
$fu = New-Object System.Collections.ArrayList
foreach ($c in $cuts) {
    $cd = [datetime]::Parse($c, $inv)
    foreach ($months in @(0, 12, 18, 24)) {
        $limit = $snap.AddMonths(-$months)
        $post = @($ai | Where-Object { $_.OrigDate -and $_.OrigDate -ge $cd -and $_.OrigDate -le $limit })
        $pre = @($ai | Where-Object { $_.OrigDate -and $_.OrigDate -lt $cd -and $_.OrigDate -le $limit })
        if ($post.Count -lt 5) {
            [void]$fu.Add([pscustomobject]@{ CutPoint = $c; MinFollowUpMonths = $months; NPost = $post.Count; NPre = $pre.Count
                    MillPctPost = 'insufficient'; MillPostLo = ''; MillPostHi = ''; MillPctPre = ''; MedianLagPost = ''; MedianLagPre = '' })
            continue
        }
        $kPost = @($post | Where-Object { $_.IsMill }).Count; $kPre = @($pre | Where-Object { $_.IsMill }).Count
        $wPost = Get-Wilson $kPost $post.Count; $wPre = Get-Wilson $kPre $pre.Count
        $lPost = [double[]]@($post | Where-Object { $null -ne $_.LagDays } | ForEach-Object { [double]$_.LagDays })
        $lPre = [double[]]@($pre | Where-Object { $null -ne $_.LagDays } | ForEach-Object { [double]$_.LagDays })
        [void]$fu.Add([pscustomobject]@{ CutPoint = $c; MinFollowUpMonths = $months; NPost = $post.Count; NPre = $pre.Count
                MillPctPost = $wPost.P; MillPostLo = $wPost.Lo; MillPostHi = $wPost.Hi; MillPctPre = $wPre.P
                MedianLagPost = [Math]::Round((Get-Med $lPost), 1); MedianLagPre = [Math]::Round((Get-Med $lPre), 1) })
    }
}
$fu | Export-Csv (Join-Path $OutDir 'tables\t21_followup_restricted_regime.csv') -NoTypeInformation -Encoding UTF8

# ================ ISSUE 5: expanded generative-AI term search ================
Write-Host "Issue 5: expanded generative-AI term search ..." -ForegroundColor Cyan
$terms = [ordered]@{
    'ChatGPT'                     = '(?i)chatgpt'
    'GPT (word boundary)'         = '(?i)\bgpt(-?[0-9o]+)?\b'
    'LLM (word boundary)'         = '(?i)\bllms?\b'
    'large language model'        = '(?i)large language model'
    'language model'              = '(?i)language model'
    'generative AI'               = '(?i)generative\s+(a\.?i\.?|artificial intelligence)'
    'AI-generated'                = '(?i)\bai[\s\-]generated'
    'artificial intelligence'     = '(?i)artificial intelligence'
    'Bard / Gemini / Copilot / Claude' = '(?i)\b(bard|gemini|copilot|claude)\b'
    'SCIgen'                      = '(?i)scigen'
    'tortured phrase'             = '(?i)tortured phrase'
    'paraphrasing tool'           = '(?i)paraphras'
}
$hits = New-Object System.Collections.ArrayList
$termCounts = [ordered]@{}
foreach ($k in $terms.Keys) { $termCounts[$k] = 0 }
foreach ($r in $ret) {
    foreach ($k in $terms.Keys) {
        $rx = $terms[$k]
        $where = @()
        if ($r.Title -match $rx) { $where += 'Title' }
        if ($r.Notes -match $rx) { $where += 'Notes' }
        if ($where.Count -eq 0) { continue }
        $termCounts[$k]++
        $snip = ''
        $src = $r.Title + ' || ' + $r.Notes
        $m = [regex]::Match($src, $rx)
        if ($m.Success) {
            $st = [Math]::Max(0, $m.Index - 60); $len = [Math]::Min(160, $src.Length - $st)
            $snip = ($src.Substring($st, $len) -replace '\s+', ' ')
        }
        [void]$hits.Add([pscustomobject]@{
                RecordId = $r.'Record ID'; OriginalPaperDOI = $r.OriginalPaperDOI; RetractionDOI = $r.RetractionDOI
                Term = $k; MatchedText = $m.Value; Field = ($where -join '+')
                MachineAttributed = $r.IsMachine; PaperMill = $r.IsMill
                Journal = $r.Journal; Publisher = $r.Publisher; RetractionDate = $r.RetractionDate
                Snippet = $snip; HumanVerified = ''; TrulyGenerativeAI = ''
            })
    }
}
$hits | Export-Csv (Join-Path $OutDir 'tables\t22_ai_term_matches.csv') -NoTypeInformation -Encoding UTF8
$distinctHits = @($hits | Group-Object RecordId).Count
$machHits = @($hits | Where-Object { $_.MachineAttributed } | Group-Object RecordId).Count

# ================ ISSUE 6: Mantel-Haenszel adjusted analysis ================
Write-Host "Issue 6: Mantel-Haenszel adjusted analysis ..." -ForegroundColor Cyan
$bigPubs = @(@($ai | Where-Object { $_.Publisher } | Group-Object Publisher | Sort-Object Count -Descending | Select-Object -First 6) | ForEach-Object { $_.Name })
function Get-PubGroup { param($P) if ($bigPubs -contains $P) { return $P } return 'Other publishers' }
$strata = @{}
foreach ($r in $ret) {
    if (-not $r.RetYear) { continue }
    $key = "$($r.RetYear)|$(Get-PubGroup $r.Publisher)"
    if (-not $strata.ContainsKey($key)) { $strata[$key] = [pscustomobject]@{ A = 0; B = 0; C = 0; D = 0 } }
    $s = $strata[$key]
    if ($r.IsMachine) { if ($r.IsMill) { $s.A++ } else { $s.B++ } }
    else { if ($r.IsMill) { $s.C++ } else { $s.D++ } }
}
$sumNum = 0.0; $sumDen = 0.0; $rNum = 0.0; $rDen = 0.0
$rbgP = 0.0; $rbgQ = 0.0; $rbgR = 0.0; $rbgS = 0.0
$strataUsed = New-Object System.Collections.ArrayList
foreach ($k in $strata.Keys) {
    $s = $strata[$k]
    $n = $s.A + $s.B + $s.C + $s.D
    if ($n -eq 0) { continue }
    if (($s.A + $s.B) -eq 0 -or ($s.C + $s.D) -eq 0) { continue }
    if (($s.A + $s.C) -eq 0 -or ($s.B + $s.D) -eq 0) { continue }
    $sumNum += ($s.A * $s.D) / $n
    $sumDen += ($s.B * $s.C) / $n
    $rNum += ($s.A * ($s.C + $s.D)) / $n
    $rDen += ($s.C * ($s.A + $s.B)) / $n
    $pI = ($s.A + $s.D) / $n; $qI = ($s.B + $s.C) / $n
    $rI = ($s.A * $s.D) / $n; $sI = ($s.B * $s.C) / $n
    $rbgP += $pI * $rI; $rbgQ += ($pI * $sI + $qI * $rI); $rbgR += $qI * $sI
    $rbgS += 0
    $orI = 'undefined'
    if ($s.B -gt 0 -and $s.C -gt 0 -and $s.A -gt 0 -and $s.D -gt 0) { $orI = [Math]::Round((($s.A * $s.D) / ($s.B * $s.C)), 3) }
    [void]$strataUsed.Add([pscustomobject]@{ Stratum = $k; MachineMill = $s.A; MachineNoMill = $s.B; OtherMill = $s.C; OtherNoMill = $s.D; StratumOR = $orI })
}
$mhOr = 0; $mhRr = 0; $orLo = 0; $orHi = 0
if ($sumDen -gt 0) {
    $mhOr = $sumNum / $sumDen
    $varLog = $rbgP / (2 * $sumNum * $sumNum) + $rbgQ / (2 * $sumNum * $sumDen) + $rbgR / (2 * $sumDen * $sumDen)
    $seLog = [Math]::Sqrt($varLog)
    $orLo = [Math]::Round(($mhOr * [Math]::Exp(-1.959963985 * $seLog)), 3)
    $orHi = [Math]::Round(($mhOr * [Math]::Exp(1.959963985 * $seLog)), 3)
}
if ($rDen -gt 0) { $mhRr = $rNum / $rDen }
# Cochran heterogeneity across strata with defined ORs
$logOrs = @($strataUsed | Where-Object { $_.StratumOR -ne 'undefined' })
$qStat = 0.0; $dfQ = 0
if ($logOrs.Count -gt 1) {
    $wsum = 0.0; $wlsum = 0.0
    $items = New-Object System.Collections.ArrayList
    foreach ($s in $logOrs) {
        $v = 1.0 / $s.MachineMill + 1.0 / $s.MachineNoMill + 1.0 / $s.OtherMill + 1.0 / $s.OtherNoMill
        $w = 1.0 / $v
        $l = [Math]::Log([double]$s.StratumOR)
        [void]$items.Add(@($l, $w)); $wsum += $w; $wlsum += $w * $l
    }
    $lbar = $wlsum / $wsum
    foreach ($it in $items) { $qStat += $it[1] * [Math]::Pow(($it[0] - $lbar), 2) }
    $dfQ = $items.Count - 1
}
$mh = [pscustomobject]@{
    StrataDefinition = 'retraction year x publisher group (six largest publishers plus Other)'
    StrataContributing = $strataUsed.Count
    CrudeRiskRatio = [Math]::Round((( @($ai | Where-Object { $_.IsMill }).Count / [double]$ai.Count) / ( @($other | Where-Object { $_.IsMill }).Count / [double]$other.Count)), 3)
    MantelHaenszelRiskRatio = [Math]::Round($mhRr, 3)
    MantelHaenszelOddsRatio = [Math]::Round($mhOr, 3)
    MhOddsRatioCiLo = $orLo
    MhOddsRatioCiHi = $orHi
    CochranQ = [Math]::Round($qStat, 1)
    CochranDf = $dfQ
    HeterogeneityNote = 'Q far exceeding df indicates the association differs across strata, so the adjusted summary should not be read as a common effect'
}
$mh | Export-Csv (Join-Path $OutDir 'tables\t23_mantel_haenszel.csv') -NoTypeInformation -Encoding UTF8
$strataUsed | Sort-Object Stratum | Export-Csv (Join-Path $OutDir 'tables\t24_mh_strata.csv') -NoTypeInformation -Encoding UTF8

# ================ ISSUE 7: proper notice-level aggregation ================
Write-Host "Issue 7: notice-level aggregation ..." -ForegroundColor Cyan
function Build-NoticeTable { param($Set, [string]$Label)
    $out = New-Object System.Collections.ArrayList
    foreach ($g in @($Set | Group-Object Notice)) {
        $lags = [double[]]@($g.Group | Where-Object { $null -ne $_.LagDays } | ForEach-Object { [double]$_.LagDays })
        if ($lags.Length -eq 0) { continue }
        [Array]::Sort($lags)
        $mill = @($g.Group | Where-Object { $_.IsMill }).Count
        [void]$out.Add([pscustomobject]@{ Cohort = $Label; Notice = $g.Name; Articles = $g.Count
                MedianLag = [Math]::Round((Get-Q $lags 0.5), 1); MinLag = $lags[0]; MaxLag = $lags[$lags.Length - 1]
                LagSpread = [Math]::Round(($lags[$lags.Length - 1] - $lags[0]), 1)
                PropPaperMill = [Math]::Round(($mill / [double]$g.Count), 3) })
    }
    return $out
}
$nAi = Build-NoticeTable $ai 'Machine-attributed'
$nOt = Build-NoticeTable $other 'All other reasons'
$nAi | Export-Csv (Join-Path $OutDir 'tables\t25_notice_level_machine.csv') -NoTypeInformation -Encoding UTF8
$medAi = [double[]]@($nAi | ForEach-Object { [double]$_.MedianLag })
$medOt = [double[]]@($nOt | ForEach-Object { [double]$_.MedianLag })
$mwN = Test-MW -A $medAi -B $medOt
$bfN = Test-BF -A $medAi -B $medOt
$multi = @($nAi | Where-Object { $_.Articles -gt 1 })
$noticeSummary = [pscustomobject]@{
    MachineNotices = $nAi.Count; ComparisonNotices = $nOt.Count
    MachineNoticesMultiArticle = $multi.Count
    MedianArticlesPerMultiNotice = [Math]::Round((Get-Med ([double[]]@($multi | ForEach-Object { [double]$_.Articles }))), 1)
    MedianWithinNoticeLagSpread = [Math]::Round((Get-Med ([double[]]@($multi | ForEach-Object { [double]$_.LagSpread }))), 1)
    NoticeMedianLagMachine = [Math]::Round((Get-Med $medAi), 1)
    NoticeMedianLagComparison = [Math]::Round((Get-Med $medOt), 1)
    LocationDelta = $mwN.Delta; LocationZ = $mwN.Z; LocationP = $mwN.Pstr
    DispersionMadMachine = $bfN.Mad1; DispersionMadComparison = $bfN.Mad2; DispersionRatio = $bfN.Ratio; DispersionT = $bfN.T; DispersionP = $bfN.Pstr
}
$noticeSummary | Export-Csv (Join-Path $OutDir 'tables\t26_notice_level_summary.csv') -NoTypeInformation -Encoding UTF8

# ---------------- report ----------------
$sb = New-Object System.Text.StringBuilder
[void]$sb.AppendLine("# Supplementary analyses")
[void]$sb.AppendLine("")
[void]$sb.AppendLine("Generated $(Get-Date -Format 'yyyy-MM-dd HH:mm'). Source: authoritative Crossref GitLab CSV.")
[void]$sb.AppendLine("Retractions $($ret.Count); machine-attributed $($ai.Count); comparison $($other.Count).")
[void]$sb.AppendLine("")
[void]$sb.AppendLine("## Issue 4. Regime analysis restricted by minimum possible follow-up")
[void]$sb.AppendLine("Snapshot date $SnapshotDate. A minimum follow-up of k months keeps only articles published on or before")
[void]$sb.AppendLine("the snapshot date minus k months, so every retained article had at least k months in which it could have")
[void]$sb.AppendLine("been investigated and retracted.")
[void]$sb.AppendLine("")
[void]$sb.AppendLine("| Cut-point | Min follow-up (months) | n before | n on or after | Paper mill % before | Paper mill % on or after (95% CI) | Median lag before | Median lag on or after |")
[void]$sb.AppendLine("|---|---|---|---|---|---|---|---|")
foreach ($f in $fu) {
    $pc = $f.MillPctPost
    if ($pc -ne 'insufficient') { $pc = "$($f.MillPctPost) ($($f.MillPostLo)-$($f.MillPostHi))" }
    [void]$sb.AppendLine("| $($f.CutPoint) | $($f.MinFollowUpMonths) | $($f.NPre) | $($f.NPost) | $($f.MillPctPre) | $pc | $($f.MedianLagPre) | $($f.MedianLagPost) |")
}
[void]$sb.AppendLine("")
[void]$sb.AppendLine("## Issue 5. Expanded generative-AI terminology search")
[void]$sb.AppendLine("Word-boundary regular expressions applied to the Title and Notes fields of all $($ret.Count) retractions.")
[void]$sb.AppendLine("Distinct records matching at least one term: **$distinctHits**, of which **$machHits** are machine-attributed.")
[void]$sb.AppendLine("Every match is listed in tables\t22_ai_term_matches.csv with a snippet and two blank columns for manual adjudication.")
[void]$sb.AppendLine("")
[void]$sb.AppendLine("| Term | Records matching |")
[void]$sb.AppendLine("|---|---|")
foreach ($k in $termCounts.Keys) { [void]$sb.AppendLine("| $k | $($termCounts[$k]) |") }
[void]$sb.AppendLine("")
[void]$sb.AppendLine("Note: the Notes field is not the full text of the retraction notice, so this is a search of available")
[void]$sb.AppendLine("database metadata, not of notice texts.")
[void]$sb.AppendLine("")
[void]$sb.AppendLine("## Issue 6. Mantel-Haenszel adjusted analysis of paper-mill co-coding")
[void]$sb.AppendLine("Strata: $($mh.StrataDefinition). Contributing strata: $($mh.StrataContributing).")
[void]$sb.AppendLine("")
[void]$sb.AppendLine("| Estimate | Value |")
[void]$sb.AppendLine("|---|---|")
[void]$sb.AppendLine("| Crude risk ratio | $($mh.CrudeRiskRatio) |")
[void]$sb.AppendLine("| Mantel-Haenszel adjusted risk ratio | $($mh.MantelHaenszelRiskRatio) |")
[void]$sb.AppendLine("| Mantel-Haenszel adjusted odds ratio (95% CI) | $($mh.MantelHaenszelOddsRatio) ($($mh.MhOddsRatioCiLo)-$($mh.MhOddsRatioCiHi)) |")
[void]$sb.AppendLine("| Cochran Q for heterogeneity (df) | $($mh.CochranQ) ($($mh.CochranDf)) |")
[void]$sb.AppendLine("")
[void]$sb.AppendLine("$($mh.HeterogeneityNote).")
[void]$sb.AppendLine("")
[void]$sb.AppendLine("## Issue 7. Notice-level aggregation")
[void]$sb.AppendLine("Every retraction notice is summarised by the median, minimum and maximum lag of its constituent articles")
[void]$sb.AppendLine("and by the proportion carrying a paper-mill flag, rather than by selecting one arbitrary record.")
[void]$sb.AppendLine("")
[void]$sb.AppendLine("| Measure | Value |")
[void]$sb.AppendLine("|---|---|")
[void]$sb.AppendLine("| Machine-attributed notices | $($noticeSummary.MachineNotices) |")
[void]$sb.AppendLine("| Comparison notices | $($noticeSummary.ComparisonNotices) |")
[void]$sb.AppendLine("| Machine notices covering more than one article | $($noticeSummary.MachineNoticesMultiArticle) |")
[void]$sb.AppendLine("| Median articles per multi-article notice | $($noticeSummary.MedianArticlesPerMultiNotice) |")
[void]$sb.AppendLine("| Median within-notice lag spread (max minus min), days | $($noticeSummary.MedianWithinNoticeLagSpread) |")
[void]$sb.AppendLine("| Median of notice-level median lags, machine | $($noticeSummary.NoticeMedianLagMachine) |")
[void]$sb.AppendLine("| Median of notice-level median lags, comparison | $($noticeSummary.NoticeMedianLagComparison) |")
[void]$sb.AppendLine("| Location, notice level | delta $($noticeSummary.LocationDelta), z $($noticeSummary.LocationZ), p $($noticeSummary.LocationP) |")
[void]$sb.AppendLine("| Dispersion, notice level | MAD $($noticeSummary.DispersionMadMachine) vs $($noticeSummary.DispersionMadComparison), ratio $($noticeSummary.DispersionRatio), t $($noticeSummary.DispersionT), p $($noticeSummary.DispersionP) |")
Set-Content -Path (Join-Path $OutDir 'supplement-v3.md') -Value $sb.ToString() -Encoding UTF8

Write-Host "`nWrote supplement-v3.md and tables t21-t26" -ForegroundColor Green
$fu | Where-Object { $_.CutPoint -eq '2024-07-01' } | Format-Table CutPoint, MinFollowUpMonths, NPost, MillPctPost, MedianLagPost -AutoSize
$mh | Format-List
$noticeSummary | Format-List
