Public/Export-HDTMachineFact.ps1

function Export-HDTMachineFact {
    <#
        .SYNOPSIS
            Writes gathered facts to Gather\facts.json.
 
        .DESCRIPTION
            facts.json - the resolved machine facts - sits beside
            provenance.json in the log directory of every deployment. This writes
            it, through the injected IFileSystem rather than a file-writing
            cmdlet, so the whole path is provable with nothing on disk (PROJECT
            constraint 4).
 
            The sibling of Export-HDTVariableProvenance, and it carries the same
            rule: THE TIMESTAMP IS FORMATTED BEFORE SERIALISATION. ConvertTo-Json
            renders a raw [datetime] as an ISO 8601 string under pwsh 7 and as
            "\/Date(1786579862481)\/" under Windows PowerShell 5.1 - and 5.1 is
            the engine that runs in WinPE, where this file is written. The
            round-trip 'o' format in the invariant culture is identical on both.
 
            The document:
 
              { "schemaVersion": 1,
                "generated": "2026-08-13T00:11:02.4810000Z",
                "fact": { "HDTSerialNumber": "...", "HDTMacAddress": [ ... ] } }
 
            Facts keep the order they were gathered in, so a facts.json diff
            between two machines stays readable. An array-valued fact stays an
            array: a multi-NIC machine has more than one MAC, and collapsing that
            to a scalar would make a driver rule matching on it silently wrong.
 
        .PARAMETER Fact
            A Get-HDTMachineFact result, or any dictionary of name to value.
 
        .PARAMETER Path
            Where to write it. Conventionally <_HDTLogPath>\Gather\facts.json.
            Parent directories are created by the filesystem service.
 
        .PARAMETER FileSystem
            An IFileSystem - New-HDTFileSystem in production,
            New-HDTFakeFileSystem in a test.
 
        .PARAMETER Timestamp
            The instant recorded as "generated". Mandatory, and deliberately so:
            the engine has an IClock and must pass its answer, and a test passes a
            fixed one so it can assert an exact document. A default of "now" would
            put a real clock reading inside engine code, which PROJECT constraint
            4 exists to prevent.
 
        .OUTPUTS
            None.
 
        .EXAMPLE
            Export-HDTMachineFact -Fact $fact `
                -Path (Join-Path $logPath 'Gather\facts.json') `
                -FileSystem (New-HDTFileSystem) -Timestamp $clock.GetUtcNow()
    #>

    [CmdletBinding(SupportsShouldProcess = $true)]
    [OutputType([void])]
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [ValidateNotNull()]
        [System.Collections.IDictionary] $Fact,

        [Parameter(Mandatory = $true, Position = 1)]
        [ValidateNotNullOrEmpty()]
        [string] $Path,

        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [object] $FileSystem,

        [Parameter(Mandatory = $true)]
        [datetime] $Timestamp
    )

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

    $entry = [ordered] @{}
    foreach ($key in @($Fact.Keys)) {
        $entry[[string] $key] = $Fact[$key]
    }

    # A [string], never the [datetime] itself - see the description.
    $generated = $Timestamp.ToUniversalTime().ToString('o', [System.Globalization.CultureInfo]::InvariantCulture)

    $document = [ordered] @{
        schemaVersion = 1
        generated     = $generated
        fact          = $entry
    }

    if ($PSCmdlet.ShouldProcess($Path, 'Write machine facts')) {
        $FileSystem.WriteAllText($Path, (ConvertTo-Json -InputObject $document -Depth 6))
    }
}