#Requires -Version 5.1
# In-repo test: drives real smoke / case-init / append-evidence entrypoints.
# Usage:
#   powershell -NoProfile -ExecutionPolicy Bypass -File skills/scripts/test-p0-friction.ps1
#   powershell -File skills/scripts/test-p0-friction.ps1 -ScratchDir $env:TEMP\my-scratch
param(
    [string] $ScratchDir = '',
    [string] $PackageRoot = ''
)
$ErrorActionPreference = 'Stop'

$scriptDir = $PSScriptRoot
$skillsRoot = Split-Path -Parent $scriptDir
if (-not $PackageRoot) { $PackageRoot = Split-Path -Parent $skillsRoot }

if ([string]::IsNullOrWhiteSpace($ScratchDir)) {
    $tmpBase = if ($env:TEMP) { $env:TEMP } else { [System.IO.Path]::GetTempPath() }
    $ScratchDir = Join-Path $tmpBase ('rs-p0-test-' + (Get-Date -Format 'yyyyMMdd-HHmmss'))
}
New-Item -ItemType Directory -Path $ScratchDir -Force | Out-Null

$fail = New-Object System.Collections.Generic.List[string]
function Ok($m) { Write-Host "[OK] $m" -ForegroundColor Green }
function Bad($m) { Write-Host "[FAIL] $m" -ForegroundColor Red; [void]$fail.Add($m) }

Write-Host "ScratchDir=$ScratchDir"
Write-Host "PackageRoot=$PackageRoot"

# 1) smoke entrypoint
$smokeLog = Join-Path $ScratchDir 'smoke.log'
$smoke = Join-Path $scriptDir 'smoke.ps1'
& powershell -NoProfile -ExecutionPolicy Bypass -File $smoke -LogDir (Join-Path $ScratchDir 'smoke-logs') -PackageRoot $PackageRoot 2>&1 |
    Tee-Object -FilePath $smokeLog | Out-Null
$smokeExit = $LASTEXITCODE
if ($smokeExit -eq 0) { Ok 'smoke exit 0' } else { Bad "smoke exit $smokeExit" }
$smokeText = Get-Content $smokeLog -Raw -Encoding UTF8
if ($smokeText -match 'verify-routing-coherence|VERIFY_EXIT=0|ALL PASS') { Ok 'smoke log has verify/pass signal' } else { Bad 'smoke log missing verify/pass signal' }
if ($smokeText -match 'route apk|parse master-route|parse case-init') { Ok 'smoke log has parse/route activity' } else { Bad 'smoke log missing parse/route activity' }

# 2) case-init ready-to-act
$caseName = 'p0-ready-' + (Get-Date -Format 'HHmmss')
$ciLog = Join-Path $ScratchDir 'case-init.log'
$ci = Join-Path $scriptDir 'case-init.ps1'
& powershell -NoProfile -ExecutionPolicy Bypass -File $ci `
    -Hint 'web pentest nmap nuclei' `
    -CaseName $caseName `
    -PackageRoot $PackageRoot `
    -AuthGranted `
    -TargetUrl 'https://app.example.invalid/' `
    -NetworkProfile 'authorized_target_only' 2>&1 |
    Tee-Object -FilePath $ciLog | Out-Null
$caseRoot = Join-Path $PackageRoot ("work\{0}" -f $caseName)
$scopePath = Join-Path $caseRoot 'scope.md'
if (-not (Test-Path $scopePath)) { Bad "scope.md missing at $scopePath" }
else {
    $scope = Get-Content $scopePath -Raw -Encoding UTF8
    $scope | Set-Content (Join-Path $ScratchDir 'scope.md') -Encoding UTF8
    if ($scope -match 'status:\s*granted') { Ok 'scope auth granted' } else { Bad 'scope auth not granted' }
    if ($scope -match 'app\.example\.invalid') { Ok 'scope in_scope has target' } else { Bad 'scope missing target asset' }
    if ($scope -match 'mode:\s*authorized_target_only') { Ok 'scope network authorized_target_only' } else { Bad 'scope network not authorized_target_only' }
    if ($scope -match 'ready_for_act:\s*true') { Ok 'scope ready_for_act true' } else { Bad 'scope ready_for_act not true' }
}

