Private/Add-SqlSpnProvenance.ps1

# =============================================================================
# Script : Add-SqlSpnProvenance.ps1
# Author : Keith Ramsey
# Created : 2026-05-28
# =============================================================================
# Change Log
# -----------------------------------------------------------------------------
# 2026-05-28 cross-suite meta Claude Initial: provenance helper per DR-312
# (in the internal companion). Stamps the suite-
# standard five fields (ModuleVersion / SchemaVersion
# / CommitSha / RunId / Timestamp) on any
# [pscustomobject] before it leaves a Public command.
# Matches the cross-suite factory pattern used by
# SqlInstanceForge New-SqlForgeResult, SqlCertForge
# New-SqlCertResult, ClusterValidator Add-ClvResult.
# =============================================================================

function Add-SqlSpnProvenance {
    <#
    .SYNOPSIS
        Stamp the suite-standard provenance fields on a result object.
    .DESCRIPTION
        Adds ModuleVersion, SchemaVersion, CommitSha, RunId, and Timestamp to
        a [pscustomobject] in place and returns the same object. The first
        three fields are captured once at module load (see SqlSpnManager.psm1)
        so a single invocation pays no per-call manifest-read cost. RunId is
        a per-call GUID; Timestamp is per-call UTC ISO 8601.

        If a caller already attached one of these fields by name (defensive —
        the helper supports being called twice on the same object), the
        existing value is replaced. Never throws; if a captured script-scope
        provenance variable is missing (e.g. the module was dot-sourced
        standalone in a test), the field gets a sentinel string ('unknown')
        so the shape is preserved.
    .PARAMETER Result
        The result object to stamp. Mutated in place AND returned, so the
        caller may simply `return $Result | Add-SqlSpnProvenance` or
        `Add-SqlSpnProvenance -Result $r ; $r`.
    .OUTPUTS
        The same [pscustomobject] passed in, with the five fields added.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '',
        Justification = 'Mutates the result object in place; no external state. ShouldProcess would be noise.')]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [pscustomobject] $Result
    )

    process {
        $mv = if (Test-Path variable:script:moduleVersion) { $script:moduleVersion } else { 'unknown' }
        $sv = if (Test-Path variable:script:schemaVersion) { $script:schemaVersion } else { 'unknown' }
        $sh = if (Test-Path variable:script:commitSha)    { $script:commitSha }    else { $null }

        $stamp = [ordered]@{
            ModuleVersion = $mv
            SchemaVersion = $sv
            CommitSha     = $sh
            RunId         = [guid]::NewGuid().ToString()
            Timestamp     = [datetime]::UtcNow.ToString('o')
        }

        foreach ($k in $stamp.Keys) {
            if ($Result.PSObject.Properties.Match($k).Count -gt 0) {
                $Result.$k = $stamp[$k]
            } else {
                $Result | Add-Member -MemberType NoteProperty -Name $k -Value $stamp[$k]
            }
        }

        $Result
    }
}