p = '.spt/preserved/hertz-fp-driver-review/d2/d2_capture.ps1'
s = open(p, 'rb').read().decode('ascii')


def rep(a, b):
    global s
    if a not in s:
        raise SystemExit('MISS: ' + a[:90])
    s = s.replace(a, b, 1)


# --- a timed-query helper, so every record carries its own observation interval -------------------
rep("""function Read-RuleRecord {""",
"""# Every query records its OWN start and end. The document's single `utc` is the document's instant,
# not the observation time of each sequential query inside it, and one value cannot stand for all of
# them (doyle, 2026-09-13).
function New-QueryInterval {
    param([datetime]$Start, [datetime]$End)
    return [ordered]@{
        started_utc = $Start.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
        ended_utc   = $End.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
        elapsed_ms  = [int]([math]::Round(($End - $Start).TotalMilliseconds))
    }
}

# Every returned object is preserved with its own identity. The FIRST match is never presented as the
# complete result: more than one match is marked ambiguous and all of them are recorded.
function Get-ObjectProps {
    param($Objects, [string[]]$Props, [string]$QueryOutcome, [string]$Store, [switch]$RuleScoped)

    $out = New-Object System.Collections.ArrayList
    $list = @($Objects)
    if ($list.Count -eq 0) { $list = @($null) }      # one record carrying NO_RULE / DENIED / ERROR
    $idx = 0
    foreach ($o in $list) {
        $props = [ordered]@{}
        foreach ($p in $Props) {
            $entry = Get-PropState -Object $o -Name $p -QueryOutcome $QueryOutcome
            if ($RuleScoped) {
                $entry['defined_for_store'] = if (($DefinedActiveStoreOnly -contains $p) -and ($Store -ne 'ActiveStore')) { $false } else { $true }
            }
            $props[$p] = $entry
        }
        $null = $out.Add([ordered]@{ match_index = $idx; properties = $props })
        $idx++
    }
    return @($out.ToArray())
}

function Read-RuleRecord {""")

# --- rule record: preserve all matches, time the query ---------------------------------------------
rep("""    $obj = $null; $outcome = 'OK'; $found = $false; $matched = 0
    try {""",
"""    $objs = @(); $outcome = 'OK'; $found = $false; $matched = 0
    $qStart = Get-Date
    try {""")

rep("""        $obj = if ($TraceSource) {
            Get-NetFirewallRule -PolicyStore $Store -Name $Name -TracePolicyStoreSource
        } else {
            Get-NetFirewallRule -PolicyStore $Store -Name $Name
        }
        $items   = @($obj)                       # @() so one match is still an array
        $matched = $items.Count
        $obj     = if ($matched -ge 1) { $items[0] } else { $null }
        $found   = ($matched -ge 1)
    } catch {""",
"""        $raw = if ($TraceSource) {
            Get-NetFirewallRule -PolicyStore $Store -Name $Name -TracePolicyStoreSource
        } else {
            Get-NetFirewallRule -PolicyStore $Store -Name $Name
        }
        $objs    = @($raw)                       # @() so one match is still an array
        $matched = $objs.Count
        $found   = ($matched -ge 1)
    } catch {""")

rep("""        Add-CaptureError -Command $cmd -ErrorRecord $_
    }

    $props = [ordered]@{}
    foreach ($p in $RuleProps) {
        $entry = Get-PropState -Object $obj -Name $p -QueryOutcome $outcome
        $entry['defined_for_store'] = if ($DefinedActiveStoreOnly -contains $p -and $Store -ne 'ActiveStore') { $false } else { $true }
        if ($p -in @('PolicyStoreSource', 'PolicyStoreSourceType')) {
            $entry['trace_policy_store_source_used'] = [bool]$TraceSource
        }
        $props[$p] = $entry
    }
""",
"""        Add-CaptureError -Command $cmd -ErrorRecord $_
    }
    $interval = New-QueryInterval -Start $qStart -End (Get-Date)

    $matches = Get-ObjectProps -Objects $objs -Props $RuleProps -QueryOutcome $outcome -Store $Store -RuleScoped
""")

rep("""            setup_interval      = [ordered]@{ start = $SetupIntervalStart; end = $SetupIntervalEnd }
            provider_creation   = 'SEE properties.Description/InstanceID -- no instant is invented between the interval bounds'
        }""",
"""            # The setup interval IS the creation record. No provider creation timestamp is claimed:
            # neither Description nor InstanceID is one, and no instant is invented between the bounds.
            setup_interval      = [ordered]@{ start = $SetupIntervalStart; end = $SetupIntervalEnd }
        }""")