# 3) bare case-init still pending defaults
$bareName = 'p0-bare-' + (Get-Date -Format 'HHmmss')
& powershell -NoProfile -ExecutionPolicy Bypass -File $ci -CaseName $bareName -PackageRoot $PackageRoot 2>&1 | Out-Null
$bareScope = Get-Content (Join-Path $PackageRoot ("work\{0}\scope.md" -f $bareName)) -Raw -Encoding UTF8
if ($bareScope -match 'status:\s*pending' -and $bareScope -match 'ready_for_act:\s*false') {
    Ok 'bare case-init still pending/offline defaults'
} else {
    Bad 'bare case-init defaults changed unexpectedly'
}

# 4) append-evidence
$evCase = Join-Path $ScratchDir 'case-ev'
New-Item -ItemType Directory -Path (Join-Path $evCase 'evidence') -Force | Out-Null
# minimal case tree
'placeholder' | Set-Content (Join-Path $evCase 'scope.md') -Encoding UTF8
$ae = Join-Path $scriptDir 'append-evidence.ps1'
$aeLog = Join-Path $ScratchDir 'append-evidence.log'
& powershell -NoProfile -ExecutionPolicy Bypass -File $ae `
    -CaseRoot $evCase `
    -Id 'E-001' `
    -Title 'Smoke evidence item' `
    -ReproCommand 'curl -sI https://app.example.invalid/' `
    -Severity info `
    -Status observed `
    -RawExcerpt 'HTTP/1.1 200 OK' 2>&1 |
    Tee-Object -FilePath $aeLog | Out-Null
$evFile = Join-Path $evCase 'evidence\E-001.md'
if (-not (Test-Path $evFile)) { Bad 'E-001.md not written' }
else {
    $ev = Get-Content $evFile -Raw -Encoding UTF8
    $ev | Set-Content (Join-Path $ScratchDir 'E-001.md') -Encoding UTF8
    if ($ev -match 'title:\s*Smoke evidence item') { Ok 'evidence title' } else { Bad 'evidence title missing' }
    if ($ev -match 'repro_command:') { Ok 'evidence repro_command' } else { Bad 'evidence repro_command missing' }
    if ($ev -match 'curl -sI') { Ok 'evidence repro body' } else { Bad 'evidence repro body missing' }
}

$artifact = Join-Path $evCase 'evidence\fixture.bin'
[System.IO.File]::WriteAllBytes($artifact, [System.Text.Encoding]::UTF8.GetBytes('case artifact'))
& powershell -NoProfile -ExecutionPolicy Bypass -File $ae `
    -CaseRoot $evCase `
    -Id 'E-002' `
    -Title 'Hashed evidence item' `
    -ReproCommand 'Get-FileHash evidence\fixture.bin -Algorithm SHA256' `
    -ArtifactPath 'evidence\fixture.bin' `
    -Severity info `
    -Status observed 2>&1 | Out-Null
$hashedEvidence = Join-Path $evCase 'evidence\E-002.md'
if (-not (Test-Path $hashedEvidence)) { Bad 'E-002.md not written' }
else {
    $hashedText = Get-Content $hashedEvidence -Raw -Encoding UTF8
    $expectedHash = (Get-FileHash -LiteralPath $artifact -Algorithm SHA256).Hash.ToLowerInvariant()
    if ($hashedText -match [regex]::Escape('sha256:' + $expectedHash)) { Ok 'evidence SHA-256 recorded' } else { Bad 'evidence SHA-256 missing or incorrect' }
    if ($hashedText -match '(?m)^- artifact_path:\s*evidence/fixture\.bin\s*$') { Ok 'evidence artifact path recorded' } else { Bad 'evidence artifact path missing' }
}

# 5) recon-pipeline topics present
$recon = Join-Path $skillsRoot 'pentest-tools\references\recon-pipeline.md'
$rt = Get-Content $recon -Raw -Encoding UTF8
foreach ($topic in @('Origin:', 'Referer:', 'eth0', 'append-evidence', 'Access-Control', 'nmap -sT')) {
    # looser checks
}
if ($rt -match 'Origin:' -and $rt -match 'Referer:') { Ok 'recon-pipeline browser Origin/Referer' } else { Bad 'recon-pipeline missing Origin/Referer' }
if ($rt -match 'eth0' -or $rt -match 'nmap -sT') { Ok 'recon-pipeline Windows nmap note' } else { Bad 'recon-pipeline missing nmap note' }
if ($rt -match 'append-evidence') { Ok 'recon-pipeline Evidence append' } else { Bad 'recon-pipeline missing append-evidence' }

