mirror of
https://github.com/microsoft/BCQuality.git
synced 2026-10-05 14:46:55 +01:00
* Add bounded knowledge retrieval Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0d8b7764-f15a-49ea-8d50-d9147334f8af * Fix bc-version overflow and pathless-row identity in bounded retrieval Catalog matching compared an Int32 -BCVersion against a bigint range bound. PowerShell coerces the right operand to the left operand's type, so a bound wider than Int32 threw a conversion error and failed the whole domain catalog rather than the single row. Metadata validation already accepts such bounds, so compare as bigint on both sides. The shared pager built its oversized-row message with $row.path, which throws under Set-StrictMode -Version Latest when a row carries no path, replacing the explicit bound failure with a property-lookup error. Resolve the path defensively for dictionary and object rows so the offset-based fallback is reachable. Both paths gain regression coverage that fails without these fixes. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Jesper Schulz-Wedde <jesper.schulzwedde@microsoft.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0d8b7764-f15a-49ea-8d50-d9147334f8af
187 lines
7.3 KiB
PowerShell
187 lines
7.3 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
Reads a bounded prefix of exact article or sample paths without altering bodies.
|
|
.DESCRIPTION
|
|
The UTF-8 byte size bound covers the complete serialized JSON plus its output
|
|
newline. A body that cannot fit fails explicitly; it is never summarized or
|
|
truncated. Samples are loaded only with -Samples and must be linked by their
|
|
sibling article using READ's exact link convention.
|
|
#>
|
|
#requires -Version 7.2
|
|
[CmdletBinding()]
|
|
param(
|
|
[ValidateNotNullOrEmpty()] [string] $BCQualityRoot = (Split-Path $PSScriptRoot -Parent),
|
|
[Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string[]] $Paths,
|
|
[ValidateRange(1, 8)] [int] $MaxArticles = 8,
|
|
[ValidateRange(1024, 16000)] [int] $MaxBytes = 16000,
|
|
[ValidateSet('microsoft', 'community', 'custom')]
|
|
[AllowEmptyCollection()] [string[]] $EnabledLayers = @('microsoft', 'community', 'custom'),
|
|
[string] $IndexPath,
|
|
[ValidatePattern('^[a-f0-9]{64}$')] [string] $Snapshot,
|
|
[switch] $Samples
|
|
)
|
|
|
|
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference = 'Stop'
|
|
. (Join-Path $PSScriptRoot 'Knowledge-Retrieval.ps1')
|
|
. (Join-Path $PSScriptRoot 'Bounded-Results.ps1')
|
|
|
|
$BCQualityRoot = Resolve-KnowledgeRoot $BCQualityRoot
|
|
if (-not $IndexPath) {
|
|
$IndexPath = Join-Path $BCQualityRoot 'knowledge-index.json'
|
|
}
|
|
if ($Paths.Count -gt $MaxArticles) {
|
|
throw "Paths count $($Paths.Count) exceeds MaxArticles=$MaxArticles. Split the worklist into stable chunks of at most $MaxArticles exact paths."
|
|
}
|
|
if ($null -eq $EnabledLayers) {
|
|
throw 'EnabledLayers must be an array.'
|
|
}
|
|
if (@($EnabledLayers | Where-Object { $_ -cnotin @('microsoft', 'community', 'custom') }).Count -or
|
|
@($EnabledLayers | Group-Object -CaseSensitive | Where-Object Count -gt 1).Count) {
|
|
throw 'EnabledLayers must contain unique canonical lowercase layer names.'
|
|
}
|
|
|
|
$recovery = "Run Entry preparation once before dispatch, or use READ's bounded native-file fallback. Do not rebuild in a leaf."
|
|
$preparedIndex = Read-PreparedKnowledgeIndex -IndexPath $IndexPath -Recovery $recovery
|
|
$index = $preparedIndex.index
|
|
$byPath = $preparedIndex.byPath
|
|
$unrestricted = $index.enabledLayers.Count -eq 0 -or
|
|
($index.enabledLayers.Count -eq 1 -and $null -eq $index.enabledLayers[0])
|
|
$indexedLayers = @(
|
|
if ($unrestricted) { 'microsoft', 'community', 'custom' } else { $index.enabledLayers }
|
|
)
|
|
if (@($EnabledLayers | Where-Object { $_ -cnotin $indexedLayers }).Count) {
|
|
throw "Index layer coverage does not cover EnabledLayers. $recovery"
|
|
}
|
|
|
|
$resolved = [Collections.Generic.List[string]]::new()
|
|
$records = [Collections.Generic.List[object]]::new()
|
|
$seen = [Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal)
|
|
$articleTexts = [Collections.Generic.Dictionary[string, string]]::new([StringComparer]::Ordinal)
|
|
$sampleContents = [Collections.Generic.Dictionary[string, object]]::new([StringComparer]::Ordinal)
|
|
foreach ($path in $Paths) {
|
|
$kind = if ($Samples) { 'sample' } else { 'article' }
|
|
$fullPath = Resolve-KnowledgePath -Root $BCQualityRoot -Path $path -Kind $kind
|
|
if ($path.Split('/')[0] -cnotin $EnabledLayers) {
|
|
throw "Layer disabled for path: $path"
|
|
}
|
|
if (-not $seen.Add($path)) {
|
|
throw "Duplicate requested path: $path"
|
|
}
|
|
if ($Samples) {
|
|
$articlePath = $path -replace '\.(good|bad)\.[a-z0-9]+$', '.md'
|
|
if (-not $byPath.ContainsKey($articlePath)) {
|
|
throw "Sample article is absent from the prepared index: $articlePath"
|
|
}
|
|
$fullArticlePath = Resolve-KnowledgePath -Root $BCQualityRoot -Path $articlePath
|
|
if (-not $articleTexts.ContainsKey($articlePath)) {
|
|
$articleContent = Read-KnowledgeText -Path $fullArticlePath
|
|
if ($articleContent.sha256 -cne $byPath[$articlePath].sourceSha256) {
|
|
throw "Selected article hash does not match the prepared index: $articlePath"
|
|
}
|
|
$articleTexts.Add($articlePath, $articleContent.text)
|
|
}
|
|
Assert-SampleLink -ArticleText $articleTexts[$articlePath] -SamplePath $fullPath
|
|
$sampleContent = Read-KnowledgeText -Path $fullPath
|
|
$sampleContents.Add($path, $sampleContent)
|
|
$records.Add([ordered]@{
|
|
path = $path
|
|
articlePath = $articlePath
|
|
articleSha256 = $byPath[$articlePath].sourceSha256
|
|
sampleSha256 = $sampleContent.sha256
|
|
sampleBytes = $sampleContent.bytes
|
|
})
|
|
}
|
|
else {
|
|
if (-not $byPath.ContainsKey($path)) {
|
|
throw "Selected article is absent from the prepared index: $path"
|
|
}
|
|
$records.Add([ordered]@{
|
|
path = $path
|
|
expectedSha256 = $byPath[$path].sourceSha256
|
|
})
|
|
}
|
|
$resolved.Add($fullPath)
|
|
}
|
|
|
|
$requestSnapshot = Get-ResultSnapshot -Value ([ordered]@{
|
|
root = $BCQualityRoot
|
|
preparedIndexSha256 = $preparedIndex.content.sha256
|
|
kind = if ($Samples) { 'samples' } else { 'articles' }
|
|
enabledLayers = @($EnabledLayers)
|
|
files = @($records)
|
|
})
|
|
if ($Snapshot -and $Snapshot -cne $requestSnapshot) {
|
|
throw 'Article snapshot changed or continuation belongs to another exact path batch. Discard partial results and restart.'
|
|
}
|
|
|
|
$articles = [Collections.Generic.List[object]]::new()
|
|
function ConvertTo-BatchJson {
|
|
param([int] $ReadCount)
|
|
|
|
$remaining = @(
|
|
if ($ReadCount -lt $Paths.Count) {
|
|
$Paths[$ReadCount..($Paths.Count - 1)]
|
|
}
|
|
)
|
|
$remainingRecords = @(
|
|
if ($ReadCount -lt $records.Count) {
|
|
$records[$ReadCount..($records.Count - 1)]
|
|
}
|
|
)
|
|
$continuation = if ($remaining.Count) {
|
|
[ordered]@{
|
|
snapshot = Get-ResultSnapshot -Value ([ordered]@{
|
|
root = $BCQualityRoot
|
|
preparedIndexSha256 = $preparedIndex.content.sha256
|
|
kind = if ($Samples) { 'samples' } else { 'articles' }
|
|
enabledLayers = @($EnabledLayers)
|
|
files = $remainingRecords
|
|
})
|
|
}
|
|
}
|
|
else {
|
|
$null
|
|
}
|
|
return [ordered]@{
|
|
version = 1
|
|
kind = if ($Samples) { 'samples' } else { 'articles' }
|
|
snapshot = $requestSnapshot
|
|
requestedCount = $Paths.Count
|
|
returnedCount = $ReadCount
|
|
complete = ($ReadCount -eq $Paths.Count)
|
|
articles = @($articles)
|
|
remainingPaths = $remaining
|
|
continuation = $continuation
|
|
} | ConvertTo-Json -Depth 8 -Compress
|
|
}
|
|
|
|
$json = ''
|
|
for ($i = 0; $i -lt [Math]::Min($MaxArticles, $Paths.Count); $i++) {
|
|
$content = if ($Samples) {
|
|
$sampleContents[$Paths[$i]]
|
|
}
|
|
else {
|
|
Read-KnowledgeText -Path $resolved[$i]
|
|
}
|
|
if (-not $Samples -and $content.sha256 -cne $records[$i].expectedSha256) {
|
|
throw "Selected article hash does not match the prepared index: $($Paths[$i])"
|
|
}
|
|
$articles.Add([ordered]@{
|
|
path = $Paths[$i]
|
|
bytes = $content.bytes
|
|
sha256 = $content.sha256
|
|
body = $content.text
|
|
})
|
|
$next = ConvertTo-BatchJson -ReadCount ($i + 1)
|
|
if ((Get-SerializedByteCount -Json $next) -gt $MaxBytes) {
|
|
$articles.RemoveAt($articles.Count - 1)
|
|
if ($i -eq 0) {
|
|
throw "No complete body plus continuation fits MaxBytes=$MaxBytes at $($Paths[$i]). Use a smaller exact path batch or READ's bounded native-file fallback; never truncate."
|
|
}
|
|
break
|
|
}
|
|
$json = $next
|
|
}
|
|
|
|
$json
|