Payload/scripts/kaden/verify-local.ps1

#Requires -Version 5.1
<#
.SYNOPSIS
    Agent/harness bookend quality gate (full bolus: test-design Semgrep, Vitest, ESLint, PowerShell lint).

.DESCRIPTION
    This is the harness bookend gate used by turn-start, turn-stop, and harness-sensor -
    NOT the git pre-commit or pre-push hook. Husky runs staged/focused checks separately
    (.husky/pre-commit, .husky/pre-push).

    Fail-fast order (core gates): lint:tests, test, lint, lint:powershell.
    Use -Security for Tier A (security:pre-push). Use -SecurityFull for Tier A + B (security:pr).

    Each step announces itself as "[verify-local] npm run <name>" so FAIL sensors can
    attribute the failure to a named step without changing the single exit-code contract.
    When package.json is absent, Node gates are skipped and lint-powershell.ps1 is the bookend.

.PARAMETER Security
    Run Tier A security gate (security:pre-push) when defined in package.json.

.PARAMETER SecurityFull
    Run full pre-PR security gate (security:pr = Tier A + corporate-parity Semgrep) when defined.

.EXAMPLE
    .\scripts\kaden\verify-local.ps1

.EXAMPLE
    .\scripts\kaden\verify-local.ps1 -Security

.EXAMPLE
    .\scripts\kaden\verify-local.ps1 -SecurityFull
#>

[CmdletBinding()]
param(
    [switch]$Security,
    [switch]$SecurityFull
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot (Join-Path '..' '..'))).Path
$runtimeModule = Join-Path $PSScriptRoot 'harness\lib\RepoRuntime.psm1'
$contractModule = Join-Path $PSScriptRoot 'harness\lib\HarnessContract.psm1'
Import-Module $runtimeModule -Force
Import-Module $contractModule -Force

# Nested helpers live outside try/finally so PowerShell Editor Services can parse the script.
function Write-VerifyLocalLine {
    param([AllowEmptyString()][string]$Message = '')
    # Console.Out reaches the process stdout for sensor capture without becoming
    # PowerShell pipeline output (which would pollute Invoke-HarnessFailFastNamedSequence returns).
    [Console]::Out.WriteLine($Message)
}

function Invoke-LocalNpmScript {
    param(
        [Parameter(Mandatory = $true)][string]$Name,
        [Parameter(Mandatory = $true)]$ScriptsTable
    )
    if (-not $ScriptsTable.PSObject.Properties[$Name]) {
        Write-VerifyLocalLine "[verify-local] skip $Name (not in package.json)"
        return 0
    }
    Write-VerifyLocalLine "[verify-local] npm run $Name"
    # Start-Process (not `& npm | ForEach-Object`) so Vitest/ESLint stdout is not
    # enumerated line-by-line on the PowerShell pipeline -- that adds ~1ms per line
    # and turns a ~15s suite into 100s+. Child inherits this console (sensor capture
    # still sees announce lines + npm output). Function return stays a single int.
    $npm = Resolve-HarnessExternalCommandPath -Command 'npm'
    $proc = Start-Process -FilePath $npm -ArgumentList @('run', $Name) -WorkingDirectory (Get-Location).Path `
        -NoNewWindow -Wait -PassThru
    if ($null -eq $proc -or $null -eq $proc.ExitCode) {
        return 0
    }
    return [int]$proc.ExitCode
}

function Invoke-LocalPowerShellLint {
    $lintScript = Join-Path $repoRoot 'scripts\kaden\harness\lint-powershell.ps1'
    if (-not (Test-Path -LiteralPath $lintScript)) {
        Write-VerifyLocalLine '[verify-local] skip lint:powershell (scripts/kaden/harness/lint-powershell.ps1 missing)'
        return 0
    }
    Write-VerifyLocalLine '[verify-local] lint:powershell'
    $proc = Start-Process -FilePath (Get-Process -Id $PID).Path -ArgumentList @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $lintScript) `
        -WorkingDirectory $repoRoot -NoNewWindow -Wait -PassThru
    if ($null -eq $proc -or $null -eq $proc.ExitCode) {
        return 0
    }
    return [int]$proc.ExitCode
}

Push-Location $repoRoot
try {
    Initialize-RepoShellEnvironment -LogPrefix 'verify-local'

    if (-not (Test-Path 'package.json')) {
        Write-VerifyLocalLine '[verify-local] skip lint:tests (not in package.json)'
        Write-VerifyLocalLine '[verify-local] skip test (not in package.json)'
        Write-VerifyLocalLine '[verify-local] skip lint (not in package.json)'
        $lintCode = Invoke-LocalPowerShellLint
        if ($lintCode -ne 0) {
            throw "lint:powershell failed with exit code $lintCode"
        }
        if ($SecurityFull) {
            Write-VerifyLocalLine '[verify-local] skip security:pr (not in package.json)'
        }
        elseif ($Security) {
            Write-VerifyLocalLine '[verify-local] skip security:pre-push (not in package.json)'
        }
        Write-VerifyLocalLine '[verify-local] All requested gates passed.'
        return
    }

    $pkg = Get-Content 'package.json' -Raw | ConvertFrom-Json
    # StrictMode: package.json may exist without a scripts table (template / deps-only checkouts).
    $hasScripts = $pkg.PSObject.Properties.Name -contains 'scripts' -and $null -ne $pkg.scripts
    if (-not $hasScripts) {
        Write-VerifyLocalLine '[verify-local] skip lint:tests (no package.json scripts)'
        Write-VerifyLocalLine '[verify-local] skip test (no package.json scripts)'
        Write-VerifyLocalLine '[verify-local] skip lint (no package.json scripts)'
        $lintCode = Invoke-LocalPowerShellLint
        if ($lintCode -ne 0) {
            throw "lint:powershell failed with exit code $lintCode"
        }
        if ($SecurityFull) {
            Write-VerifyLocalLine '[verify-local] skip security:pr (no package.json scripts)'
        }
        elseif ($Security) {
            Write-VerifyLocalLine '[verify-local] skip security:pre-push (no package.json scripts)'
        }
        Write-VerifyLocalLine '[verify-local] All requested gates passed.'
        return
    }
    $scripts = $pkg.scripts

    # Canonical core gate order (single source of truth for announce lines + fail-fast).
    # Security switches append after these; they are optional and not part of the core order contract.
    $coreGateScripts = @('lint:tests', 'test', 'lint', 'lint:powershell')

    $coreResult = Invoke-HarnessFailFastNamedSequence -Names $coreGateScripts -InvokeName {
        param($Name)
        return [int](Invoke-LocalNpmScript -Name $Name -ScriptsTable $scripts)
    }
    if ($coreResult.ExitCode -ne 0) {
        $failed = $coreResult.Ran[-1]
        throw "npm run $failed failed with exit code $($coreResult.ExitCode)"
    }

    if ($SecurityFull) {
        $code = Invoke-LocalNpmScript -Name 'security:pr' -ScriptsTable $scripts
        if ($code -ne 0) {
            throw "npm run security:pr failed with exit code $code"
        }
    }
    elseif ($Security) {
        $code = Invoke-LocalNpmScript -Name 'security:pre-push' -ScriptsTable $scripts
        if ($code -ne 0) {
            throw "npm run security:pre-push failed with exit code $code"
        }
    }

    Write-VerifyLocalLine '[verify-local] All requested gates passed.'
}
finally {
    Pop-Location
}