Public/Export-RaidinessReport.ps1

# psrunner-lint allow: New-Item — creates the local output directory for the report; never a tenant object
function Export-RaidinessReport {
    <#
    .SYNOPSIS
        Fills the bundled single-file HTML report with the collected data and
        opens it in your browser.

    .DESCRIPTION
        The report template (assets/ReportTemplate.html) carries the whole
        assessment engine — the checks, the scorecard, the roadmap and the
        licence advisor — compiled into one self-contained page. This cmdlet
        only injects the collected files into the page's data slot and writes
        the result; every calculation happens in your browser when the page
        opens. No network, no upload, nothing leaves the machine.

        -Markdown and -LLM ask the page for extra downloads next to the HTML
        report: a markdown version (report.md) and an LLM-ready bundle
        (llm-bundle.zip) you can hand to a model of your choice, with your
        own key, at your own cost — Raidiness itself never calls a model.
        Both hash user names and e-mail addresses; -IncludeIdentities keeps
        them as they are. The HTML report itself always shows real identities
        and stays on your machine.

    .PARAMETER InputPath
        Directory with the collected data. Default: ./raidiness-data

    .PARAMETER OutputPath
        The report file to write. Default: ./raidiness-report.html

    .PARAMETER Customer
        Organisation name shown on the report.

    .PARAMETER NoOpen
        Write the file without opening the browser.

    .PARAMETER Markdown
        Offer a markdown version of the report (report.md) as a download in
        the page.

    .PARAMETER LLM
        Offer an LLM-ready bundle (llm-bundle.zip: markdown report, findings
        and measurements) as a download in the page, for use with your own
        model and key. Identities are hashed unless -IncludeIdentities.

    .PARAMETER IncludeIdentities
        Keep real user names and e-mail addresses in the markdown/LLM files
        instead of hashing them. Only meaningful with -Markdown or -LLM.

    .EXAMPLE
        Export-RaidinessReport -Customer 'Contoso Ltd'

    .EXAMPLE
        Export-RaidinessReport -Markdown -LLM
    #>

    [CmdletBinding(DefaultParameterSetName = 'Legacy')]
    param(
        # A run folder from Invoke-Raidiness -Path: the evidence is read from
        # its data/ subfolder and every artifact is written back into it.
        [Parameter(ParameterSetName = 'RunFolder', Mandatory = $true)]
        [string] $RunPath,

        [Parameter(ParameterSetName = 'Legacy')]
        [string] $InputPath = './raidiness-data',

        [Parameter(ParameterSetName = 'Legacy')]
        [string] $OutputPath = './raidiness-report.html',

        [string] $Customer = 'Your organisation',
        [switch] $NoOpen,
        [switch] $Markdown,
        [switch] $LLM,
        [switch] $IncludeIdentities,

        # Also print the report to PDF (run folder only; needs the browser).
        [switch] $Pdf,

        # Do not drive a browser: write the report and open it, as before.
        [switch] $NoBrowser,

        [string] $BrowserPath,

        [int] $TimeoutSeconds = 180
    )

    $ErrorActionPreference = 'Stop'

    $runFolder = $PSCmdlet.ParameterSetName -eq 'RunFolder'
    if ($runFolder) {
        if (-not (Test-Path -Path $RunPath)) { throw "No run folder at $RunPath." }
        $RunPath = (Resolve-Path -Path $RunPath).Path
        $InputPath = Join-Path $RunPath 'data'
        $OutputPath = Join-Path $RunPath 'report.html'
        if (-not (Get-RaidinessLogPath)) { Open-RaidinessLog -Path $RunPath | Out-Null }
    }

    $template = Join-Path $PSScriptRoot '..' 'assets' 'ReportTemplate.html'
    if (-not (Test-Path $template)) {
        throw "Report template not found at $template. In a source checkout, build it first: bun run template:build"
    }
    if (-not (Test-Path $InputPath)) {
        throw "No collected data at $InputPath. Run Connect-Raidiness and Invoke-Raidiness first."
    }

    $files = @(Get-ChildItem -Path $InputPath -File | Where-Object { $_.Extension -in '.json', '.csv' })
    if ($files.Count -eq 0) {
        throw "No .json or .csv files found in $InputPath."
    }

    if ($LLM) {
        Write-Host 'Note: any costs incurred with your LLM provider are your own; Raidiness does not call any model.' -ForegroundColor Cyan
        Write-Host 'User names and e-mail addresses are hashed in the bundle unless -IncludeIdentities is given.' -ForegroundColor Cyan
    }
    if ($IncludeIdentities) {
        if ($LLM -or $Markdown) {
            Write-Warning '-IncludeIdentities: real user identities will be included in files meant for an external model. Share them only where that is acceptable.'
        }
        else {
            Write-Warning '-IncludeIdentities has no effect without -Markdown or -LLM.'
        }
    }

    $payload = [ordered]@{
        raidinessCollection = [ordered]@{
            version     = '1.0'
            customer    = $Customer
            generatedAt = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
            exports     = [ordered]@{
                markdown          = [bool] $Markdown
                llm               = [bool] $LLM
                includeIdentities = [bool] $IncludeIdentities
            }
        }
        files               = @(
            foreach ($file in $files) {
                [ordered]@{
                    name = $file.Name
                    text = Get-Content -Path $file.FullName -Raw
                }
            }
        )
    }

    # `</` must not terminate the surrounding <script> block early.
    $json = (ConvertTo-Json -InputObject $payload -Depth 8 -Compress).Replace('</', '<\/')

    $html = Get-Content -Path $template -Raw
    $pattern = '(?s)(<script id="raidiness-data" type="application/json">).*?(</script>)'
    if ($html -notmatch $pattern) {
        throw 'The report template has no data slot — it looks like a broken build.'
    }
    $filled = [regex]::Replace($html, $pattern, { param($m) $m.Groups[1].Value + $json + $m.Groups[2].Value })

    # -OutputPath used to be written without its directory existing.
    $parent = Split-Path -Path $OutputPath -Parent
    if ($parent -and -not (Test-Path -Path $parent)) {
        New-Item -ItemType Directory -Path $parent -Force | Out-Null
    }

    $filled | Out-File -FilePath $OutputPath -Encoding utf8
    $resolved = (Resolve-Path $OutputPath).Path
    Write-Host "Report written to $resolved ($($files.Count) input file(s))." -ForegroundColor Green
    # A run folder finishes itself: the same page, run headlessly, writes
    # run.json, report.md, the LLM bundle and the PDF next to the report
    # instead of asking someone to click four download buttons.
    $bridged = $false
    if ($runFolder -and -not $NoBrowser) {
        $browser = Get-RaidinessBrowser -BrowserPath $BrowserPath
        if ($browser) {
            $exported = Export-RaidinessArtifact -ReportPath $resolved -RunPath $RunPath -Browser $browser -Pdf:$Pdf -TimeoutSeconds $TimeoutSeconds
            if ($exported.Ok) {
                $bridged = $true
                Write-Host "Wrote $($exported.Files -join ', ') into $RunPath" -ForegroundColor Green
            }
            else {
                Write-Warning "The exports could not be built: $($exported.Reason)"
                Write-Host 'Open the report and use its download buttons instead.' -ForegroundColor Yellow
            }
        }
        else {
            Write-Warning 'No Chromium-based browser found, so only report.html was written. Install Microsoft Edge, or open the report and use its download buttons.'
        }
        Write-Host 'DOCX, XLSX and PPTX are produced by the Node CLI only (bun cli/src/index.ts --exports docx,xlsx).' -ForegroundColor DarkGray
    }

    if ((-not $bridged) -and ($Markdown -or $LLM)) {
        $offers = @(($Markdown ? 'report.md' : $null), ($LLM ? 'llm-bundle.zip' : $null) | Where-Object { $_ })
        Write-Host "The page offers $($offers -join ' and ') as download(s)." -ForegroundColor Green
    }

    if ($runFolder) { Close-RaidinessLog | Out-Null }

    # A run folder that already has its files does not need a browser window.
    if ((-not $NoOpen) -and (-not $bridged)) {
# psrunner-lint allow: Start-Process — opens the generated local report in the default browser; never touches the tenant
        Start-Process $resolved
    }
}