Private/Lead.ps1

# Leads: what several Findings point to together.
#
# A blue screen from a read error, eleven disk errors and a drive that reports wear are
# three Findings in three places, and seeing that they are one story is the step that
# takes experience. Once every Check has run, a Run reads its Findings together and says
# what several of them point to. A Lead is not a Finding: it observes nothing, has no
# Severity and is not counted. See ADR-0005.
#
# A rule is a name and the Signals that support it. It goes by Signals and never by what
# a Finding says, so that rewording a Finding cannot stop a Lead from forming.

$script:LeadRules = @(
    [pscustomobject]@{ Name = 'drive'; Signals = @('disk-errors', 'drive-health-warning', 'stop-code-disk', 'disk-write-slow', 'program-not-paged-in') }
    # Of the hardware errors Windows logs, only those of the memory: a processor or a bus
    # error beside a memory stop code is no second sign of the memory.
    [pscustomobject]@{ Name = 'memory'; Signals = @('stop-code-memory', 'memory-errors', 'access-violations-spread') }
    [pscustomobject]@{ Name = 'graphics'; Signals = @('live-kernel-graphics', 'stop-code-graphics', 'graphics-driver-old') }
    # Several Servers affected is this machine's connection; one Server affected is that
    # Server. The two go by different Signals, so that neither is taken for the other.
    #
    # Background is a Signal that is how most machines are and no observation of anything
    # going wrong: power saving on the adapter is what Windows sets. It supports a Lead
    # beside something that was observed, from a Finding that is in order too - and by
    # itself it starts nothing, not even "no Lead possible": that stood in every Report
    # of a Run that left Checks out, about a machine with nothing wrong.
    [pscustomobject]@{ Name = 'connection'; Signals = @('link-dropped', 'servers-lost', 'cable-wifi-switching', 'loss-on-own-network', 'usb-network-adapter', 'adapter-power-saving', 'crash-with-network-disruption')
        Background = @('adapter-power-saving') }
    # One Lead for each Server, or for one of its Services: its Findings are read together
    # by what they are about. A Server that answered apart from what fails on it - its ping,
    # a Service nothing failed on - is up, and what fails is read Service by Service: one
    # service, a firewall rule or a hung service. Nothing of it answering, and more than one
    # Service failing or the Server itself, is the Server or the way to it. What answered is
    # what a Finding cleared of Answered: it looked, and the Server was not unreachable. A
    # ping unanswered is the Server's own, beside its Services refused: the second Signal.
    # ADR-0015.
    [pscustomobject]@{ Name = 'server'; PerSubject = $true; Signals = @('server-lost', 'server-slow', 'server-unreachable', 'ping-unanswered', 'dependency-on-server')
        Answered = @('server-unreachable') }
    # Drives mapped by the domain's name, and one of the domain controllers behind it
    # that takes long: the drives come late on some days, and three Checks each say a
    # part of why. Seen on a desktop whose sign-in took ninety seconds every second day. A
    # folder of the namespace no Server serves any more is the drive's path dead (#172).
    [pscustomobject]@{ Name = 'domain-drives'; Signals = @('drive-mapping-slow', 'share-not-found', 'sign-in-intermittent', 'sign-in-by-controller', 'controller-differs', 'namespace-target-gone', 'namespace-unreachable', 'policies-differ') }
    # A clock that is off, and Servers that refused this machine's Kerberos tickets: the clock
    # is the cause. The clock is the machine's, so this is about no Server (#183).
    [pscustomobject]@{ Name = 'clock-refuses-kerberos'; Signals = @('clock-off', 'kerberos-refused') }
    # A Service that refused a sign-in, and beside it on the same Service what makes it refuse:
    # no credential saved, one it no longer takes, no SPN or one on two accounts, or the
    # Service in maintenance. Read Service by Service, keyed as the Report keys it (ADR-0013,
    # ADR-0015): a refusal on one Service and a cause on another are two stories (#183).
    # Without the refusal it is no Lead: a credential missing beside an SPN missing is two
    # causes of nothing that was seen to fail. An SPN is asked for an HTTPS Service only:
    # a Check that did not look for one is no gap beside a download over plain HTTP.
    [pscustomobject]@{ Name = 'service-sign-in'; PerService = $true; Requires = @('service-denied')
        Signals = @('service-denied', 'credential-refused', 'credential-missing', 'spn-missing', 'spn-duplicate', 'service-maintenance')
        HttpsOnly = @('spn-missing', 'spn-duplicate') }
)

