build/Build-DFCategoryDb.ps1

#Requires -Version 7.0

<#
.SYNOPSIS
    Merges build/categories/*.jsonc fragments into data/tool-categories.json.
    Author-side tooling — never loaded by the DotForge module.
.PARAMETER CategoriesDir
    Directory of *.jsonc fragments (default: build/categories next to this script).
.PARAMETER OutPath
    Output file (default: data/tool-categories.json at the repo root).
.EXAMPLE
    ./build/Build-DFCategoryDb.ps1
    Regenerates data/tool-categories.json from the checked-in fragments.
#>

[CmdletBinding()]
param(
    [string]$CategoriesDir = (Join-Path $PSScriptRoot 'categories'),
    [string]$OutPath = (Join-Path $PSScriptRoot '../data/tool-categories.json')
)

. (Join-Path $PSScriptRoot '../Private/Test-DFCategoryDbSchema.ps1')

function Read-DFCategoryFragment {
    param([string]$Path)
    $raw = Get-Content $Path -Raw
    # Strip //-prefixed line comments — these fragments are author-controlled,
    # not general JSONC, so a simple non-quoted-// strip is sufficient.
    $stripped = ($raw -split "`n" | ForEach-Object {
        if ($_ -match '^(?<code>(?:[^"]|"[^"]*")*?)//') { $Matches.code } else { $_ }
    }) -join "`n"
    $stripped | ConvertFrom-Json
}

$fragmentFiles = Get-ChildItem $CategoriesDir -Filter '*.jsonc' | Sort-Object Name
$taxonomyFile = $fragmentFiles | Where-Object Name -eq 'taxonomy.jsonc'
if (-not $taxonomyFile) { throw "Build-DFCategoryDb: no taxonomy.jsonc found in $CategoriesDir" }

$taxonomy = Read-DFCategoryFragment -Path $taxonomyFile.FullName
$tools = [ordered]@{}
$seenIn = @{}

foreach ($file in ($fragmentFiles | Where-Object Name -ne 'taxonomy.jsonc')) {
    $fragment = Read-DFCategoryFragment -Path $file.FullName
    foreach ($prop in $fragment.PSObject.Properties) {
        if ($tools.Contains($prop.Name)) {
            throw "Build-DFCategoryDb: duplicate tool key '$($prop.Name)' in $($file.Name) (already defined in $($seenIn[$prop.Name]))"
        }
        $tools[$prop.Name] = $prop.Value
        $seenIn[$prop.Name] = $file.Name
    }
}

$sortedTools = [ordered]@{}
foreach ($key in ($tools.Keys | Sort-Object)) { $sortedTools[$key] = $tools[$key] }

$doc = [pscustomobject]@{
    schemaVersion = 1
    updated       = [datetime]::UtcNow.ToString('yyyy-MM-dd')
    taxonomy      = $taxonomy
    # Cast the [ordered] dictionary to PSCustomObject so Test-DFCategoryDbSchema's
    # PSObject.Properties enumeration walks tool keys, not the dictionary's own
    # .NET members (Count, Keys, Values, ...).
    tools         = [pscustomobject]$sortedTools
}

$errs = $null
if (-not (Test-DFCategoryDbSchema -Database $doc -Errors ([ref]$errs))) {
    throw "Build-DFCategoryDb: generated document failed validation:`n$($errs -join "`n")"
}

$doc | ConvertTo-Json -Depth 8 | Set-Content -Path $OutPath -Encoding UTF8
Write-Host "Wrote $OutPath ($($sortedTools.Count) tools)"