SqlCertForge.Audit.psm1

# =============================================================================
# Script : SqlCertForge.psm1
# Author : Keith Ramsey
# Created : 2026-05-15
# =============================================================================
# Change Log
# -----------------------------------------------------------------------------
# 2026-05-15 Keith Ramsey Initial creation (Phase 1): loader; provenance
# captured ONCE here (DR-005); dot-source Private
# then Public; export only Public names.
# =============================================================================
# Decision Contract (see Docs/DECISION_REGISTER.md -- keep in sync)
# -----------------------------------------------------------------------------
# Must : ONLY file permitted to use $PSScriptRoot (suite rule 2);
# loader arrays $privateFiles / $publicFiles; dot-source
# Private/*.ps1 then Public/*.ps1; capture provenance ONCE
# (DR-005) -- ModuleVersion from manifest, SchemaVersion baseline,
# CommitSha best-effort from a trusted Program Files git only
# (suite pattern P / CWE-427); export only Public function names.
# Status: SCAFFOLD -- loader implemented; Private/Public populate in Phase 2.
# =============================================================================

Set-StrictMode -Version Latest

# --- Provenance, captured once at load (DR-005) ------------------------------
# Single source of truth. New-SqlCertResult stamps these onto every result so
# a downstream consumer can prove which build produced which answer.
$script:schemaVersion = '1.0'   # SqlCert.Result contract revision
$script:runId = [guid]::NewGuid().ToString()   # correlates all results in this session

$script:moduleVersion = 'unknown'
try {
    $manifestPath = Join-Path $PSScriptRoot 'SqlCertForge.Audit.psd1'
    if (Test-Path -LiteralPath $manifestPath -PathType Leaf) {
        $manifest = Import-PowerShellDataFile -LiteralPath $manifestPath -ErrorAction Stop
        if ($manifest.ModuleVersion) { $script:moduleVersion = [string]$manifest.ModuleVersion }
    }
} catch {
    # Provenance is best-effort; a missing/unreadable manifest must not stop
    # the module loading. Leave ModuleVersion = 'unknown' (honest, not faked).
    Write-Verbose "SqlCertForge: ModuleVersion not resolved ($($_.Exception.Message)); using 'unknown'."
}

# CommitSha is best-effort and only trusted from a Program Files git install
# (suite pattern P / CWE-427 -- never trust an arbitrary git.exe on PATH).
$script:commitSha = $null
try {
    $gitCandidates = @(
        (Join-Path $env:ProgramFiles        'Git\cmd\git.exe'),
        (Join-Path $env:ProgramFiles        'Git\bin\git.exe'),
        (Join-Path ${env:ProgramFiles(x86)} 'Git\cmd\git.exe'),
        (Join-Path ${env:ProgramFiles(x86)} 'Git\bin\git.exe')
    ) | Where-Object { $_ -and (Test-Path -LiteralPath $_ -PathType Leaf) }
    if ($gitCandidates) {
        $sha = & $gitCandidates[0] -C $PSScriptRoot rev-parse --short HEAD 2>$null
        if ($LASTEXITCODE -eq 0 -and $sha) { $script:commitSha = ([string]$sha).Trim() }
    }
} catch {
    # Null CommitSha is the correct, honest value when git is absent or the
    # tree is a shipped (non-repo) copy. Never fabricate a SHA.
    Write-Verbose "SqlCertForge: CommitSha not resolved ($($_.Exception.Message)); leaving null."
}

# --- Dot-source: Private first (so Public can call it), then Public ----------
$privateFiles = @(Get-ChildItem -Path (Join-Path $PSScriptRoot 'Private') -Filter '*.ps1' -ErrorAction SilentlyContinue)
$publicFiles  = @(Get-ChildItem -Path (Join-Path $PSScriptRoot 'Public')  -Filter '*.ps1' -ErrorAction SilentlyContinue)

foreach ($file in @($privateFiles) + @($publicFiles)) {
    try {
        . $file.FullName
    } catch {
        # A broken source file is a load-time defect -- surface it loudly via
        # the engine's own error stream, but do not throw from the loader.
        Write-Error "SqlCertForge: failed to load '$($file.Name)': $($_.Exception.Message)"
    }
}

# Member-access enumeration throws under StrictMode when Public/ is empty
# (Phase 1 scaffold). Project the names safely.
$exportNames = @($publicFiles | ForEach-Object { $_.BaseName })
if ($exportNames.Count -gt 0) {
    Export-ModuleMember -Function $exportNames
} else {
    # No public functions yet (Phase 1 scaffold). Loading the module for
    # private-scope unit tests is still valid; export nothing.
    Export-ModuleMember -Function @()
}