Add AL implementation guidance skill

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Jesper Schulz-Wedde 2026-09-16 13:20:57 +02:00
parent d608d89cf0
commit 1cb2b32afe
16 changed files with 1120 additions and 31 deletions

View file

@ -0,0 +1,206 @@
<#
.SYNOPSIS
Deterministic, offline regressions for implementation-guidance fixtures.
.DESCRIPTION
Exercises the shared guidance scorer with controlled reports and standalone
synthetic AL repositories. No model, compiler, deployment, or network access
is used. The uniquely named regression directory is removed in finally.
#>
[CmdletBinding()]
param()
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
. (Join-Path $PSScriptRoot 'DevelopmentGuidance.Evidence.ps1')
$sourceRoot = (Get-Item -LiteralPath (Join-Path $PSScriptRoot '..')).FullName
$evaluator = Join-Path $PSScriptRoot 'Test-DevelopmentGuidanceFixtures.ps1'
$sourceManifestPath = Join-Path $sourceRoot 'evaluation\implementation-guidance-fixtures.json'
$scratch = Join-Path $sourceRoot ".implementation-guidance-regression-$([guid]::NewGuid().ToString('N'))"
$knowledgeRoot = Join-Path $scratch 'knowledge-checkout'
$runnerRoot = Join-Path $scratch 'runner'
$results = Join-Path $runnerRoot 'results'
$mapPath = Join-Path $runnerRoot 'workspace-map.json'
$baselinePath = Join-Path $runnerRoot 'baseline.json'
$manifestPath = Join-Path $knowledgeRoot 'evaluation\implementation-guidance-fixtures.json'
$tests = [Collections.Generic.List[string]]::new()
function Set-TestJson([string] $Path, $Value) {
[IO.Directory]::CreateDirectory((Split-Path $Path -Parent)) | Out-Null
[IO.File]::WriteAllText($Path, ($Value | ConvertTo-Json -Depth 64))
}
function Invoke-TestGit([string] $Directory, [string[]] $Arguments) {
$null = @(& git --no-optional-locks -C $Directory @Arguments 2>&1)
if ($LASTEXITCODE -ne 0) { throw 'Regression Git setup failed.' }
}
function Initialize-TestRepository([string] $Directory, [string] $CaseId) {
[IO.Directory]::CreateDirectory($Directory) | Out-Null
Invoke-TestGit $Directory @('init', '--quiet')
Invoke-TestGit $Directory @('config', 'user.email', 'implementation-guidance@example.invalid')
Invoke-TestGit $Directory @('config', 'user.name', 'Implementation guidance fixture')
Invoke-TestGit $Directory @('config', 'commit.gpgSign', 'false')
Invoke-TestGit $Directory @('config', 'core.autocrlf', 'false')
[IO.File]::WriteAllText((Join-Path $Directory 'app.json'), '{"name":"Synthetic implementation fixture","application":"28.0.0.0"}')
[IO.File]::WriteAllText((Join-Path $Directory 'CurrentImplementation.al'), "codeunit 71000 `"$CaseId`" { }")
Invoke-TestGit $Directory @('add', '.')
Invoke-TestGit $Directory @('commit', '--quiet', '-m', 'Synthetic implementation baseline')
}
function Invoke-Evaluator([string[]] $Arguments, [bool] $ShouldPass, [string] $Name, [string] $Diagnostic = '') {
$output = @(& pwsh -NoProfile -File $evaluator @Arguments 2>&1) -join "`n"
if (($LASTEXITCODE -eq 0) -ne $ShouldPass -or
$output -notmatch $(if ($ShouldPass) { 'PASSED|captured' } else { 'FAILED' })) {
throw "Regression '$Name' failed unexpectedly. $output"
}
if ($Diagnostic -and $output -notmatch [regex]::Escape($Diagnostic)) {
throw "Regression '$Name' missed diagnostic '$Diagnostic'. $output"
}
$tests.Add($Name)
}
function New-ControlledReport($Case, [string] $KnowledgeSha) {
$notApplicable = $Case.expectedOutcome -eq 'not-applicable'
$decision = if ($notApplicable) {
[ordered]@{
phase = 'unavailable'; decision = 'Current implementation decision was not supplied.'
'decision-key' = 'unavailable'; 'evidence-fingerprint' = 'unavailable'
'affected-files' = @(); 'affected-symbols' = @(); 'changed-tokens' = @()
'development-plan-pin' = 'unpinned'; 'implementation-evidence-pin' = 'unpinned'
}
} else { $Case.'decision-context' }
$knowledge = @(
foreach ($path in $Case.requiredKnowledge) {
[ordered]@{
path = $path
sha = $KnowledgeSha
'used-for' = $decision.decision
constraints = @('Apply the opened article normative constraint to this current decision.')
'sample-paths' = @()
}
}
)
$omitted = @(
foreach ($path in $Case.expectedOmitted) {
$consumed = @($Case.'consumed-guidance' | Where-Object path -CEQ $path)[0]
[ordered]@{
path = $path
'decision-key' = $consumed.'decision-key'
'evidence-fingerprint' = $consumed.'evidence-fingerprint'
'prior-decision' = $consumed.'prior-decision'
}
}
)
return [ordered]@{
skill = [ordered]@{ id = 'al-implementation-guidance'; version = 1 }
outcome = $Case.expectedOutcome
summary = [ordered]@{
request = Get-GuidancePlanRequest $Case.'development-plan'
phase = $decision.phase
decision = $decision.decision
'decision-key' = $decision.'decision-key'
'evidence-fingerprint' = $decision.'evidence-fingerprint'
candidates = $knowledge.Count + $omitted.Count
selected = $knowledge.Count
'omitted-consumed' = $omitted.Count
}
pins = [ordered]@{
'knowledge-checkout' = $KnowledgeSha
'development-plan' = $decision.'development-plan-pin'
'implementation-evidence' = $decision.'implementation-evidence-pin'
}
context = [ordered]@{
'bc-version' = $Case.context.'bc-version'
technologies = @($Case.context.technologies)
countries = @($Case.context.countries)
'application-area' = @($Case.context.'application-area')
'affected-files' = @($decision.'affected-files')
'affected-symbols' = @($decision.'affected-symbols')
'changed-tokens' = @($decision.'changed-tokens')
unknown = @($Case.context.unknown)
}
knowledge = $knowledge
'validation-considerations' = @()
deduplication = [ordered]@{
strategy = 'omit-exact-consumed-match'
omitted = $omitted
}
suppressed = @()
unresolved = @()
}
}
try {
$manifest = Read-GuidanceJson $sourceManifestPath
$references = @($manifest.skill, 'tools/Build-KnowledgeIndex.ps1',
'evaluation/implementation-guidance-fixtures.json') +
@($manifest.cases | ForEach-Object {
$_.requiredKnowledge
$_.optionalKnowledge
$_.expectedOmitted
})
foreach ($reference in @($references | Sort-Object -Unique)) {
$source = Join-Path $sourceRoot $reference.Replace('/', [IO.Path]::DirectorySeparatorChar)
$destination = Join-Path $knowledgeRoot $reference.Replace('/', [IO.Path]::DirectorySeparatorChar)
[IO.Directory]::CreateDirectory((Split-Path $destination -Parent)) | Out-Null
[IO.File]::Copy($source, $destination)
}
Initialize-TestRepository $knowledgeRoot 'Knowledge Checkout'
[IO.Directory]::CreateDirectory($results) | Out-Null
$workspaces = [ordered]@{}
foreach ($case in $manifest.cases) {
$workspace = Join-Path $scratch "target-$($case.id)"
Initialize-TestRepository $workspace $case.id
$workspaces[$case.id] = $workspace
}
Set-TestJson $mapPath $workspaces
Invoke-Evaluator @('-Root', $knowledgeRoot, '-ManifestPath', $manifestPath) $true 'public implementation manifest validates'
$prepared = Join-Path $runnerRoot 'prepared'
Invoke-Evaluator @('-Root', $knowledgeRoot, '-ManifestPath', $manifestPath,
'-PrepareDirectory', $prepared, '-WorkspaceMapPath', $mapPath) $true 'all implementation requests prepare'
$missingCase = @($manifest.cases | Where-Object id -CEQ 'missing-current-decision-and-diff')[0]
$missingRequest = Read-GuidanceJson (Join-Path $prepared "request-$(Get-GuidanceCaseId $missingCase.id).json")
if ($missingRequest.'implementation-diff' -cne '' -or $missingRequest.'decision-context'.Count) {
throw 'Preparation invented missing current implementation context.'
}
$tests.Add('missing context remains missing during preparation')
Invoke-Evaluator @('-Root', $knowledgeRoot, '-ManifestPath', $manifestPath,
'-CaptureBaseline', '-WorkspaceMapPath', $mapPath, '-BaselinePath', $baselinePath) $true 'read-only baselines capture'
$knowledgeSha = Invoke-GuidanceGit $knowledgeRoot @('rev-parse', 'HEAD')
foreach ($case in $manifest.cases) {
Set-TestJson (Join-Path $results "result-$(Get-GuidanceCaseId $case.id).json") ([ordered]@{
caseId = Get-GuidanceCaseId $case.id
guidanceReport = New-ControlledReport $case $knowledgeSha
})
}
$scoreArgs = @('-Root', $knowledgeRoot, '-ManifestPath', $manifestPath,
'-ResultsDirectory', $results, '-BaselinePath', $baselinePath,
'-BaselineSha256', (Get-GuidanceHash $baselinePath))
Invoke-Evaluator $scoreArgs $true 'focused, deduplicated, clean, missing, and boundary reports score'
$consumedCase = @($manifest.cases | Where-Object id -CEQ 'exact-consumed-guidance-is-omitted')[0]
$consumedPath = Join-Path $results "result-$(Get-GuidanceCaseId $consumedCase.id).json"
$consumedResult = Read-GuidanceJson $consumedPath
$consumedResult.guidanceReport.deduplication.omitted = @()
$consumedResult.guidanceReport.summary.'omitted-consumed' = 0
Set-TestJson $consumedPath $consumedResult
Invoke-Evaluator $scoreArgs $false 'exact consumed guidance cannot disappear from deduplication' 'omitted set'
Set-TestJson $consumedPath ([ordered]@{
caseId = Get-GuidanceCaseId $consumedCase.id
guidanceReport = New-ControlledReport $consumedCase $knowledgeSha
})
[IO.File]::AppendAllText((Join-Path $workspaces['read-only-public-interface-checkpoint'] 'CurrentImplementation.al'), "`n// forbidden mutation")
Invoke-Evaluator $scoreArgs $false 'target repository mutation is detected' 'identity/content changed'
Write-Host "Implementation guidance evaluator regressions PASSED: $($tests.Count) checks."
} finally {
if (Test-Path -LiteralPath $scratch) {
Remove-Item -LiteralPath $scratch -Recurse -Force
}
}
exit 0