core/private/Get-JaxTaskIndexFingerprint.ps1

function Get-JaxTaskIndexFingerprint {
    <#
    .SYNOPSIS
    Fingerprint of everything that can change task names, or $null outside git.

    .DESCRIPTION
    HEAD plus every changed or untracked .ps1/.psm1/.yml/.yaml file with its
    write time and size, plus the Jax version. Task names come from psake files
    and YAML configs, so edits to other files keep the index valid. A file
    edited twice keeps the same git status, which is why write time is included.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string] $RepoRoot
    )

    if (-not (Get-Command -Name git -ErrorAction SilentlyContinue)) {
        return $null
    }
    $head = & git -C $RepoRoot rev-parse HEAD 2>$null
    if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($head)) {
        return $null
    }
    # Porcelain paths are relative to the top level, and a RepoRoot below it
    # (a nested Jax repository) must still see changes anywhere in the repo:
    # includes and task paths may point outside RepoRoot.
    $topLevel = & git -C $RepoRoot rev-parse --show-toplevel 2>$null
    if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($topLevel)) {
        return $null
    }
    $status = & git -C $topLevel status --porcelain=v1 --untracked-files=all -- '*.ps1' '*.psm1' '*.yml' '*.yaml' 2>$null
    if ($LASTEXITCODE -ne 0) {
        return $null
    }

    $parts = [System.Collections.Generic.List[string]]::new()
    $parts.Add([string]$head)
    $parts.Add($RepoRoot)
    # Discovery itself: a Jax update or an edited Jax checkout changes the index.
    $toolRoot = Get-JaxToolRoot
    $versionFile = Join-Path $toolRoot 'VERSION'
    if (Test-Path -LiteralPath $versionFile -PathType Leaf) {
        $parts.Add((Get-Content -LiteralPath $versionFile -Raw).Trim())
    }
    $toolFiles = @(Get-ChildItem -LiteralPath (Join-Path $toolRoot 'core'), (Join-Path $toolRoot 'plugins') -Recurse -File -Filter '*.ps*1' -ErrorAction SilentlyContinue)
    if ($toolFiles.Count -gt 0) {
        $parts.Add([string]($toolFiles | Measure-Object -Property LastWriteTimeUtc -Maximum).Maximum.Ticks)
    }
    foreach ($line in @($status | Sort-Object)) {
        $path = ([string]$line).Substring(3).Trim('"')
        if ($path -match ' -> ') { $path = ($path -split ' -> ')[-1].Trim('"') }
        $full = Join-Path $topLevel $path
        $stamp = 'missing'
        if (Test-Path -LiteralPath $full -PathType Leaf) {
            $item = Get-Item -LiteralPath $full
            $stamp = '{0}|{1}' -f $item.LastWriteTimeUtc.Ticks, $item.Length
        }
        $parts.Add("$line|$stamp")
    }

    $bytes = [System.Text.Encoding]::UTF8.GetBytes(($parts -join "`n"))
    return [System.Convert]::ToHexString([System.Security.Cryptography.SHA256]::HashData($bytes))
}