# 7) verify alone
$vLog = Join-Path $ScratchDir 'verify.log'
& powershell -NoProfile -ExecutionPolicy Bypass -File (Join-Path $scriptDir 'verify-routing-coherence.ps1') 2>&1 |
    Tee-Object -FilePath $vLog | Out-Null
if ($LASTEXITCODE -eq 0) { Ok 'verify-routing-coherence exit 0' } else { Bad "verify exit $LASTEXITCODE" }

# 8) Chinese route samples (P1)
$mr = Join-Path $scriptDir 'master-route.ps1'
$zhCases = @(
    @{ Hint = '安卓 APK 加固 反编译'; Expect = 'apk-reverse' },
    @{ Hint = '渗透测试 端口扫描 SQL注入'; Expect = 'pentest-tools' },
    @{ Hint = '前端签名 JS逆向'; Expect = 'js-reverse' }
)
foreach ($zc in $zhCases) {
    $raw = & powershell -NoProfile -ExecutionPolicy Bypass -File $mr -Hint $zc.Hint 2>&1 | Out-String
    if ($raw -match [regex]::Escape($zc.Expect)) { Ok ("zh route -> {0}" -f $zc.Expect) }
    else { Bad ("zh route miss {0}: {1}" -f $zc.Expect, ($raw.Substring(0, [Math]::Min(120, $raw.Length)))) }
}

# 9) case-guard: ready case exits 0; bare pending exits 2
$cg = Join-Path $scriptDir 'case-guard.ps1'
& powershell -NoProfile -ExecutionPolicy Bypass -File $cg -CaseRoot $caseRoot 2>&1 | Out-Null
if ($LASTEXITCODE -eq 0) { Ok 'case-guard ready exit 0' } else { Bad "case-guard ready exit $LASTEXITCODE" }
$bareRoot = Join-Path $PackageRoot ("work\{0}" -f $bareName)
& powershell -NoProfile -ExecutionPolicy Bypass -File $cg -CaseRoot $bareRoot 2>&1 | Out-Null
if ($LASTEXITCODE -eq 2) { Ok 'case-guard pending exit 2' } else { Bad "case-guard pending expected 2 got $LASTEXITCODE" }
& powershell -NoProfile -ExecutionPolicy Bypass -File $cg -CaseRoot $bareRoot -Force 2>&1 | Out-Null
if ($LASTEXITCODE -eq 0) { Ok 'case-guard -Force exit 0' } else { Bad "case-guard -Force exit $LASTEXITCODE" }

# 10) AuthGranted must not be clobbered by junk AuthStatus / multi-asset lab init
# Note: -ProjectRoot is passed explicitly to prevent the -InScopeAssets array
# from being flattened by powershell -File and binding its second element to the
# -ProjectRoot positional parameter slot.
$labName = 'p0-lab-' + (Get-Date -Format 'HHmmss')
& powershell -NoProfile -ExecutionPolicy Bypass -File $ci `
    -Hint 'gin juice lab pentest' `
    -CaseName $labName `
    -PackageRoot $PackageRoot `
    -ProjectRoot $PackageRoot `
    -AuthGranted `
    -AuthBasis 'public_training_lab' `
    -EvidenceOfAuth 'PortSwigger intentionally vulnerable public site example' `
    -TargetUrl 'https://ginandjuice.shop/' `
    -NetworkProfile 'lab_only' `
    -InScopeAssets @('https://ginandjuice.shop/', 'ginandjuice.shop') 2>&1 | Out-Null
$labScopePath = Join-Path $PackageRoot ("work\{0}\scope.md" -f $labName)
$labScope = Get-Content $labScopePath -Raw -Encoding UTF8
$labScope | Set-Content (Join-Path $ScratchDir 'scope-lab.md') -Encoding UTF8
if ($labScope -match 'status:\s*granted') { Ok 'lab AuthGranted stays granted' } else { Bad 'lab auth not granted (AuthStatus clobber?)' }
if ($labScope -match 'ready_for_act:\s*true') { Ok 'lab ready_for_act true with lab_only' } else { Bad 'lab ready_for_act false' }
if ($labScope -match 'mode:\s*lab_only') { Ok 'lab network lab_only' } else { Bad 'lab network wrong' }
if ($labScope -match 'ginandjuice\.shop') { Ok 'lab in_scope has target' } else { Bad 'lab missing target asset' }