# How many Leads a Report shows. More than this at the top of a Report is a list again,
# and a list is what a Lead is there to spare the reader.
$script:LeadMaximum = 3

function Get-LeadText {
    <#
    .SYNOPSIS
        What a Lead is called and what to do about it. A rule this has no words for is
        called by its name, which shows that the words are missing.
    .DESCRIPTION
        Topic is what a note about a Lead that could not form names it by, where its Title
        says a conclusion: "the cause is found" stood in a note that no suspicion was
        possible. Without one, the Title is the Topic.
    #>

    [CmdletBinding()]
    [OutputType([psobject])]
    param(
        [Parameter(Mandatory)][string]$Name, [AllowNull()][AllowEmptyString()][string]$Subject, [AllowNull()]$Situation,
        # The Signals the Lead rests on. A rule says the same of whatever supports it,
        # but for one case: a connection that keeps failing over from cable to Wi-Fi and
        # back is said as that, because what to do about it is not what to do about any
        # connection that drops.
        [AllowNull()][AllowEmptyCollection()][string[]]$Signal,
        # Of a Lead about one Service: that the rest of its Server answered.
        [bool]$Answered,
        # Of the drives Lead: the roots of the Namespaces a folder of which no Server serves,
        # as the Findings it rests on name them in their parts (#172).
        [AllowNull()][AllowEmptyCollection()][string[]]$Namespace,
        # The Run's Network Map, and the keys of the Server and the Service the Lead is about
        # on it: what names them as the Report does (Get-ReportServerName,
        # Get-ReportServiceName). Without the Map, a Server is named as its Finding wrote it.
        [AllowNull()]$NetworkMap,
        [AllowNull()][AllowEmptyString()][string]$ServerKey = '',
        [AllowNull()][AllowEmptyString()][string]$ServiceKey = ''
    )

    switch ($Name) {
        'service' {
            # The Service as a Technician knows it: SMB on fs01, not fs01:445; what its
            # Definition calls it, or the Apps that use it, where the Map knows them
            # (Get-ReportServiceName). A URL is what it is called, with that name before it
            # (Get-ReportServiceText). Its Server as the Report shows it: the Map's of the
            # Lead's key, else as written (Get-ReportServerName).
            $service = Split-ServiceSubject -Name $Subject
            if ($service -and $service.Url) {
                $server = Get-ReportServerName -Map $NetworkMap -Key $ServerKey -Name ([uri]$service.Url).Host
                $title = (Get-Text 'Lead.Service.Url.Title') -f (Get-ReportServiceText -Map $NetworkMap -Subject $Subject)
            }
            else {
                $server = Get-ReportServerName -Map $NetworkMap -Key $ServerKey -Name $(if ($service) { $service.Server } else { "$Subject" })
                $known = Find-NetworkMapService -Map $NetworkMap -Key $ServiceKey
                $called = Get-ReportServiceName -Service $(if ($known) { $known } else { $service })
                $port = $(if ($service) { $service.Port })
                $what = $(if ($called) { (Get-Text 'Lead.Service.Port') -f $called, $port } else { (Get-Text 'Lead.Service.PortOnly') -f $port })
                $title = (Get-Text 'Lead.Service.Title') -f $server, $what
            }
            $hint = (Get-Text 'Lead.Service.Hint') -f $server
            if ($Answered) { $hint = '{0} | {1}' -f ((Get-Text 'Lead.Service.Hint.Answers') -f $server), $hint }
            return [pscustomobject]@{ Title = $title; Hint = $hint }
        }
        'connection' {
            if (@($Signal) -contains 'cable-wifi-switching') {
                return [pscustomobject]@{ Title = Get-Text 'Lead.Connection.Switching.Title'; Hint = Get-Text 'Lead.Connection.Switching.Hint'
                    Topic = Get-Text 'Lead.Connection.Title' }
            }
            return [pscustomobject]@{ Title = Get-Text 'Lead.Connection.Title'
                Hint = (Get-Text 'Lead.Connection.Hint') -f (Get-SituationLinkText -Situation $Situation) }
        }
        'server' {
            $server = Get-ReportServerName -Map $NetworkMap -Key $ServerKey -Name $Subject
            return [pscustomobject]@{ Title = (Get-Text 'Lead.Server.Title') -f $server; Hint = (Get-Text 'Lead.Server.Hint') -f $server }
        }
        'domain-drives' {
            # Which controller it is, is known where the sign-ins part by it.
            $hint = Get-Text 'Lead.DomainDrives.Hint'
            if (@($Signal) -contains 'sign-in-by-controller') { $hint = '{0} | {1}' -f (Get-Text 'Lead.DomainDrives.Hint.ByController'), $hint }
            # A cause that was found is said in front of everything that is only to be checked.
            if (@($Signal) -contains 'policies-differ')       { $hint = '{0} | {1}' -f (Get-Text 'Lead.DomainDrives.Hint.Policies'), $hint }
            if (@($Signal) -contains 'namespace-target-gone') { $hint = '{0} | {1}' -f (Get-Text 'Lead.DomainDrives.Hint.Namespace'), $hint }
            # And a path that is dead before a Server gone from a folder another one serves,
            # named by its Namespace where the Finding names one.
            if (@($Signal) -contains 'namespace-unreachable') {
                $roots = @($Namespace | Where-Object { $_ })
                $dead = $(if ($roots.Count) { (Get-Text 'Lead.DomainDrives.Hint.Unreachable.Named') -f ($roots -join ', ') } else { Get-Text 'Lead.DomainDrives.Hint.Unreachable' })
                $hint = '{0} | {1}' -f $dead, $hint
            }
            return [pscustomobject]@{ Title = Get-Text 'Lead.DomainDrives.Title'; Hint = $hint }
        }
        'clock-refuses-kerberos' {
            return [pscustomobject]@{ Title = Get-Text 'Lead.KerberosClock.Title'; Hint = Get-Text 'Lead.KerberosClock.Hint'; Topic = Get-Text 'Lead.KerberosClock.Topic' }
        }
        'service-sign-in' {
            # The Service as the Report names it (Get-ReportServiceText), and each cause that
            # was found said in front of what is to be done whatever it is: the one that
            # settles it first, maintenance before the directory before the credential.
            $called = Get-ReportServiceText -Map $NetworkMap -Subject $Subject
            if (-not $called) { $called = "$Subject" }
            $hint = Get-Text 'Lead.ServiceSignIn.Hint'
            foreach ($cause in @(
                    @{ Signal = 'credential-missing';  Text = (Get-Text 'Lead.ServiceSignIn.Hint.CredentialMissing') }
                    @{ Signal = 'credential-refused';  Text = (Get-Text 'Lead.ServiceSignIn.Hint.CredentialRefused') }
                    @{ Signal = 'spn-missing';         Text = (Get-Text 'Lead.ServiceSignIn.Hint.SpnMissing') }
                    @{ Signal = 'spn-duplicate';       Text = (Get-Text 'Lead.ServiceSignIn.Hint.SpnDuplicate') }
                    @{ Signal = 'service-maintenance'; Text = (Get-Text 'Lead.ServiceSignIn.Hint.Maintenance') })) {
                if (@($Signal) -contains $cause.Signal) { $hint = '{0} | {1}' -f $cause.Text, $hint }
            }
            return [pscustomobject]@{ Title = (Get-Text 'Lead.ServiceSignIn.Title') -f $called; Hint = $hint; Topic = (Get-Text 'Lead.ServiceSignIn.Topic') -f $called }
        }
        'drive'    { return [pscustomobject]@{ Title = Get-Text 'Lead.Drive.Title';    Hint = Get-Text 'Lead.Drive.Hint' } }
        'memory'   { return [pscustomobject]@{ Title = Get-Text 'Lead.Memory.Title';   Hint = Get-Text 'Lead.Memory.Hint' } }
        'graphics' { return [pscustomobject]@{ Title = Get-Text 'Lead.Graphics.Title'; Hint = Get-Text 'Lead.Graphics.Hint' } }
    }
    [pscustomobject]@{ Title = $Name; Hint = '' }
}

