hypertwist/scripts/Write-HyperTwistRepoLicenseEvidenceAudit.ps1
2026-05-27 01:48:01 +02:00

713 lines
27 KiB
PowerShell

param(
[string]$Stamp = (Get-Date).ToString("yyyy-MM-dd")
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
function Get-NormalizedRepoId {
param(
[string]$RepoUrl,
[string]$FallbackName
)
if ($RepoUrl) {
$trimmed = $RepoUrl.Trim()
if ($trimmed -match '^(?:https?|ssh)://[^/]+/(?<id>.+?)(?:\.git)?/?$') {
return $Matches["id"]
}
if ($trimmed -match '^[^:]+:(?<id>.+?)(?:\.git)?/?$') {
return $Matches["id"]
}
}
if ($FallbackName) {
return ($FallbackName -replace '\s*-\s*', '/' -replace '\s+', '-')
}
return "unknown"
}
function Get-GitRoot {
param([string]$Path)
try {
$root = (git -C $Path rev-parse --show-toplevel 2>$null)
if ($LASTEXITCODE -eq 0 -and $root) {
return $root.Trim()
}
} catch {
}
return $null
}
function Get-RemoteOriginUrl {
param([string]$Path)
try {
$remote = (git -C $Path config --get remote.origin.url 2>$null)
if ($LASTEXITCODE -eq 0 -and $remote) {
return $remote.Trim()
}
} catch {
}
return ""
}
function Test-StandaloneGitRoot {
param(
[string]$Path,
[string]$GitRoot
)
if (-not $GitRoot) {
return $false
}
return ([System.IO.Path]::GetFullPath($GitRoot).TrimEnd('\') -eq
[System.IO.Path]::GetFullPath($Path).TrimEnd('\'))
}
function Get-RootFilesByPattern {
param(
[string]$Path,
[string]$Pattern
)
return @(Get-ChildItem -LiteralPath $Path -File -ErrorAction SilentlyContinue |
Where-Object { $_.Name -match $Pattern })
}
function Get-LicenseSignalFromText {
param(
[string]$FileName,
[string]$Text
)
$name = $FileName.ToUpperInvariant()
if ($name -match 'MIT' -or
(($Text -match 'Permission is hereby granted, free of charge, to any person obtaining a copy') -and
($Text -match 'THE SOFTWARE IS PROVIDED "AS IS"'))) {
return "MIT"
}
if ($name -match 'APACHE' -or $Text -match 'Apache License' -or $Text -match 'Apache-2\.0') {
return "Apache-2.0"
}
if ($name -match 'MPL' -or $Text -match 'Mozilla Public License' -or $Text -match 'MPL-2\.0') {
return "MPL-2.0"
}
$trimmedText = $Text.TrimStart()
if ($name -match 'AGPL' -or $trimmedText -match '^[#\s]*GNU AFFERO GENERAL PUBLIC LICENSE') {
return "AGPL-3.0"
}
if ($name -match 'GPL' -or $trimmedText -match '^[#\s]*GNU GENERAL PUBLIC LICENSE') {
return "GPL-3.0-or-later"
}
if ($name -match 'BSD' -or $Text -match 'Redistribution and use in source and binary forms') {
return "BSD-family"
}
if ($name -match 'ISC' -or $Text -match 'ISC License') {
return "ISC"
}
if ($name -match 'POLYFORM' -or $Text -match 'PolyForm') {
return "PolyForm-family"
}
if ($name -match 'BUSINESS' -or $Text -match 'Business Source License' -or $Text -match '\bBSL\b') {
return "BSL"
}
if ($Text -match 'Custom broad-use license') {
return "custom-broad-use"
}
return "other_root_license_file"
}
function Resolve-RootLicenseSignal {
param([array]$LicenseFileRows)
$signals = @($LicenseFileRows | Select-Object -ExpandProperty Signal -Unique)
if (@($signals).Count -eq 0) {
return ""
}
if (($signals -contains "MPL-2.0") -and
@($signals | Where-Object { $_ -like "GPL*" -or $_ -like "AGPL*" }).Count -gt 0) {
return "MPL-2.0 OR GPL-3.0-or-later"
}
if (($signals -contains "MIT") -and ($signals -contains "Apache-2.0")) {
return "MIT OR Apache-2.0"
}
if (($signals -contains "MIT") -and @($signals | Where-Object { $_ -like "GPL*" -or $_ -like "AGPL*" }).Count -gt 0) {
return "MIT with GPL-family companion file"
}
return ($signals -join " + ")
}
function Get-MetadataSignal {
param(
[string]$Path,
[string]$ManifestLicense
)
$candidates = @("package.json", "Cargo.toml", "pyproject.toml", "setup.cfg", "setup.py", "composer.json")
foreach ($candidate in $candidates) {
$fullPath = Join-Path $Path $candidate
if (-not (Test-Path -LiteralPath $fullPath)) {
continue
}
$text = Get-Content -LiteralPath $fullPath -Raw -Encoding UTF8 -ErrorAction SilentlyContinue
if (-not $text) {
continue
}
if ($candidate -eq "package.json") {
try {
$packageJson = $text | ConvertFrom-Json -ErrorAction Stop
$packageLicense = [string]$packageJson.license
if ($packageLicense) {
$packageLicense = $packageLicense.Trim()
if ($packageLicense -and
$packageLicense -notmatch '^SEE LICENSE IN\b' -and
((-not $ManifestLicense) -or
($ManifestLicense -match [regex]::Escape($packageLicense)) -or
($packageLicense -match [regex]::Escape($ManifestLicense)))) {
return @{
Signal = $packageLicense
Path = $candidate
}
}
}
} catch {
}
}
if ($ManifestLicense -match 'MIT' -and $text -match '\bMIT\b') {
return @{
Signal = "MIT"
Path = $candidate
}
}
}
return $null
}
function Get-CopyrightLine {
param([string]$Text)
foreach ($line in ($Text -split "`r?`n")) {
if ($line -match 'copyright') {
return $line.Trim()
}
}
return ""
}
function Get-MITTextSource {
param([array]$LicenseFileRows)
foreach ($row in $LicenseFileRows) {
if ($row.Signal -eq "MIT") {
return $row
}
}
foreach ($row in $LicenseFileRows) {
if ($row.Text -match 'MIT License') {
return $row
}
}
return $null
}
function Get-SafeFileStem {
param([string]$RepoId)
$stem = $RepoId -replace '[\\/:\*\?"<>\| ]', '__'
return $stem.Trim('_')
}
function Write-Utf8NoBomTextFile {
param(
[string]$Path,
[string]$Content
)
$encoding = [System.Text.UTF8Encoding]::new($false)
[System.IO.File]::WriteAllText($Path, $Content, $encoding)
}
$repoRoot = Split-Path -Parent $PSScriptRoot
$docsRoot = Join-Path $repoRoot "docs"
$generatedRoot = Join-Path $docsRoot "generated\\license_audit"
$manifestPath = "C:\\Workspaces\\HyperTwist\\repos.manifest.json"
$legacyMirrorsRoot = Join-Path $repoRoot "mirrors"
$csvPath = Join-Path $generatedRoot "HYPERTWIST_REPO_LICENSE_EVIDENCE_AUDIT_$Stamp.csv"
$markdownPath = Join-Path $docsRoot "HYPERTWIST_REPO_LICENSE_EVIDENCE_AUDIT_$Stamp.md"
$mitTextDir = Join-Path $generatedRoot "MIT_LICENSE_TEXTS_$Stamp"
$thirdPartyPath = Join-Path $generatedRoot "THIRD_PARTY_NOTICES_MIT_DRAFT_$Stamp.txt"
$canonicalMitText = @"
MIT License
[README-only or metadata-only MIT signal captured by HyperTwist legal audit.
No standalone root MIT LICENSE file was present in the repo-local mirror at the
recorded commit. Preserve the evidence path, source URL, and commit hash from
the paired audit ledger and third-party notices draft.]
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"@
New-Item -ItemType Directory -Path $generatedRoot -Force | Out-Null
New-Item -ItemType Directory -Path $mitTextDir -Force | Out-Null
Get-ChildItem -LiteralPath $mitTextDir -File -ErrorAction SilentlyContinue | Remove-Item -Force
$manifest = Get-Content $manifestPath -Raw -Encoding UTF8 | ConvertFrom-Json
$entries = New-Object System.Collections.Generic.List[object]
foreach ($repo in $manifest.repositories) {
if ($repo.localPath -like 'C:\Workspaces\HyperTwist\mirrors\*') {
$entries.Add([pscustomobject]@{
RepoId = Get-NormalizedRepoId -RepoUrl $repo.repoUrl -FallbackName $repo.name
RepoUrl = $repo.repoUrl
PrimaryPath = $repo.localPath
AllLocalPaths = @($repo.localPath)
CurrentManifestMirrorClass = $repo.mirrorClass
CurrentManifestLicense = $repo.license
CurrentManifestLicenseStatus = $repo.licenseStatus
Source = "manifest"
})
}
}
if (Test-Path -LiteralPath $legacyMirrorsRoot) {
$legacyDirs = @(Get-ChildItem -LiteralPath $legacyMirrorsRoot -Directory -ErrorAction SilentlyContinue)
foreach ($legacyDir in $legacyDirs) {
$gitRoot = Get-GitRoot -Path $legacyDir.FullName
if (-not (Test-StandaloneGitRoot -Path $legacyDir.FullName -GitRoot $gitRoot)) {
continue
}
$repoUrl = Get-RemoteOriginUrl -Path $legacyDir.FullName
$repoId = Get-NormalizedRepoId -RepoUrl $repoUrl -FallbackName $legacyDir.Name
if (@($entries | Where-Object { $_.PrimaryPath -eq $legacyDir.FullName -or $_.RepoId -eq $repoId }).Count -gt 0) {
continue
}
$entries.Add([pscustomobject]@{
RepoId = $repoId
RepoUrl = $repoUrl
PrimaryPath = $legacyDir.FullName
AllLocalPaths = @($legacyDir.FullName)
CurrentManifestMirrorClass = "legacy"
CurrentManifestLicense = ""
CurrentManifestLicenseStatus = ""
Source = "legacy"
})
}
}
$auditRows = New-Object System.Collections.Generic.List[object]
$excludedRows = New-Object System.Collections.Generic.List[object]
foreach ($entry in $entries) {
$path = $entry.PrimaryPath
$exists = Test-Path -LiteralPath $path
if (-not $exists) {
$missingReason = if ($entry.CurrentManifestLicenseStatus -eq "historical-alias-unavailable") {
"historical_alias_not_expected_to_be_mirrored"
} elseif ($entry.CurrentManifestLicenseStatus -eq "upstream-unavailable") {
"upstream_unavailable_or_not_publicly_resolvable"
} else {
"missing_local_path"
}
$excludedRows.Add([pscustomobject]@{
RepoId = $entry.RepoId
RepoUrl = $entry.RepoUrl
Path = $path
GitRoot = ""
Reason = $missingReason
})
continue
}
$gitRoot = Get-GitRoot -Path $path
if (-not (Test-StandaloneGitRoot -Path $path -GitRoot $gitRoot)) {
$reason = "not_a_git_root"
if ($gitRoot) {
$rootRepoId = Get-NormalizedRepoId -RepoUrl (Get-RemoteOriginUrl -Path $gitRoot) -FallbackName (Split-Path $gitRoot -Leaf)
if ($rootRepoId -eq $entry.RepoId) {
$reason = "package_subpath_under_parent_root_authority"
} else {
$reason = "non_standalone_git_subpath"
}
}
$excludedRows.Add([pscustomobject]@{
RepoId = $entry.RepoId
RepoUrl = $entry.RepoUrl
Path = $path
GitRoot = $gitRoot
Reason = $reason
})
continue
}
$licenseFiles = Get-RootFilesByPattern -Path $path -Pattern '^(LICENSE|LICEN[CS]E|COPYING)([-_.].+)?(\..+)?$'
$noticeFiles = Get-RootFilesByPattern -Path $path -Pattern '^NOTICE(\..+)?$'
$readmeFiles = Get-RootFilesByPattern -Path $path -Pattern '^README([-.].+)?(\..+)?$'
$licenseFileRows = foreach ($licenseFile in $licenseFiles) {
$text = Get-Content -LiteralPath $licenseFile.FullName -Raw -Encoding UTF8 -ErrorAction SilentlyContinue
[pscustomobject]@{
Name = $licenseFile.Name
Signal = Get-LicenseSignalFromText -FileName $licenseFile.Name -Text $text
Text = $text
}
}
$resolvedLicenseSignal = Resolve-RootLicenseSignal -LicenseFileRows $licenseFileRows
$evidenceTier = ""
$mitBucket = ""
$evidencePaths = ""
$readmeSignal = ""
$readmeSignalPath = ""
$metadataSignal = ""
$metadataSignalPath = ""
$note = ""
$copyrightLine = ""
if ($resolvedLicenseSignal) {
$evidenceTier = "clear_repo_license_file"
$evidencePaths = ($licenseFiles.Name -join "; ")
if ($resolvedLicenseSignal -match 'MIT') {
$mitBucket = "mit_clear_root_license_file"
$mitSource = Get-MITTextSource -LicenseFileRows $licenseFileRows
if ($mitSource) {
$copyrightLine = Get-CopyrightLine -Text $mitSource.Text
}
} else {
foreach ($licenseFileRow in $licenseFileRows) {
$copyrightLine = Get-CopyrightLine -Text $licenseFileRow.Text
if ($copyrightLine) {
break
}
}
}
} else {
foreach ($readmeFile in $readmeFiles) {
$readmeText = Get-Content -LiteralPath $readmeFile.FullName -Raw -Encoding UTF8 -ErrorAction SilentlyContinue
if ($entry.CurrentManifestLicense -match 'MIT' -and $readmeText -match '\bMIT\b') {
$resolvedLicenseSignal = "MIT"
$evidenceTier = "readme_only"
$mitBucket = "mit_readme_only"
$evidencePaths = $readmeFile.Name
$readmeSignal = "MIT"
$readmeSignalPath = $readmeFile.Name
break
}
}
if (-not $resolvedLicenseSignal) {
$metadataResult = Get-MetadataSignal -Path $path -ManifestLicense $entry.CurrentManifestLicense
if ($metadataResult) {
$resolvedLicenseSignal = $metadataResult.Signal
$evidenceTier = "metadata_only"
if ($resolvedLicenseSignal -eq "MIT") {
$mitBucket = "mit_metadata_only"
}
$evidencePaths = $metadataResult.Path
$metadataSignal = $metadataResult.Signal
$metadataSignalPath = $metadataResult.Path
}
}
}
if (-not $resolvedLicenseSignal) {
$resolvedLicenseSignal = "no_clear_repo_local_license_signal"
}
$repoHead = (git -C $path rev-parse HEAD).Trim()
if ($entry.CurrentManifestLicense -and $resolvedLicenseSignal -ne "no_clear_repo_local_license_signal") {
if ($entry.CurrentManifestLicense -notmatch [regex]::Escape($resolvedLicenseSignal)) {
$note = "Manifest/tracker posture is narrower or more contextual than the root license signal."
}
} elseif ($entry.CurrentManifestLicense -and $resolvedLicenseSignal -eq "no_clear_repo_local_license_signal") {
$note = "Manifest/tracker carries the current legal call, but no standalone repo-local license signal was captured at the mirror root."
}
$licenseFileNames = @($licenseFiles | ForEach-Object { $_.Name })
$noticeFileNames = @($noticeFiles | ForEach-Object { $_.Name })
$readmeFileNames = @($readmeFiles | ForEach-Object { $_.Name })
$auditRows.Add([pscustomobject]@{
repo_id = $entry.RepoId
repo_url = $entry.RepoUrl
primary_path = $path
all_local_paths = ($entry.AllLocalPaths -join "; ")
current_manifest_mirror_class = $entry.CurrentManifestMirrorClass
current_manifest_license = $entry.CurrentManifestLicense
current_manifest_license_status = $entry.CurrentManifestLicenseStatus
license_file_count = @($licenseFiles).Count
license_files = ($licenseFileNames -join "; ")
notice_files = ($noticeFileNames -join "; ")
readme_files = ($readmeFileNames -join "; ")
resolved_license_signal = $resolvedLicenseSignal
evidence_tier = $evidenceTier
mit_bucket = $mitBucket
evidence_paths = $evidencePaths
readme_signal = $readmeSignal
readme_signal_path = $readmeSignalPath
metadata_signal = $metadataSignal
metadata_signal_path = $metadataSignalPath
copyright_line = $copyrightLine
repo_head = $repoHead
note = $note
})
}
$orderedAuditRows = @($auditRows | Sort-Object repo_id)
$orderedAuditRows | Export-Csv -LiteralPath $csvPath -NoTypeInformation -Encoding UTF8
$mitNoticeBlocks = New-Object System.Collections.Generic.List[string]
foreach ($row in $orderedAuditRows | Where-Object { $_.mit_bucket }) {
$mitText = ""
if ($row.mit_bucket -eq "mit_clear_root_license_file") {
$licenseCandidates = ($row.license_files -split '; ' | Where-Object { $_ })
foreach ($candidate in $licenseCandidates) {
$fullPath = Join-Path $row.primary_path $candidate
$candidateText = Get-Content -LiteralPath $fullPath -Raw -Encoding UTF8 -ErrorAction SilentlyContinue
if ((Get-LicenseSignalFromText -FileName $candidate -Text $candidateText) -eq "MIT") {
$mitText = $candidateText
break
}
}
}
if (-not $mitText) {
$mitText = $canonicalMitText
}
$mitFilePath = Join-Path $mitTextDir ("{0}.LICENSE.txt" -f (Get-SafeFileStem -RepoId $row.repo_id))
Write-Utf8NoBomTextFile -Path $mitFilePath -Content ($mitText.TrimEnd() + "`r`n`r`n")
$block = @(
"Component: $($row.repo_id)",
"Source: $($row.repo_url)",
"Version: $($row.repo_head)",
"Evidence tier: $($row.evidence_tier)",
"Evidence path(s): $($row.evidence_paths)"
)
if ($row.readme_signal_path) {
$block += "README evidence: $($row.readme_signal_path)"
}
if ($row.metadata_signal_path) {
$block += "Metadata evidence: $($row.metadata_signal_path)"
}
$block += ""
if ($row.copyright_line) {
$block += $row.copyright_line
$block += ""
}
$block += "Licensed under the MIT License:"
$block += ""
$block += $mitText.TrimEnd()
$block += ""
$block += "-----"
$mitNoticeBlocks.Add(($block -join "`r`n"))
}
Write-Utf8NoBomTextFile -Path $thirdPartyPath -Content (($mitNoticeBlocks -join "`r`n`r`n").TrimEnd() + "`r`n`r`n")
$scannedCount = @($orderedAuditRows).Count
$clearRootCount = @($orderedAuditRows | Where-Object { $_.evidence_tier -eq "clear_repo_license_file" }).Count
$mitClearCount = @($orderedAuditRows | Where-Object { $_.mit_bucket -eq "mit_clear_root_license_file" }).Count
$mitReadmeCount = @($orderedAuditRows | Where-Object { $_.mit_bucket -eq "mit_readme_only" }).Count
$mitMetadataCount = @($orderedAuditRows | Where-Object { $_.mit_bucket -eq "mit_metadata_only" }).Count
$nonMitMetadataCount = @($orderedAuditRows | Where-Object {
$_.evidence_tier -eq "metadata_only" -and
-not $_.mit_bucket
}).Count
$noClearCount = @($orderedAuditRows | Where-Object { $_.resolved_license_signal -eq "no_clear_repo_local_license_signal" }).Count
$noClearWithCanonCount = @($orderedAuditRows | Where-Object {
$_.resolved_license_signal -eq "no_clear_repo_local_license_signal" -and
$_.current_manifest_license
}).Count
$normalizedSubpathCount = @($excludedRows | Where-Object { $_.Reason -eq "package_subpath_under_parent_root_authority" }).Count
$historicalAliasCount = @($excludedRows | Where-Object { $_.Reason -eq "historical_alias_not_expected_to_be_mirrored" }).Count
$remainingExcludedCount = @($excludedRows | Where-Object {
$_.Reason -ne "package_subpath_under_parent_root_authority" -and
$_.Reason -ne "historical_alias_not_expected_to_be_mirrored"
}).Count
$excludedCount = $excludedRows.Count
$markdown = New-Object System.Collections.Generic.List[string]
$markdown.Add("# HyperTwist Repo License Evidence Audit - $Stamp")
$markdown.Add("")
$markdown.Add('Scope: deduped local mirror custody across `C:\Workspaces\HyperTwist\mirrors\{permissive,restrictive}` plus standalone legacy mirrors under `C:\HyperTwist\mirrors`.')
$markdown.Add("")
$markdown.Add("Boundary:")
$markdown.Add("- this is the repo-local legal-evidence pass for mirrored HyperTwist custody, not a rewrite of the broader historical workbook universe")
$markdown.Add("- only standalone git roots count in the live audit")
$markdown.Add("- package-subpath rows that live inside a mirrored monorepo and resolve to the same repo URL are normalized under the parent git-root authority instead of being treated as standalone repo roots")
$markdown.Add("- historical alias rows whose standalone repo surfaces are gone but whose maintained public source surfaces now live elsewhere are kept visible as historical references, not as unresolved mirror-restoration work")
$markdown.Add("- upstream-unavailable rows, unresolved missing mirror paths, and genuinely unrelated non-standalone paths stay visible outside the standalone count instead of being silently discarded")
$markdown.Add('- when later repos are mirrored or their root-license evidence changes, rerun `C:\HyperTwist\scripts\Write-HyperTwistRepoLicenseEvidenceAudit.ps1` in the same pass that updates the legal tracker')
$markdown.Add("")
$markdown.Add("Counts:")
$markdown.Add("- total deduped repos scanned: $scannedCount")
$markdown.Add("- clear repo-local license file or explicit mixed root-license declaration: $clearRootCount")
$markdown.Add("- MIT with clear root license file: $mitClearCount")
$markdown.Add("- MIT from README only, no root license file: $mitReadmeCount")
$markdown.Add("- MIT from repo metadata only, no root license file: $mitMetadataCount")
$markdown.Add("- non-MIT license from repo metadata only, no root license file: $nonMitMetadataCount")
$markdown.Add("- no clear repo-local license signal: $noClearCount")
$markdown.Add("- no-clear bucket where current HyperTwist manifest already carries a legal call: $noClearWithCanonCount")
$markdown.Add("- package-subpath rows normalized under parent root authority: $normalizedSubpathCount")
$markdown.Add("- historical alias rows outside active mirror expectation: $historicalAliasCount")
$markdown.Add("- unresolved upstream-unavailable, missing, or unrelated non-standalone rows outside the live standalone count: $remainingExcludedCount")
$markdown.Add("- total rows outside the standalone live count: $excludedCount")
$markdown.Add("")
$markdown.Add("Artifacts:")
$markdown.Add('- audit CSV: `docs/generated/license_audit/HYPERTWIST_REPO_LICENSE_EVIDENCE_AUDIT_' + $Stamp + '.csv`')
$markdown.Add('- MIT license text capture folder: `docs/generated/license_audit/MIT_LICENSE_TEXTS_' + $Stamp + '/`')
$markdown.Add('- MIT third-party notices draft: `docs/generated/license_audit/THIRD_PARTY_NOTICES_MIT_DRAFT_' + $Stamp + '.txt`')
$markdown.Add("")
$readmeOnlyRows = @($orderedAuditRows | Where-Object { $_.mit_bucket -eq "mit_readme_only" })
$markdown.Add("MIT without root license file - README only:")
if ($readmeOnlyRows.Count -eq 0) {
$markdown.Add("- none")
} else {
foreach ($row in $readmeOnlyRows) {
$markdown.Add("- $($row.repo_id)")
}
}
$markdown.Add("")
$metadataOnlyRows = @($orderedAuditRows | Where-Object { $_.mit_bucket -eq "mit_metadata_only" })
$markdown.Add("MIT without root license file - metadata only:")
if ($metadataOnlyRows.Count -eq 0) {
$markdown.Add("- none")
} else {
foreach ($row in $metadataOnlyRows) {
$markdown.Add("- $($row.repo_id)")
}
}
$markdown.Add("")
$nonMitMetadataRows = @($orderedAuditRows | Where-Object {
$_.evidence_tier -eq "metadata_only" -and
-not $_.mit_bucket
})
$markdown.Add("Non-MIT license from repo metadata only, no root license file:")
if ($nonMitMetadataRows.Count -eq 0) {
$markdown.Add("- none")
} else {
foreach ($row in $nonMitMetadataRows) {
$detail = if ($row.metadata_signal_path) {
"$($row.resolved_license_signal) via ``$($row.metadata_signal_path)``"
} else {
$row.resolved_license_signal
}
$markdown.Add("- $($row.repo_id) - signal: $detail")
}
}
$markdown.Add("")
$noClearRows = @($orderedAuditRows | Where-Object { $_.resolved_license_signal -eq "no_clear_repo_local_license_signal" })
$markdown.Add("No clear repo-local license signal:")
if ($noClearRows.Count -eq 0) {
$markdown.Add("- none")
} else {
foreach ($row in $noClearRows) {
if ($row.current_manifest_license) {
$markdown.Add("- $($row.repo_id) - current manifest call: ``$($row.current_manifest_license)``")
} else {
$markdown.Add("- $($row.repo_id)")
}
}
}
$markdown.Add("")
$normalizedRows = @($excludedRows | Where-Object { $_.Reason -eq "package_subpath_under_parent_root_authority" } | Sort-Object RepoId, Path)
$markdown.Add("Package-subpath rows normalized under parent root authority:")
if ($normalizedRows.Count -eq 0) {
$markdown.Add("- none")
} else {
foreach ($row in $normalizedRows) {
$markdown.Add("- $($row.Path) -> $($row.GitRoot) (``$($row.Reason)``)")
}
}
$markdown.Add("")
$historicalAliasRows = @($excludedRows | Where-Object { $_.Reason -eq "historical_alias_not_expected_to_be_mirrored" } | Sort-Object RepoId, Path)
$markdown.Add("Historical alias rows not expected to be mirrored:")
if ($historicalAliasRows.Count -eq 0) {
$markdown.Add("- none")
} else {
foreach ($row in $historicalAliasRows) {
$markdown.Add("- $($row.Path) (``$($row.Reason)``)")
}
}
$markdown.Add("")
$remainingExcludedRows = @($excludedRows | Where-Object {
$_.Reason -ne "package_subpath_under_parent_root_authority" -and
$_.Reason -ne "historical_alias_not_expected_to_be_mirrored"
} | Sort-Object RepoId, Path)
$markdown.Add("Excluded from the live standalone-git-root count:")
if ($remainingExcludedRows.Count -eq 0) {
$markdown.Add("- none")
} else {
foreach ($row in $remainingExcludedRows) {
if ($row.GitRoot) {
$markdown.Add("- $($row.Path) -> $($row.GitRoot) (``$($row.Reason)``)")
} else {
$markdown.Add("- $($row.Path) (``$($row.Reason)``)")
}
}
}
$markdown.Add("")
Write-Utf8NoBomTextFile -Path $markdownPath -Content (($markdown -join "`r`n").TrimEnd() + "`r`n`r`n")
Write-Output "Wrote:"
Write-Output $csvPath
Write-Output $markdownPath
Write-Output $thirdPartyPath
Write-Output $mitTextDir