diff --git a/Directory.Packages.props b/Directory.Packages.props
index ffefe7a..5a149f3 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -12,7 +12,7 @@
-
+
diff --git a/README.md b/README.md
index 986bfa7..a50ea4b 100644
--- a/README.md
+++ b/README.md
@@ -366,7 +366,7 @@ run once from its Git root:
```powershell
dotnet new tool-manifest --output .config
-dotnet tool install Trellis.AgentDocs --version 0.1.0-preview.14 --tool-manifest .config/dotnet-tools.json
+dotnet tool install Trellis.AgentDocs --version 0.1.0-preview.17 --tool-manifest .config/dotnet-tools.json
dotnet tool run agentdocs init
```
diff --git a/Trellis.ServiceLevelIndicators.Asp.ApiVersioning/src/README.md b/Trellis.ServiceLevelIndicators.Asp.ApiVersioning/src/README.md
index 7436148..b35ebe3 100644
--- a/Trellis.ServiceLevelIndicators.Asp.ApiVersioning/src/README.md
+++ b/Trellis.ServiceLevelIndicators.Asp.ApiVersioning/src/README.md
@@ -55,7 +55,7 @@ run from its Git root:
```powershell
dotnet new tool-manifest --output .config
-dotnet tool install Trellis.AgentDocs --version 0.1.0-preview.14 --tool-manifest .config/dotnet-tools.json
+dotnet tool install Trellis.AgentDocs --version 0.1.0-preview.17 --tool-manifest .config/dotnet-tools.json
dotnet tool run agentdocs init
```
diff --git a/Trellis.ServiceLevelIndicators.Asp/src/README.md b/Trellis.ServiceLevelIndicators.Asp/src/README.md
index 14231b7..c9651f8 100644
--- a/Trellis.ServiceLevelIndicators.Asp/src/README.md
+++ b/Trellis.ServiceLevelIndicators.Asp/src/README.md
@@ -212,7 +212,7 @@ run from its Git root:
```powershell
dotnet new tool-manifest --output .config
-dotnet tool install Trellis.AgentDocs --version 0.1.0-preview.14 --tool-manifest .config/dotnet-tools.json
+dotnet tool install Trellis.AgentDocs --version 0.1.0-preview.17 --tool-manifest .config/dotnet-tools.json
dotnet tool run agentdocs init
```
diff --git a/Trellis.ServiceLevelIndicators/src/README.md b/Trellis.ServiceLevelIndicators/src/README.md
index 33f7585..6af28af 100644
--- a/Trellis.ServiceLevelIndicators/src/README.md
+++ b/Trellis.ServiceLevelIndicators/src/README.md
@@ -125,7 +125,7 @@ run from its Git root:
```powershell
dotnet new tool-manifest --output .config
-dotnet tool install Trellis.AgentDocs --version 0.1.0-preview.14 --tool-manifest .config/dotnet-tools.json
+dotnet tool install Trellis.AgentDocs --version 0.1.0-preview.17 --tool-manifest .config/dotnet-tools.json
dotnet tool run agentdocs init
```
diff --git a/build/test-guidance-packages.ps1 b/build/test-guidance-packages.ps1
index e699f83..65bc96e 100644
--- a/build/test-guidance-packages.ps1
+++ b/build/test-guidance-packages.ps1
@@ -10,86 +10,100 @@ $references = [ordered]@{
'Trellis.ServiceLevelIndicators.Asp.ApiVersioning' = 'trellis-api-sli-apiversioning.md'
}
-foreach ($id in $references.Keys) {
- $packages = @(Get-ChildItem -LiteralPath $PackagesDirectory -Filter '*.nupkg' |
- Where-Object { $_.Name -match "^$([regex]::Escape($id))\.\d" -and $_.Name -notlike '*.symbols.nupkg' })
- if ($packages.Count -ne 1) {
- throw "Expected one packed $id in $PackagesDirectory, found $($packages.Count)."
- }
+# The published validator checks the contract and discoverability (links that leave the package, front matter,
+# size budgets); --strict makes a warning fail like an error. It is pinned to the packaging helper's version, which
+# is published in lockstep with the tool.
+$props = [xml] (Get-Content -LiteralPath (Join-Path (Join-Path $PSScriptRoot '..') 'Directory.Packages.props') -Raw)
+$toolVersion = $props.SelectSingleNode('//PackageVersion[@Include="Trellis.AgentDocs.Packaging"]').Version
+$toolDirectory = Join-Path ([System.IO.Path]::GetTempPath()) "agentdocs-validate-$([guid]::NewGuid().ToString('N'))"
+$install = & dotnet tool install Trellis.AgentDocs --version $toolVersion --tool-path $toolDirectory 2>&1
+if ($LASTEXITCODE -ne 0) { throw "Could not install Trellis.AgentDocs $toolVersion for validation:`n$($install | Out-String)" }
- $archive = [System.IO.Compression.ZipFile]::OpenRead($packages[0].FullName)
- try {
- $manifestEntry = $archive.GetEntry('guidance/reference-manifest.json')
- $referencePath = $references[$id]
- $referenceEntry = $archive.GetEntry($referencePath)
- if (-not $manifestEntry -or -not $referenceEntry) {
- throw "$id must pack its guidance manifest and $referencePath."
- }
- if (@($archive.Entries | Where-Object {
- $_.FullName.StartsWith('trellis/', [StringComparison]::OrdinalIgnoreCase)
- }).Count -ne 0) {
- throw "$id must not pack a trellis/ directory."
+try {
+ foreach ($id in $references.Keys) {
+ $packages = @(Get-ChildItem -LiteralPath $PackagesDirectory -Filter '*.nupkg' |
+ Where-Object { $_.Name -match "^$([regex]::Escape($id))\.\d" -and $_.Name -notlike '*.symbols.nupkg' })
+ if ($packages.Count -ne 1) {
+ throw "Expected one packed $id in $PackagesDirectory, found $($packages.Count)."
}
- $reader = [System.IO.StreamReader]::new($manifestEntry.Open())
- try { $manifest = $reader.ReadToEnd() | ConvertFrom-Json }
- finally { $reader.Dispose() }
- $referenceBytes = [System.IO.MemoryStream]::new()
+ $archive = [System.IO.Compression.ZipFile]::OpenRead($packages[0].FullName)
try {
- $referenceEntry.Open().CopyTo($referenceBytes)
- $hash = [Convert]::ToHexString(
- [System.Security.Cryptography.SHA256]::HashData($referenceBytes.ToArray())).ToLowerInvariant()
- $referenceText = [System.Text.Encoding]::UTF8.GetString($referenceBytes.ToArray())
- }
- finally { $referenceBytes.Dispose() }
+ $manifestEntry = $archive.GetEntry('guidance/reference-manifest.json')
+ $referencePath = $references[$id]
+ $referenceEntry = $archive.GetEntry($referencePath)
+ if (-not $manifestEntry -or -not $referenceEntry) {
+ throw "$id must pack its guidance manifest and $referencePath."
+ }
+ if (@($archive.Entries | Where-Object {
+ $_.FullName.StartsWith('trellis/', [StringComparison]::OrdinalIgnoreCase)
+ }).Count -ne 0) {
+ throw "$id must not pack a trellis/ directory."
+ }
- $description = [string] $manifest.documents[0].description
- if ($manifest.schemaVersion -ne 1 -or @($manifest.documents).Count -ne 1 -or
- $manifest.documents[0].path -ne $referencePath -or
- $manifest.documents[0].sha256 -ne $hash -or
- $manifest.documents[0].usage -ne 'onDemand' -or
- $description.Length -eq 0 -or $description.Length -gt 200 -or $description -notmatch '^Open when ' -or
- $null -ne $manifest.PSObject.Properties['entryPoints']) {
- throw "$id manifest does not match the packed reference bytes, on-demand usage, and description."
- }
- # Each package installs into its own directory, so a relative link to a sibling package's reference breaks.
- # Any non-absolute link to a trellis-*.md file counts, whatever its prefix (./, ../, subfolders) or anchor,
- # in inline form [text](target) or reference form [id]: target.
- if ($referenceText -match '\]\(\s*(?!https?:|mailto:|#)[^)\s]*trellis-[a-z0-9-]+\.md' -or
- $referenceText -match '(?m)^\s*\[[^\]]+\]:\s*(?!https?:|mailto:|#)\S*trellis-[a-z0-9-]+\.md') {
- throw "$id links to another package's reference file; refer to other packages by ID instead."
- }
+ $reader = [System.IO.StreamReader]::new($manifestEntry.Open())
+ try { $manifest = $reader.ReadToEnd() | ConvertFrom-Json }
+ finally { $reader.Dispose() }
+ $referenceBytes = [System.IO.MemoryStream]::new()
+ try {
+ $referenceEntry.Open().CopyTo($referenceBytes)
+ $hash = [Convert]::ToHexString(
+ [System.Security.Cryptography.SHA256]::HashData($referenceBytes.ToArray())).ToLowerInvariant()
+ $referenceText = [System.Text.Encoding]::UTF8.GetString($referenceBytes.ToArray())
+ }
+ finally { $referenceBytes.Dispose() }
- if (@($archive.Entries | Where-Object {
- $_.FullName -match '^(build|buildTransitive)/'
- }).Count -ne 0) {
- throw "$id still ships a consumer build target."
- }
- $nuspec = $archive.GetEntry("$id.nuspec")
- if (-not $nuspec) { throw "$id is missing its package metadata." }
- $metadataReader = [System.IO.StreamReader]::new($nuspec.Open())
- try { [xml] $metadata = $metadataReader.ReadToEnd() }
- finally { $metadataReader.Dispose() }
- if ($metadata.SelectSingleNode('//*[local-name()="dependency" and (@id="Trellis.AgentDocs.Packaging" or @id="Trellis.Core")]')) {
- throw "$id leaks a publisher-only dependency or introduces a Core dependency."
- }
- if ($metadata.SelectSingleNode('//*[local-name()="readme"]')?.InnerText -ne 'README.md') {
- throw "$id must declare its packed README.md as the NuGet readme."
- }
- $readmeEntry = $archive.GetEntry('README.md')
- if (-not $readmeEntry) { throw "$id is missing its NuGet readme." }
- $readmeReader = [System.IO.StreamReader]::new($readmeEntry.Open())
- try { $readme = $readmeReader.ReadToEnd() }
- finally { $readmeReader.Dispose() }
- if ($readme -notmatch '(?m)^dotnet tool install Trellis\.AgentDocs --version \S+ --tool-manifest \.config/dotnet-tools\.json\r?$' -or
- -not $readme.Contains('dotnet tool run agentdocs init ')) {
- throw "$id NuGet readme must explain how to install and initialize AgentDocs."
- }
- # Restoring a package never activates its guide: the readme must describe approval and sync.
- if (-not $readme.Contains('approvedPackages') -or -not $readme.Contains('dotnet tool run agentdocs sync')) {
- throw "$id NuGet readme must explain approving the package and running sync."
+ $description = [string] $manifest.documents[0].description
+ if ($manifest.schemaVersion -ne 1 -or @($manifest.documents).Count -ne 1 -or
+ $manifest.documents[0].path -ne $referencePath -or
+ $manifest.documents[0].sha256 -ne $hash -or
+ $manifest.documents[0].usage -ne 'onDemand' -or
+ $description.Length -eq 0 -or $description.Length -gt 200 -or $description -notmatch '^Open when ' -or
+ $null -ne $manifest.PSObject.Properties['entryPoints']) {
+ throw "$id manifest does not match the packed reference bytes, on-demand usage, and description."
+ }
+ # Each package installs into its own directory, so a relative link to a sibling package's reference breaks.
+ # Any non-absolute link to a trellis-*.md file counts, whatever its prefix (./, ../, subfolders) or anchor,
+ # in inline form [text](target) or reference form [id]: target.
+ if ($referenceText -match '\]\(\s*(?!https?:|mailto:|#)[^)\s]*trellis-[a-z0-9-]+\.md' -or
+ $referenceText -match '(?m)^\s*\[[^\]]+\]:\s*(?!https?:|mailto:|#)\S*trellis-[a-z0-9-]+\.md') {
+ throw "$id links to another package's reference file; refer to other packages by ID instead."
+ }
+
+ if (@($archive.Entries | Where-Object {
+ $_.FullName -match '^(build|buildTransitive)/'
+ }).Count -ne 0) {
+ throw "$id still ships a consumer build target."
+ }
+ $nuspec = $archive.GetEntry("$id.nuspec")
+ if (-not $nuspec) { throw "$id is missing its package metadata." }
+ $metadataReader = [System.IO.StreamReader]::new($nuspec.Open())
+ try { [xml] $metadata = $metadataReader.ReadToEnd() }
+ finally { $metadataReader.Dispose() }
+ if ($metadata.SelectSingleNode('//*[local-name()="dependency" and (@id="Trellis.AgentDocs.Packaging" or @id="Trellis.Core")]')) {
+ throw "$id leaks a publisher-only dependency or introduces a Core dependency."
+ }
+ if ($metadata.SelectSingleNode('//*[local-name()="readme"]')?.InnerText -ne 'README.md') {
+ throw "$id must declare its packed README.md as the NuGet readme."
+ }
+ $readmeEntry = $archive.GetEntry('README.md')
+ if (-not $readmeEntry) { throw "$id is missing its NuGet readme." }
+ $readmeReader = [System.IO.StreamReader]::new($readmeEntry.Open())
+ try { $readme = $readmeReader.ReadToEnd() }
+ finally { $readmeReader.Dispose() }
+ if ($readme -notmatch '(?m)^dotnet tool install Trellis\.AgentDocs --version \S+ --tool-manifest \.config/dotnet-tools\.json\r?$' -or
+ -not $readme.Contains('dotnet tool run agentdocs init ')) {
+ throw "$id NuGet readme must explain how to install and initialize AgentDocs."
+ }
+ # Restoring a package never activates its guide: the readme must describe approval and sync.
+ if (-not $readme.Contains('approvedPackages') -or -not $readme.Contains('dotnet tool run agentdocs sync')) {
+ throw "$id NuGet readme must explain approving the package and running sync."
+ }
+ $validation = & (Join-Path $toolDirectory 'agentdocs') validate $packages[0].FullName --strict 2>&1
+ if ($LASTEXITCODE -ne 0) { throw "agentdocs validate --strict rejected ${id}:`n$($validation | Out-String)" }
+ Write-Host "PASS $($packages[0].Name) ships validated guidance without consumer targets"
}
- Write-Host "PASS $($packages[0].Name) ships validated guidance without consumer targets"
+ finally { $archive.Dispose() }
}
- finally { $archive.Dispose() }
}
+finally { Remove-Item -LiteralPath $toolDirectory -Recurse -Force -ErrorAction SilentlyContinue }