Public/Test-OntologyCompliance.ps1
|
# Copyright (c) 2026 Jeffrey Snover. All rights reserved. # Licensed under the MIT License. See LICENSE file in the project root. function Test-OntologyCompliance { <# .SYNOPSIS Tests taxonomy data for ontological compliance (DOLCE, BDI, AIF). .DESCRIPTION Runs structured checks against the taxonomy, summaries, edges, conflicts, and debates to verify compliance with the project's ontological framework: - Schema validation against JSON schemas - Referential integrity (edges, situation_refs, conflict_ids) - DOLCE checks (genus-differentia descriptions, D&S roles) - BDI checks (category/bdi_layer alignment) - AIF checks (canonical edge types, node_scope population) Each check emits pass/fail with actionable fix instructions. .PARAMETER RepoRoot Repository root path. Defaults to module-resolved root. .PARAMETER PassThru Return the results object for piping. .PARAMETER Quiet Only show failures and warnings. .EXAMPLE Test-OntologyCompliance .EXAMPLE $r = Test-OntologyCompliance -PassThru; $r.Failures #> [CmdletBinding()] param( [string]$RepoRoot = $script:RepoRoot, [switch]$PassThru, [switch]$Quiet ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $TaxDir = Get-TaxonomyDir $SummariesDir = Get-SummariesDir $ConflictsDir = Get-ConflictsDir $DebatesDir = Get-DebatesDir $Results = [System.Collections.Generic.List[PSObject]]::new() $Passed = 0; $Failed = 0; $Warned = 0 function Add-Check { param( [string]$Category, [string]$Check, [ValidateSet('pass','fail','warn')] [string]$Status, [string]$Detail, [string[]]$Fix = @() ) $Results.Add([PSCustomObject][ordered]@{ Category = $Category Check = $Check Status = $Status Detail = $Detail Fix = $Fix -join '; ' }) switch ($Status) { 'pass' { Set-Variable -Name Passed -Value ($Passed + 1) -Scope 1; if (-not $Quiet) { Write-Host " PASS $Category / $Check" -ForegroundColor Green } } 'fail' { Set-Variable -Name Failed -Value ($Failed + 1) -Scope 1; Write-Host " FAIL $Category / $Check — $Detail" -ForegroundColor Red; if ($Fix) { foreach ($F in $Fix) { Write-Host " Fix: $F" -ForegroundColor Yellow } } } 'warn' { Set-Variable -Name Warned -Value ($Warned + 1) -Scope 1; Write-Host " WARN $Category / $Check — $Detail" -ForegroundColor Yellow } } } Write-Host "`n══════════════════════════════════════════════════════════════" -ForegroundColor Cyan Write-Host " ONTOLOGY COMPLIANCE AUDIT" -ForegroundColor White Write-Host "══════════════════════════════════════════════════════════════`n" -ForegroundColor Cyan # ── Load data ───────────────────────────────────────────────────────────── $AllNodes = @{} $PovFiles = @{ accelerationist = 'accelerationist.json' safetyist = 'safetyist.json' skeptic = 'skeptic.json' situations = 'situations.json' } foreach ($PovKey in $PovFiles.Keys) { $FilePath = Join-Path $TaxDir $PovFiles[$PovKey] if (Test-Path $FilePath) { try { $Data = Get-Content -Raw -Path $FilePath | ConvertFrom-Json foreach ($Node in $Data.nodes) { $AllNodes[$Node.id] = @{ Node = $Node; POV = $PovKey } } } catch { Add-Check -Category 'Schema' -Check "Parse $($PovFiles[$PovKey])" -Status 'fail' ` -Detail "JSON parse failed: $($_.Exception.Message)" ` -Fix "Fix JSON syntax in $($PovFiles[$PovKey])" } } } $EdgesPath = Join-Path $TaxDir 'edges.json' $AllEdges = @() if (Test-Path $EdgesPath) { try { $EdgesData = Get-Content -Raw -Path $EdgesPath | ConvertFrom-Json $AllEdges = @($EdgesData.edges) } catch { Add-Check -Category 'Schema' -Check 'Parse edges.json' -Status 'fail' ` -Detail "JSON parse failed: $($_.Exception.Message)" } } # ══════════════════════════════════════════════════════════════════════════ # 1. SCHEMA VALIDATION # ══════════════════════════════════════════════════════════════════════════ Write-Host "── Schema ──" -ForegroundColor Cyan # Check all taxonomy files parse foreach ($PovKey in $PovFiles.Keys) { $FilePath = Join-Path $TaxDir $PovFiles[$PovKey] if (Test-Path $FilePath) { Add-Check -Category 'Schema' -Check "Parse $($PovFiles[$PovKey])" -Status 'pass' -Detail 'OK' } else { Add-Check -Category 'Schema' -Check "Exists $($PovFiles[$PovKey])" -Status 'fail' ` -Detail "File not found" -Fix "Ensure $($PovFiles[$PovKey]) exists in taxonomy/Origin/" } } # Check required fields on all nodes $MissingId = 0; $MissingLabel = 0; $MissingDesc = 0 foreach ($Entry in $AllNodes.Values) { $N = $Entry.Node if (-not $N.PSObject.Properties['id'] -or -not $N.id) { $MissingId++ } if (-not $N.PSObject.Properties['label'] -or -not $N.label) { $MissingLabel++ } if (-not $N.PSObject.Properties['description'] -or -not $N.description) { $MissingDesc++ } } if ($MissingId -eq 0 -and $MissingLabel -eq 0 -and $MissingDesc -eq 0) { Add-Check -Category 'Schema' -Check 'Required fields (id, label, description)' -Status 'pass' -Detail "All $($AllNodes.Count) nodes OK" } else { Add-Check -Category 'Schema' -Check 'Required fields' -Status 'fail' ` -Detail "Missing: id=$MissingId, label=$MissingLabel, description=$MissingDesc" ` -Fix 'Run: grep for nodes with null id/label/description fields' } # ══════════════════════════════════════════════════════════════════════════ # 2. REFERENTIAL INTEGRITY # ══════════════════════════════════════════════════════════════════════════ Write-Host "── Referential Integrity ──" -ForegroundColor Cyan # Edge references valid nodes $OrphanEdges = 0 $OrphanEdgeDetails = [System.Collections.Generic.List[string]]::new() foreach ($Edge in $AllEdges) { $SrcOk = $AllNodes.ContainsKey($Edge.source) -or $Edge.source -match '^pol-' $TgtOk = $AllNodes.ContainsKey($Edge.target) -or $Edge.target -match '^pol-' if (-not $SrcOk -or -not $TgtOk) { $OrphanEdges++ if ($OrphanEdgeDetails.Count -lt 5) { $OrphanEdgeDetails.Add("$($Edge.source) → $($Edge.target)") } } } if ($OrphanEdges -eq 0) { Add-Check -Category 'Integrity' -Check 'Edge node references' -Status 'pass' -Detail "All $($AllEdges.Count) edges valid" } else { Add-Check -Category 'Integrity' -Check 'Edge node references' -Status 'fail' ` -Detail "$OrphanEdges edge(s) reference missing nodes (e.g. $($OrphanEdgeDetails[0]))" ` -Fix 'Run: Get-Edge | Where-Object orphan to find and remove stale edges' } # situation_refs reference valid cc- nodes $BadRefs = 0 foreach ($Entry in $AllNodes.Values) { $N = $Entry.Node $Refs = $null if ($N.PSObject.Properties['situation_refs']) { $Refs = $N.situation_refs } elseif ($N.PSObject.Properties['cross_cutting_refs']) { $Refs = $N.cross_cutting_refs } if ($Refs) { foreach ($Ref in @($Refs)) { if ($Ref -and -not $AllNodes.ContainsKey($Ref)) { $BadRefs++ } } } } if ($BadRefs -eq 0) { Add-Check -Category 'Integrity' -Check 'situation_refs targets' -Status 'pass' -Detail 'All refs valid' } else { Add-Check -Category 'Integrity' -Check 'situation_refs targets' -Status 'fail' ` -Detail "$BadRefs ref(s) point to missing nodes" ` -Fix 'Grep for situation_refs values that are not in situations.json node IDs' } # parent_id references valid nodes in same file $BadParents = 0 foreach ($Entry in $AllNodes.Values) { $N = $Entry.Node if ($N.PSObject.Properties['parent_id'] -and $N.parent_id) { if (-not $AllNodes.ContainsKey($N.parent_id)) { $BadParents++ } } } if ($BadParents -eq 0) { Add-Check -Category 'Integrity' -Check 'parent_id references' -Status 'pass' -Detail 'All valid' } else { Add-Check -Category 'Integrity' -Check 'parent_id references' -Status 'fail' ` -Detail "$BadParents node(s) have parent_id pointing to missing nodes" ` -Fix 'Set orphaned parent_id to $null or correct the reference' } # ══════════════════════════════════════════════════════════════════════════ # 3. DOLCE CHECKS # ══════════════════════════════════════════════════════════════════════════ Write-Host "── DOLCE ──" -ForegroundColor Cyan # Genus-differentia pattern compliance $GenusOk = 0; $GenusFail = 0 $GenusFailIds = [System.Collections.Generic.List[string]]::new() foreach ($Entry in $AllNodes.Values) { $N = $Entry.Node; $Pov = $Entry.POV if (-not $N.description) { $GenusFail++; continue } if ($Pov -eq 'situations') { $IsGenus = $N.description -match '^A\s+situation\s+that\s+' } else { $IsGenus = $N.description -match '^An?\s+(Belief|Desire|Intention)\s+within\s+' } if ($IsGenus) { $GenusOk++ } else { $GenusFail++ if ($GenusFailIds.Count -lt 10) { $GenusFailIds.Add($N.id) } } } $GenusPct = [Math]::Round($GenusOk / [Math]::Max(1, $AllNodes.Count) * 100, 1) if ($GenusPct -ge 90) { Add-Check -Category 'DOLCE' -Check "Genus-differentia descriptions ($GenusPct%)" -Status 'pass' ` -Detail "$GenusOk/$($AllNodes.Count) nodes compliant" } elseif ($GenusPct -ge 50) { Add-Check -Category 'DOLCE' -Check "Genus-differentia descriptions ($GenusPct%)" -Status 'warn' ` -Detail "$GenusFail node(s) non-compliant (e.g. $($GenusFailIds[0]))" ` -Fix 'Run Invoke-AttributeExtraction to regenerate descriptions' } else { Add-Check -Category 'DOLCE' -Check "Genus-differentia descriptions ($GenusPct%)" -Status 'fail' ` -Detail "$GenusFail node(s) non-compliant" ` -Fix @('Run Invoke-AttributeExtraction -Force to regenerate', "First 10: $($GenusFailIds -join ', ')") } # Category field present on POV nodes $PovNoCat = 0 foreach ($Entry in $AllNodes.Values) { if ($Entry.POV -eq 'situations') { continue } $N = $Entry.Node if (-not $N.PSObject.Properties['category'] -or -not $N.category) { $PovNoCat++ } } if ($PovNoCat -eq 0) { Add-Check -Category 'DOLCE' -Check 'Category field on POV nodes' -Status 'pass' -Detail 'All POV nodes have category' } else { Add-Check -Category 'DOLCE' -Check 'Category field on POV nodes' -Status 'fail' ` -Detail "$PovNoCat POV node(s) missing category" ` -Fix 'Add category (Beliefs/Desires/Intentions) to nodes missing it' } # ══════════════════════════════════════════════════════════════════════════ # 4. BDI CHECKS # ══════════════════════════════════════════════════════════════════════════ Write-Host "── BDI ──" -ForegroundColor Cyan # Category values are valid BDI names $ValidCats = @('Beliefs', 'Desires', 'Intentions') $InvalidCats = 0; $LegacyCats = 0 foreach ($Entry in $AllNodes.Values) { if ($Entry.POV -eq 'situations') { continue } $N = $Entry.Node if ($N.PSObject.Properties['category'] -and $N.category) { if ($N.category -in @('Data/Facts', 'Goals/Values', 'Methods/Arguments')) { $LegacyCats++ } elseif ($N.category -notin $ValidCats) { $InvalidCats++ } } } if ($LegacyCats -eq 0 -and $InvalidCats -eq 0) { Add-Check -Category 'BDI' -Check 'Category values (Beliefs/Desires/Intentions)' -Status 'pass' -Detail 'All valid' } else { $Msg = @() if ($LegacyCats -gt 0) { $Msg += "$LegacyCats legacy (pre-BDI migration)" } if ($InvalidCats -gt 0) { $Msg += "$InvalidCats invalid" } Add-Check -Category 'BDI' -Check 'Category values' -Status 'fail' ` -Detail ($Msg -join ', ') ` -Fix 'Run Invoke-BDIMigration.ps1 to migrate remaining legacy values' } # bdi_layer values on debates $BadBdi = 0 $ValidBdiLayers = @('belief', 'desire', 'intention') $LegacyBdiLayers = @('value', 'conceptual') if (Test-Path $DebatesDir) { foreach ($DebFile in Get-ChildItem -Path $DebatesDir -Filter '*.json' -File -ErrorAction SilentlyContinue) { try { $Raw = Get-Content -Raw -Path $DebFile.FullName foreach ($Legacy in $LegacyBdiLayers) { if ($Raw -match """bdi_layer""\s*:\s*""$Legacy""") { $BadBdi++ } } } catch { } } } if ($BadBdi -eq 0) { Add-Check -Category 'BDI' -Check 'bdi_layer values (no legacy)' -Status 'pass' -Detail 'No legacy values found' } else { Add-Check -Category 'BDI' -Check 'bdi_layer values' -Status 'fail' ` -Detail "$BadBdi occurrence(s) of legacy bdi_layer values" ` -Fix 'Run Invoke-BDIMigration.ps1 to fix value→desire, conceptual→intention' } # ══════════════════════════════════════════════════════════════════════════ # 5. AIF CHECKS # ══════════════════════════════════════════════════════════════════════════ Write-Host "── AIF ──" -ForegroundColor Cyan # Canonical edge types $CanonicalTypes = @('SUPPORTS', 'CONTRADICTS', 'ASSUMES', 'WEAKENS', 'RESPONDS_TO', 'TENSION_WITH', 'INTERPRETS') $NonCanonical = 0 $NonCanonicalTypes = @{} foreach ($Edge in $AllEdges) { if ($Edge.type -notin $CanonicalTypes) { $NonCanonical++ if (-not $NonCanonicalTypes.ContainsKey($Edge.type)) { $NonCanonicalTypes[$Edge.type] = 0 } $NonCanonicalTypes[$Edge.type]++ } } if ($NonCanonical -eq 0) { Add-Check -Category 'AIF' -Check 'Canonical edge types' -Status 'pass' -Detail "All $($AllEdges.Count) edges use canonical types" } else { $TypeList = ($NonCanonicalTypes.GetEnumerator() | Sort-Object Value -Descending | ForEach-Object { "$($_.Key):$($_.Value)" }) -join ', ' Add-Check -Category 'AIF' -Check 'Canonical edge types' -Status 'warn' ` -Detail "$NonCanonical edge(s) use non-canonical types ($TypeList)" ` -Fix 'Map legacy edge types to canonical 7 (see AGENTS.md edge type list)' } # node_scope coverage $ScopeCount = 0 foreach ($Entry in $AllNodes.Values) { $N = $Entry.Node if ($N.PSObject.Properties['graph_attributes']) { $GA = $N.graph_attributes } else { $GA = $null } if ($GA -and $GA.PSObject.Properties['node_scope'] -and $GA.node_scope) { $ScopeCount++ } } $ScopePct = [Math]::Round($ScopeCount / [Math]::Max(1, $AllNodes.Count) * 100, 1) if ($ScopePct -ge 90) { Add-Check -Category 'AIF' -Check "node_scope populated ($ScopePct%)" -Status 'pass' -Detail "$ScopeCount/$($AllNodes.Count) nodes" } elseif ($ScopePct -ge 50) { Add-Check -Category 'AIF' -Check "node_scope populated ($ScopePct%)" -Status 'warn' ` -Detail "$($AllNodes.Count - $ScopeCount) node(s) missing node_scope" ` -Fix 'Run Invoke-AttributeExtraction for nodes without graph_attributes' } else { Add-Check -Category 'AIF' -Check "node_scope populated ($ScopePct%)" -Status 'fail' ` -Detail "$($AllNodes.Count - $ScopeCount) node(s) missing node_scope" ` -Fix 'Run Invoke-AttributeExtraction -Force to populate all nodes' } # ══════════════════════════════════════════════════════════════════════════ # SUMMARY # ══════════════════════════════════════════════════════════════════════════ Write-Host "`n══════════════════════════════════════════════════════════════" -ForegroundColor Cyan Write-Host " RESULTS: $Passed passed, $Warned warnings, $Failed failed" -ForegroundColor $(if ($Failed -gt 0) { 'Red' } elseif ($Warned -gt 0) { 'Yellow' } else { 'Green' }) Write-Host "══════════════════════════════════════════════════════════════`n" -ForegroundColor Cyan if ($PassThru) { return [PSCustomObject][ordered]@{ Timestamp = (Get-Date).ToString('o') NodeCount = $AllNodes.Count EdgeCount = $AllEdges.Count Passed = $Passed Warned = $Warned Failed = $Failed Checks = $Results.ToArray() Failures = @($Results | Where-Object { $_.Status -eq 'fail' }) Warnings = @($Results | Where-Object { $_.Status -eq 'warn' }) } } } |