713 lines
27 KiB
PowerShell
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
|