bcquality/tools/Bounded-Results.ps1
Jesper Schulz-Wedde 6a89831c5c Add bounded knowledge retrieval
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 0d8b7764-f15a-49ea-8d50-d9147334f8af
2026-09-11 11:46:01 +02:00

110 lines
3.8 KiB
PowerShell

# Shared deterministic paging. Callers build the complete immutable result first.
#requires -Version 7.2
Set-StrictMode -Version Latest
function Get-ResultSnapshot {
param([Parameter(Mandatory)] $Value)
$json = ConvertTo-Json -InputObject $Value -Depth 30 -Compress
return [Convert]::ToHexString(
[Security.Cryptography.SHA256]::HashData([Text.Encoding]::UTF8.GetBytes($json))
).ToLowerInvariant()
}
function Get-SerializedByteCount {
param([Parameter(Mandatory)] [string] $Json)
# PowerShell writes one platform newline after the returned JSON string.
return [Text.Encoding]::UTF8.GetByteCount($Json) +
[Text.Encoding]::UTF8.GetByteCount([Environment]::NewLine)
}
function ConvertTo-BoundedPage {
param(
[Parameter(Mandatory)] [Collections.IDictionary] $Header,
[Parameter(Mandatory)] [Collections.IDictionary] $Groups,
[ValidateRange(0, 2147483647)] [int] $Offset = 0,
[string] $Snapshot,
[ValidateRange(1024, 16000)] [int] $MaxBytes = 16000
)
$total = 0
foreach ($name in $Groups.Keys) {
$total += $Groups[$name].Count
}
if (($total -eq 0 -and $Offset -ne 0) -or ($total -gt 0 -and $Offset -ge $total)) {
throw "Invalid Offset=$Offset for totalCount=$total; no rows were returned."
}
if ($Offset -gt 0 -and -not $Snapshot) {
throw 'Continuation requires Snapshot from the preceding page.'
}
if ($Snapshot -and $Snapshot -cne $Header.snapshot) {
throw 'Snapshot changed or continuation belongs to another request. Discard partial results and restart at Offset=0.'
}
$page = [ordered]@{}
foreach ($key in $Header.Keys) {
$page[$key] = $Header[$key]
}
$page.offset = $Offset
$page.returnedCount = 0
$page.totalCount = $total
$page.remainingCount = $total - $Offset
$page.complete = ($total -eq 0)
$page.continuation = if ($total) {
[ordered]@{ offset = $Offset; snapshot = $Header.snapshot }
}
else {
$null
}
foreach ($name in $Groups.Keys) {
$page[$name] = [Collections.Generic.List[object]]::new()
}
$json = ConvertTo-Json -InputObject $page -Depth 30 -Compress
if ((Get-SerializedByteCount -Json $json) -gt $MaxBytes) {
throw "Page envelope exceeds MaxBytes=$MaxBytes. Use READ's path-discovery fallback; never truncate."
}
$position = 0
foreach ($name in $Groups.Keys) {
foreach ($row in $Groups[$name]) {
if ($position++ -lt $Offset) {
continue
}
$page[$name].Add($row)
$page.returnedCount++
$page.remainingCount--
$page.complete = ($page.remainingCount -eq 0)
$page.continuation = if ($page.complete) {
$null
}
else {
[ordered]@{
offset = $Offset + $page.returnedCount
snapshot = $Header.snapshot
}
}
$next = ConvertTo-Json -InputObject $page -Depth 30 -Compress
if ((Get-SerializedByteCount -Json $next) -gt $MaxBytes) {
$page[$name].RemoveAt($page[$name].Count - 1)
$page.returnedCount--
$page.remainingCount++
$page.complete = $false
$page.continuation = [ordered]@{
offset = $Offset + $page.returnedCount
snapshot = $Header.snapshot
}
if ($page.returnedCount -eq 0) {
$identity = if ($row.path) { " at $($row.path)" } else { " at Offset=$Offset" }
throw "One complete $name row plus envelope exceeds MaxBytes=$MaxBytes$identity. No row was clipped."
}
return $json
}
$json = $next
}
}
return $json
}