Public/Measure-TaxonomyBaseline.ps1
|
# Copyright (c) 2026 Jeffrey Snover. All rights reserved. # Licensed under the MIT License. See LICENSE file in the project root. function Measure-TaxonomyBaseline { <# .SYNOPSIS Measures quality baselines for the taxonomy, summaries, edges, and conflicts. .DESCRIPTION Produces a structured report of data quality metrics that can be compared before and after prompt or schema changes. Covers: - Node mapping rates and consistency across summaries - Density distribution (points per camp scaled by document size) - Edge type distribution and potential misclassification indicators - Conflict quality (temporal ambiguity, single-instance conflicts) - Fallacy flagging rates - Description quality signals (length, structure) Run this before any BFO-related prompt changes to establish a baseline, then re-run after changes to measure impact. .PARAMETER OutputPath Optional path to write the JSON report. If omitted, prints to console. .PARAMETER SampleDocIds Optional array of doc IDs to focus analysis on. If omitted, analyzes all. .EXAMPLE Measure-TaxonomyBaseline .EXAMPLE Measure-TaxonomyBaseline -OutputPath ./baseline-2026-03-28.json #> [CmdletBinding()] param( [string]$OutputPath, [string[]]$SampleDocIds ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $SummariesDir = Get-SummariesDir $SourcesDir = Get-SourcesDir $TaxDir = Get-TaxonomyDir $ConflictsDir = Get-ConflictsDir Write-Host "`n=== Taxonomy Baseline Measurement ===" -ForegroundColor Cyan Write-Host " Timestamp: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" -ForegroundColor Gray # ── Load taxonomy ────────────────────────────────────────────────────── $AllNodes = @{} foreach ($File in (Get-ChildItem $TaxDir -Filter '*.json' | Where-Object { $_.Name -notin 'embeddings.json','edges.json','policy_actions.json','Temp.json','_archived_edges.json' })) { $Data = Get-Content -Raw $File.FullName | ConvertFrom-Json foreach ($Node in $Data.nodes) { $AllNodes[$Node.id] = $Node } } Write-Host " Taxonomy: $($AllNodes.Count) nodes" -ForegroundColor Gray # ── Load summaries ───────────────────────────────────────────────────── $SummaryFiles = Get-ChildItem $SummariesDir -Filter '*.json' -ErrorAction SilentlyContinue if ($SampleDocIds) { $SummaryFiles = $SummaryFiles | Where-Object { $_.BaseName -in $SampleDocIds } } $Summaries = @{} foreach ($F in $SummaryFiles) { try { $Summaries[$F.BaseName] = Get-Content -Raw $F.FullName | ConvertFrom-Json } catch { Write-Warning "Bad JSON: $($F.Name)" } } Write-Host " Summaries: $($Summaries.Count)" -ForegroundColor Gray # ── Load edges ───────────────────────────────────────────────────────── $EdgesPath = Join-Path $TaxDir 'edges.json' $Edges = @() if (Test-Path $EdgesPath) { $EdgesData = Get-Content -Raw $EdgesPath | ConvertFrom-Json $Edges = $EdgesData.edges } Write-Host " Edges: $($Edges.Count)" -ForegroundColor Gray # ── Load conflicts ───────────────────────────────────────────────────── $Conflicts = @() if (Test-Path $ConflictsDir) { foreach ($F in (Get-ChildItem $ConflictsDir -Filter '*.json' -ErrorAction SilentlyContinue)) { try { $Conflicts += Get-Content -Raw $F.FullName | ConvertFrom-Json } catch {} } } Write-Host " Conflicts: $($Conflicts.Count)" -ForegroundColor Gray # ══════════════════════════════════════════════════════════════════════ # METRIC 1: Node Mapping Quality # ══════════════════════════════════════════════════════════════════════ Write-Host "`n Analyzing node mapping quality..." -ForegroundColor Yellow $TotalKP = 0; $NullMapped = 0; $InvalidNodeRef = 0 $NodeRefCounts = @{} # nodeId -> count of times referenced across all summaries $CategoryPerNode = @{} # nodeId -> set of categories assigned $Camps = @('accelerationist','safetyist','skeptic') foreach ($Sum in $Summaries.Values) { foreach ($Camp in $Camps) { $CampData = $Sum.pov_summaries.$Camp if (-not $CampData -or -not $CampData.key_points) { continue } foreach ($KP in $CampData.key_points) { $TotalKP++ $NodeId = $KP.taxonomy_node_id if ($null -eq $NodeId -or $NodeId -eq '') { $NullMapped++ } else { if (-not $AllNodes.ContainsKey($NodeId)) { $InvalidNodeRef++ } if (-not $NodeRefCounts.ContainsKey($NodeId)) { $NodeRefCounts[$NodeId] = 0 } $NodeRefCounts[$NodeId]++ # Track category consistency if ($KP.category) { if (-not $CategoryPerNode.ContainsKey($NodeId)) { $CategoryPerNode[$NodeId] = [System.Collections.Generic.HashSet[string]]::new() } [void]$CategoryPerNode[$NodeId].Add($KP.category) } } } } } $CategoryInconsistencies = @($CategoryPerNode.GetEnumerator() | Where-Object { $_.Value.Count -gt 1 }) $UnreferencedNodes = @($AllNodes.Keys | Where-Object { -not $NodeRefCounts.ContainsKey($_) }) $MappingMetrics = [ordered]@{ total_key_points = $TotalKP null_mapped = $NullMapped null_mapped_pct = if ($TotalKP -gt 0) { [Math]::Round($NullMapped / $TotalKP * 100, 1) } else { 0 } invalid_node_refs = $InvalidNodeRef category_inconsistencies = $CategoryInconsistencies.Count category_inconsistent_ids = @($CategoryInconsistencies | ForEach-Object { $_.Key }) unreferenced_node_count = $UnreferencedNodes.Count unreferenced_node_pct = [Math]::Round($UnreferencedNodes.Count / $AllNodes.Count * 100, 1) } # ══════════════════════════════════════════════════════════════════════ # METRIC 2: Density Distribution (scaled by document size) # ══════════════════════════════════════════════════════════════════════ Write-Host " Analyzing density distribution..." -ForegroundColor Yellow $DensityRecords = [System.Collections.Generic.List[object]]::new() foreach ($DocId in $Summaries.Keys) { $Sum = $Summaries[$DocId] $SnapPath = Join-Path (Join-Path $SourcesDir $DocId) 'snapshot.md' $WordCount = 0 if (Test-Path $SnapPath) { $Text = Get-Content -Raw $SnapPath $WordCount = ($Text -split '\s+').Count } foreach ($Camp in $Camps) { $CampData = $Sum.pov_summaries.$Camp if ($CampData -and $CampData.key_points) { $KPCount = @($CampData.key_points).Count } else { $KPCount = 0 } $DensityRecords.Add([PSCustomObject]@{ DocId = $DocId Camp = $Camp WordCount = $WordCount KPCount = $KPCount KPPer1K = if ($WordCount -gt 0) { [Math]::Round($KPCount / ($WordCount / 1000), 2) } else { 0 } }) } } $AllKPPer1K = @($DensityRecords | Where-Object { $_.WordCount -gt 0 } | ForEach-Object { $_.KPPer1K }) $SortedKP = $AllKPPer1K | Sort-Object $DensityMetrics = [ordered]@{ doc_count = $Summaries.Count median_kp_per_1k = if ($SortedKP.Count -gt 0) { $SortedKP[[int]($SortedKP.Count / 2)] } else { 0 } p10_kp_per_1k = if ($SortedKP.Count -gt 9) { $SortedKP[[int]($SortedKP.Count * 0.1)] } else { 0 } p90_kp_per_1k = if ($SortedKP.Count -gt 9) { $SortedKP[[int]($SortedKP.Count * 0.9)] } else { 0 } zero_kp_camp_entries = @($DensityRecords | Where-Object { $_.KPCount -eq 0 }).Count # Outlier docs: very low density low_density_docs = @($DensityRecords | Where-Object { $_.WordCount -gt 2000 -and $_.KPPer1K -lt 1.0 } | Sort-Object KPPer1K | Select-Object -First 10 DocId, Camp, WordCount, KPCount, KPPer1K) # Outlier docs: very high density (possible padding) high_density_docs = @($DensityRecords | Where-Object { $_.WordCount -gt 500 -and $_.KPPer1K -gt 15 } | Sort-Object KPPer1K -Descending | Select-Object -First 10 DocId, Camp, WordCount, KPCount, KPPer1K) } # ══════════════════════════════════════════════════════════════════════ # METRIC 3: Edge Quality # ══════════════════════════════════════════════════════════════════════ Write-Host " Analyzing edge quality..." -ForegroundColor Yellow $TypeCounts = @{} $OrphanEdges = 0 $SelfEdges = 0 $GoalSupportsData = 0 # Domain violation: Desires SUPPORTS Beliefs foreach ($E in $Edges) { $Type = $E.type if (-not $TypeCounts.ContainsKey($Type)) { $TypeCounts[$Type] = 0 } $TypeCounts[$Type]++ if ($E.source -eq $E.target) { $SelfEdges++ } # Policy nodes (pol-*) are in the policy registry, not in $AllNodes — skip orphan check for them $SrcIsPolicy = $E.source -match '^pol-' $TgtIsPolicy = $E.target -match '^pol-' if ($SrcIsPolicy) { $SrcNode = $true } else { $SrcNode = $AllNodes[$E.source] } if ($TgtIsPolicy) { $TgtNode = $true } else { $TgtNode = $AllNodes[$E.target] } if (-not $SrcNode -or -not $TgtNode) { $OrphanEdges++ continue } # Check domain violation: Desires SUPPORTS Beliefs (skip policy nodes) if ($SrcIsPolicy -or $TgtIsPolicy) { continue } if ($SrcNode.PSObject.Properties['category']) { $SrcCat = $SrcNode.category } else { $SrcCat = $null } if ($TgtNode.PSObject.Properties['category']) { $TgtCat = $TgtNode.category } else { $TgtCat = $null } if ($Type -eq 'SUPPORTS' -and $SrcCat -eq 'Desires' -and $TgtCat -eq 'Beliefs') { $GoalSupportsData++ } } $CanonicalTypes = @('SUPPORTS','CONTRADICTS','ASSUMES','WEAKENS','RESPONDS_TO','TENSION_WITH','INTERPRETS') $NonCanonical = @($TypeCounts.GetEnumerator() | Where-Object { $_.Key -notin $CanonicalTypes }) $EdgeMetrics = [ordered]@{ total_edges = $Edges.Count type_distribution = [ordered]@{} canonical_type_count = ($CanonicalTypes | ForEach-Object { $TypeCounts[$_] } | Measure-Object -Sum).Sum non_canonical_type_count = ($NonCanonical | ForEach-Object { $_.Value } | Measure-Object -Sum).Sum non_canonical_types = @($NonCanonical | Sort-Object Value -Descending | ForEach-Object { [ordered]@{ type = $_.Key; count = $_.Value } }) orphan_edges = $OrphanEdges self_edges = $SelfEdges goals_supports_data = $GoalSupportsData } foreach ($T in ($TypeCounts.GetEnumerator() | Sort-Object Value -Descending)) { $EdgeMetrics.type_distribution[$T.Key] = $T.Value } # ══════════════════════════════════════════════════════════════════════ # METRIC 4: Conflict Quality # ══════════════════════════════════════════════════════════════════════ Write-Host " Analyzing conflict quality..." -ForegroundColor Yellow $SingleInstance = @($Conflicts | Where-Object { @($_.instances).Count -le 1 }).Count $MultiInstance = @($Conflicts | Where-Object { @($_.instances).Count -gt 1 }).Count $ConflictMetrics = [ordered]@{ total_conflicts = $Conflicts.Count single_instance = $SingleInstance single_instance_pct = if ($Conflicts.Count -gt 0) { [Math]::Round($SingleInstance / $Conflicts.Count * 100, 1) } else { 0 } multi_instance = $MultiInstance status_open = @($Conflicts | Where-Object { $_.status -eq 'open' }).Count status_resolved = @($Conflicts | Where-Object { $_.status -eq 'resolved' }).Count } # ══════════════════════════════════════════════════════════════════════ # METRIC 5: Fallacy Flagging Rates # ══════════════════════════════════════════════════════════════════════ Write-Host " Analyzing fallacy flagging..." -ForegroundColor Yellow $FallacyTotal = 0; $FallacyLikely = 0; $FallacyPossible = 0; $FallacyBorderline = 0 $NodesWithFallacies = 0; $NodesWithoutFallacies = 0 $FallacyTypeCounts = @{} foreach ($Node in $AllNodes.Values) { if ($Node.PSObject.Properties['graph_attributes']) { $GA = $Node.graph_attributes } else { $GA = $null } $HasFallacies = $GA -and $GA.PSObject.Properties['possible_fallacies'] -and $GA.possible_fallacies if ($HasFallacies) { $Fallacies = @($GA.possible_fallacies) if ($Fallacies.Count -gt 0) { $NodesWithFallacies++ foreach ($F in $Fallacies) { $FallacyTotal++ switch ($F.confidence) { 'likely' { $FallacyLikely++ } 'possible' { $FallacyPossible++ } 'borderline' { $FallacyBorderline++ } } $Key = $F.fallacy if (-not $FallacyTypeCounts.ContainsKey($Key)) { $FallacyTypeCounts[$Key] = 0 } $FallacyTypeCounts[$Key]++ } } else { $NodesWithoutFallacies++ } } else { $NodesWithoutFallacies++ } } $FallacyMetrics = [ordered]@{ nodes_with_fallacies = $NodesWithFallacies nodes_without_fallacies = $NodesWithoutFallacies flagging_rate_pct = if ($AllNodes.Count -gt 0) { [Math]::Round($NodesWithFallacies / $AllNodes.Count * 100, 1) } else { 0 } total_flags = $FallacyTotal avg_per_flagged_node = if ($NodesWithFallacies -gt 0) { [Math]::Round($FallacyTotal / $NodesWithFallacies, 1) } else { 0 } confidence_likely = $FallacyLikely confidence_possible = $FallacyPossible confidence_borderline = $FallacyBorderline top_fallacy_types = @($FallacyTypeCounts.GetEnumerator() | Sort-Object Value -Descending | Select-Object -First 15 | ForEach-Object { [ordered]@{ type = $_.Key; count = $_.Value } }) } # ══════════════════════════════════════════════════════════════════════ # METRIC 6: Description Quality Signals # ══════════════════════════════════════════════════════════════════════ Write-Host " Analyzing description quality..." -ForegroundColor Yellow $DescLengths = @() $GenusPattern = 0 # Starts with "A [category] within [POV]" $ShortDescs = 0 # < 50 chars $StubDescs = 0 # Description == label (placeholder) foreach ($Node in $AllNodes.Values) { $Desc = $Node.description if (-not $Desc) { $StubDescs++; continue } $DescLengths += $Desc.Length if ($Desc.Length -lt 50) { $ShortDescs++ } if ($Desc -eq $Node.label) { $StubDescs++ } if ($Desc -match '^An?\s+(Belief|Desire|Intention)\s+within\s+(accelerationist|safetyist|skeptic)\s+discourse\s+that\s+' -or $Desc -match '^A\s+cross-cutting\s+concept\s+that\s+') { $GenusPattern++ } } $SortedDesc = $DescLengths | Sort-Object $DescriptionMetrics = [ordered]@{ total_nodes = $AllNodes.Count median_desc_length = if ($SortedDesc.Count -gt 0) { $SortedDesc[[int]($SortedDesc.Count / 2)] } else { 0 } p10_desc_length = if ($SortedDesc.Count -gt 9) { $SortedDesc[[int]($SortedDesc.Count * 0.1)] } else { 0 } p90_desc_length = if ($SortedDesc.Count -gt 9) { $SortedDesc[[int]($SortedDesc.Count * 0.9)] } else { 0 } short_descriptions = $ShortDescs stub_descriptions = $StubDescs genus_differentia_pattern = $GenusPattern genus_differentia_pct = [Math]::Round($GenusPattern / [Math]::Max(1, $AllNodes.Count) * 100, 1) } # ══════════════════════════════════════════════════════════════════════ # METRIC 7: Unmapped Concepts Across Summaries # ══════════════════════════════════════════════════════════════════════ Write-Host " Analyzing unmapped concepts..." -ForegroundColor Yellow $TotalUnmapped = 0; $ResolvedUnmapped = 0 foreach ($Sum in $Summaries.Values) { if ($Sum.unmapped_concepts) { foreach ($UC in $Sum.unmapped_concepts) { $TotalUnmapped++ if ($UC.PSObject.Properties['resolved_node_id'] -and $UC.resolved_node_id) { $ResolvedUnmapped++ } } } } $UnmappedMetrics = [ordered]@{ total_unmapped_concepts = $TotalUnmapped resolved = $ResolvedUnmapped unresolved = $TotalUnmapped - $ResolvedUnmapped resolved_pct = if ($TotalUnmapped -gt 0) { [Math]::Round($ResolvedUnmapped / $TotalUnmapped * 100, 1) } else { 0 } } # ══════════════════════════════════════════════════════════════════════ # METRIC 8: Ontology Coverage # ══════════════════════════════════════════════════════════════════════ Write-Host " Analyzing ontology coverage..." -ForegroundColor Yellow $NodeScopeCount = 0 $ParentRelCount = 0 $FallacyTierCount = 0 foreach ($Node in $AllNodes.Values) { # node_scope coverage if ($Node.PSObject.Properties['graph_attributes']) { $GA = $Node.graph_attributes } else { $GA = $null } if ($GA -and $GA.PSObject.Properties['node_scope'] -and $GA.node_scope) { $NodeScopeCount++ } # parent_relationship coverage (nodes with parent_id) if ($Node.PSObject.Properties['parent_id'] -and $Node.parent_id) { $ParentRelCount++ } } # Fallacy tier coverage (fallacies with 'type' field) $FallacyWithTier = 0 foreach ($Node in $AllNodes.Values) { if ($Node.PSObject.Properties['graph_attributes']) { $GA = $Node.graph_attributes } else { $GA = $null } if ($GA -and $GA.PSObject.Properties['possible_fallacies'] -and $GA.possible_fallacies) { foreach ($F in @($GA.possible_fallacies)) { if ($F.PSObject.Properties['type'] -and $F.type) { $FallacyWithTier++ } } } } # temporal_scope coverage on factual_claims $TotalClaims = 0; $ClaimsWithTemporal = 0 foreach ($Sum in $Summaries.Values) { if ($Sum.factual_claims) { foreach ($Claim in @($Sum.factual_claims)) { $TotalClaims++ if ($Claim.PSObject.Properties['temporal_scope'] -and $Claim.temporal_scope) { $ClaimsWithTemporal++ } } } } # bdi_layer and argument_map coverage on debates $DebatesDir = Get-DebatesDir $TotalDebates = 0; $DebatesWithBdiLayer = 0; $DebatesWithArgMap = 0 $TotalDisagreements = 0; $DisagreementsWithBdi = 0 if (Test-Path $DebatesDir) { foreach ($DebFile in Get-ChildItem -Path $DebatesDir -Filter '*.json' -File -ErrorAction SilentlyContinue) { try { $Debate = Get-Content -Raw -Path $DebFile.FullName | ConvertFrom-Json $TotalDebates++ # argument_map coverage if ($Debate.PSObject.Properties['argument_map'] -and $Debate.argument_map) { $DebatesWithArgMap++ } # bdi_layer on synthesis disagreements if ($Debate.PSObject.Properties['synthesis'] -and $Debate.synthesis.PSObject.Properties['disagreements']) { foreach ($D in @($Debate.synthesis.disagreements)) { $TotalDisagreements++ if ($D.PSObject.Properties['bdi_layer'] -and $D.bdi_layer) { $DisagreementsWithBdi++ } } } } catch { } } } $OntologyMetrics = [ordered]@{ node_scope_coverage_pct = [Math]::Round($NodeScopeCount / [Math]::Max(1, $AllNodes.Count) * 100, 1) genus_differentia_pct = $DescriptionMetrics.genus_differentia_pct bdi_layer_coverage_pct = if ($TotalDisagreements -gt 0) { [Math]::Round($DisagreementsWithBdi / $TotalDisagreements * 100, 1) } else { 0 } argument_map_coverage_pct = if ($TotalDebates -gt 0) { [Math]::Round($DebatesWithArgMap / $TotalDebates * 100, 1) } else { 0 } parent_relationship_coverage_pct = [Math]::Round($ParentRelCount / [Math]::Max(1, $AllNodes.Count) * 100, 1) fallacy_tier_coverage_pct = if ($FallacyTotal -gt 0) { [Math]::Round($FallacyWithTier / $FallacyTotal * 100, 1) } else { 0 } temporal_scope_coverage_pct = if ($TotalClaims -gt 0) { [Math]::Round($ClaimsWithTemporal / $TotalClaims * 100, 1) } else { 0 } _counts = [ordered]@{ nodes_with_scope = $NodeScopeCount nodes_with_parent = $ParentRelCount fallacies_with_tier = $FallacyWithTier claims_with_temporal = $ClaimsWithTemporal debates_total = $TotalDebates debates_with_argmap = $DebatesWithArgMap disagreements_total = $TotalDisagreements disagreements_with_bdi = $DisagreementsWithBdi } } # ══════════════════════════════════════════════════════════════════════ # ASSEMBLE REPORT # ══════════════════════════════════════════════════════════════════════ $Report = [ordered]@{ metadata = [ordered]@{ generated_at = (Get-Date).ToString('yyyy-MM-ddTHH:mm:ssZ') taxonomy_version = if (Test-Path (Join-Path (Split-Path $TaxDir) 'TAXONOMY_VERSION')) { (Get-Content (Join-Path (Split-Path $TaxDir) 'TAXONOMY_VERSION') -Raw).Trim() } else { 'unknown' } node_count = $AllNodes.Count summary_count = $Summaries.Count edge_count = $Edges.Count conflict_count = $Conflicts.Count sample_doc_ids = if ($SampleDocIds) { $SampleDocIds } else { 'all' } } node_mapping = $MappingMetrics density = $DensityMetrics edges = $EdgeMetrics conflicts = $ConflictMetrics fallacies = $FallacyMetrics descriptions = $DescriptionMetrics unmapped_concepts = $UnmappedMetrics ontology_coverage = $OntologyMetrics } # ── Output ───────────────────────────────────────────────────────────── $Json = $Report | ConvertTo-Json -Depth 10 if ($OutputPath) { Set-Content -Path $OutputPath -Value $Json -Encoding UTF8 Write-Host "`n Report saved: $OutputPath" -ForegroundColor Green } # ── Console Summary ──────────────────────────────────────────────────── Write-Host "`n── Node Mapping ──" -ForegroundColor Cyan Write-Host " Key points: $TotalKP total, $NullMapped unmapped ($($MappingMetrics.null_mapped_pct)%)" Write-Host " Invalid node refs: $InvalidNodeRef" Write-Host " Category inconsistencies: $($CategoryInconsistencies.Count) nodes assigned different categories across summaries" Write-Host " Unreferenced nodes: $($UnreferencedNodes.Count)/$($AllNodes.Count) ($($MappingMetrics.unreferenced_node_pct)%)" Write-Host "`n── Density ──" -ForegroundColor Cyan Write-Host " Median KP per 1K words: $($DensityMetrics.median_kp_per_1k)" Write-Host " P10-P90 range: $($DensityMetrics.p10_kp_per_1k) - $($DensityMetrics.p90_kp_per_1k)" Write-Host " Zero-KP camp entries: $($DensityMetrics.zero_kp_camp_entries)" Write-Host "`n── Edges ──" -ForegroundColor Cyan Write-Host " Total: $($Edges.Count)" Write-Host " Canonical types: $($EdgeMetrics.canonical_type_count), Non-canonical: $($EdgeMetrics.non_canonical_type_count)" Write-Host " Orphans: $OrphanEdges, Self-edges: $SelfEdges" Write-Host " Desires SUPPORTS Beliefs (domain violation): $GoalSupportsData" Write-Host "`n── Conflicts ──" -ForegroundColor Cyan Write-Host " Total: $($Conflicts.Count), Single-instance: $SingleInstance ($($ConflictMetrics.single_instance_pct)%)" Write-Host "`n── Fallacies ──" -ForegroundColor Cyan Write-Host " Nodes flagged: $NodesWithFallacies/$($AllNodes.Count) ($($FallacyMetrics.flagging_rate_pct)%)" Write-Host " Total flags: $FallacyTotal (likely: $FallacyLikely, possible: $FallacyPossible, borderline: $FallacyBorderline)" Write-Host " Avg per flagged node: $($FallacyMetrics.avg_per_flagged_node)" Write-Host "`n── Descriptions ──" -ForegroundColor Cyan Write-Host " Median length: $($DescriptionMetrics.median_desc_length) chars" Write-Host " Short (<50): $ShortDescs, Stubs: $StubDescs" Write-Host " Already genus-differentia: $GenusPattern ($($DescriptionMetrics.genus_differentia_pct)%)" Write-Host "`n── Unmapped Concepts ──" -ForegroundColor Cyan Write-Host " Total: $TotalUnmapped, Resolved: $ResolvedUnmapped ($($UnmappedMetrics.resolved_pct)%)" Write-Host "`n── Ontology Coverage ──" -ForegroundColor Cyan Write-Host " node_scope: $($OntologyMetrics.node_scope_coverage_pct)% ($NodeScopeCount/$($AllNodes.Count))" Write-Host " genus-differentia: $($OntologyMetrics.genus_differentia_pct)%" Write-Host " bdi_layer: $($OntologyMetrics.bdi_layer_coverage_pct)% ($DisagreementsWithBdi/$TotalDisagreements disagreements)" Write-Host " argument_map: $($OntologyMetrics.argument_map_coverage_pct)% ($DebatesWithArgMap/$TotalDebates debates)" Write-Host " parent_relationship: $($OntologyMetrics.parent_relationship_coverage_pct)% ($ParentRelCount/$($AllNodes.Count))" Write-Host " fallacy_tier: $($OntologyMetrics.fallacy_tier_coverage_pct)% ($FallacyWithTier/$FallacyTotal fallacies)" Write-Host " temporal_scope: $($OntologyMetrics.temporal_scope_coverage_pct)% ($ClaimsWithTemporal/$TotalClaims claims)" Write-Host "" # final newline # Return the report object for pipeline use return [PSCustomObject]$Report } |