::nsmcp::Tool ::nsmcp::Tool describe execute init jsonArguments name permission validate ::nx::Object ::nx::Object __default_accessor __default_method_call_protection __object_configureparameter __resolve_method_path contains copy delete object method delete object property delete object variable destroy_on_cleanup info consts info info info lookup parameters info lookup slots info lookup syntax info lookup variables info object method args info object method body info object method callprotection info object method debug info object method definition info object method definitionhandle info object method deprecated info object method exists info object method handle info object method origin info object method parameters info object method registrationhandle info object method returns info object method submethods info object method syntax info object method type info object slots info object variables info variable definition info variable name info variable parameter move object alias object forward object method object property object variable private protected public qn require namespace require object method require private object method require protected object method require public object method serialize ::nsmcp::Tool->::nx::Object ::nsmcp::AccessDiagnosticTool ::nsmcp::AccessDiagnosticTool accumulateTraffic aggregateKey argumentTriples classify configuredSlowPath contextComplete evidencePriority execute init logDirectory modules nativeClassify options page recordFields scanBounds scanStep seekSample source trafficTriples validate ::nsmcp::AccessDiagnosticTool->::nsmcp::Tool

Class ::nsmcp::AccessDiagnosticTool

::nsmcp::AccessDiagnosticTool[i] create ... \
           [ -adminExample (default "") ] \
           [ -adminSummary (default "") ] \
           [ -authorizer:required authorizer:required ] \
           [ -defaultMode (default "scan") ] \
           [ -description:required description:required ] \
           [ -inputSchema (default " { "type": "object", "properties": { "mode": { "type": "string", "enum": [ "scan", "page" ], "description": "Bot routing: aggregateTraffic true adds address-free source concentration and User-Agent rankings; path selects an exact queryless path. scan aggregates all pages in a background job, polled with log_results. page returns one bounded byte page. Slow requests remain paged." }, "authenticationState": { "type": "string", "enum": [ "anonymous", "authenticated", "unknown" ], "description": "Filter by logged authentication state; no identity is disclosed." }, "aggregateTraffic": { "type": "boolean", "default": false, "description": "Scan mode only: address-free source concentration and User-Agent rankings over all selected records, independent of evidence limits. No peer-disclosure permission required." }, "path": { "type": "string", "description": "Exact queryless path, e.g. /bugtracker/openacs/" }, "scanStrategy": { "type": "string", "enum": [ "window", "full" ], "default": "window", "description": "Background scans: window seeks by completion timestamps with a 300s margin and explicit ordering assumptions; full examines the entire current file." }, "lastMinutes": { "type": "integer", "minimum": 1, "maximum": 1440, "default": 60 }, "from": { "type": "number" }, "to": { "type": "number" }, "limit": { "type": "integer", "minimum": 1, "maximum": 100, "default": 20 }, "pool": { "type": "string", "default": "bots" }, "module": { "type": "string", "default": "nslog" }, "category": { "type": "string", "enum": [ "matching-rule-in-target", "matching-rule-other-pool-candidate", "bot-like-unmatched-heuristic", "no-target-rule-match", "unknown", "native-target-in-target", "native-target-other-pool-candidate", "bot-like-routed-elsewhere-heuristic", "native-other-pool" ], "description": "Bot routing only: retain evidence for this exact category. categoryTotals still covers all matching records; selectedMatches and filteredOutRecords distinguish selection from omitted evidence." }, "includePeerAddresses": { "type": "boolean", "default": false }, "beforeOffset": { "type": "integer", "minimum": 0 }, "sourceBytes": { "type": "integer", "minimum": 0 }, "sourceIdentity": { "type": "string" }, "mappingIdentity": { "type": "string" } }, "additionalProperties": false } ") ] \
           [ -maxAggregateSources:integer (default "100000") ] \
           [ -maxAggregateUserAgents:integer (default "4096") ] \
           [ -maxReadBytes:integer (default "4194304") ] \
           [ -name:required name:required ] \
           [ -operation (default "bot") ] \
           [ -outputSchema (default "") ] \
           [ -peerRedactor:required peerRedactor:required ] \
           [ -permission (default "diagnostics.logs.read") ] \
           [ -redactor:required redactor:required ] \
           [ -routing:required routing:required ] \
           [ -seekMinimumBytes:integer (default "8388608") ] \
           [ -service (default "") ]

