build/Build-DFPackageUniverseLinks.ps1
|
#Requires -Version 7.0 <# .SYNOPSIS Phase B of the package-universe pipeline: cross-catalog identity clustering. .DESCRIPTION Reads raw_packages (produced by Phase A / Build-DFPackageUniverseRaw.ps1) from the shared universe.db, derives a confidence-scored pairwise link graph (github repo > homepage > publisher+name > name-only, with a conflict tier), applies the version-controlled curation file (human-verified same/different), computes clusters by constrained union-find, and writes identity_links / identity_clusters / cluster_members / curated_links plus review candidates to pipeline_log (stage='link', level='review'). See docs/superpowers/specs/2026-07-16-package-universe-identity-clustering-design.md. Set-StrictMode -Version Latest is dynamically scoped over the dot-sourced helpers -- intended, not incidental. .PARAMETER DatabasePath Path to the shared SQLite working database (default: the standard build/.package-universe/universe.db next to this script). .PARAMETER CurationPath Path to the curation .jsonc (default: data/package-universe-curation.jsonc). A missing file means no curation yet; clustering still runs. .PARAMETER Threshold Minimum edge confidence that forms a cluster (default 0.60). .OUTPUTS A reconciliation summary object (RowsRead, Edges, Clusters, Members, Review). .EXAMPLE ./build/Build-DFPackageUniverseLinks.ps1 #> [CmdletBinding()] param( [string]$DatabasePath = (Join-Path $PSScriptRoot '.package-universe/universe.db'), [string]$CurationPath = (Join-Path $PSScriptRoot '../data/package-universe-curation.jsonc'), [double]$Threshold = 0.60, [int]$FamilySizeThreshold = 5 ) Set-StrictMode -Version Latest if (-not (Get-Module -ListAvailable -Name 'PSSQLite')) { throw "Build-DFPackageUniverseLinks: required module 'PSSQLite' is not installed. Install it with: Install-Module PSSQLite -Scope CurrentUser" } Import-Module PSSQLite -ErrorAction Stop # Private/*.ps1 functions aren't exported; dot-source directly (mirroring # Build-DFPackageUniverseRaw.ps1). ../Private supplies ConvertTo-DFNormalizedHomepage; # ./Private supplies the DB helpers and the Phase B link helpers. if (-not (Get-Command ConvertTo-DFNormalizedHomepage -ErrorAction Ignore)) { Get-ChildItem -Path (Join-Path $PSScriptRoot '../Private') -Filter '*.ps1' | ForEach-Object { . $_.FullName } } if (-not (Get-Command Invoke-DFPackageUniverseLinkBuild -ErrorAction Ignore)) { Get-ChildItem -Path (Join-Path $PSScriptRoot 'Private') -Filter '*.ps1' | ForEach-Object { . $_.FullName } } if (-not (Test-Path $DatabasePath)) { throw "Build-DFPackageUniverseLinks: database not found at '$DatabasePath'. Run Build-DFPackageUniverseRaw.ps1 (Phase A) first." } Initialize-DFPackageUniverseLinksSchema -DatabasePath $DatabasePath $conn = New-SQLiteConnection -DataSource $DatabasePath try { $summary = Invoke-DFPackageUniverseLinkBuild -Connection $conn -CurationPath $CurationPath -Threshold $Threshold -FamilySizeThreshold $FamilySizeThreshold } finally { $conn.Close() } # Reconciliation: population found vs artifacts emitted, so a silent collapse is # visible (design Error Handling rule). Edge breakdown by method comes from the DB. $byMethod = @(Invoke-SqliteQuery -DataSource $DatabasePath -Query 'SELECT method, COUNT(*) n FROM identity_links GROUP BY method ORDER BY method') Write-Host 'Phase B (identity clustering) complete:' Write-Host " raw_packages read : $($summary.RowsRead)" Write-Host " edges : $($summary.Edges)" foreach ($m in $byMethod) { Write-Host (' {0,-16} {1}' -f $m.method, $m.n) } Write-Host " clusters : $($summary.Clusters)" Write-Host " clustered members : $($summary.Members)" Write-Host " review candidates : $($summary.Review)" Write-Host " families (review) : $($summary.Families)" $summary |