Public/Test-TaxonomyDirContents.ps1
|
# Copyright (c) 2026 Jeffrey Snover. All rights reserved. # Licensed under the MIT License. See LICENSE file in the project root. function Test-TaxonomyDirContents { <# .SYNOPSIS Validates the JSON files in TAXONOMY_DIR before an Update-TaxEmbeddings run. .DESCRIPTION Update-TaxEmbeddings (embed_taxonomy.py) iterates every *.json file in the taxonomy directory that is not in its SKIP_FILES set, treating each file's `nodes` field as a list of node objects. A file whose `nodes` is a dict — e.g. an embedding-index artifact like embeddings-orgstance-6733.json — makes the script iterate string keys and crash with "'str' object has no attribute 'get'" (see t/1652). This cmdlet mirrors embed_taxonomy.py's skip logic (SKIP_FILES plus the `embeddings-` filename prefix) and inspects the `nodes` field of every file the script would actually load, flagging any that would yield non-object node values. Run it before Update-TaxEmbeddings, especially after new files land in TAXONOMY_DIR or after a data-repo pull. Output is one record per file with File, NodesType, Skipped, and Safe. A file is Unsafe only if the embedding script would load it AND its `nodes` would not iterate as a list of objects. Use -Detailed for NodeCount, Reason, and full path. .PARAMETER TaxonomyDir Taxonomy directory to inspect. Defaults to the resolved taxonomy dir from .aitriad.json (Get-TaxonomyDir). .PARAMETER Detailed Emit the full per-file breakdown (NodeCount, Reason, Path) in addition to the summary columns. .EXAMPLE Test-TaxonomyDirContents Lists every taxonomy JSON file and whether it is safe to embed. .EXAMPLE Test-TaxonomyDirContents -Detailed | Where-Object { -not $_.Safe } Shows only the files that would crash Update-TaxEmbeddings, with reasons. .OUTPUTS [PSCustomObject] with File, NodesType, Skipped, Safe (and, with -Detailed, NodeCount, Reason, Path). .LINK Update-TaxEmbeddings .LINK Get-Tax #> [CmdletBinding()] [OutputType([PSCustomObject])] param( [Parameter()] [string]$TaxonomyDir, [Parameter()] [switch]$Detailed ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' if ([string]::IsNullOrWhiteSpace($TaxonomyDir)) { $TaxonomyDir = Get-TaxonomyDir } if (-not (Test-Path -LiteralPath $TaxonomyDir -PathType Container)) { New-ActionableError ` -Goal 'Validate taxonomy directory contents before embedding' ` -Problem "Taxonomy directory not found: $TaxonomyDir" ` -Location 'Test-TaxonomyDirContents' ` -NextSteps @( 'Verify .aitriad.json points at the correct data root', 'Pass an explicit -TaxonomyDir path', 'Confirm the ai-triad-data repo is checked out') ` -Throw return } # Mirror of embed_taxonomy.py SKIP_FILES (scripts/embed_taxonomy.py:135). Keep # in sync — a file the script skips is not a crash risk, so we skip it too. $SkipFiles = @( 'embeddings.json' 'edges.json' 'policy_actions.json' 'lineage_categories.json' '_archived_edges.json' 'interpretation_embeddings.json' ) # Column set: compact by default, full breakdown with -Detailed. if ($Detailed) { $props = @('File', 'NodesType', 'Skipped', 'Safe', 'NodeCount', 'Reason', 'Path') } else { $props = @('File', 'NodesType', 'Skipped', 'Safe') } foreach ($File in (Get-ChildItem -LiteralPath $TaxonomyDir -Filter '*.json' -File | Sort-Object Name)) { # embed_taxonomy.py skips SKIP_FILES and any `embeddings-*` filename # (the embedding-index artifacts, t/1652). Skipped files never reach the # node-iteration path, so they are safe regardless of shape. $isSkipped = ($File.Name -in $SkipFiles) -or ([System.IO.Path]::GetFileNameWithoutExtension($File.Name).StartsWith('embeddings-')) if ($isSkipped) { if ($File.Name -in $SkipFiles) { $reason = 'In SKIP_FILES; not loaded' } else { $reason = 'Matches embeddings- prefix; not loaded' } [PSCustomObject]@{ File = $File.Name NodesType = '(skipped)' Skipped = $true Safe = $true NodeCount = $null Reason = $reason Path = $File.FullName } | Select-Object $props continue } # Parse and classify the `nodes` field the way embed_taxonomy.py would. $nodesType = $null $nodeCount = $null $safe = $true $reason = '' try { $data = Get-Content -Raw -LiteralPath $File.FullName -Encoding utf8 | ConvertFrom-Json } catch { # embed_taxonomy.py catches JSONDecodeError/OSError and skips the file # with a warning — a parse error does not crash the run. [PSCustomObject]@{ File = $File.Name NodesType = '(parse error)' Skipped = $false Safe = $true NodeCount = $null Reason = "JSON parse error; script will skip: $($_.Exception.Message)" Path = $File.FullName } | Select-Object $props continue } if (-not $data.PSObject.Properties['nodes']) { # Missing key → embed_taxonomy uses data.get("nodes", []) → empty, safe. $nodesType = '(no nodes key)' $safe = $true $reason = 'No `nodes` key; script iterates an empty list' } else { $nodes = $data.nodes if ($null -eq $nodes) { # nodes:null → data.get returns None → `for node in None` crashes. $nodesType = 'null' $safe = $false $reason = '`nodes` is null; script would fail to iterate it' } elseif ($nodes -is [System.Management.Automation.PSCustomObject]) { # A JSON object (dict) — iterating yields string keys → crash. $nodesType = 'dict' $safe = $false $reason = '`nodes` is a dict (embedding index?); iterating yields keys, not node objects' } elseif ($nodes -is [System.Collections.IEnumerable] -and $nodes -isnot [string]) { $elems = @($nodes) $nodeCount = $elems.Count $nonObjects = @($elems | Where-Object { $_ -isnot [System.Management.Automation.PSCustomObject] }) if ($elems.Count -eq 0) { $nodesType = 'list (empty)' $safe = $true $reason = 'Empty node list; nothing to embed' } elseif ($nonObjects.Count -gt 0) { $elemType = $nonObjects[0].GetType().Name $nodesType = "list of $elemType" $safe = $false $reason = "$($nonObjects.Count)/$($elems.Count) elements are not objects (e.g. $elemType); node access would crash" } else { $nodesType = 'list' $safe = $true $reason = "$($elems.Count) node objects" } } else { # Scalar (string/number/bool) — not iterable as node list. $nodesType = $nodes.GetType().Name $safe = $false $reason = "`nodes` is a scalar ($nodesType); not a list of node objects" } } if (-not $safe) { Write-Warning "Unsafe for Update-TaxEmbeddings: $($File.Name) — $reason" } [PSCustomObject]@{ File = $File.Name NodesType = $nodesType Skipped = $false Safe = $safe NodeCount = $nodeCount Reason = $reason Path = $File.FullName } | Select-Object $props } } |