rep("""        found                           = $found
        matched_count                   = $matched      # cardinality of the match, not just a boolean
        query_outcome                   = $outcome      # OK | DENIED | ERROR -- from the ERROR, never a value
        elevated                        = $script:Elevated
        properties                      = $props
    }
}""",
"""        found                           = $found
        matched_count                   = $matched      # cardinality of the match, not just a boolean
        ambiguous                       = ($matched -gt 1)   # more than one object answered to this name
        query_outcome                   = $outcome      # OK | DENIED | ERROR -- from the ERROR, never a value
        query_interval                  = $interval
        elevated                        = $script:Elevated
        matches                         = $matches      # EVERY returned object, each with its own properties
    }
}""")

# --- filter records: same treatment, and they were losing the match count entirely -----------------
rep("""        $obj = $null; $outcome = 'OK'
        try {""",
"""        $objs = @(); $outcome = 'OK'
        $qStart = Get-Date
        try {""")

rep("""            $items = @(Get-NetFirewallRule -PolicyStore $Store -Name $Name | & $spec.cmd)
            $obj   = if ($items.Count -ge 1) { $items[0] } else { $null }
        } catch {""",
"""            $objs = @(Get-NetFirewallRule -PolicyStore $Store -Name $Name | & $spec.cmd)
        } catch {""")

rep("""        $props = [ordered]@{}
        foreach ($p in $spec.props) { $props[$p] = Get-PropState -Object $obj -Name $p -QueryOutcome $outcome }
""",
"""        $interval = New-QueryInterval -Start $qStart -End (Get-Date)
        $matches  = Get-ObjectProps -Objects $objs -Props $spec.props -QueryOutcome $outcome -Store $Store
""")

rep("""            query         = $cmd
            query_outcome = $outcome
            elevated      = $script:Elevated
            properties    = $props""",
"""            query          = $cmd
            query_outcome  = $outcome
            query_interval = $interval
            matched_count  = $objs.Count                 # filters were losing this entirely
            ambiguous      = ($objs.Count -gt 1)
            elevated       = $script:Elevated
            matches        = $matches""")

# --- host state: time each of the three queries ----------------------------------------------------
rep("""    $outcome    = [ordered]@{ interfaces = 'OK'; profiles = 'OK'; addresses = 'OK' }""",
"""    $outcome    = [ordered]@{ interfaces = 'OK'; profiles = 'OK'; addresses = 'OK' }
    $interval   = [ordered]@{}
    $qStart     = Get-Date""")

for key, cmd in (('interfaces', 'Get-NetConnectionProfile'),
                 ('profiles', 'Get-NetFirewallProfile -All'),
                 ('addresses', 'Get-NetIPAddress')):
    rep("    } catch { $outcome['%s'] = $(if (Test-Denied $_) { 'DENIED' } else { 'ERROR' }); Add-CaptureError -Command '%s' -ErrorRecord $_ }" % (key, cmd),
        "    } catch { $outcome['%s'] = $(if (Test-Denied $_) { 'DENIED' } else { 'ERROR' }); Add-CaptureError -Command '%s' -ErrorRecord $_ }\n    $interval['%s'] = New-QueryInterval -Start $qStart -End (Get-Date); $qStart = Get-Date" % (key, cmd, key))

rep("""        query_outcome = $outcome          # per-query, because an empty array and a denied read look identical""",
"""        query_outcome  = $outcome         # per-query, because an empty array and a denied read look identical
        query_interval = $interval         # per-query, because the document's utc is not their observation time""")

# --- the numeric control must test for a NUMBER, not merely reject strings -------------------------
rep("""function Test-NumbersAreNumbers {
    param($Records)""",
"""function Test-NumbersAreNumbers {
    param($Records)
    # Rejecting strings is not the same as requiring a number: a boolean, a datetime or a PSObject
    # would all have passed the earlier form of this check. The required types are named explicitly,
    # and the raw `type` evidence stays in the document either way (doyle, 2026-09-13).
    $numeric = @('System.Byte', 'System.SByte', 'System.Int16', 'System.UInt16',
                 'System.Int32', 'System.UInt32', 'System.Int64', 'System.UInt64')""")

rep("""        $e = $r.properties['EnforcementStatus']
        if ($e.state -ne 'PRESENT') { continue }
        foreach ($v in @($e.value)) {
            if ($v -is [string]) { return $false }
        }""",
"""        foreach ($m in @($r.matches)) {
            $e = $m.properties['EnforcementStatus']
            if ($e.state -ne 'PRESENT') { continue }
            foreach ($v in @($e.value)) {
                if ($null -eq $v) { return $false }
                if ($numeric -notcontains $v.GetType().FullName) { return $false }
            }
        }""")

open(p, 'wb').write(s.encode('ascii'))
print('patched bytes=%d' % len(s))