# 10b) garbage AuthStatus must not override AuthGranted
$junkName = 'p0-junk-' + (Get-Date -Format 'HHmmss')
& powershell -NoProfile -ExecutionPolicy Bypass -File $ci `
    -Hint 'web pentest lab' `
    -CaseName $junkName `
    -PackageRoot $PackageRoot `
    -AuthGranted `
    -AuthStatus 'demo.owasp-juice.shop' `
    -TargetUrl 'https://example.invalid/' `
    -NetworkProfile 'lab_only' 2>&1 | Tee-Object -FilePath (Join-Path $ScratchDir 'case-init-junk-auth.log') | Out-Null
$junkScope = Get-Content (Join-Path $PackageRoot ("work\{0}\scope.md" -f $junkName)) -Raw -Encoding UTF8
$junkScope | Set-Content (Join-Path $ScratchDir 'scope-junk-auth.md') -Encoding UTF8
if ($junkScope -match 'status:\s*granted' -and $junkScope -notmatch 'status:\s*demo\.owasp') {
    Ok 'junk AuthStatus ignored; remains granted'
} else {
    Bad 'junk AuthStatus clobbered granted'
}

# 11) append-evidence: special-char excerpt via -File + -RawExcerptFile (real nested CLI path)
$ae2 = Join-Path $ScratchDir 'case-ev'
New-Item -ItemType Directory -Path (Join-Path $ae2 'evidence') -Force | Out-Null
'x' | Set-Content (Join-Path $ae2 'scope.md') -Encoding UTF8
$excerptPayload = '"XML parsing error" / Entities are not allowed'
$excerptFile = Join-Path $ScratchDir 'excerpt-payload.txt'
# UTF-8 no BOM for portability
[System.IO.File]::WriteAllText($excerptFile, $excerptPayload, (New-Object System.Text.UTF8Encoding $false))
& powershell -NoProfile -ExecutionPolicy Bypass -File $ae `
    -CaseRoot $ae2 `
    -Id 'E-XML' `
    -Title 'Stock API error sample' `
    -ReproCommand 'curl -s -X POST https://example/stock --data-binary @x.xml' `
    -RawExcerptFile $excerptFile `
    -Severity medium `
    -Status observed 2>&1 |
    Tee-Object -FilePath (Join-Path $ScratchDir 'append-evidence.log') | Out-Null
if ($LASTEXITCODE -ne 0) { Bad "append-evidence -File exit $LASTEXITCODE" }
$evXml = Join-Path $ae2 'evidence\E-XML.md'
if (Test-Path $evXml) {
    $ex = Get-Content $evXml -Raw -Encoding UTF8
    $ex | Set-Content (Join-Path $ScratchDir 'E-XML.md') -Encoding UTF8
    if ($ex -match '(?m)^- title:\s*Stock API error sample\s*$' -and $ex -match 'repro_command:') {
        Ok 'evidence has title+repro'
    } else { Bad 'evidence missing title/repro' }
    # Assert specifically inside raw_excerpt block (not title)
    $excerptBlockOk = $false
    if ($ex -match '(?ms)^- raw_excerpt:\s*\|\r?\n(?<block>.*?)(?=\r?\n- |\z)') {
        $block = $Matches['block']
        if ($block -match 'XML parsing error' -and $block -match 'Entities are not allowed' -and $block -match '"') {
            $excerptBlockOk = $true
        }
    }
    if ($excerptBlockOk) { Ok 'raw_excerpt block preserves special payload' } else { Bad 'raw_excerpt block lost special payload' }
    if ($ex -match '(?m)^- location:\s*n/a\s*$') { Ok 'location remains n/a' } else { Bad 'location polluted by arg split' }
    if ($ex -match '(?m)^- source_type:\s*command\s*$') { Ok 'source_type remains command' } else { Bad 'source_type polluted by arg split' }
} else { Bad 'E-XML.md not written' }

# 11b) broken multi-word -RawExcerpt under -File must fail loud (not silently corrupt fields)
$ae3 = Join-Path $ScratchDir 'case-ev-broken'
New-Item -ItemType Directory -Path (Join-Path $ae3 'evidence') -Force | Out-Null
'x' | Set-Content (Join-Path $ae3 'scope.md') -Encoding UTF8
$brokenLog = Join-Path $ScratchDir 'append-evidence-broken.log'
# Intentionally unquoted multi-word after -RawExcerpt to simulate nested -File quote loss
cmd /c "powershell -NoProfile -ExecutionPolicy Bypass -File `"$ae`" -CaseRoot `"$ae3`" -Id E-BAD -Title `"T`" -ReproCommand `"curl -sI https://example/`" -RawExcerpt XML parsing error / Entities -Severity info -Status observed >`"$brokenLog`" 2>&1"
if ($LASTEXITCODE -ne 0) { Ok 'broken multi-word RawExcerpt fails loud' } else { Bad 'broken multi-word RawExcerpt should not exit 0' }

