Add bounded knowledge retrieval

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 0d8b7764-f15a-49ea-8d50-d9147334f8af
This commit is contained in:
Jesper Schulz-Wedde 2026-09-11 11:46:01 +02:00
parent c12b2f0a88
commit 6a89831c5c
26 changed files with 1852 additions and 55 deletions

110
tools/Bounded-Results.ps1 Normal file
View file

@ -0,0 +1,110 @@
# 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
}

View file

@ -30,6 +30,8 @@
expected to prune its clone to policy first). For provenance and to
reproduce a consumer's exact view, pass -EnabledLayers to restrict the walk
to those layers and to record the policy in the index header.
Invalid articles are omitted with a path-specific warning so one bad
optional layer article cannot block valid siblings.
.PARAMETER BCQualityRoot
Path to the BCQuality content root to index (typically a filtered clone).
@ -69,6 +71,17 @@ param(
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
. (Join-Path $PSScriptRoot 'Knowledge-Retrieval.ps1')
if ($PSBoundParameters.ContainsKey('EnabledLayers')) {
if ($null -eq $EnabledLayers) {
throw 'EnabledLayers must be an array; omit it to index all layers.'
}
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.'
}
}
# Default to the clone root (parent of this script's tools/ folder) so the
# agent's Entry preparation step can invoke this with no arguments from the
@ -93,6 +106,45 @@ function Get-RelativePath {
return ($rel -replace '\\', '/')
}
function Get-BytesSha256 {
param([byte[]] $Bytes)
$sha = [Security.Cryptography.SHA256]::Create()
try {
return ([BitConverter]::ToString($sha.ComputeHash($Bytes)) -replace '-', '').ToLowerInvariant()
}
finally {
$sha.Dispose()
}
}
function Read-ArticleSource {
param([string] $Path)
$bytes = [IO.File]::ReadAllBytes($Path)
try {
$text = [Text.UTF8Encoding]::new($false, $true).GetString($bytes)
}
catch [Text.DecoderFallbackException] {
throw [IO.InvalidDataException]::new('invalid UTF-8', $_.Exception)
}
return [pscustomobject]@{
bytes = $bytes
text = $text
sha256 = Get-BytesSha256 -Bytes $bytes
}
}
function Get-ValueSha256 {
param([Parameter(Mandatory)] $Value)
$bytes = [Text.Encoding]::UTF8.GetBytes((ConvertTo-Json -InputObject $Value -Depth 8 -Compress))
$sha = [Security.Cryptography.SHA256]::Create()
try {
return ([BitConverter]::ToString($sha.ComputeHash($bytes)) -replace '-', '').ToLowerInvariant()
}
finally {
$sha.Dispose()
}
}
# Trims a Description to a single short line (<= $Max chars) for the lean
# index. Takes the first sentence; truncates on a word boundary if still long.
function Get-LeanDescription {
@ -116,9 +168,12 @@ function ConvertFrom-ArticleFrontmatter {
# Pattern) is included; the index is a lossless substitute for the
# frontmatter + Description the worklist predicate reads, not a
# substitute for the article's normative guidance.
param([string] $Path)
param(
[string] $Path,
[string] $Text
)
$lines = Get-Content -LiteralPath $Path -ErrorAction Stop
$lines = [regex]::Split($Text.TrimStart([char]0xfeff), '\r\n|\n|\r')
# Frontmatter is the first '---'-delimited block.
if ($lines.Count -lt 1 -or $lines[0].Trim() -ne '---') { return $null }
@ -129,19 +184,42 @@ function ConvertFrom-ArticleFrontmatter {
if ($fmEnd -lt 0) { return $null }
$fm = @{}
$arrayFields = @('bc-version', 'keywords', 'technologies', 'countries', 'application-area')
for ($i = 1; $i -lt $fmEnd; $i++) {
$line = $lines[$i]
if ($line -match '^\s*([a-zA-Z][\w-]*)\s*:\s*(.*)$') {
$key = $Matches[1]
$val = $Matches[2].Trim()
if ($val -match '^\[(.*)\]$') {
if ($key -in $arrayFields) {
if ($val -notmatch '^\[(.*)\]$') {
throw [IO.InvalidDataException]::new(
"frontmatter field '$key' must use non-empty bracket-array syntax"
)
}
$inner = $Matches[1].Trim()
if ($inner -eq '') { $fm[$key] = @() }
else { $fm[$key] = @($inner -split '\s*,\s*' | ForEach-Object { $_.Trim() }) }
if ($inner -eq '') {
throw [IO.InvalidDataException]::new(
"frontmatter field '$key' must use non-empty bracket-array syntax"
)
}
$values = @($inner -split '\s*,\s*' | ForEach-Object { $_.Trim() })
if (@($values | Where-Object { [string]::IsNullOrWhiteSpace($_) }).Count) {
throw [IO.InvalidDataException]::new(
"frontmatter field '$key' must use non-empty bracket-array syntax"
)
}
$fm[$key] = $values
}
elseif ($val -ne '') { $fm[$key] = $val }
}
}
foreach ($field in $arrayFields) {
if (-not $fm.ContainsKey($field) -or $fm[$field] -isnot [array] -or -not $fm[$field].Count) {
throw [IO.InvalidDataException]::new(
"frontmatter field '$field' must use non-empty bracket-array syntax"
)
}
}
# Body parsing: H1 title and the full Description section. The Description
# is the article's primary retrieval target per READ and is captured
@ -185,42 +263,71 @@ $indexArticles = [System.Collections.Generic.List[object]]::new()
foreach ($layerDir in @('microsoft', 'community', 'custom')) {
$kbRoot = Join-Path $BCQualityRoot (Join-Path $layerDir 'knowledge')
if (-not (Test-Path $kbRoot)) { continue }
if ($EnabledLayers -and ($EnabledLayers -notcontains $layerDir)) { continue }
if ($EnabledLayers -and ($EnabledLayers -cnotcontains $layerDir)) { continue }
Get-ChildItem -LiteralPath $kbRoot -Recurse -File -Filter '*.md' -ErrorAction SilentlyContinue |
Sort-Object FullName |
ForEach-Object {
$rel = Get-RelativePath -Root $BCQualityRoot -Full $_.FullName
$parsed = $null
try { $parsed = ConvertFrom-ArticleFrontmatter -Path $_.FullName } catch { $parsed = $null }
$files = @(
Get-ChildItem -LiteralPath $kbRoot -Recurse -File -Filter '*.md' -ErrorAction SilentlyContinue |
Sort-Object FullName
)
foreach ($file in $files) {
$rel = Get-RelativePath -Root $BCQualityRoot -Full $file.FullName
try {
$source = Read-ArticleSource -Path $file.FullName
$parsed = ConvertFrom-ArticleFrontmatter -Path $file.FullName -Text $source.text
if (-not $parsed) {
# Invalid/unparseable file: list path + domain-from-path so it
# is never silently dropped from discovery. Consumers fall back
# to reading it in full.
$domainFromPath = if ($rel -match '/knowledge/([^/]+)/') { $Matches[1] } else { '' }
$indexArticles.Add([pscustomobject]@{
path = $rel; layer = $layerDir; domain = $domainFromPath
'bc-version' = @(); technologies = @(); countries = @(); 'application-area' = @()
keywords = @(); title = ''; description = ''; parsed = $false
}) | Out-Null
return
throw [IO.InvalidDataException]::new('missing or unterminated frontmatter')
}
foreach ($required in @(
@('domain', $parsed.domain),
@('H1 title', $parsed.title),
@('Description', $parsed.description)
)) {
if ([string]::IsNullOrWhiteSpace([string]$required[1])) {
throw [IO.InvalidDataException]::new("missing $($required[0])")
}
}
$indexArticles.Add([pscustomobject]@{
path = $rel
layer = $layerDir
domain = $parsed.domain
'bc-version' = @($parsed.'bc-version')
technologies = @($parsed.technologies)
countries = @($parsed.countries)
'application-area' = @($parsed.'application-area')
keywords = @($parsed.keywords)
title = $parsed.title
description = if ($FullIndex) { $parsed.description } else { Get-LeanDescription -Text $parsed.description }
parsed = $true
}) | Out-Null
}
catch [IO.InvalidDataException] {
Write-Warning "Skipping invalid knowledge article '$rel': $($_.Exception.Message)."
continue
}
$article = [ordered]@{
path = $rel
layer = $layerDir
domain = $parsed.domain
'bc-version' = @($parsed.'bc-version')
technologies = @($parsed.technologies)
countries = @($parsed.countries)
'application-area' = @($parsed.'application-area')
keywords = @($parsed.keywords)
title = $parsed.title
description = if ($FullIndex) { $parsed.description } else { Get-LeanDescription -Text $parsed.description }
parsed = $true
sourceSha256 = $source.sha256
}
$problem = Get-KnowledgeMetadataProblem -Row $article
if ($problem) {
Write-Warning "Skipping invalid knowledge article '$rel': $problem."
continue
}
$indexArticles.Add($article) | Out-Null
}
}
$articlesByPath = [Collections.Generic.Dictionary[string, object]]::new([StringComparer]::Ordinal)
foreach ($article in $indexArticles) {
if (-not $articlesByPath.TryAdd($article.path, $article)) {
throw "Duplicate knowledge path while building source snapshot: $($article.path)"
}
}
$sourcePaths = [string[]]@($articlesByPath.Keys)
[Array]::Sort($sourcePaths, [StringComparer]::Ordinal)
$sourceManifest = @(
foreach ($path in $sourcePaths) {
[ordered]@{ path = $path; sha256 = $articlesByPath[$path].sourceSha256 }
}
)
$index = [pscustomobject]@{
version = 1
generatedAt = (Get-Date).ToUniversalTime().ToString('o')
@ -228,6 +335,7 @@ $index = [pscustomobject]@{
knowledgeAllow= @($KnowledgeAllow)
knowledgeDeny = @($KnowledgeDeny)
articleCount = $indexArticles.Count
sourceSnapshot= Get-ValueSha256 -Value $sourceManifest
articles = @($indexArticles)
}

View file

@ -0,0 +1,187 @@
<#
.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

View file

@ -0,0 +1,298 @@
# Shared filesystem guards for catalog and exact article retrieval.
Set-StrictMode -Version Latest
function Resolve-KnowledgeRoot {
param([string] $Root)
$item = Get-Item -LiteralPath $Root -Force -ErrorAction Stop
if ($item.PSProvider.Name -ne 'FileSystem' -or -not $item.PSIsContainer) {
throw "BCQuality root must be a filesystem directory: $Root"
}
if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) {
throw "Linked BCQuality roots are not supported: $Root"
}
return $item.FullName
}
function Assert-KnowledgePath {
param(
[string] $Path,
[ValidateSet('article', 'sample')] [string] $Kind = 'article'
)
if ([string]::IsNullOrWhiteSpace($Path) -or
$Path -cnotmatch '^(microsoft|community|custom)/knowledge/[^/]+/.+' -or
$Path -match '[\\:*?"<>|\x00-\x1f]' -or
@($Path.Split('/') | Where-Object { $_ -in '', '.', '..' -or $_ -match '[. ]$' }).Count) {
throw "Invalid knowledge path: $Path"
}
if (($Kind -eq 'article' -and -not $Path.EndsWith('.md', [StringComparison]::Ordinal)) -or
($Kind -eq 'sample' -and $Path -cnotmatch '\.(good|bad)\.[a-z0-9]+$')) {
throw "Expected an exact $Kind path: $Path"
}
}
function Resolve-KnowledgePath {
param(
[string] $Root,
[string] $Path,
[ValidateSet('article', 'sample')] [string] $Kind = 'article'
)
Assert-KnowledgePath -Path $Path -Kind $Kind
$current = $Root
foreach ($part in $Path.Split('/')) {
$items = @(
Get-ChildItem -LiteralPath $current -Filter $part -Force -ErrorAction Stop |
Where-Object Name -CEQ $part
)
if ($items.Count -ne 1) {
throw "Knowledge path does not exist with exact casing: $Path"
}
$item = $items[0]
if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) {
throw "Linked knowledge paths are not supported: $Path"
}
$current = $item.FullName
}
if ($item.PSIsContainer) {
throw "Knowledge path is not a file: $Path"
}
return $item.FullName
}
function Read-KnowledgeText {
param([string] $Path)
$bytes = [IO.File]::ReadAllBytes($Path)
try {
$text = [Text.UTF8Encoding]::new($false, $true).GetString($bytes)
}
catch {
throw "Knowledge file is not valid strict UTF-8: $Path"
}
return [pscustomobject]@{
text = $text
bytes = $bytes.Length
sha256 = [Convert]::ToHexString(
[Security.Cryptography.SHA256]::HashData($bytes)
).ToLowerInvariant()
}
}
function Get-NormalizedKnowledgeVersions {
param([string[]] $Values)
foreach ($value in $Values) {
if ($value -match '^"([^"]*)"$' -or $value -match "^'([^']*)'$") {
$Matches[1]
}
else {
$value
}
}
}
function Get-KnowledgeMetadataProblem {
param([Collections.IDictionary] $Row)
if ($Row['parsed'] -isnot [bool] -or -not $Row['parsed']) {
return 'unparsed frontmatter'
}
if ($Row['domain'] -isnot [string] -or
$Row['domain'] -cnotmatch '^[a-z0-9]+(-[a-z0-9]+)*$') {
return 'missing/invalid domain'
}
foreach ($field in @('bc-version', 'technologies', 'countries', 'application-area', 'keywords')) {
if ($Row[$field] -isnot [array] -or -not $Row[$field].Count) {
return "missing/invalid $field"
}
foreach ($value in $Row[$field]) {
if ($value -isnot [string] -or [string]::IsNullOrWhiteSpace($value)) {
return "invalid $field value"
}
}
}
foreach ($field in @('title', 'description')) {
if ($Row[$field] -isnot [string] -or
[string]::IsNullOrWhiteSpace($Row[$field]) -or
$Row[$field] -match '[\r\n]') {
return "missing/invalid $field"
}
}
$versions = @(Get-NormalizedKnowledgeVersions -Values $Row['bc-version'])
if ($versions -ccontains 'all') {
if ($versions.Count -ne 1) {
return 'mixed bc-version sentinel'
}
}
elseif ($versions.Count -eq 1 -and $versions[0] -match '^(\d+)\.\.(\d+)?$') {
$start = [bigint]::Parse($Matches[1])
if ($start -le 0 -or ($Matches[2] -and [bigint]::Parse($Matches[2]) -le 0)) {
return 'invalid bc-version range bound'
}
if ($Matches[2] -and $start -gt [bigint]::Parse($Matches[2])) {
return 'descending bc-version range'
}
}
else {
foreach ($version in $versions) {
if ($version -notmatch '^\d+$' -or [bigint]::Parse($version) -le 0) {
return 'invalid bc-version'
}
}
}
if (@($Row.technologies | Where-Object { $_ -cnotmatch '^[a-z0-9]+(-[a-z0-9]+)*$' }).Count) {
return 'invalid technologies'
}
if ($Row.technologies -ccontains 'all') {
return 'invalid technologies sentinel'
}
if ($Row.countries -ccontains 'w1') {
if ($Row.countries.Count -ne 1) {
return 'mixed countries sentinel'
}
}
elseif (@($Row.countries | Where-Object { $_ -cnotmatch '^[a-z]{2}$' }).Count) {
return 'invalid countries'
}
if (@($Row['application-area'] | Where-Object { $_ -cnotmatch '^(all|[a-z0-9]+(-[a-z0-9]+)*)$' }).Count) {
return 'invalid application-area'
}
if ($Row['application-area'] -ccontains 'all' -and $Row['application-area'].Count -ne 1) {
return 'mixed application-area sentinel'
}
if (@($Row.keywords | Where-Object { $_ -cnotmatch '^[a-z0-9]+(-[a-z0-9]+)*$' }).Count) {
return 'invalid keywords'
}
return ''
}
function Get-PreparedManifestSha256 {
param(
[string[]] $Paths,
[Collections.Generic.Dictionary[string, object]] $ByPath
)
$hash = [Security.Cryptography.IncrementalHash]::CreateHash(
[Security.Cryptography.HashAlgorithmName]::SHA256
)
try {
$hash.AppendData([byte[]][char]'[')
for ($i = 0; $i -lt $Paths.Count; $i++) {
if ($i) {
$hash.AppendData([byte[]][char]',')
}
$row = [ordered]@{
path = $Paths[$i]
sha256 = $ByPath[$Paths[$i]].sourceSha256
}
$hash.AppendData([Text.Encoding]::UTF8.GetBytes(
(ConvertTo-Json -InputObject $row -Depth 8 -Compress)
))
}
$hash.AppendData([byte[]][char]']')
return [Convert]::ToHexString($hash.GetHashAndReset()).ToLowerInvariant()
}
finally {
$hash.Dispose()
}
}
function Read-PreparedKnowledgeIndex {
param(
[string] $IndexPath,
[string] $Recovery
)
if (-not (Test-Path -LiteralPath $IndexPath -PathType Leaf)) {
throw "Knowledge index missing: $IndexPath. $Recovery"
}
$indexItem = Get-Item -LiteralPath $IndexPath -Force -ErrorAction Stop
if ($indexItem.PSProvider.Name -ne 'FileSystem' -or
($indexItem.Attributes -band [IO.FileAttributes]::ReparsePoint)) {
throw "Knowledge index must be an unlinked filesystem file: $IndexPath. $Recovery"
}
$content = Read-KnowledgeText -Path $indexItem.FullName
try {
$index = $content.text.TrimStart([char]0xfeff) |
ConvertFrom-Json -AsHashtable -ErrorAction Stop
}
catch {
throw "Malformed knowledge index JSON: $($_.Exception.Message). $Recovery"
}
if ($index -isnot [Collections.IDictionary] -or
$index.version -ne 1 -or
$index.articles -isnot [array] -or
$index.articleCount -ne $index.articles.Count -or
$index.enabledLayers -isnot [array] -or
$index.knowledgeAllow -isnot [array] -or
$index.knowledgeDeny -isnot [array] -or
$index.sourceSnapshot -isnot [string] -or
$index.sourceSnapshot -cnotmatch '^[a-f0-9]{64}$') {
throw "Invalid knowledge index envelope. $Recovery"
}
$generatedAt = [DateTimeOffset]::MinValue
if ($index.generatedAt -is [DateTime]) {
$generatedAt = [DateTimeOffset]$index.generatedAt
}
elseif (-not [DateTimeOffset]::TryParse(
[string]$index.generatedAt,
[Globalization.CultureInfo]::InvariantCulture,
[Globalization.DateTimeStyles]::RoundtripKind,
[ref]$generatedAt
)) {
throw "Invalid knowledge index generatedAt. $Recovery"
}
if ($generatedAt -gt [DateTimeOffset]::UtcNow.AddMinutes(1)) {
throw "Invalid knowledge index generatedAt. $Recovery"
}
$byPath = [Collections.Generic.Dictionary[string, object]]::new([StringComparer]::Ordinal)
foreach ($row in $index.articles) {
if ($row -isnot [Collections.IDictionary] -or $row.path -isnot [string]) {
throw "Index row has no exact path. $Recovery"
}
Assert-KnowledgePath -Path $row.path
if ($row.layer -cne $row.path.Split('/')[0] -or
$row.layer -cnotin @('microsoft', 'community', 'custom')) {
throw "Invalid index layer: $($row.path). $Recovery"
}
if ($row.sourceSha256 -isnot [string] -or
$row.sourceSha256 -cnotmatch '^[a-f0-9]{64}$') {
throw "Invalid source hash in knowledge index: $($row.path). $Recovery"
}
if (-not $byPath.TryAdd($row.path, $row)) {
throw "Duplicate index path: $($row.path). $Recovery"
}
}
$paths = [string[]]@($byPath.Keys)
[Array]::Sort($paths, [StringComparer]::Ordinal)
if ((Get-PreparedManifestSha256 -Paths $paths -ByPath $byPath) -cne $index.sourceSnapshot) {
throw "Stale or internally inconsistent prepared index snapshot. $Recovery"
}
return [pscustomobject]@{
index = $index
content = $content
byPath = $byPath
paths = $paths
}
}
function Assert-SampleLink {
param(
[string] $ArticleText,
[string] $SamplePath
)
$sampleName = [IO.Path]::GetFileName($SamplePath)
$expected = '[`' + $sampleName + '`](' + $sampleName + ')'
if (-not $ArticleText.Contains($expected, [StringComparison]::Ordinal)) {
throw "Sample is not linked by its article using the READ convention: $sampleName"
}
}

241
tools/Search-Knowledge.ps1 Normal file
View file

@ -0,0 +1,241 @@
<#
.SYNOPSIS
Returns bounded pages of every domain/layer/READ-applicable catalog row.
.DESCRIPTION
Consumes Entry's prepared index read-only. Results are never ranked, sampled,
top-k limited, deduplicated by basename, or narrowed by query text. Omit an
unknown task dimension; an explicit empty array is a known empty set.
#>
#requires -Version 7.2
[CmdletBinding()]
param(
[ValidateNotNullOrEmpty()] [string] $BCQualityRoot = (Split-Path $PSScriptRoot -Parent),
[Parameter(Mandatory)] [ValidateNotNullOrEmpty()]
[ValidateScript({ -not [string]::IsNullOrWhiteSpace($_) })] [string] $Domain,
[ValidateSet('microsoft', 'community', 'custom')]
[AllowEmptyCollection()] [string[]] $EnabledLayers = @('microsoft', 'community', 'custom'),
[ValidateRange(1, 2147483647)] [int] $BCVersion,
[AllowEmptyCollection()] [string[]] $Technologies,
[AllowEmptyCollection()] [string[]] $Countries,
[AllowEmptyCollection()] [string[]] $ApplicationAreas,
[switch] $ExcludeConditional,
[string] $IndexPath,
[ValidateRange(1024, 16000)] [int] $MaxBytes = 16000,
[ValidateRange(0, 2147483647)] [int] $Offset = 0,
[ValidatePattern('^[a-f0-9]{64}$')] [string] $Snapshot
)
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 ($null -eq $EnabledLayers) {
throw 'EnabledLayers must be an array; use an empty array to disable all layers.'
}
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.'
}
$context = [ordered]@{}
foreach ($pair in @(
@('BCVersion', 'bc-version'),
@('Technologies', 'technologies'),
@('Countries', 'countries'),
@('ApplicationAreas', 'application-area')
)) {
if (-not $PSBoundParameters.ContainsKey($pair[0])) {
continue
}
$value = $PSBoundParameters[$pair[0]]
if ($null -eq $value) {
throw "Omit unknown context; do not pass null for $($pair[0])."
}
if ($pair[0] -ne 'BCVersion') {
foreach ($entry in $value) {
if ([string]::IsNullOrWhiteSpace($entry) -or $entry -cne $entry.Trim()) {
throw "Invalid context value for $($pair[0]): '$entry'"
}
}
}
$context[$pair[1]] = $value
}
if ($context.Contains('technologies') -and $context['technologies'] -ccontains 'all') {
throw "Technologies has no 'all' sentinel. Omit unknown context."
}
$recovery = "Run Entry preparation once before dispatch, or use READ's path-discovery fallback. Do not rebuild in a leaf."
$preparedIndex = Read-PreparedKnowledgeIndex -IndexPath $IndexPath -Recovery $recovery
$index = $preparedIndex.index
$indexContent = $preparedIndex.content
$byPath = $preparedIndex.byPath
$paths = $preparedIndex.paths
# The v1 generator historically serialized an omitted EnabledLayers parameter as [null].
$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 (@($indexedLayers | Where-Object { $_ -cnotin @('microsoft', 'community', 'custom') }).Count -or
@($indexedLayers | Group-Object -CaseSensitive | Where-Object Count -gt 1).Count -or
@($EnabledLayers | Where-Object { $_ -cnotin $indexedLayers }).Count) {
throw "Index layer coverage does not cover EnabledLayers. $recovery"
}
foreach ($path in $paths) {
$row = $byPath[$path]
if ($row.layer -cnotin $indexedLayers) {
throw "Index row layer is outside index coverage: $path. $recovery"
}
$problem = Get-KnowledgeMetadataProblem -Row $row
if ($problem) {
throw "Malformed knowledge index row at ${path}: $problem. $recovery"
}
}
$defaults = [ordered]@{
'bc-version' = @('all')
technologies = @('al')
countries = @('w1')
'application-area' = @('all')
}
$candidates = [Collections.Generic.List[object]]::new()
$excluded = [Collections.Generic.List[object]]::new()
foreach ($path in $paths) {
$row = $byPath[$path]
if ($row.domain -cne $Domain) {
continue
}
$unknown = [Collections.Generic.List[string]]::new()
$matchesContext = $true
foreach ($field in $defaults.Keys) {
$values = $row[$field]
if ($field -eq 'bc-version') {
$values = @(Get-NormalizedKnowledgeVersions -Values $values)
}
$sentinel = switch ($field) {
'bc-version' { 'all' }
'countries' { 'w1' }
'application-area' { 'all' }
default { '' }
}
if ($sentinel -and $values -ccontains $sentinel) {
continue
}
if (-not $context.Contains($field)) {
$unknown.Add($field)
continue
}
$target = $context[$field]
$matched = $false
if ($field -eq 'bc-version') {
if ($values.Count -eq 1 -and $values[0] -match '^(\d+)\.\.(\d+)?$') {
$matched = $target -ge [bigint]::Parse($Matches[1]) -and
(-not $Matches[2] -or $target -le [bigint]::Parse($Matches[2]))
}
else {
$matched = @($values | Where-Object { [bigint]::Parse($_) -eq $target }).Count -gt 0
}
}
else {
$matched = @($values | Where-Object { $target -ccontains $_ }).Count -gt 0
}
if (-not $matched) {
$matchesContext = $false
break
}
}
if (-not $matchesContext -or ($ExcludeConditional -and $unknown.Count)) {
continue
}
$null = Resolve-KnowledgePath -Root $BCQualityRoot -Path $path
$candidate = [ordered]@{
path = $path
layer = $row.layer
keywords = $row.keywords
title = $row.title
description = $row.description
}
foreach ($field in $defaults.Keys) {
if (($row[$field] -join "`0") -cne ($defaults[$field] -join "`0")) {
$candidate[$field] = $row[$field]
}
}
$candidate.applicability = if ($unknown.Count) { 'conditional' } else { 'applicable' }
$candidate.unknownDimensions = @($unknown)
if ($row.layer -cin $EnabledLayers) {
$candidates.Add($candidate)
}
else {
$excluded.Add($candidate)
}
}
$header = [ordered]@{
version = 2
domain = $Domain
context = $context
enabledLayers = @($EnabledLayers)
indexedLayers = @($indexedLayers)
excludeConditional = [bool]$ExcludeConditional
defaults = $defaults
candidateCount = $candidates.Count
excludedByConfigurationCount = $excluded.Count
}
function Get-CatalogSnapshot {
param(
[string] $PreparedIndexSha256,
[Collections.IDictionary] $Request,
[Collections.IDictionary] $Groups
)
$hash = [Security.Cryptography.IncrementalHash]::CreateHash(
[Security.Cryptography.HashAlgorithmName]::SHA256
)
try {
foreach ($value in @(
$PreparedIndexSha256,
(ConvertTo-Json -InputObject $Request -Depth 8 -Compress)
)) {
$hash.AppendData([Text.Encoding]::UTF8.GetBytes($value))
$hash.AppendData([byte[]](10))
}
foreach ($groupName in $Groups.Keys) {
$hash.AppendData([Text.Encoding]::UTF8.GetBytes("[$groupName]"))
$hash.AppendData([byte[]](10))
foreach ($row in $Groups[$groupName]) {
$hash.AppendData([Text.Encoding]::UTF8.GetBytes(
(ConvertTo-Json -InputObject $row -Depth 8 -Compress)
))
$hash.AppendData([byte[]](10))
}
}
return [Convert]::ToHexString($hash.GetHashAndReset()).ToLowerInvariant()
}
finally {
$hash.Dispose()
}
}
$groups = [ordered]@{
candidates = $candidates
excludedByConfiguration = $excluded
}
$header.snapshot = Get-CatalogSnapshot -PreparedIndexSha256 $indexContent.sha256 -Request $header -Groups $groups
ConvertTo-BoundedPage -Header $header -Groups $groups -Offset $Offset -Snapshot $Snapshot -MaxBytes $MaxBytes

View file

@ -0,0 +1,759 @@
<#
.SYNOPSIS
Validates lossless bounded catalog and exact-body retrieval.
#>
#requires -Version 7.2
[CmdletBinding()]
param(
[string] $Root = (Resolve-Path (Join-Path $PSScriptRoot '..'))
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$Root = (Resolve-Path -LiteralPath $Root).Path
$generator = Join-Path $Root 'tools/Build-KnowledgeIndex.ps1'
$search = Join-Path $Root 'tools/Search-Knowledge.ps1'
$getArticles = Join-Path $Root 'tools/Get-KnowledgeArticles.ps1'
$utf8 = [Text.UTF8Encoding]::new($false, $true)
function Assert-True {
param([bool] $Condition, [string] $Message)
if (-not $Condition) {
throw "Assertion failed: $Message"
}
}
function Assert-Equal {
param($Actual, $Expected, [string] $Message)
if ($Actual -cne $Expected) {
throw "Assertion failed: $Message. Expected '$Expected', got '$Actual'."
}
}
function Assert-Sequence {
param($Actual, $Expected, [string] $Message)
$actualJson = ConvertTo-Json -InputObject @($Actual) -Compress
$expectedJson = ConvertTo-Json -InputObject @($Expected) -Compress
if ($actualJson -cne $expectedJson) {
throw "Assertion failed: $Message. Expected $expectedJson, got $actualJson."
}
}
function Assert-Throws {
param([scriptblock] $Action, [string] $Pattern, [string] $Message)
try {
& $Action
}
catch {
if ($_.Exception.Message -notmatch $Pattern) {
throw "Assertion failed: $Message. Wrong error: $($_.Exception.Message)"
}
return
}
throw "Assertion failed: $Message. No error was thrown."
}
function Get-OutputByteCount {
param([string] $Text)
return [Text.Encoding]::UTF8.GetByteCount($Text) +
[Text.Encoding]::UTF8.GetByteCount([Environment]::NewLine)
}
function Invoke-CatalogPages {
param(
[hashtable] $Arguments,
[int] $MaxBytes = 4096
)
$allCandidates = [Collections.Generic.List[object]]::new()
$allExcluded = [Collections.Generic.List[object]]::new()
$offset = 0
$snapshot = ''
$shared = ''
$pageCount = 0
$lastPage = $null
do {
$pageArguments = @{} + $Arguments
$pageArguments.MaxBytes = $MaxBytes
$pageArguments.Offset = $offset
if ($snapshot) {
$pageArguments.Snapshot = $snapshot
}
$raw = & $search @pageArguments
Assert-True ($raw -is [string]) 'catalog helper emitted exactly one JSON string'
Assert-True ((Get-OutputByteCount -Text $raw) -le $MaxBytes) 'catalog page includes its newline in MaxBytes'
$page = $raw | ConvertFrom-Json
$pageCount++
Assert-True ($pageCount -le 1000) 'catalog continuation terminates'
Assert-Equal $page.offset $offset 'catalog offset is exact'
Assert-Equal $page.returnedCount (@($page.candidates).Count + @($page.excludedByConfiguration).Count) 'page returnedCount matches rows'
Assert-Equal $page.remainingCount ($page.totalCount - $offset - $page.returnedCount) 'page remainingCount is exact'
$currentShared = [ordered]@{
version = $page.version
domain = $page.domain
context = $page.context
enabledLayers = $page.enabledLayers
indexedLayers = $page.indexedLayers
excludeConditional = $page.excludeConditional
defaults = $page.defaults
candidateCount = $page.candidateCount
excludedByConfigurationCount = $page.excludedByConfigurationCount
snapshot = $page.snapshot
totalCount = $page.totalCount
} | ConvertTo-Json -Depth 8 -Compress
if (-not $shared) {
$shared = $currentShared
$snapshot = $page.snapshot
}
else {
Assert-Equal $currentShared $shared 'catalog pages repeat shared context, defaults, totals, and snapshot'
}
foreach ($row in @($page.candidates)) {
$allCandidates.Add($row)
}
foreach ($row in @($page.excludedByConfiguration)) {
$allExcluded.Add($row)
}
if (-not $page.complete) {
Assert-True ($null -ne $page.continuation) 'incomplete page has continuation'
Assert-Equal $page.continuation.snapshot $snapshot 'continuation is snapshot-bound'
Assert-True ($page.continuation.offset -gt $offset) 'continuation makes progress'
$offset = $page.continuation.offset
}
$lastPage = $page
} while (-not $page.complete)
Assert-True ($null -eq $lastPage.continuation) 'final page has no continuation'
Assert-Equal $allCandidates.Count $lastPage.candidateCount 'candidate total survives paging'
Assert-Equal $allExcluded.Count $lastPage.excludedByConfigurationCount 'excluded total survives paging'
return [pscustomobject]@{
candidates = @($allCandidates)
excluded = @($allExcluded)
pages = $pageCount
snapshot = $snapshot
lastPage = $lastPage
}
}
function Test-BodyRoundTrip {
param(
[string[]] $Paths,
[string] $IndexPath,
[switch] $Samples
)
$seen = [Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal)
for ($start = 0; $start -lt $Paths.Count; $start += 8) {
$end = [Math]::Min($start + 7, $Paths.Count - 1)
$remaining = @($Paths[$start..$end])
$snapshot = ''
do {
$arguments = @{
BCQualityRoot = $Root
IndexPath = $IndexPath
Paths = $remaining
MaxArticles = 8
MaxBytes = 16000
}
if ($Samples) {
$arguments.Samples = $true
}
if ($snapshot) {
$arguments.Snapshot = $snapshot
}
$raw = & $getArticles @arguments
Assert-True ($raw -is [string]) 'article helper emitted exactly one JSON string'
Assert-True ((Get-OutputByteCount -Text $raw) -le 16000) 'article batch includes its newline in MaxBytes'
$batch = $raw | ConvertFrom-Json
Assert-True ($batch.returnedCount -gt 0) 'article batching makes progress'
Assert-Equal $batch.returnedCount @($batch.articles).Count 'article returnedCount matches rows'
Assert-Equal $batch.complete (@($batch.remainingPaths).Count -eq 0) 'article completion matches remaining paths'
if ($batch.complete) {
Assert-True ($null -eq $batch.continuation) 'complete article batch has no continuation'
}
else {
Assert-True ($batch.continuation.snapshot -match '^[a-f0-9]{64}$') 'article continuation is snapshot-bound'
}
foreach ($article in @($batch.articles)) {
Assert-True ($seen.Add($article.path)) "body returned once: $($article.path)"
$fullPath = Join-Path $Root ($article.path.Replace('/', [IO.Path]::DirectorySeparatorChar))
$bytes = [IO.File]::ReadAllBytes($fullPath)
$text = $utf8.GetString($bytes)
$hash = [Convert]::ToHexString(
[Security.Cryptography.SHA256]::HashData($bytes)
).ToLowerInvariant()
Assert-Equal $article.bytes $bytes.Length "byte count round-trips: $($article.path)"
Assert-Equal $article.sha256 $hash "SHA-256 round-trips: $($article.path)"
Assert-Equal $article.body $text "body round-trips: $($article.path)"
}
$remaining = @($batch.remainingPaths)
$snapshot = if ($batch.complete) { '' } else { $batch.continuation.snapshot }
} while ($remaining.Count)
}
Assert-Equal $seen.Count $Paths.Count 'every requested body round-trips without loss'
}
function New-NeutralArticle {
param(
[string] $FixtureRoot,
[string] $Layer,
[string] $Slug,
[string] $Version = 'all',
[string] $Technology = 'al',
[string] $Country = 'w1',
[string] $Area = 'all',
[string] $Title = 'Neutral retrieval example',
[string] $Description = 'Neutral retrieval metadata for deterministic tests.'
)
$directory = Join-Path $FixtureRoot "$Layer\knowledge\neutral"
New-Item -ItemType Directory -Force -Path $directory | Out-Null
$content = @"
---
bc-version: [$Version]
domain: neutral
keywords: [neutral, retrieval, deterministic]
technologies: [$Technology]
countries: [$Country]
application-area: [$Area]
---
# $Title
## Description
$Description
"@
Set-Content -LiteralPath (Join-Path $directory "$Slug.md") -Value $content -Encoding utf8NoBOM
}
function Test-InvalidSourceIndexing {
param(
[string] $FixtureRoot,
[string] $Field,
[string] $ValidValue,
[string] $InvalidValue
)
New-NeutralArticle -FixtureRoot $FixtureRoot -Layer microsoft -Slug valid-source
New-NeutralArticle -FixtureRoot $FixtureRoot -Layer community -Slug invalid-source
$articlePath = Join-Path $FixtureRoot 'community\knowledge\neutral\invalid-source.md'
$text = [IO.File]::ReadAllText($articlePath, $utf8)
$text = $text.Replace("$Field`: $ValidValue", "$Field`: $InvalidValue")
[IO.File]::WriteAllText($articlePath, $text, $utf8)
$indexPath = Join-Path (Split-Path $FixtureRoot -Parent) ("$Field-index.json")
$generation = @(& $generator -BCQualityRoot $FixtureRoot -IndexPath $indexPath 3>&1)
$warnings = @($generation | Where-Object { $_ -is [Management.Automation.WarningRecord] })
Assert-Equal $warnings.Count 1 "scalar $Field source emits one omission warning"
Assert-True (
$warnings[0].Message -match
"Skipping invalid knowledge article 'community/knowledge/neutral/invalid-source\.md': frontmatter field '$([regex]::Escape($Field))' must use non-empty bracket-array syntax\."
) "scalar $Field warning identifies the exact path and reason"
$prepared = Get-Content -LiteralPath $indexPath -Raw -Encoding utf8 | ConvertFrom-Json
Assert-Equal $prepared.articleCount 1 "scalar $Field source is omitted while its valid sibling is indexed"
Assert-Sequence $prepared.articles.path @('microsoft/knowledge/neutral/valid-source.md') "scalar $Field index contains only the valid sibling"
$catalog = & $search -BCQualityRoot $FixtureRoot -IndexPath $indexPath -Domain neutral |
ConvertFrom-Json
Assert-Sequence $catalog.candidates.path @('microsoft/knowledge/neutral/valid-source.md') "scalar $Field catalog retrieves the valid sibling"
$valid = & $getArticles -BCQualityRoot $FixtureRoot -IndexPath $indexPath `
-Paths 'microsoft/knowledge/neutral/valid-source.md' |
ConvertFrom-Json
Assert-True $valid.complete "scalar $Field valid sibling body retrieves completely"
Assert-Throws {
& $getArticles -BCQualityRoot $FixtureRoot -IndexPath $indexPath `
-Paths 'community/knowledge/neutral/invalid-source.md'
} 'Selected article is absent from the prepared index' "scalar $Field omitted source cannot be retrieved"
}
function Test-InvalidSemanticIndexing {
param(
[string] $FixtureRoot,
[string] $CaseName,
[string] $Field,
[string] $ValidValue,
[string] $InvalidValue,
[string] $ExpectedReason
)
New-NeutralArticle -FixtureRoot $FixtureRoot -Layer microsoft -Slug valid-source
New-NeutralArticle -FixtureRoot $FixtureRoot -Layer community -Slug invalid-source
$articlePath = Join-Path $FixtureRoot 'community\knowledge\neutral\invalid-source.md'
$text = [IO.File]::ReadAllText($articlePath, $utf8)
$text = $text.Replace("$Field`: $ValidValue", "$Field`: $InvalidValue")
[IO.File]::WriteAllText($articlePath, $text, $utf8)
$indexPath = Join-Path (Split-Path $FixtureRoot -Parent) ("$CaseName-index.json")
$generation = @(& $generator -BCQualityRoot $FixtureRoot -IndexPath $indexPath 3>&1)
$warnings = @($generation | Where-Object { $_ -is [Management.Automation.WarningRecord] })
Assert-Equal $warnings.Count 1 "$CaseName emits one omission warning"
Assert-Equal $warnings[0].Message "Skipping invalid knowledge article 'community/knowledge/neutral/invalid-source.md': $ExpectedReason." "$CaseName warning identifies exact path and reason"
$prepared = Get-Content -LiteralPath $indexPath -Raw -Encoding utf8 | ConvertFrom-Json
Assert-Equal $prepared.articleCount 1 "$CaseName omits invalid source and retains valid sibling"
Assert-Sequence $prepared.articles.path @('microsoft/knowledge/neutral/valid-source.md') "$CaseName index contains only valid sibling"
Assert-True ($prepared.sourceSnapshot -match '^[a-f0-9]{64}$') "$CaseName source snapshot remains valid"
$validRow = $prepared.articles[0]
$manifest = @(
[ordered]@{ path = $validRow.path; sha256 = $validRow.sourceSha256 }
)
$manifestBytes = [Text.Encoding]::UTF8.GetBytes(
(ConvertTo-Json -InputObject $manifest -Depth 8 -Compress)
)
$expectedSnapshot = [Convert]::ToHexString(
[Security.Cryptography.SHA256]::HashData($manifestBytes)
).ToLowerInvariant()
Assert-Equal $prepared.sourceSnapshot $expectedSnapshot "$CaseName source snapshot covers only retained rows"
$catalog = & $search -BCQualityRoot $FixtureRoot -IndexPath $indexPath -Domain neutral |
ConvertFrom-Json
Assert-Sequence $catalog.candidates.path @('microsoft/knowledge/neutral/valid-source.md') "$CaseName catalog retains valid sibling"
$valid = & $getArticles -BCQualityRoot $FixtureRoot -IndexPath $indexPath `
-Paths 'microsoft/knowledge/neutral/valid-source.md' |
ConvertFrom-Json
Assert-True $valid.complete "$CaseName valid sibling body retrieves"
Assert-Throws {
& $getArticles -BCQualityRoot $FixtureRoot -IndexPath $indexPath `
-Paths 'community/knowledge/neutral/invalid-source.md'
} 'Selected article is absent from the prepared index' "$CaseName invalid source cannot be retrieved"
}
function Test-InvalidEnabledLayers {
param(
[string] $FixtureRoot,
[string] $CaseName,
$Layers,
[string] $ExpectedPattern
)
$indexPath = Join-Path (Split-Path $FixtureRoot -Parent) ("layers-$CaseName.json")
$arguments = @{
BCQualityRoot = $FixtureRoot
IndexPath = $indexPath
EnabledLayers = $Layers
}
Assert-Throws {
& $generator @arguments
} $ExpectedPattern "$CaseName EnabledLayers fails"
Assert-True (-not (Test-Path -LiteralPath $indexPath)) "$CaseName fails before index creation"
}
$tmp = Join-Path ([IO.Path]::GetTempPath()) ("bcquality_retrieval_" + [guid]::NewGuid().ToString('N'))
New-Item -ItemType Directory -Force -Path $tmp | Out-Null
try {
$indexPath = Join-Path $tmp 'knowledge-index.json'
& $generator -BCQualityRoot $Root -IndexPath $indexPath | Out-Null
$index = Get-Content -LiteralPath $indexPath -Raw -Encoding utf8 | ConvertFrom-Json
$diskArticlePaths = @(
foreach ($layer in 'microsoft', 'community', 'custom') {
$knowledge = Join-Path $Root "$layer\knowledge"
if (Test-Path -LiteralPath $knowledge) {
Get-ChildItem -LiteralPath $knowledge -Recurse -File -Filter '*.md' |
ForEach-Object {
[IO.Path]::GetRelativePath($Root, $_.FullName).Replace('\', '/')
}
}
}
) | Sort-Object
Assert-Equal $index.articleCount $diskArticlePaths.Count 'index covers every current article'
Assert-True ($index.sourceSnapshot -match '^[a-f0-9]{64}$') 'index carries an exact source snapshot'
$allCatalogRows = [Collections.Generic.List[object]]::new()
$domains = @($index.articles.domain | Sort-Object -Unique)
foreach ($domain in $domains) {
$catalog = Invoke-CatalogPages -Arguments @{
BCQualityRoot = $Root
IndexPath = $indexPath
Domain = $domain
}
Assert-Equal $catalog.excluded.Count 0 "all layers enabled for $domain"
foreach ($row in $catalog.candidates) {
$allCatalogRows.Add($row)
}
}
$expectedRows = @($index.articles | Sort-Object path)
$actualRows = @($allCatalogRows | Sort-Object path)
Assert-Equal $actualRows.Count $expectedRows.Count 'paged union has no top-k or query-based loss'
Assert-Sequence ($actualRows.path) ($expectedRows.path) 'paged union equals all READ-filtered candidates'
Assert-Equal @($actualRows.path | Sort-Object -Unique).Count $actualRows.Count 'catalog does not deduplicate distinct paths'
$defaults = [ordered]@{
'bc-version' = @('all')
technologies = @('al')
countries = @('w1')
'application-area' = @('all')
}
for ($i = 0; $i -lt $actualRows.Count; $i++) {
$actual = $actualRows[$i]
$expected = $expectedRows[$i]
Assert-Equal $actual.path $expected.path 'catalog preserves exact path'
Assert-Equal $actual.layer $expected.layer 'catalog preserves layer'
Assert-Sequence $actual.keywords $expected.keywords 'catalog preserves full keywords'
Assert-Equal $actual.title $expected.title 'catalog preserves title'
Assert-Equal $actual.description $expected.description 'catalog preserves one-line description'
$unknown = [Collections.Generic.List[string]]::new()
foreach ($field in $defaults.Keys) {
$expectedValues = @($expected.$field)
$sentinel = switch ($field) {
'bc-version' { 'all' }
'countries' { 'w1' }
'application-area' { 'all' }
default { '' }
}
if (-not $sentinel -or $expectedValues -notcontains $sentinel) {
$unknown.Add($field)
}
$hasField = $actual.PSObject.Properties.Name -ccontains $field
if (($expectedValues -join "`0") -ceq (@($defaults[$field]) -join "`0")) {
Assert-True (-not $hasField) "default field is inherited from page: $field"
}
else {
Assert-True $hasField "non-default field survives paging: $field"
Assert-Sequence $actual.$field $expectedValues "non-default field is exact: $field"
}
}
Assert-Equal $actual.applicability ($(if ($unknown.Count) { 'conditional' } else { 'applicable' })) 'applicability verdict is explicit'
Assert-Sequence $actual.unknownDimensions @($unknown) 'unknown dimensions are explicit'
}
$performanceFirst = & $search -BCQualityRoot $Root -IndexPath $indexPath -Domain performance -MaxBytes 4096 |
ConvertFrom-Json
Assert-True (-not $performanceFirst.complete) 'large domain produces deterministic continuation'
Assert-Throws {
& $search -BCQualityRoot $Root -IndexPath $indexPath -Domain performance -MaxBytes 4096 -Offset $performanceFirst.continuation.offset
} 'Continuation requires Snapshot' 'continuation without snapshot fails'
Assert-Throws {
& $search -BCQualityRoot $Root -IndexPath $indexPath -Domain performance -Offset $performanceFirst.totalCount
} 'Invalid Offset' 'offset at total fails'
Assert-Throws {
& $search -BCQualityRoot $Root -IndexPath $indexPath -Domain performance -Offset 1 -Snapshot ('0' * 64)
} 'Snapshot changed' 'wrong snapshot fails'
$changedRawIndex = Join-Path $tmp 'changed-raw-index.json'
$changedRaw = Get-Content -LiteralPath $indexPath -Raw -Encoding utf8 | ConvertFrom-Json
$changedRaw.generatedAt = [DateTimeOffset]::UtcNow.ToString('O')
$changedRaw | ConvertTo-Json -Depth 8 -Compress |
Set-Content -LiteralPath $changedRawIndex -Encoding utf8NoBOM -NoNewline
Assert-Throws {
& $search -BCQualityRoot $Root -IndexPath $changedRawIndex -Domain performance `
-MaxBytes 4096 -Offset $performanceFirst.continuation.offset `
-Snapshot $performanceFirst.continuation.snapshot
} 'Snapshot changed' 'continuation is bound to the exact prepared index bytes'
$malformedIndex = Join-Path $tmp 'malformed.json'
Set-Content -LiteralPath $malformedIndex -Value '{not-json' -Encoding utf8NoBOM
Assert-Throws {
& $search -BCQualityRoot $Root -IndexPath $malformedIndex -Domain performance
} 'Malformed knowledge index JSON' 'malformed JSON fails'
$unsafeIndex = Join-Path $tmp 'unsafe.json'
$unsafe = Get-Content -LiteralPath $indexPath -Raw -Encoding utf8 | ConvertFrom-Json
$unsafe.articles[0].path = '../outside.md'
$unsafe | ConvertTo-Json -Depth 8 -Compress |
Set-Content -LiteralPath $unsafeIndex -Encoding utf8NoBOM
Assert-Throws {
& $search -BCQualityRoot $Root -IndexPath $unsafeIndex -Domain performance
} 'Invalid knowledge path' 'unsafe indexed path fails'
$invalidRowIndex = Join-Path $tmp 'invalid-row.json'
$invalidRow = Get-Content -LiteralPath $indexPath -Raw -Encoding utf8 | ConvertFrom-Json
$invalidRow.articles[0].keywords = @()
$invalidRow | ConvertTo-Json -Depth 8 -Compress |
Set-Content -LiteralPath $invalidRowIndex -Encoding utf8NoBOM
Assert-Throws {
& $search -BCQualityRoot $Root -IndexPath $invalidRowIndex -Domain performance
} 'Malformed knowledge index row' 'malformed index row fails'
$semanticCorruptIndex = Join-Path $tmp 'semantic-corrupt-row.json'
$semanticCorrupt = Get-Content -LiteralPath $indexPath -Raw -Encoding utf8 | ConvertFrom-Json
$semanticCorrupt.articles[0].countries = @('usa')
$semanticCorrupt | ConvertTo-Json -Depth 8 -Compress |
Set-Content -LiteralPath $semanticCorruptIndex -Encoding utf8NoBOM
Assert-Throws {
& $search -BCQualityRoot $Root -IndexPath $semanticCorruptIndex -Domain performance
} 'Malformed knowledge index row.*invalid countries' 'search rejects semantically invalid external index rows'
$corruptSemanticCases = @(
@{ name = 'uppercase-all'; field = 'bc-version'; value = @('ALL'); reason = 'invalid bc-version' },
@{ name = 'uppercase-w1'; field = 'countries'; value = @('W1'); reason = 'invalid countries' },
@{ name = 'zero-open-range'; field = 'bc-version'; value = @('"0.."'); reason = 'invalid bc-version range bound' },
@{ name = 'zero-closed-range'; field = 'bc-version'; value = @('"0..0"'); reason = 'invalid bc-version range bound' }
)
foreach ($case in $corruptSemanticCases) {
$corruptPath = Join-Path $tmp ("corrupt-$($case.name).json")
$corrupt = Get-Content -LiteralPath $indexPath -Raw -Encoding utf8 | ConvertFrom-Json
$corrupt.articles[0].PSObject.Properties[$case.field].Value = $case.value
$corrupt | ConvertTo-Json -Depth 8 -Compress |
Set-Content -LiteralPath $corruptPath -Encoding utf8NoBOM
Assert-Throws {
& $search -BCQualityRoot $Root -IndexPath $corruptPath -Domain performance
} "Malformed knowledge index row.*$([regex]::Escape($case.reason))" "search rejects $($case.name) in an external index"
}
$invalidUtf8Root = Join-Path $tmp 'invalid-utf8-source'
New-NeutralArticle -FixtureRoot $invalidUtf8Root -Layer microsoft -Slug valid-catalog
New-NeutralArticle -FixtureRoot $invalidUtf8Root -Layer community -Slug invalid-utf8-source
$invalidUtf8Article = Join-Path $invalidUtf8Root 'community\knowledge\neutral\invalid-utf8-source.md'
$validBytes = [IO.File]::ReadAllBytes($invalidUtf8Article)
[IO.File]::WriteAllBytes($invalidUtf8Article, [byte[]]@($validBytes + @(0xc3, 0x28)))
$invalidUtf8Index = Join-Path $tmp 'invalid-utf8-index.json'
$generation = @(& $generator -BCQualityRoot $invalidUtf8Root -IndexPath $invalidUtf8Index 3>&1)
$warnings = @($generation | Where-Object { $_ -is [Management.Automation.WarningRecord] })
Assert-Equal $warnings.Count 1 'malformed UTF-8 source emits one omission warning'
Assert-Equal $warnings[0].Message "Skipping invalid knowledge article 'community/knowledge/neutral/invalid-utf8-source.md': invalid UTF-8." 'malformed UTF-8 warning identifies the exact path and reason'
$invalidUtf8Prepared = Get-Content -LiteralPath $invalidUtf8Index -Raw -Encoding utf8 |
ConvertFrom-Json
Assert-Equal $invalidUtf8Prepared.articleCount 1 'malformed UTF-8 source is omitted while its valid sibling is indexed'
Assert-Sequence $invalidUtf8Prepared.articles.path @('microsoft/knowledge/neutral/valid-catalog.md') 'malformed UTF-8 index contains only the valid sibling'
$validCatalog = & $search -BCQualityRoot $invalidUtf8Root -IndexPath $invalidUtf8Index -Domain neutral |
ConvertFrom-Json
Assert-Sequence $validCatalog.candidates.path @('microsoft/knowledge/neutral/valid-catalog.md') 'catalog retrieves the valid sibling after malformed UTF-8 omission'
$validBody = & $getArticles -BCQualityRoot $invalidUtf8Root -IndexPath $invalidUtf8Index `
-Paths 'microsoft/knowledge/neutral/valid-catalog.md' |
ConvertFrom-Json
Assert-True $validBody.complete 'valid sibling body retrieves after malformed UTF-8 omission'
Assert-Throws {
& $getArticles -BCQualityRoot $invalidUtf8Root -IndexPath $invalidUtf8Index `
-Paths 'community/knowledge/neutral/invalid-utf8-source.md'
} 'Selected article is absent from the prepared index' 'omitted malformed UTF-8 source cannot be retrieved'
$scalarCases = @(
@{ field = 'bc-version'; valid = '[all]'; invalid = 'all' },
@{ field = 'keywords'; valid = '[neutral, retrieval, deterministic]'; invalid = 'neutral' },
@{ field = 'technologies'; valid = '[al]'; invalid = 'al' },
@{ field = 'countries'; valid = '[w1]'; invalid = 'w1' },
@{ field = 'application-area'; valid = '[all]'; invalid = 'all' }
)
foreach ($case in $scalarCases) {
Test-InvalidSourceIndexing -FixtureRoot (Join-Path $tmp "scalar-$($case.field)") `
-Field $case.field -ValidValue $case.valid -InvalidValue $case.invalid
}
$semanticCases = @(
@{ name = 'mixed-version-sentinel'; field = 'bc-version'; valid = '[all]'; invalid = '[all, 27]'; reason = 'mixed bc-version sentinel' },
@{ name = 'invalid-country'; field = 'countries'; valid = '[w1]'; invalid = '[usa]'; reason = 'invalid countries' },
@{ name = 'descending-version-range'; field = 'bc-version'; valid = '[all]'; invalid = '["28..27"]'; reason = 'descending bc-version range' },
@{ name = 'malformed-version-range'; field = 'bc-version'; valid = '[all]'; invalid = '[twenty-seven]'; reason = 'invalid bc-version' },
@{ name = 'malformed-keyword'; field = 'keywords'; valid = '[neutral, retrieval, deterministic]'; invalid = '[neutral, Bad_Token, deterministic]'; reason = 'invalid keywords' },
@{ name = 'malformed-technology'; field = 'technologies'; valid = '[al]'; invalid = '[AL]'; reason = 'invalid technologies' },
@{ name = 'malformed-application-area'; field = 'application-area'; valid = '[all]'; invalid = '[finance_]'; reason = 'invalid application-area' },
@{ name = 'uppercase-version-sentinel'; field = 'bc-version'; valid = '[all]'; invalid = '[ALL]'; reason = 'invalid bc-version' },
@{ name = 'uppercase-country-sentinel'; field = 'countries'; valid = '[w1]'; invalid = '[W1]'; reason = 'invalid countries' },
@{ name = 'zero-open-version-range'; field = 'bc-version'; valid = '[all]'; invalid = '["0.."]'; reason = 'invalid bc-version range bound' },
@{ name = 'zero-closed-version-range'; field = 'bc-version'; valid = '[all]'; invalid = '["0..0"]'; reason = 'invalid bc-version range bound' }
)
foreach ($case in $semanticCases) {
Test-InvalidSemanticIndexing -FixtureRoot (Join-Path $tmp "semantic-$($case.name)") `
-CaseName $case.name -Field $case.field -ValidValue $case.valid `
-InvalidValue $case.invalid -ExpectedReason $case.reason
}
Assert-Throws {
& $search -BCQualityRoot $Root -IndexPath $indexPath -Domain ('x' * 2000) -MaxBytes 1024
} 'Page envelope exceeds' 'oversized page envelope fails'
$articlePaths = @($index.articles.path | Sort-Object)
Assert-Sequence $articlePaths $diskArticlePaths 'exact article path union matches disk'
Assert-Throws {
& $getArticles -BCQualityRoot $Root -IndexPath $indexPath -Paths @($articlePaths[0..8])
} 'exceeds MaxArticles=8' 'exact retrieval rejects path batches larger than eight'
$samplePaths = @(
foreach ($layer in 'microsoft', 'community', 'custom') {
$knowledge = Join-Path $Root "$layer\knowledge"
if (Test-Path -LiteralPath $knowledge) {
Get-ChildItem -LiteralPath $knowledge -Recurse -File |
Where-Object Name -Match '\.(good|bad)\.[a-z0-9]+$' |
ForEach-Object {
[IO.Path]::GetRelativePath($Root, $_.FullName).Replace('\', '/')
}
}
}
) | Sort-Object
Test-BodyRoundTrip -Paths $articlePaths -IndexPath $indexPath
Test-BodyRoundTrip -Paths $samplePaths -IndexPath $indexPath -Samples
$fixtureRoot = Join-Path $tmp 'neutral'
New-NeutralArticle -FixtureRoot $fixtureRoot -Layer microsoft -Slug default
New-NeutralArticle -FixtureRoot $fixtureRoot -Layer community -Slug versioned -Version '"27.."' -Technology javascript -Country dk -Area finance -Title 'Versioned neutral example'
New-NeutralArticle -FixtureRoot $fixtureRoot -Layer custom -Slug localized -Version 28 -Technology al -Country de -Area service -Title 'Localized neutral example'
$fixtureIndex = Join-Path $tmp 'neutral-index.json'
& $generator -BCQualityRoot $fixtureRoot -IndexPath $fixtureIndex | Out-Null
foreach ($case in @(
@{ name = 'uppercase'; layers = @('Microsoft'); pattern = 'unique canonical lowercase layer names' },
@{ name = 'duplicate'; layers = @('microsoft', 'microsoft'); pattern = 'unique canonical lowercase layer names' },
@{ name = 'unknown'; layers = @('partner'); pattern = 'unique canonical lowercase layer names' },
@{ name = 'null'; layers = $null; pattern = 'must be an array' }
)) {
Test-InvalidEnabledLayers -FixtureRoot $fixtureRoot -CaseName $case.name `
-Layers $case.layers -ExpectedPattern $case.pattern
}
$subsetIndex = Join-Path $tmp 'community-only-index.json'
& $generator -BCQualityRoot $fixtureRoot -IndexPath $subsetIndex `
-EnabledLayers @('community') | Out-Null
$subset = & $search -BCQualityRoot $fixtureRoot -IndexPath $subsetIndex `
-Domain neutral -EnabledLayers @('community') |
ConvertFrom-Json
Assert-Equal $subset.candidateCount 1 'valid EnabledLayers subset builds and is consumable'
Assert-Sequence $subset.candidates.path @('community/knowledge/neutral/versioned.md') 'valid subset contains only its exact layer'
$applicable = Invoke-CatalogPages -Arguments @{
BCQualityRoot = $fixtureRoot
IndexPath = $fixtureIndex
Domain = 'neutral'
BCVersion = 28
Technologies = @('al', 'javascript')
Countries = @('dk', 'de')
ApplicationAreas = @('finance', 'service')
} -MaxBytes 16000
Assert-Equal $applicable.candidates.Count 3 'neutral layer/version rows all survive matching context'
Assert-True (@($applicable.candidates | Where-Object applicability -CEQ applicable).Count -eq 3) 'matching rows are applicable'
$versioned = $applicable.candidates | Where-Object path -CEQ 'community/knowledge/neutral/versioned.md'
Assert-Equal $versioned.layer community 'non-default layer survives'
Assert-Sequence $versioned.'bc-version' @('"27.."') 'original version metadata survives'
Assert-Equal $versioned.applicability applicable 'lowercase sentinels and positive open range remain applicable'
Assert-Sequence $versioned.technologies @('javascript') 'non-default technology survives'
Assert-Sequence $versioned.countries @('dk') 'non-default country survives'
Assert-Sequence $versioned.'application-area' @('finance') 'non-default application area survives'
$conditional = Invoke-CatalogPages -Arguments @{
BCQualityRoot = $fixtureRoot
IndexPath = $fixtureIndex
Domain = 'neutral'
BCVersion = 28
Technologies = @('al', 'javascript')
} -MaxBytes 16000
$conditionalVersioned = $conditional.candidates |
Where-Object path -CEQ 'community/knowledge/neutral/versioned.md'
Assert-Equal $conditionalVersioned.applicability conditional 'unknown context produces conditional verdict'
Assert-Sequence $conditionalVersioned.unknownDimensions @('countries', 'application-area') 'unknown dimensions survive'
$layerFiltered = Invoke-CatalogPages -Arguments @{
BCQualityRoot = $fixtureRoot
IndexPath = $fixtureIndex
Domain = 'neutral'
EnabledLayers = @('microsoft')
} -MaxBytes 16000
Assert-Equal $layerFiltered.candidates.Count 1 'enabled layer remains a candidate'
Assert-Equal $layerFiltered.excluded.Count 2 'disabled layers remain explicit'
Assert-Sequence ($layerFiltered.excluded.layer | Sort-Object) @('community', 'custom') 'excluded rows preserve layer'
$oldSnapshot = $conditional.snapshot
Add-Content -LiteralPath (Join-Path $fixtureRoot 'community\knowledge\neutral\versioned.md') -Value ' ' -Encoding utf8NoBOM
$preparedCatalog = & $search -BCQualityRoot $fixtureRoot -IndexPath $fixtureIndex -Domain neutral |
ConvertFrom-Json
Assert-Equal $preparedCatalog.candidateCount 3 'catalog uses the prepared index without rehashing article bodies'
Assert-Throws {
& $getArticles -BCQualityRoot $fixtureRoot -IndexPath $fixtureIndex `
-Paths 'community/knowledge/neutral/versioned.md'
} 'Selected article hash does not match the prepared index' 'exact retrieval detects selected article changes'
& $generator -BCQualityRoot $fixtureRoot -IndexPath $fixtureIndex | Out-Null
Assert-Throws {
& $search -BCQualityRoot $fixtureRoot -IndexPath $fixtureIndex -Domain neutral -Offset 1 -Snapshot $oldSnapshot
} 'Snapshot changed' 'continuation cannot cross rebuilt snapshots'
$largeRoot = Join-Path $tmp 'large-catalog'
New-NeutralArticle -FixtureRoot $largeRoot -Layer microsoft -Slug huge-title -Title ('T' * 3000)
$largeIndex = Join-Path $tmp 'large-index.json'
& $generator -BCQualityRoot $largeRoot -IndexPath $largeIndex | Out-Null
Assert-Throws {
& $search -BCQualityRoot $largeRoot -IndexPath $largeIndex -Domain neutral -MaxBytes 1024
} 'One complete candidates row|Page envelope exceeds' 'oversized catalog row fails without clipping'
$bodyRoot = Join-Path $tmp 'body-failures'
New-NeutralArticle -FixtureRoot $bodyRoot -Layer microsoft -Slug huge-body -Description ('x' * 3000)
New-NeutralArticle -FixtureRoot $bodyRoot -Layer microsoft -Slug broken-link
New-NeutralArticle -FixtureRoot $bodyRoot -Layer microsoft -Slug continuation-one -Description ('a' * 300)
New-NeutralArticle -FixtureRoot $bodyRoot -Layer microsoft -Slug continuation-two -Description ('b' * 300)
New-NeutralArticle -FixtureRoot $bodyRoot -Layer microsoft -Slug invalid-utf8
New-NeutralArticle -FixtureRoot $bodyRoot -Layer microsoft -Slug sample-one
New-NeutralArticle -FixtureRoot $bodyRoot -Layer microsoft -Slug sample-two
Add-Content -LiteralPath (Join-Path $bodyRoot 'microsoft\knowledge\neutral\sample-one.md') `
-Value '[`sample-one.good.al`](sample-one.good.al)' -Encoding utf8NoBOM
Add-Content -LiteralPath (Join-Path $bodyRoot 'microsoft\knowledge\neutral\sample-two.md') `
-Value '[`sample-two.good.al`](sample-two.good.al)' -Encoding utf8NoBOM
Set-Content -LiteralPath (Join-Path $bodyRoot 'microsoft\knowledge\neutral\sample-one.good.al') `
-Value ('a' * 900) -Encoding utf8NoBOM
Set-Content -LiteralPath (Join-Path $bodyRoot 'microsoft\knowledge\neutral\sample-two.good.al') `
-Value ('b' * 900) -Encoding utf8NoBOM
$bodyIndex = Join-Path $tmp 'body-index.json'
& $generator -BCQualityRoot $bodyRoot -IndexPath $bodyIndex | Out-Null
Assert-Throws {
& $getArticles -BCQualityRoot $bodyRoot -IndexPath $bodyIndex `
-Paths 'microsoft/knowledge/neutral/huge-body.md' -MaxBytes 1024
} 'No complete body plus continuation fits' 'oversized body fails without truncation'
Assert-Throws {
& $getArticles -BCQualityRoot $bodyRoot -IndexPath $bodyIndex -Paths '../outside.md'
} 'Invalid knowledge path' 'unsafe requested path fails'
Assert-Throws {
& $getArticles -BCQualityRoot $bodyRoot -IndexPath $bodyIndex `
-Paths 'microsoft/knowledge/neutral/huge-body.md' -EnabledLayers community
} 'Layer disabled' 'disabled article layer fails'
Assert-Throws {
& $getArticles -BCQualityRoot $bodyRoot -IndexPath $bodyIndex -Paths @(
'microsoft/knowledge/neutral/huge-body.md',
'microsoft/knowledge/neutral/huge-body.md'
)
} 'Duplicate requested path' 'duplicate exact paths fail'
$brokenSample = Join-Path $bodyRoot 'microsoft\knowledge\neutral\broken-link.good.al'
Set-Content -LiteralPath $brokenSample -Value 'codeunit 1 Neutral { }' -Encoding utf8NoBOM
Assert-Throws {
& $getArticles -BCQualityRoot $bodyRoot -IndexPath $bodyIndex `
-Paths 'microsoft/knowledge/neutral/broken-link.good.al' -Samples
} 'Sample is not linked' 'unlinked sample fails'
$sampleContinuationPaths = @(
'microsoft/knowledge/neutral/sample-one.good.al',
'microsoft/knowledge/neutral/sample-two.good.al'
)
$firstSamplePage = & $getArticles -BCQualityRoot $bodyRoot -IndexPath $bodyIndex `
-Paths $sampleContinuationPaths -Samples -MaxBytes 1600 |
ConvertFrom-Json
Assert-True (-not $firstSamplePage.complete) 'bounded sample batch produces continuation'
Assert-Sequence $firstSamplePage.remainingPaths @('microsoft/knowledge/neutral/sample-two.good.al') 'sample continuation preserves pending path'
Add-Content -LiteralPath (Join-Path $bodyRoot 'microsoft\knowledge\neutral\sample-two.good.al') `
-Value 'changed' -Encoding utf8NoBOM
Assert-Throws {
& $getArticles -BCQualityRoot $bodyRoot -IndexPath $bodyIndex `
-Paths @($firstSamplePage.remainingPaths) -Samples `
-Snapshot $firstSamplePage.continuation.snapshot
} 'Article snapshot changed' 'sample continuation rejects a changed pending sample'
$continuationPaths = @(
'microsoft/knowledge/neutral/continuation-one.md',
'microsoft/knowledge/neutral/continuation-two.md'
)
$firstBodyPage = & $getArticles -BCQualityRoot $bodyRoot -IndexPath $bodyIndex `
-Paths $continuationPaths -MaxBytes 1300 |
ConvertFrom-Json
Assert-True (-not $firstBodyPage.complete) 'bounded article batch produces continuation'
Assert-Throws {
& $getArticles -BCQualityRoot $bodyRoot -IndexPath $bodyIndex `
-Paths @($firstBodyPage.remainingPaths) -Snapshot ('0' * 64)
} 'Article snapshot changed' 'wrong article continuation snapshot fails'
Add-Content -LiteralPath (Join-Path $bodyRoot 'microsoft\knowledge\neutral\continuation-two.md') -Value 'changed' -Encoding utf8NoBOM
Assert-Throws {
& $getArticles -BCQualityRoot $bodyRoot -IndexPath $bodyIndex `
-Paths @($firstBodyPage.remainingPaths) -Snapshot $firstBodyPage.continuation.snapshot
} 'Selected article hash does not match the prepared index' 'article continuation rejects a changed remaining body'
$invalidUtf8 = Join-Path $bodyRoot 'microsoft\knowledge\neutral\invalid-utf8.md'
$indexedBytes = [IO.File]::ReadAllBytes($invalidUtf8)
[IO.File]::WriteAllBytes($invalidUtf8, [byte[]]@($indexedBytes + @(0xc3, 0x28)))
Assert-Throws {
& $getArticles -BCQualityRoot $bodyRoot -IndexPath $bodyIndex `
-Paths 'microsoft/knowledge/neutral/invalid-utf8.md'
} 'Knowledge file is not valid strict UTF-8' 'invalid UTF-8 fails'
Write-Host "Knowledge retrieval check PASSED: $($articlePaths.Count) articles and $($samplePaths.Count) samples round-tripped; catalog union was lossless and bounded." -ForegroundColor Green
}
finally {
Remove-Item -LiteralPath $tmp -Recurse -Force -ErrorAction SilentlyContinue
}