Defined in /usr/local/ns/tcl/nsmcp/traffic-tools.tcl

Class Relations

  • class: ::nx::Class[i]
  • superclass: ::nsmcp::Tool[i]
::nx::Class create ::nsmcp::AccessDiagnosticTool \
     -superclass ::nsmcp::Tool

Methods (to be applied on instances)

  • classify (scripted, public)

     <instance of nsmcp::AccessDiagnosticTool[i]> classify record maps \
        target
    Parameters:
    record (required)
    maps (required)
    target (required)

    Testcases:
    No testcase defined.
    set matches {}; set unknown 0; set index 0
    foreach map $maps {
        set id $index; incr index
        if {[llength $map] ni {4 5}} {incr unknown; continue}
        lassign $map method base glob inherit condition
        if {$method ne [dict get $record method]} continue
        if {$base ne "/" || $glob ne "*" || $inherit ne "inherit" || [llength $map]!=5} {incr unknown; continue}
        if {[llength $condition]!=2 || ![string equal -nocase [lindex $condition 0] user-agent]} {incr unknown; continue}
        if {![dict exists $record userAgent]} {incr unknown; continue}
        if {[string match [lindex $condition 1] [dict get $record userAgent]]} {lappend matches $id}
    }
    if {[dict get $record pool] eq "unknown"} {
        set category unknown
    } elseif {[llength $matches]} {
        set category [expr {[dict get $record pool] eq $target ? "matching-rule-in-target" : "matching-rule-other-pool-candidate"}]
    } elseif {$unknown || ![dict exists $record userAgent]} {
        set category unknown
    } elseif {[regexp -nocase {bot|crawl|spider|baidu|knowledge} [dict get $record userAgent]] && [dict get $record pool] ne $target} {
        set category bot-like-unmatched-heuristic
    } else {set category no-target-rule-match}
    return [dict create category $category matchingRuleIndexes $matches unknownRules $unknown]
  • contextComplete (scripted, public)

     <instance of nsmcp::AccessDiagnosticTool[i]> contextComplete record \
        snapshot
    Parameters:
    record (required)
    snapshot (required)

    Testcases:
    No testcase defined.
    if {[dict size [dict get $snapshot availability]]>0} {return false}
    dict for {pool maps} [dict get $snapshot pools] {
        foreach map $maps {
            if {[llength $map] ni {4 5}} {return false}
            if {[lindex $map 0] ne [dict get $record method]} {continue}
            if {[llength $map]==4} {continue}
            set condition [lindex $map 4]
            if {[llength $condition]%2} {return false}
            foreach {field pattern} $condition {
                switch -nocase -- $field {
                    user-agent {
                        if {![dict exists $record userAgent] || [dict get $record userAgent] in {{} - {[REDACTED]}}} {return false}
                    }
                    x-ns-ip {
                        if {![ns_ip valid [dict get $record peerAddress]]} {return false}
                    }
                    default {return false}
                }
            }
        }
    }
    return true
  • execute (scripted, public)

     <instance of nsmcp::AccessDiagnosticTool[i]> execute context \
        arguments
    Parameters:
    context (required)
    arguments (required)

    Testcases:
    No testcase defined.
    set options [:options $arguments]
    if {${:operation} eq "bot" && [dict get $options mode] eq "scan"} {
        ${:authorizer} require $context diagnostics.runtime.read
        if {[dict get $options includePeerAddresses]} {${:authorizer} require $context diagnostics.logs.peers.read}
        if {${:service} eq ""} {error "Routing background service is not configured"}
        dict set options mode page
        dict unset options lastMinutes
        return [${:service} start $context routingSummary [dict create tool [self] options $options]]
    }
    if {[dict get $options aggregateTraffic]} {error "aggregateTraffic requires bot routing mode scan"}
    return [:page $context $arguments]
  • nativeClassify (scripted, public)

     <instance of nsmcp::AccessDiagnosticTool[i]> nativeClassify server \
        record snapshot target
    Parameters:
    server (required)
    record (required)
    snapshot (required)
    target (required)

    Testcases:
    No testcase defined.
    set result [dict create lookupMode native-context expectedPool unknown  contextComplete [:contextComplete $record $snapshot] category unknown  matchingRuleIndexes {} unknownRules 0 lookupError {}]
    if {![dict get $result contextComplete]} {return $result}
    if {[catch {${:routing} mapped $server $record} expected]} {
        # A row-specific lookup error must not silently become fallback
        # rule membership or a claim that the default pool is correct.
        dict set result lookupError {Native route lookup failed for this record.}
        return $result
    }
    dict set result expectedPool $expected
    set actual [dict get $record pool]
    if {$actual eq "unknown"} {return $result}
    if {$expected eq $target} {
        dict set result category [expr {$actual eq $target ? "native-target-in-target" : "native-target-other-pool-candidate"}]
    } elseif {[dict exists $record userAgent] &&
              [regexp -nocase {bot|crawl|spider|baidu|knowledge} [dict get $record userAgent]] && $actual ne $target} {
        dict set result category bot-like-routed-elsewhere-heuristic
    } else {
        dict set result category native-other-pool
    }
    return $result
  • page (scripted, public)

     <instance of nsmcp::AccessDiagnosticTool[i]> page context arguments \
        [ floor ] [ aggregationVar ]
    Parameters:
    context (required)
    arguments (required)
    floor (optional, defaults to "0")
    aggregationVar (optional)

    Testcases:
    No testcase defined.
    if {$aggregationVar ne ""} {upvar 1 $aggregationVar traffic}
    set options [:options $arguments]
    set server [$context server]
    set peers [dict get $options includePeerAddresses]
    if {$peers} {${:authorizer} require $context diagnostics.logs.peers.read}
    set redactor [expr {$peers ? ${:peerRedactor} : ${:redactor}}]
    set result [list server string $server from number [dict get $options from] to number [dict get $options to]]
    foreach key {path authenticationState} {if {[dict exists $options $key]} {lappend result $key string [dict get $options $key]}}
    set snapshot {}; set maps {}; set mappingIdentity {}
    set lookupMode fallback-rule-membership
    set routeCache {}
    if {${:operation} eq "bot"} {
        ${:authorizer} require $context diagnostics.runtime.read
        set snapshot [${:routing} snapshot $server]
        set target [dict get $options pool]
        if {![dict exists $snapshot pools $target]} {
            return [list {} object [concat $result [list available boolean false reason string {Effective target-pool mappings unavailable.}]]]
        }
        set maps [dict get $snapshot pools $target]
        if {[${:routing} contextSupported $server]} {set lookupMode native-context}
        set category [dict get $options category]
        if {$category ne "" && $category ne "unknown"} {
            set nativeCategory [expr {$category in {
                native-target-in-target native-target-other-pool-candidate
                bot-like-routed-elsewhere-heuristic native-other-pool
            }}]
            if {$nativeCategory != ($lookupMode eq "native-context")} {
                return -code error -errorcode {NSMCP RPC -32602}  "Category '$category' is not produced by lookupMode '$lookupMode'; select a category for the current mode."
            }
        }
        set mappingIdentity [ns_crypto::md string -digest sha256 -- [list [dict get $snapshot pools] $lookupMode]]
        if {[dict exists $options mappingIdentity] && [dict get $options mappingIdentity] ne $mappingIdentity} {error "Effective mappings changed; start a new analysis"}
        lappend result pool string $target lookupMode string $lookupMode mappingIdentity string $mappingIdentity {*}[${:routing} encodeSnapshot $snapshot]
    }
    if {${:operation} eq "slow"} {
        set thresholds {}
        foreach key {slowlogconntime slowlogfiltertime slowlogqueuetime} {
            set value [ns_config ns/server/$server/module/[dict get $options module] $key {}]
            lappend thresholds $key string $value
        }
        lappend result configuredThresholds object $thresholds
    }
    set path [:source $server $options]
    if {$path eq "" || ![file isfile $path] || ![file readable $path]} {
        return [list {} object [concat $result [list available boolean false reason string {Configured log is missing, disabled or unreadable.}]]]
    }
    set path [file normalize $path]
    file stat $path before
    set channel [open $path rb]
    try {
        set prefix [read $channel [expr {min(256,$before(size))}]]
        set identity [ns_crypto::md string -digest sha256 -- [list $path $before(dev) $before(ino) $prefix]]
        set sourceBytes $before(size); set end $sourceBytes
        if {[dict exists $options beforeOffset]} {
            if {$identity ne [dict get $options sourceIdentity] || $sourceBytes<[dict get $options sourceBytes]} {error "Log rotated or truncated; start a new query"}
            set sourceBytes [dict get $options sourceBytes]
            set end [dict get $options beforeOffset]
        }
        set start [expr {max($floor,$end-${:maxReadBytes})}]
        seek $channel $start
        set bytes [read $channel [expr {$end-$start}]]
        set discarded 0
        if {$start>0} {
            seek $channel [expr {$start-1}]
            if {[read $channel 1] ne "\n"} {
                set newline [string first \n $bytes]
                if {$newline>=0} {set discarded [expr {$newline+1}]} else {set discarded [string length $bytes]}
            }
        }
        file stat $path after
        if {[list $before(dev) $before(ino)] ne [list $after(dev) $after(ino)] || $after(size)<$sourceBytes} {error "Log changed during read; start a new query"}
    } finally {close $channel}
    set position [expr {$start+$discarded}]
    set cursor $position
    if {$cursor==$end && $start>0} {set cursor $start}
    set bytes [string range $bytes $discarded end]
    set pending [expr {$bytes ne "" && [string index $bytes end] ne "\n"}]
    set records {}; set groups {}; set totals {}; set matches 0; set unrecognized 0; set oversized 0; set missingUA 0; set missingPool 0
    set selectedMatches 0
    foreach line [lrange [split $bytes \n] 0 end-1] {
        set offset $position
        incr position [expr {[string length $line]+1}]
        if {[string length $line]>65536} {incr oversized; continue}
        set record [::nsmcp::accessRecordParser parse [encoding convertfrom utf-8 [string trimright $line \r]]]
        if {$record eq ""} {incr unrecognized; continue}
        set at [dict get $record startTime]
        if {$at<[dict get $options from] || $at>=[dict get $options to]} continue
        if {[dict exists $options path] && [dict get $record path] ne [dict get $options path]} continue
        if {[dict exists $options authenticationState] && [dict get $record authenticationState] ne [dict get $options authenticationState]} continue
        incr matches
        dict set record offset $offset; dict set record endOffset $position
        if {![dict exists $record userAgent]} {incr missingUA}
        if {[dict get $record pool] eq "unknown"} {incr missingPool}
        if {${:operation} eq "bot"} {
            if {$lookupMode eq "native-context"} {
                set routeKey [list [dict get $record method] [dict get $record path]  [dict get $record peerAddress] [dict get $record pool]  [dict exists $record userAgent] [expr {[dict exists $record userAgent] ? [dict get $record userAgent] : ""}]]
                if {[dict exists $routeCache $routeKey]} {
                    set classification [dict get $routeCache $routeKey]
                } else {
                    set classification [:nativeClassify $server $record $snapshot [dict get $options pool]]
                    if {[dict size $routeCache]<4096} {dict set routeCache $routeKey $classification}
                }
            } else {
                set classification [:classify $record $maps [dict get $options pool]]
                dict set classification lookupMode fallback-rule-membership
                dict set classification expectedPool unknown
                dict set classification contextComplete false
                dict set classification lookupError {}
            }
            set category [dict get $classification category]
            dict incr totals $category
            if {[dict get $options category] ne "" && [dict get $options category] ne $category} {continue}
            incr selectedMatches
            if {$aggregationVar ne ""} {:accumulateTraffic traffic $record}
            # Group only after peer and URL redaction. Omitted groups still
            # contribute to category totals and omittedRecords.
            set record [$redactor record $record]
            if {[dict exists $record userAgent]} {dict set record userAgent [$redactor message [dict get $record userAgent]]}
            set record [dict merge $record $classification]
            set key [list [dict get $record category] [dict get $record method] [dict get $record path] [dict get $record pool] [dict get $record status]]
            lappend key [expr {[dict exists $record userAgent] ? [dict get $record userAgent] : "unknown"}]
            lappend key [dict get $record expectedPool] [dict get $record contextComplete] [dict get $record lookupError]
            if {$peers} {lappend key [dict get $record peerAddress]}
            if {![dict exists $groups $key]} {
                if {[dict size $groups]>=[dict get $options limit]} {
                    set priority [:evidencePriority $category]
                    set victim {}
                    set worst $priority
                    dict for {existingKey existingGroup} $groups {
                        set rank [:evidencePriority [dict get $existingGroup category]]
                        if {$rank>$worst} {
                            set worst $rank
                            set victim $existingKey
                        }
                    }
                    if {$victim eq ""} {continue}
                    dict unset groups $victim
                }
                set group [dict create method [dict get $record method] path [$redactor queryValues [dict get $record path]] pool [dict get $record pool] status [dict get $record status] userAgent [lindex $key 5] category [dict get $record category] matchingRuleIndexes [dict get $record matchingRuleIndexes] unknownRules [dict get $record unknownRules] sampleRequestIdentifier [dict get $record requestIdentifier] firstAt $at lastAt $at count 0 runTotal 0 runMax 0 timingCount 0]
                foreach field {lookupMode expectedPool contextComplete lookupError} {
                    dict set group $field [dict get $record $field]
                }
                if {$peers} {dict set group peerAddress [dict get $record peerAddress]}
                dict set groups $key $group
            }
            set group [dict get $groups $key]; dict incr group count
            dict set group firstAt [expr {min([dict get $group firstAt],$at)}]
            dict set group lastAt [expr {max([dict get $group lastAt],$at)}]
            if {[dict exists $record runTime]} {dict incr group timingCount; dict set group runMax [expr {max([dict get $group runMax],[dict get $record runTime])}]; dict set group runTotal [expr {[dict get $group runTotal]+[dict get $record runTime]}]}
            dict set groups $key $group
        } else {
            set record [$redactor record $record]
            if {[dict exists $record userAgent]} {dict set record userAgent [$redactor message [dict get $record userAgent]]}
            lappend records $record
            set records [lrange $records end-[expr {[dict get $options limit]-1}] end]
        }
    }
    if {${:operation} eq "bot"} {
        set latest [${:routing} snapshot $server]
        set latestIdentity [ns_crypto::md string -digest sha256 -- [list [dict get $latest pools] $lookupMode]]
        if {$latestIdentity ne $mappingIdentity} {error "Effective mappings changed during analysis; start a new query"}
    }
    set entries {}; set i 0
    if {${:operation} eq "bot"} {
        set ordered {}
        dict for {key group} $groups {
            lappend ordered [list [:evidencePriority [dict get $group category]] $group]
        }
        # The common output loop reverses records for slow-log chronology.
        set records {}
        foreach pair [lsort -integer -index 0 $ordered] {lappend records [lindex $pair 1]}
        set records [lreverse $records]
    } else {
        set selectedMatches $matches
    }
    set returnedRecords {}; set resultBytes 0
    foreach record [lreverse $records] {
        set fields [:recordFields $record]
        set bytes [string length [ns_json value -- [list {} object $fields]]]
        if {$resultBytes+$bytes>65536} {
            continue
        }
        incr resultBytes $bytes
        lappend returnedRecords $record
        lappend entries $i object $fields
        incr i
    }
    set records $returnedRecords
    set counts {}; dict for {key value} $totals {lappend counts $key number $value}
    set omittedCounts {}
    set returnedMatches [llength $records]
    if {${:operation} eq "bot"} {
        set remaining {}
        dict for {category value} $totals {
            if {[dict get $options category] eq "" || [dict get $options category] eq $category} {
                dict set remaining $category $value
            }
        }
        set returnedMatches 0
        foreach record $records {
            set count [dict get $record count]
            incr returnedMatches $count
            dict incr remaining [dict get $record category] [expr {-$count}]
        }
        dict for {category value} $remaining {lappend omittedCounts $category number $value}
    }
    set next {}
    if {$cursor>$floor} {
        foreach key {from to limit pool module includePeerAddresses mode} {
            lappend next $key [expr {$key eq "includePeerAddresses" ? "boolean" : ($key in {from to limit} ? "number" : "string")}] [dict get $options $key]
        }
        if {${:operation} eq "bot" && [dict get $options category] ne ""} {
            lappend next category string [dict get $options category]
        }
        foreach key {path authenticationState} {if {[dict exists $options $key]} {lappend next $key string [dict get $options $key]}}
        lappend next beforeOffset number $cursor sourceBytes number $sourceBytes sourceIdentity string $identity
        if {$mappingIdentity ne ""} {lappend next mappingIdentity string $mappingIdentity}
    }
    set evidencePolicy {Newest matching slow-request records within evidence limits.}
    if {${:operation} eq "bot"} {
        set evidencePolicy {Routing candidates first, then unknowns, then routine groups; first encounter within priority. Category filtering selects evidence only; categoryTotals still includes all observed records.}
    }
    lappend result available boolean true sourcePath string $path sourceIdentity string $identity  matches number $matches categoryTotals object $counts records array $entries  selectedCategory string [dict get $options category] selectedMatches number $selectedMatches  filteredOutRecords number [expr {$matches-$selectedMatches}]  omittedRecords number [expr {$selectedMatches-$returnedMatches}] omittedCategoryTotals object $omittedCounts  evidencePolicy string $evidencePolicy  more boolean [expr {$cursor>$floor}] nextArguments object $next  coverage object [list sourceBytes number $sourceBytes examinedFromOffset number [expr {$start+$discarded}] examinedToOffset number $end  unreadEarlierBytes number $cursor pendingFinalRecord boolean $pending unrecognizedRecords number $unrecognized oversizedRecords number $oversized  missingUserAgent number $missingUA missingPool number $missingPool rotationsExamined boolean false windowComplete boolean false  limitations string {Page-local counts; sum disjoint pages. Current file only. Full-window coverage is not certified. Mappings reflect query time, not historical configuration. lookupMode identifies native-context or fallback-rule-membership. Native lookup uses current URL/path precedence and logged User-Agent and valid peer IP, without disclosing peers by default. Unlogged context predicates remain unknown; completeness checks conservatively inspect all same-method rules. Older versions retain root wildcard User-Agent membership analysis, not precedence replay. Bot-like unmatched uses a separate case-insensitive heuristic, not proof of a bot. Slow-log timings are access-log timings, not SQL profiles.}]
    return [list {} object $result]
  • scanStep (scripted, public)

     <instance of nsmcp::AccessDiagnosticTool[i]> scanStep context state
    Parameters:
    context (required)
    state (required)

    Testcases:
    No testcase defined.
    set options [dict get $state options]
    if {![dict exists $state bounds]} {
        if {![dict exists $options scanStrategy]} {dict set options scanStrategy window}
        set bounds [:scanBounds $context $options]
        dict set state bounds $bounds
        if {[dict get $bounds strategy] eq "completion-window"} {
            dict set options beforeOffset [dict get $bounds to]
            dict set options sourceBytes [dict get $bounds sourceBytes]
            dict set options sourceIdentity [dict get $bounds identity]
        }
    }
    set bounds [dict get $state bounds]
    set aggregateTraffic [expr {[dict exists $options aggregateTraffic] && [dict get $options aggregateTraffic]}]
    set traffic {}
    if {$aggregateTraffic} {
        if {[dict exists $state traffic]} {set traffic [dict get $state traffic]} else {
            set traffic {sources {} agents {} authenticationStates {} requestCount 0 status503Count 0 successfulCount 0 successfulTimingCount 0 successfulRunTotal 0.0 unknownPeerCount 0 untrackedSourceRecords 0 unknownUserAgentCount 0 untrackedUserAgentRecords 0}
            dict set traffic salt [ns_crypto::randombytes -encoding hex 32]
        }
    }
    set page [:page $context [:argumentTriples $options] [dict get $bounds from] [expr {$aggregateTraffic ? "traffic" : ""}]]
    if {$aggregateTraffic} {dict set state traffic $traffic}
    set value [ns_json parse [ns_json value -- $page]]
    if {![dict get $value available]} {return [list $page {} {}]}
    if {![dict exists $state fields]} {
        dict set state fields [lindex $page 2]
        dict set state groups {}
        dict set state totals {}
        dict set state matches 0
        dict set state selected 0
        dict set state pages 0
        dict set state coverage [dict get $value coverage]
    } else {
        set initial [ns_json parse [ns_json value -- [list {} object [dict get $state fields]]]]
        foreach key {sourceIdentity mappingIdentity from to lookupMode selectedCategory} {
            if {[dict get $initial $key] ne [dict get $value $key]} {error "Routing scan identity changed; start a new query"}
        }
        set coverage [dict get $state coverage]
        set nextCoverage [dict get $value coverage]
        foreach key {unrecognizedRecords oversizedRecords missingUserAgent missingPool} {
            dict incr coverage $key [dict get $nextCoverage $key]
        }
        dict set coverage pendingFinalRecord [expr {[dict get $coverage pendingFinalRecord] || [dict get $nextCoverage pendingFinalRecord]}]
        dict set coverage examinedFromOffset [dict get $nextCoverage examinedFromOffset]
        dict set coverage unreadEarlierBytes [dict get $nextCoverage unreadEarlierBytes]
        dict set state coverage $coverage
    }
    dict incr state pages
    dict incr state matches [dict get $value matches]
    dict incr state selected [dict get $value selectedMatches]
    set totals [dict get $state totals]
    dict for {category count} [dict get $value categoryTotals] {dict incr totals $category $count}
    dict set state totals $totals
    set groups [dict get $state groups]
    foreach record [dict get $value records] {
        set key [:aggregateKey $record]
        if {[dict exists $groups $key]} {
            set group [dict get $groups $key]
            foreach field {count timingCount} {dict incr group $field [dict get $record $field]}
            dict set group runTotal [expr {[dict get $group runTotal]+[dict get $record runTotal]}]
            dict set group runMax [expr {max([dict get $group runMax],[dict get $record runMax])}]
            dict set group firstAt [expr {min([dict get $group firstAt],[dict get $record firstAt])}]
            dict set group lastAt [expr {max([dict get $group lastAt],[dict get $record lastAt])}]
            dict set groups $key $group
        } else {
            if {[dict size $groups]>=[dict get $options limit]} {
                set worst [:evidencePriority [dict get $record category]]
                set victim {}
                dict for {otherKey otherRecord} $groups {
                    set rank [:evidencePriority [dict get $otherRecord category]]
                    if {$rank>$worst} {set worst $rank; set victim $otherKey}
                }
                if {$victim eq ""} {continue}
                dict unset groups $victim
            }
            dict set groups $key $record
        }
    }
    # Bound state by the same evidence byte budget used for individual pages.
    set ordered {}
    dict for {key group} $groups {lappend ordered [list [:evidencePriority [dict get $group category]] $key $group]}
    set groups {}; set entries {}; set index 0; set bytes 0; set returned 0
    set omitted {}
    dict for {category count} $totals {
        if {[dict get $value selectedCategory] eq "" || [dict get $value selectedCategory] eq $category} {dict set omitted $category $count}
    }
    foreach pair [lsort -integer -index 0 $ordered] {
        lassign $pair priority key record
        set fields [:recordFields $record]
        set size [string bytelength [ns_json value -- [list {} object $fields]]]
        if {$bytes+$size>65536} {continue}
        incr bytes $size
        dict set groups $key $record
        lappend entries $index object $fields
        incr index
        incr returned [dict get $record count]
        dict incr omitted [dict get $record category] [expr {-[dict get $record count]}]
    }
    dict set state groups $groups
    set counts {}; dict for {category count} $totals {lappend counts $category number $count}
    set omittedCounts {}; dict for {category count} $omitted {lappend omittedCounts $category number $count}
    set coverage [dict get $state coverage]
    set coverageFields {}
    foreach {key type original} [ns_json triples getvalue -output triples -path coverage -- $page] {
        lappend coverageFields $key $type [dict get $coverage $key]
    }
    lappend coverageFields scanStrategy string [dict get $bounds strategy] seekReason string [dict get $bounds reason]  seekMarginSeconds number 300 seekProbeBytes number [dict get $bounds probeBytes]  skippedEarlierBytes number [dict get $bounds from] skippedLaterBytes number [expr {[dict get $bounds sourceBytes]-[dict get $bounds to]}]  completionOrderAssumed boolean [expr {[dict get $bounds strategy] eq "completion-window"}]
    lappend coverageFields pages number [dict get $state pages] aggregationScope string all-examined-pages  sourceFullyExamined boolean [expr {[dict get $coverage unreadEarlierBytes]==0 && [dict get $coverage examinedToOffset]==[dict get $coverage sourceBytes]}]
    set fields {}
    foreach {key type original} [dict get $state fields] {
        switch -- $key {
            matches {set original [dict get $state matches]}
            selectedMatches {set original [dict get $state selected]}
            filteredOutRecords {set original [expr {[dict get $state matches]-[dict get $state selected]}]}
            categoryTotals {set original $counts}
            omittedRecords {set original [expr {[dict get $state selected]-$returned}]}
            omittedCategoryTotals {set original $omittedCounts}
            records {set original $entries}
            more {set original false}
            nextArguments {set original {}}
            coverage {set original $coverageFields}
        }
        lappend fields $key $type $original
    }
    if {$aggregateTraffic} {
        lappend fields trafficAggregation object [:trafficTriples $traffic [expr {[dict get $options to]-[dict get $options from]}] [expr {![dict get $value more]}]]
    }
    lappend fields scanComplete boolean [expr {![dict get $value more]}]
    set continuation {}
    if {[dict get $value more]} {
        set nextOptions [dict get $value nextArguments]
        dict set nextOptions aggregateTraffic $aggregateTraffic
        dict set state options $nextOptions
        set continuation $state
    }
    set progress [list examinedBytes number [expr {[dict get $bounds to]-[dict get $coverage unreadEarlierBytes]}]  sourceBytes number [expr {[dict get $bounds to]-[dict get $bounds from]}]]
    return [list [list {} object $fields] $continuation $progress]
  • validate (scripted, public)

     <instance of nsmcp::AccessDiagnosticTool[i]> validate arguments
    Parameters:
    arguments (required)

    Testcases:
    No testcase defined.
    if {[catch {:options $arguments} message]} {return -code error -errorcode {NSMCP RPC -32602} $message}