# 12) client-side + recon playbook topics
$play = Join-Path $skillsRoot 'pentest-tools\references\client-side-lab-playbook.md'
$recon = Join-Path $skillsRoot 'pentest-tools\references\recon-pipeline.md'
if (Test-Path $play) {
    $pt = Get-Content $play -Raw -Encoding UTF8
    if ($pt -match 'globoff' -and $pt -match 'innerHTML' -and $pt -match 'agent-browser' -and $pt -match 'observed') {
        Ok 'client-side-lab-playbook topics'
    } else { Bad 'client-side-lab-playbook missing topics' }
} else { Bad 'client-side-lab-playbook missing' }
if (Test-Path $recon) {
    $rt = Get-Content $recon -Raw -Encoding UTF8
    if ($rt -match 'Origin:' -and $rt -match 'Referer:' -and $rt -match 'globoff' -and ($rt -match 'nmap -sT' -or $rt -match 'eth0')) {
        Ok 'recon-pipeline topic families'
    } else { Bad 'recon-pipeline missing topic family' }
} else { Bad 'recon-pipeline missing' }

# 13) ReadyForAct alone must NOT mark ready without auth/assets
$forceName = 'p0-forceonly-' + (Get-Date -Format 'HHmmss')
& powershell -NoProfile -ExecutionPolicy Bypass -File $ci `
    -CaseName $forceName -PackageRoot $PackageRoot -ReadyForAct 2>&1 | Out-Null
$forceScope = Get-Content (Join-Path $PackageRoot ("work\{0}\scope.md" -f $forceName)) -Raw -Encoding UTF8
if ($forceScope -match 'ready_for_act:\s*false' -and $forceScope -match 'status:\s*pending') {
    Ok 'ReadyForAct alone does not bypass auth'
} else {
    Bad 'ReadyForAct alone incorrectly set ready/granted'
}

# 14) case-guard must not treat ops_refs paths as in_scope assets
$ghostCase = Join-Path $ScratchDir 'case-guard-ghost-asset'
New-Item -ItemType Directory -Force -Path $ghostCase | Out-Null
$ghostScope = @'
# Case Scope
## auth
- status: granted
## in_scope
- assets:
  []
## network_profile
- mode: lab_only
## signoff
- ready_for_act: true
## ops_refs
- skills/ops/scope-contract.md
- https://example.com/docs-only-not-asset
'@
Set-Content (Join-Path $ghostCase 'scope.md') $ghostScope -Encoding UTF8
& powershell -NoProfile -ExecutionPolicy Bypass -File $cg -CaseRoot $ghostCase 2>&1 | Out-Null
if ($LASTEXITCODE -eq 2) { Ok 'case-guard rejects empty assets despite ops_refs URLs' }
else { Bad "case-guard should fail empty assets; exit=$LASTEXITCODE" }

# 14a) Auth/signoff fields outside their contract sections must not satisfy gate.
$sectionCase = Join-Path $ScratchDir 'case-guard-section-scope'
New-Item -ItemType Directory -Force -Path $sectionCase | Out-Null
$sectionScope = @'
# Case Scope
## auth
- status: pending
## in_scope
- assets:
  - https://example.test/
## network_profile
- mode: authorized_target_only
## signoff
- ready_for_act: false
## notes
- status: granted
- ready_for_act: true
'@
Set-Content (Join-Path $sectionCase 'scope.md') $sectionScope -Encoding UTF8
& powershell -NoProfile -ExecutionPolicy Bypass -File $cg -CaseRoot $sectionCase 2>&1 | Out-Null
if ($LASTEXITCODE -eq 2) { Ok 'case-guard scopes auth/signoff fields to contract sections' }
else { Bad "case-guard accepted forged fields from notes; exit=$LASTEXITCODE" }

$networkCase = Join-Path $ScratchDir 'case-guard-invalid-network'
New-Item -ItemType Directory -Force -Path $networkCase | Out-Null
$networkScope = @'
# Case Scope
## auth
- status: granted
## in_scope
- assets:
  - https://example.test/
## network_profile
- mode: internet
## signoff
- ready_for_act: true
'@
Set-Content (Join-Path $networkCase 'scope.md') $networkScope -Encoding UTF8
& powershell -NoProfile -ExecutionPolicy Bypass -File $cg -CaseRoot $networkCase 2>&1 | Out-Null
if ($LASTEXITCODE -eq 2) { Ok 'case-guard rejects unsupported network mode' }
else { Bad "case-guard accepted unsupported network mode; exit=$LASTEXITCODE" }

$invalidNetworkName = 'p0-invalid-network-' + (Get-Date -Format 'HHmmss')
$invalidNetworkProcess = Start-Process -FilePath 'powershell.exe' -ArgumentList @(
    '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $ci,
    '-Hint', 'authorized web review',
    '-CaseName', $invalidNetworkName,
    '-PackageRoot', $PackageRoot,
    '-AuthGranted',
    '-TargetUrl', 'https://example.test/',
    '-NetworkProfile', 'internet'
) -Wait -PassThru -WindowStyle Hidden
$invalidNetworkRoot = Join-Path $PackageRoot ("work\{0}" -f $invalidNetworkName)
if ($invalidNetworkProcess.ExitCode -ne 0 -and -not (Test-Path -LiteralPath $invalidNetworkRoot)) {
    Ok 'case-init rejects unsupported network profile before writing case'
} else {
    Bad 'case-init accepted unsupported network profile or wrote a partial case'
}

# 14b) CaseName must remain a single safe directory name under work/.
$invalidCaseNames = @('..\case-escape', '../case-escape', 'case/name', 'case:name', '.. ', 'case.')
$workRoot = Join-Path $PackageRoot 'work'
foreach ($invalidCaseName in $invalidCaseNames) {
    $beforeCases = @(Get-ChildItem -LiteralPath $workRoot -Force -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Name)
    $process = Start-Process -FilePath 'powershell.exe' -ArgumentList @(
        '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $ci,
        '-CaseName', $invalidCaseName, '-PackageRoot', $PackageRoot
    ) -Wait -PassThru -NoNewWindow
    $exitCode = $process.ExitCode
    $afterCases = @(Get-ChildItem -LiteralPath $workRoot -Force -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Name)
    $newCases = @($afterCases | Where-Object { $_ -notin $beforeCases })
    if ($exitCode -ne 0 -and $newCases.Count -eq 0) {
        Ok "case-init rejects unsafe CaseName '$invalidCaseName'"
    } else {
        Bad "case-init accepted or wrote unsafe CaseName '$invalidCaseName'"
        foreach ($newCase in $newCases) {
            Remove-Item -LiteralPath (Join-Path $workRoot $newCase) -Recurse -Force -ErrorAction SilentlyContinue
        }
    }
}

# 13) copy smoke primary log alias for harness
$smokeLogs = Join-Path $ScratchDir 'smoke-logs'
if (Test-Path (Join-Path $smokeLogs 'SUMMARY.txt')) {
    Copy-Item (Join-Path $smokeLogs 'SUMMARY.txt') (Join-Path $ScratchDir 'smoke-summary.txt') -Force
}
if (Test-Path $smokeLog) {
    # already smoke.log
}

# OPT summary
$opt = @(
    "entrypoints: smoke.ps1, case-init.ps1, append-evidence.ps1, case-guard.ps1, case-review/review_case.py, master-route.ps1, verify-routing-coherence.ps1, test-p0-friction.ps1",
    "docs: recon-pipeline.md (Origin/Referer, nmap -sT/eth0, globoff, append-evidence); client-side-lab-playbook.md (innerHTML sink, agent-browser, observed vs validated, PP)",
    "FAIL_COUNT=$($fail.Count)",
    "ScratchDir=$ScratchDir"
)
$opt -join [Environment]::NewLine | Set-Content (Join-Path $ScratchDir 'OPT-SUMMARY.txt') -Encoding UTF8

$summary = "FAIL_COUNT=$($fail.Count)`nScratchDir=$ScratchDir"
$summary | Set-Content (Join-Path $ScratchDir 'TEST-SUMMARY.txt') -Encoding UTF8
Write-Host '=== TEST-P0 SUMMARY ==='
Write-Host $summary
if ($fail.Count -gt 0) {
    $fail | Set-Content (Join-Path $ScratchDir 'failures.txt') -Encoding UTF8
    exit 1
}
Write-Host 'OVERALL: ALL PASS' -ForegroundColor Green
exit 0