function Get-Lead {
    <#
    .SYNOPSIS
        The Leads a Run's Findings support. Pure.
    .DESCRIPTION
        A Lead takes two Findings that observed two different things its rule names. One
        Finding has its own Meaning and Hint and needs no reading-together. The same thing
        observed twice - two drives that report wear, two Servers that are slow - is two
        Findings about two things, and no evidence that either is what the other means.
 
        A Finding that is in order observed nothing that supports anything - but for what
        a rule calls Background: how the machine is set, which is no fault and still
        counts beside one. Background alone starts nothing.
 
        Every rule that fits is stated, the one resting on more Findings first, and no
        more than a few: a Report that showed one of two would claim to know which it is.
 
        A rule about one Server reads together the Findings whose Subjects are one Server as
        the Report draws it: by the Key of the Network Map's Server, where the Run hands in
        its Map, else by the identity module's key (Get-NetworkMapServerKey, ADR-0013). A
        Subject that names a Service is that Service on that Server, keyed as the Map keys
        it (Get-NetworkMapServiceKey), and a Finding takes part for its Subject and for
        each of its parts that is not in order (ADR-0015). Where the Server answered apart
        from what fails on it, a Lead is about one Service, else about the Server.
 
        A rule about one Service (PerService) reads together what names that Service, by
        the same key, and nothing that names only its Server. A rule that Requires a
        Signal forms nothing where it was not seen: a cause without the refusal it causes.
    #>

    [CmdletBinding()]
    [OutputType([psobject])]
    param(
        [AllowNull()][AllowEmptyCollection()]$Finding,
        [AllowNull()][AllowEmptyCollection()]$Rule = $script:LeadRules,

        # What a Lead's Hint may assume about the machine, like any Hint.
        [AllowNull()]$Situation,

        # The Run's Network Map (New-NetworkMap), whose Servers the Report draws: which
        # Subjects are one Server. Without it, the identity module says it alone.
        [AllowNull()]$NetworkMap
    )

    $all        = @($Finding | Where-Object { $_ })
    $candidates = @($all | Where-Object { "$(Get-DataProperty $_ 'Severity')" -ne 'OK' })
    # What every Finding is about, its parts too: what tells which FQDN a short name stands for.
    # Not a Namespace: its root is no Server, and its host no FQDN a short name is (#172).
    $subjects   = @($all | ForEach-Object { "$(Get-DataProperty $_ 'Subject')"; (Get-DataCollection $_ 'Subjects') | ForEach-Object { "$(Get-DataProperty $_ 'Subject')" } } |
        Where-Object { $_ -and (Split-Subject -Name $_).Kind -ne 'Namespace' })
    # What a Finding names: its Subject, and each of its parts - but a part in order of a
    # Finding that is not, which observed nothing wrong of what it names.
    $namesOf = {
        param($Of)
        $inOrder = "$(Get-DataProperty $Of 'Severity')" -eq 'OK'
        $parts = @((Get-DataCollection $Of 'Subjects') | Where-Object { $inOrder -or "$(Get-DataProperty $_ 'Severity')" -ne 'OK' } |
            ForEach-Object { "$(Get-DataProperty $_ 'Subject')" })
        @(@("$(Get-DataProperty $Of 'Subject')") + $parts | Where-Object { $_ } | Select-Object -Unique)
    }
    # The Server each Subject is, and the Service on it where it names one, asked once for
    # each: fs01:445 is the Service fs01.contoso.example:445 on fs01.contoso.example, a URL
    # the Service of that URL on its host. A Namespace is on no Server: \\contoso.example\public
    # is judged by its folders, and is never the domain's Server (#172).
    $keys = @{}
    $keyOf = {
        param([string]$Subject)
        if (-not $keys.ContainsKey($Subject)) {
            $read = Split-Subject -Name $Subject
            if ($read.Kind -eq 'Namespace') { $keys[$Subject] = [pscustomobject]@{ Server = ''; Service = ''; ServerName = '' } }
            else {
                $server = $(if ($read.Url) { ([uri]$read.Url).Host } elseif ($read.Kind -eq 'Service') { $read.Server } else { $Subject })
                $serverKey = Get-NetworkMapServerKey -Name $server -Among $subjects -Map $NetworkMap
                $serviceKey = ''
                if ($read.Kind -eq 'Service' -and $serverKey) { $serviceKey = Get-NetworkMapServiceKey -ServerKey $serverKey -Port $read.Port -Url $read.Url }
                $keys[$Subject] = [pscustomobject]@{ Server = $serverKey; Service = $serviceKey; ServerName = $server }
            }
        }
        $keys[$Subject]
    }

    $formed = New-Object 'System.Collections.Generic.List[object]'
    $open   = New-Object 'System.Collections.Generic.List[object]'
    $order  = 0
    foreach ($one in @($Rule | Where-Object { $_ })) {
        $order++
        $wanted = @((Get-DataCollection $one 'Signals') | ForEach-Object { "$_" })
        $background = @((Get-DataCollection $one 'Background') | ForEach-Object { "$_" })
        # What observed something: not in order, and about more than how the machine is set.
        $mine   = @($candidates | Where-Object { @((Get-DataCollection $_ 'Signals') | Where-Object { $_ -in $wanted -and $_ -notin $background }).Count })
        if (-not $mine.Count) { continue }
        if ($background.Count) {
            $mine += @($all | Where-Object { $mine -notcontains $_ -and @((Get-DataCollection $_ 'Signals') | Where-Object { $_ -in $background }).Count })
        }

        # A rule about one thing among several is asked once for each of them, and a
        # Finding that is about nothing in particular takes part in none. One thing is one
        # Server as the Report draws it: the Network Map's, else a short name with the one
        # FQDN that begins with it among the Run's Subjects, two FQDNs never (ADR-0013).
        $ruleName = "$(Get-DataProperty $one 'Name')"
        $groups = @([pscustomobject]@{ Name = $ruleName; Subject = ''; Server = ''; Service = ''; Answered = $false; Findings = $mine })
        if ([bool](Get-DataProperty $one 'PerSubject')) {
            # What fails on each Server: of its own - a ping, a program on it - and of each of
            # its Services. A Finding that names several is in each.
            $servers = [ordered]@{}
            foreach ($f in $mine) {
                foreach ($name in @(& $namesOf $f)) {
                    $key = & $keyOf $name
                    if (-not $key.Server) { continue }
                    if (-not $servers.Contains($key.Server)) {
                        $servers[$key.Server] = [pscustomobject]@{ Name = ''; Own = @(); Services = [ordered]@{}; Answered = $false }
                    }
                    $server = $servers[$key.Server]
                    if (-not $key.Service) {
                        if (-not $server.Name) { $server.Name = $name }
                        if ($server.Own -notcontains $f) { $server.Own += $f }
                        continue
                    }
                    if (-not $server.Services.Contains($key.Service)) {
                        $server.Services[$key.Service] = [pscustomobject]@{ Name = $name; ServerName = $key.ServerName; Findings = @() }
                    }
                    if ($server.Services[$key.Service].Findings -notcontains $f) { $server.Services[$key.Service].Findings += $f }
                }
            }
            # What answered of each, apart from what fails on it: its ping, a Service of it
            # nothing failed on. Not a Signal, and no Lead forms from it: it says which Lead.
            $answering = @((Get-DataCollection $one 'Answered') | ForEach-Object { "$_" })
            foreach ($f in @($all | Where-Object { $mine -notcontains $_ -and @((Get-DataCollection $_ 'Cleared') | Where-Object { $_ -in $answering }).Count })) {
                foreach ($name in @(& $namesOf $f)) {
                    $key = & $keyOf $name
                    if (-not $servers.Contains($key.Server)) { continue }
                    if ($key.Service -and $servers[$key.Server].Services.Contains($key.Service)) { continue }
                    $servers[$key.Server].Answered = $true
                }
            }

            # A Server that answered is up: each Service failing on it is a Lead of its own,
            # with what is about the Server itself beside it. Nothing answering, one Service
            # failing and nothing else is that Service; more is the Server or the way to it.
            $groups = @(foreach ($serverKey in @($servers.Keys | Sort-Object)) {
                $server = $servers[$serverKey]
                $services = @($server.Services.Keys | Sort-Object)
                $whole = -not $services.Count -or (-not $server.Answered -and ($server.Own.Count -or $services.Count -ge 2))
                if ($whole) {
                    $about = @($server.Own) + @($services | ForEach-Object { $server.Services[$_].Findings })
                    $named = $(if ($server.Name) { $server.Name } else { $server.Services[$services[0]].ServerName })
                    [pscustomobject]@{ Name = $ruleName; Subject = $named; Server = $serverKey; Service = ''; Answered = $server.Answered
                        Findings = @($mine | Where-Object { $about -contains $_ }) }
                    continue
                }
                foreach ($serviceKey in $services) {
                    $about = @($server.Own) + @($server.Services[$serviceKey].Findings)
                    [pscustomobject]@{ Name = 'service'; Subject = $server.Services[$serviceKey].Name; Server = $serverKey; Service = $serviceKey; Answered = $server.Answered
                        Findings = @($mine | Where-Object { $about -contains $_ }) }
                }
            })
        }
        elseif ([bool](Get-DataProperty $one 'PerService')) {
            # A rule about one Service is asked once for each, keyed as a Service Lead keys it:
            # its Server by the Map, its port or URL kept. What names a Server alone, or
            # nothing, is about no Service and takes part in none.
            $services = [ordered]@{}
            foreach ($f in $mine) {
                foreach ($name in @(& $namesOf $f)) {
                    $key = & $keyOf $name
                    if (-not $key.Service) { continue }
                    if (-not $services.Contains($key.Service)) {
                        $services[$key.Service] = [pscustomobject]@{ Name = $name; Server = $key.Server; Findings = @() }
                    }
                    if ($services[$key.Service].Findings -notcontains $f) { $services[$key.Service].Findings += $f }
                }
            }
            $groups = @(foreach ($serviceKey in @($services.Keys | Sort-Object)) {
                $service = $services[$serviceKey]
                [pscustomobject]@{ Name = $ruleName; Subject = $service.Name; Server = $service.Server; Service = $serviceKey; Answered = $false
                    Findings = @($mine | Where-Object { $service.Findings -contains $_ }) }
            })
        }
        $required = @((Get-DataCollection $one 'Requires') | ForEach-Object { "$_" })
        $httpsOnly = @((Get-DataCollection $one 'HttpsOnly') | ForEach-Object { "$_" })

        foreach ($group in $groups) {
            $support = @($group.Findings)
            $seen = @{}
            foreach ($f in $support) { foreach ($signal in @((Get-DataCollection $f 'Signals') | Where-Object { $_ -in $wanted })) { $seen["$signal"] = $true } }
            # What the rule is about must have been seen, or there is nothing to join a cause to.
            if (@($required | Where-Object { -not $seen.ContainsKey($_) }).Count) { continue }

            # What could have supported it and did not look: a Check that was skipped,
            # rights that were not given. Its Value says why. It supports nothing. Of a
            # rule about one thing, only what is about that thing or about none: of a
            # Service, what is about it, about its Server or about none. What matters for an
            # HTTPS Service only is no gap of a Service that is not one: port 443 or an https URL.
            $missed = $wanted
            if ($group.Service -and $httpsOnly.Count) {
                $service = Split-ServiceSubject -Name $group.Subject
                $https = $service -and ($service.Port -eq 443 -or ($service.Url -and ([uri]$service.Url).Scheme -eq 'https'))
                if (-not $https) { $missed = @($wanted | Where-Object { $_ -notin $httpsOnly }) }
            }
            $notChecked = @(foreach ($f in $all) {
                if (-not @((Get-DataCollection $f 'Unobserved') | Where-Object { $_ -in $missed }).Count) { continue }
                $about = @(& $namesOf $f | ForEach-Object { & $keyOf $_ } | Where-Object { $_.Server })
                if ($group.Server -and $about.Count -and -not @($about | Where-Object {
                    $_.Server -eq $group.Server -and (-not $group.Service -or -not $_.Service -or $_.Service -eq $group.Service) }).Count) { continue }
                # A Finding that speaks for several Checks - a Scope that left thirty out -
                # says which of them would have looked for this, and only those are named.
                $by = @((Get-DataCollection $f 'UnobservedBy') | ForEach-Object { $_ })
                if ($by.Count) {
                    foreach ($item in $by) {
                        if (@((Get-DataCollection $item 'Signals') | Where-Object { $_ -in $missed }).Count) {
                            [pscustomobject]@{ Check = "$(Get-DataProperty $item 'Check')"; Reason = "$(Get-DataProperty $item 'Reason')" }
                        }
                    }
                    continue
                }
                [pscustomobject]@{ Check = "$(Get-DataProperty $f 'Check')"; Reason = "$(Get-DataProperty $f 'Value')" }
            })

            $isLead = $support.Count -ge 2 -and $seen.Count -ge 2
            # One observation and nobody looked for the second: not a Lead, and not nothing.
            if (-not $isLead -and -not $notChecked.Count) { continue }

            # A dead path by the part that names it, never by a Finding's words (ADR-0005): the
            # root of each Namespace a Finding that observed it names FAIL - its worst folder.
            $dead = @($support | Where-Object { (Get-DataCollection $_ 'Signals') -contains 'namespace-unreachable' } |
                ForEach-Object { (Get-DataCollection $_ 'Subjects') | Where-Object { "$(Get-DataProperty $_ 'Severity')" -eq 'FAIL' } } |
                ForEach-Object { Split-Subject -Name "$(Get-DataProperty $_ 'Subject')" } | Where-Object { $_.Kind -eq 'Namespace' } |
                ForEach-Object { $_.Key } | Select-Object -Unique)
            $text = Get-LeadText -Name $group.Name -Subject $group.Subject -Situation $Situation -Signal @($seen.Keys) -Answered $group.Answered -Namespace $dead `
                -NetworkMap $NetworkMap -ServerKey $group.Server -ServiceKey $group.Service

            # When what it rests on was last seen: only where every Finding it rests on
            # says so - one that is about now would make any date a wrong one. A fault
            # that was repaired last week still fills the month the Findings read, and
            # without this a repaired machine reads as broken.
            $dates = @($support | ForEach-Object { ConvertTo-DataTime (Get-DataProperty $_ 'LastSeen') })
            $lastSeen = $null
            if ($dates.Count -and -not @($dates | Where-Object { $null -eq $_ }).Count) {
                $lastSeen = @($dates | Sort-Object)[-1]
                $text.Hint = '{0} | {1}' -f ((Get-Text 'Lead.LastSeen') -f $lastSeen), $text.Hint
            }
            $lead = [pscustomobject]@{
                PSTypeName = 'Gutcheck.Lead'
                Name       = "$($group.Name)"
                Subject    = "$($group.Subject)"
                Title      = $text.Title
                # What a note that it could not form names it by (Get-LeadText).
                Topic      = $(if ($text.PSObject.Properties['Topic'] -and $text.Topic) { $text.Topic } else { $text.Title })
                Hint       = $text.Hint
                Findings   = $support
                LastSeen   = $lastSeen
                Signals    = @($seen.Keys | Sort-Object)
                NotChecked = $notChecked
                # $false: what was observed is one thing only, and what could have made a
                # Lead of it was not looked at. Said under the Leads, so that it is not
                # read as none.
                Formed     = $isLead
                Order      = $order
            }
            if ($isLead) { $formed.Add($lead) } else { $open.Add($lead) }
        }
    }
    # Sort-Object is not stable on Windows PowerShell 5.1, so the order rules were given
    # in is part of the key.
    @($formed | Sort-Object @{ Expression = { $_.Findings.Count }; Descending = $true }, Order | Select-Object -First $script:LeadMaximum)
    @($open | Sort-Object Order | Select-Object -First $script:LeadMaximum)
}