From 9885a8c5adf76b59727efc3ff244a51b84f0c250 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Moreau?= Date: Sun, 15 Feb 2026 13:35:47 -0500 Subject: [PATCH 1/2] RDP broken links: Strategy A/B/D + SourceLinkMetadata fix - Strategy A: Build GuidToSection from InternalHyperlinks by matching link text to SectionToTitle (with wildcard escaping for [MS-XXX] patterns) - Strategy B: Cross-paragraph bookmark association for section_ - Strategy D: Build GuidToGlossarySlug from InternalHyperlinks with gt_ anchors - Fix SourceLinkMetadata access in Invoke-OpenSpecMarkdownCleanup for OrderedDictionary (use direct .GuidToSection instead of PSObject.Properties) - Add Analyze-DocxLinkMetadata.ps1 for link metadata analysis - Legal notice extraction, LEGAL.md template, Build-Publish updates - Ignore publish/ in .gitignore Co-authored-by: Cursor --- .gitignore | 3 +- .../Private/ConvertFrom-OpenSpecDocx.ps1 | 95 +++++++++++ .../Invoke-OpenSpecMarkdownCleanup.ps1 | 65 +++++++- .../Public/Convert-OpenSpecToMarkdown.ps1 | 13 ++ scripts/Analyze-DocxLinkMetadata.ps1 | 149 ++++++++++++++++++ scripts/Build-Publish.ps1 | 17 +- scripts/templates/LEGAL.md | 74 +++++++++ 7 files changed, 408 insertions(+), 8 deletions(-) create mode 100644 scripts/Analyze-DocxLinkMetadata.ps1 create mode 100644 scripts/templates/LEGAL.md diff --git a/.gitignore b/.gitignore index ece508e9..dfd847ea 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ artifacts/ downloads*/ converted*/ -reports*/ \ No newline at end of file +reports*/ +publish/ \ No newline at end of file diff --git a/AwakeCoding.OpenSpecs/Private/ConvertFrom-OpenSpecDocx.ps1 b/AwakeCoding.OpenSpecs/Private/ConvertFrom-OpenSpecDocx.ps1 index 9a2859f9..02f2491b 100644 --- a/AwakeCoding.OpenSpecs/Private/ConvertFrom-OpenSpecDocx.ps1 +++ b/AwakeCoding.OpenSpecs/Private/ConvertFrom-OpenSpecDocx.ps1 @@ -110,6 +110,7 @@ function ConvertFrom-OpenSpecDocxWithOpenXml { } $inGlossary = $false $glossaryHeadingLevel = 0 + $pendingSectionGuids = New-Object System.Collections.Generic.List[string] # Resolve media output directory for image extraction. $resolvedMediaDir = $null @@ -173,6 +174,14 @@ function ConvertFrom-OpenSpecDocxWithOpenXml { $sectionAnchor = $anchorInfo.SectionAnchor if (-not [string]::IsNullOrWhiteSpace($sectionAnchor)) { + # Strategy B: Resolve cross-paragraph section GUIDs from previous paragraph + foreach ($g in $pendingSectionGuids) { + if (-not $linkMetadata.GuidToSection.ContainsKey($g)) { + $linkMetadata.GuidToSection[$g] = $sectionAnchor + } + } + $pendingSectionGuids.Clear() + if (-not $linkMetadata.SectionToTitle.ContainsKey($sectionAnchor)) { $linkMetadata.SectionToTitle[$sectionAnchor] = $headingText } @@ -215,6 +224,18 @@ function ConvertFrom-OpenSpecDocxWithOpenXml { } } } + + # Strategy B: Paragraph has section_ bookmarks but no SectionAnchor — defer to next paragraph + if ([string]::IsNullOrWhiteSpace($anchorInfo.SectionAnchor)) { + foreach ($bookmarkName in @($anchorInfo.BookmarkNames)) { + if ($bookmarkName -match '(?i)^section_(?[a-f0-9]{32})$') { + $g = $Matches['guid'].ToLowerInvariant() + if (-not $linkMetadata.GuidToSection.ContainsKey($g) -and -not $pendingSectionGuids.Contains($g)) { + [void]$pendingSectionGuids.Add($g) + } + } + } + } } elseif ($child.LocalName -eq 'tbl') { $tableLines = ConvertFrom-OpenSpecOpenXmlTable -TableNode $child -NamespaceManager $nsmgr -RelationshipMap $relationshipMap -Archive $archive -MediaOutputDirectory $resolvedMediaDir @@ -231,6 +252,80 @@ function ConvertFrom-OpenSpecDocxWithOpenXml { throw 'OpenXml conversion produced empty markdown output.' } + # Strategy A: Build GuidToSection from InternalHyperlinks by matching link text to SectionToTitle + if ($linkMetadata.SectionToTitle.Count -eq 0) { + $headingRegex = [regex]::new('^(?#{1,6})\s+(?\d+(?:\.\d+)*)\s+(?.+)$', [System.Text.RegularExpressions.RegexOptions]::Multiline) + foreach ($m in $headingRegex.Matches($markdown)) { + $sectionAnchor = "Section_$($m.Groups['num'].Value)" + $fullTitle = "$($m.Groups['num'].Value) $($m.Groups['title'].Value.Trim())" + if (-not $linkMetadata.SectionToTitle.ContainsKey($sectionAnchor)) { + $linkMetadata.SectionToTitle[$sectionAnchor] = $fullTitle + } + } + } + $titleToSection = @{} + foreach ($entry in $linkMetadata.SectionToTitle.GetEnumerator()) { + $key = [string]$entry.Key + $val = ([string]$entry.Value -replace '\s+', ' ').Trim() + if (-not [string]::IsNullOrWhiteSpace($val)) { + $titleToSection[$val] = $key + $withoutNum = ($val -replace '^\d+(?:\.\d+)*\s+', '').Trim() + if ($withoutNum -and -not $titleToSection.ContainsKey($withoutNum)) { + $titleToSection[$withoutNum] = $key + } + } + } + $sectionGuidRegex = [regex]::new('^(?:[Ss]ection_)?([a-f0-9]{32})$') + $internalLinksArray = $linkMetadata.InternalHyperlinks.ToArray() + foreach ($link in $internalLinksArray) { + $anchor = [string]$link.Anchor + $text = ([string]$link.Text -replace '\s+', ' ').Trim() + $m = $sectionGuidRegex.Match($anchor) + if (-not $m.Success) { continue } + $guid = $m.Groups[1].Value.ToLowerInvariant() + if ($linkMetadata.GuidToSection.ContainsKey($guid)) { continue } + $matchedSection = $null + if ($titleToSection.ContainsKey($text)) { + $matchedSection = $titleToSection[$text] + } + else { + foreach ($tit in $titleToSection.Keys) { + if ($tit -eq $text) { $matchedSection = $titleToSection[$tit]; break } + $textEsc = [Management.Automation.WildcardPattern]::Escape($text) + $titEsc = [Management.Automation.WildcardPattern]::Escape($tit) + if ($tit -like "*$textEsc*" -and $text.Length -ge 8) { $matchedSection = $titleToSection[$tit]; break } + if ($text -like "*$titEsc*" -and $tit.Length -ge 8) { $matchedSection = $titleToSection[$tit]; break } + } + } + if ($matchedSection) { + $linkMetadata.GuidToSection[$guid] = $matchedSection + } + } + + # Strategy D: Build GuidToGlossarySlug from InternalHyperlinks with gt_<guid> anchors + $termToSlug = @{} + $glossaryDefRegex = [regex]::new('^\s*\*\*(?<term>[^*]+)\*\*\s*:\s*', [System.Text.RegularExpressions.RegexOptions]::Multiline) + foreach ($gm in $glossaryDefRegex.Matches($markdown)) { + $term = $gm.Groups['term'].Value.Trim() + $slug = Get-OpenSpecGlossarySlugFromTerm -Term $term + $termToSlug[$term] = $slug + if ($term -match '^(.+?)\s+\(([^)]+)\)\s*$') { + $termToSlug[$Matches[2].Trim()] = $slug + } + } + $gtGuidRegex = [regex]::new('^gt_([a-f0-9\-]{36})$', [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) + foreach ($link in $internalLinksArray) { + $anchor = [string]$link.Anchor + $text = ([string]$link.Text -replace '\s+', ' ').Trim() + $m = $gtGuidRegex.Match($anchor) + if (-not $m.Success) { continue } + $guid = $m.Groups[1].Value.ToLowerInvariant() + if ($linkMetadata.GuidToGlossarySlug.ContainsKey($guid)) { continue } + if ($termToSlug.ContainsKey($text)) { + $linkMetadata.GuidToGlossarySlug[$guid] = $termToSlug[$text] + } + } + $linkMetadata.Stats.GuidSectionMapCount = $linkMetadata.GuidToSection.Count $linkMetadata.Stats.TocAliasCount = $linkMetadata.TocAlias.Count $linkMetadata.Stats.GlossaryGuidMapCount = $linkMetadata.GuidToGlossarySlug.Count diff --git a/AwakeCoding.OpenSpecs/Private/Invoke-OpenSpecMarkdownCleanup.ps1 b/AwakeCoding.OpenSpecs/Private/Invoke-OpenSpecMarkdownCleanup.ps1 index 40d56686..13380789 100644 --- a/AwakeCoding.OpenSpecs/Private/Invoke-OpenSpecMarkdownCleanup.ps1 +++ b/AwakeCoding.OpenSpecs/Private/Invoke-OpenSpecMarkdownCleanup.ps1 @@ -68,7 +68,7 @@ function Invoke-OpenSpecMarkdownCleanup { $result = $tocResult.Markdown foreach ($issue in $tocResult.Issues) { [void]$issues.Add($issue) } - $sourceGuidToSection = if ($SourceLinkMetadata -and $SourceLinkMetadata.PSObject.Properties['GuidToSection']) { $SourceLinkMetadata.GuidToSection } else { $null } + $sourceGuidToSection = if ($SourceLinkMetadata -and $SourceLinkMetadata.GuidToSection) { $SourceLinkMetadata.GuidToSection } else { $null } $guidResult = Resolve-OpenSpecGuidSectionAnchors -Markdown $result -GuidToSectionMap $sourceGuidToSection $result = $guidResult.Markdown foreach ($issue in $guidResult.Issues) { [void]$issues.Add($issue) } @@ -110,7 +110,7 @@ function Invoke-OpenSpecMarkdownCleanup { }) } - $sourceSectionToTitle = if ($SourceLinkMetadata -and $SourceLinkMetadata.PSObject.Properties['SectionToTitle']) { $SourceLinkMetadata.SectionToTitle } else { $null } + $sourceSectionToTitle = if ($SourceLinkMetadata -and $null -ne $SourceLinkMetadata.SectionToTitle) { $SourceLinkMetadata.SectionToTitle } else { $null } $guidByHeadingResult = Repair-OpenSpecSectionGuidLinksByHeadingMatch -Markdown $result -SectionToTitleMap $sourceSectionToTitle $result = $guidByHeadingResult.Markdown if ($guidByHeadingResult.LinksRepaired -gt 0) { @@ -122,7 +122,7 @@ function Invoke-OpenSpecMarkdownCleanup { }) } - $sourceGuidToGlossarySlug = if ($SourceLinkMetadata -and $SourceLinkMetadata.PSObject.Properties['GuidToGlossarySlug']) { $SourceLinkMetadata.GuidToGlossarySlug } else { $null } + $sourceGuidToGlossarySlug = if ($SourceLinkMetadata -and $null -ne $SourceLinkMetadata.GuidToGlossarySlug) { $SourceLinkMetadata.GuidToGlossarySlug } else { $null } $glossaryResult = Add-OpenSpecGlossaryAnchorsAndRepairLinks -Markdown $result -GuidToGlossarySlugMap $sourceGuidToGlossarySlug $result = $glossaryResult.Markdown if ($glossaryResult.AnchorsInjected -gt 0 -or $glossaryResult.LinksRepaired -gt 0) { @@ -146,14 +146,21 @@ function Invoke-OpenSpecMarkdownCleanup { }) } + $legalResult = Add-LegalNoticeLinkAfterToc -Markdown $result + $result = $legalResult.Markdown + $newLine = [Environment]::NewLine $result = [regex]::Replace($result, "(`r?`n){3,}", "$newLine$newLine") - [pscustomobject]@{ + $out = [pscustomobject]@{ PSTypeName = 'AwakeCoding.OpenSpecs.MarkdownCleanupResult' Markdown = $result Issues = $issues.ToArray() } + if ($frontMatterResult.Removed -and $frontMatterResult.PSObject.Properties['ExtractedBoilerplate']) { + Add-Member -InputObject $out -NotePropertyName 'ExtractedBoilerplate' -NotePropertyValue $frontMatterResult.ExtractedBoilerplate + } + $out } function ConvertFrom-OpenSpecHtmlTables { @@ -935,6 +942,50 @@ function ConvertTo-OpenSpecGitHubFriendlyToc { } } +function Add-LegalNoticeLinkAfterToc { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$Markdown + ) + + $newLine = [Environment]::NewLine + $legalLine = "For the legal notice and IP terms, see [LEGAL.md](../LEGAL.md)." + + $sectionAnchorRegex = [regex]::new('<a\s+id="Section_\d', [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) + $firstSectionMatch = $sectionAnchorRegex.Match($Markdown) + if (-not $firstSectionMatch.Success) { + return [pscustomobject]@{ Markdown = $Markdown } + } + $beforeContent = $Markdown.Substring(0, $firstSectionMatch.Index) + + $detailsCloseRegex = [regex]::new('</details>', [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) + $lastDetailsMatch = $null + foreach ($m in $detailsCloseRegex.Matches($beforeContent)) { + $lastDetailsMatch = $m + } + if (-not $lastDetailsMatch) { + return [pscustomobject]@{ Markdown = $Markdown } + } + + $insertEnd = $lastDetailsMatch.Index + $lastDetailsMatch.Length + $trailing = $beforeContent.Substring($insertEnd) + $trailingNewlines = '' + if ($trailing -match '^(\r?\n)+') { + $trailingNewlines = $Matches[1] + $insertEnd += $Matches[1].Length + } + $before = $Markdown.Substring(0, $insertEnd) + $after = $Markdown.Substring($insertEnd) + + $insertion = $trailingNewlines + $legalLine + $newLine + $newLine + $result = $before + $insertion + $after + + [pscustomobject]@{ + Markdown = $result + } +} + function ConvertTo-OpenSpecNormalizedEncodedBracketUrls { [CmdletBinding()] param( @@ -1414,10 +1465,14 @@ function Remove-OpenSpecFrontMatterBoilerplate { $removed = $true } - [pscustomobject]@{ + $out = [pscustomobject]@{ Markdown = $result Removed = $removed } + if ($removed -and $blockContent) { + Add-Member -InputObject $out -NotePropertyName 'ExtractedBoilerplate' -NotePropertyValue $blockContent + } + $out } function Add-OpenSpecSectionAnchors { diff --git a/AwakeCoding.OpenSpecs/Public/Convert-OpenSpecToMarkdown.ps1 b/AwakeCoding.OpenSpecs/Public/Convert-OpenSpecToMarkdown.ps1 index da8f2b73..2073096a 100644 --- a/AwakeCoding.OpenSpecs/Public/Convert-OpenSpecToMarkdown.ps1 +++ b/AwakeCoding.OpenSpecs/Public/Convert-OpenSpecToMarkdown.ps1 @@ -191,6 +191,19 @@ function Convert-OpenSpecToMarkdown { $cleaned.Markdown | Set-Content -LiteralPath $markdownPath -Encoding UTF8 + if ($protocolId -eq 'MS-DTYP' -and $cleaned.PSObject.Properties['ExtractedBoilerplate']) { + $legalDir = Join-Path -Path $OutputPath -ChildPath '_legal' + if (-not (Test-Path -LiteralPath $legalDir)) { + [void](New-Item -Path $legalDir -ItemType Directory -Force) + } + $legalContent = $cleaned.ExtractedBoilerplate.Trim() + if ($legalContent -and -not ($legalContent -match '^(#|\*\*[^*]+\*\*)')) { + $legalContent = "# Intellectual Property Rights Notice for Open Specifications Documentation`n`n" + $legalContent + } + $legalPath = Join-Path -Path $legalDir -ChildPath 'LEGAL.md' + $legalContent | Set-Content -LiteralPath $legalPath -Encoding UTF8 + } + $layoutModelPath = Join-Path -Path $artifactDirectory -ChildPath 'layout-model.json' $allIssues | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $layoutModelPath -Encoding UTF8 diff --git a/scripts/Analyze-DocxLinkMetadata.ps1 b/scripts/Analyze-DocxLinkMetadata.ps1 new file mode 100644 index 00000000..c73fd248 --- /dev/null +++ b/scripts/Analyze-DocxLinkMetadata.ps1 @@ -0,0 +1,149 @@ +<# +.SYNOPSIS + Analyzes DOCX link metadata from Open Spec conversion for broken-link repair strategies. +.DESCRIPTION + Converts a spec DOCX to markdown, dumps LinkMetadata (GuidToSection, InternalHyperlinks, + SectionToTitle, GuidToGlossarySlug) to JSON, and reports which InternalHyperlink anchors + could be resolved via SectionToTitle or glossary term matching. +.EXAMPLE + .\Analyze-DocxLinkMetadata.ps1 -ProtocolId MS-RDPBCGR +.EXAMPLE + .\Analyze-DocxLinkMetadata.ps1 -DocxPath path\to\[MS-RDPBCGR].docx +#> +[CmdletBinding()] +param( + [Parameter(ParameterSetName = 'ByProtocol')] + [string]$ProtocolId = 'MS-RDPBCGR', + + [Parameter(ParameterSetName = 'ByPath')] + [string]$DocxPath, + + [string]$DownloadsPath = (Join-Path (Get-Location) 'downloads-convert'), + [string]$OutputDir = (Join-Path (Get-Location) 'artifacts\docx-analysis') +) + +$ErrorActionPreference = 'Stop' +$repoRoot = (Get-Item $PSScriptRoot).Parent.FullName +$privateDir = Join-Path $repoRoot 'AwakeCoding.OpenSpecs\Private' +Get-ChildItem -LiteralPath $privateDir -Filter '*.ps1' | ForEach-Object { . $_.FullName } + +if (-not (Test-Path -LiteralPath $OutputDir)) { + [void](New-Item -Path $OutputDir -ItemType Directory -Force) +} + +$docxFile = $null +if ($DocxPath) { + if (-not (Test-Path -LiteralPath $DocxPath)) { + throw "DOCX not found: $DocxPath" + } + $docxFile = $DocxPath +} +else { + Import-Module (Join-Path $repoRoot 'AwakeCoding.OpenSpecs') -Force + $dl = Save-OpenSpecDocument -ProtocolId $ProtocolId -Format DOCX -OutputPath $DownloadsPath -Force + $docxFile = $dl.Path + if (-not $docxFile -or -not (Test-Path -LiteralPath $docxFile)) { + throw "Failed to download DOCX for $ProtocolId" + } +} + +$outMd = Join-Path $OutputDir "raw-$([System.IO.Path]::GetFileNameWithoutExtension($docxFile)).md" +$toolchain = [pscustomobject]@{ HasOpenXml = $false } + +$result = ConvertFrom-OpenSpecDocxWithOpenXml -InputPath $docxFile -OutputPath $outMd -Toolchain $toolchain +$meta = $result.LinkMetadata + +$metaPath = Join-Path $OutputDir "link-metadata.json" +$metaForJson = [ordered]@{ + GuidToSection = $meta.GuidToSection + SectionToTitle = $meta.SectionToTitle + TocAlias = $meta.TocAlias + GuidToGlossarySlug = $meta.GuidToGlossarySlug + InternalHyperlinksCount = @($meta.InternalHyperlinks).Count + InternalHyperlinks = @($meta.InternalHyperlinks | Select-Object -First 500) +} +$metaForJson | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $metaPath -Encoding UTF8 + +Write-Host "`n=== Link Metadata Summary ===" +Write-Host "GuidToSection: $($meta.GuidToSection.Count)" +Write-Host "SectionToTitle: $($meta.SectionToTitle.Count)" +Write-Host "GuidToGlossarySlug: $($meta.GuidToGlossarySlug.Count)" +Write-Host "InternalHyperlinks: $(@($meta.InternalHyperlinks).Count)" +Write-Host "Metadata dumped to: $metaPath" + +$sectionGuidRegex = [regex]::new('^(?:[Ss]ection_)?([a-f0-9]{32})$') +$gtGuidRegex = [regex]::new('^gt_([a-f0-9\-]{36})$', 'IgnoreCase') +$titleToSection = @{} +foreach ($entry in $meta.SectionToTitle.GetEnumerator()) { + $title = ([string]$entry.Value -replace '\s+', ' ').Trim() + if (-not [string]::IsNullOrWhiteSpace($title) -and -not $titleToSection.ContainsKey($title)) { + $titleToSection[$title] = [string]$entry.Key + } +} +$resolvableFromHyperlinks = [System.Collections.Generic.List[object]]::new() +$unresolvableSectionGuids = [System.Collections.Generic.List[string]]::new() +$unresolvableGtGuids = [System.Collections.Generic.List[string]]::new() + +foreach ($link in @($meta.InternalHyperlinks)) { + $anchor = [string]$link.Anchor + $text = ([string]$link.Text -replace '\s+', ' ').Trim() + + $m = $sectionGuidRegex.Match($anchor) + if ($m.Success) { + $guid = $m.Groups[1].Value.ToLowerInvariant() + if (-not $meta.GuidToSection.ContainsKey($guid)) { + $matchedSection = $null + foreach ($key in $titleToSection.Keys) { + if ($key -eq $text) { $matchedSection = $titleToSection[$key]; break } + $textEsc = [Management.Automation.WildcardPattern]::Escape($text) + $keyEsc = [Management.Automation.WildcardPattern]::Escape($key) + if ($key -like "*$textEsc*" -and $text.Length -ge 10) { $matchedSection = $titleToSection[$key]; break } + if ($text -like "*$keyEsc*" -and $key.Length -ge 10) { $matchedSection = $titleToSection[$key]; break } + } + if ($matchedSection) { + [void]$resolvableFromHyperlinks.Add([pscustomobject]@{ Type = 'Section'; Guid = $guid; Text = $text; MatchedSection = $matchedSection }) + } + else { + [void]$unresolvableSectionGuids.Add($guid) + } + } + } + else { + $gtm = $gtGuidRegex.Match($anchor) + if ($gtm.Success) { + $guid = $gtm.Groups[1].Value.ToLowerInvariant() + if (-not $meta.GuidToGlossarySlug.ContainsKey($guid)) { + [void]$unresolvableGtGuids.Add($guid) + } + } + } +} + +Write-Host "`n=== Strategy A: InternalHyperlinks + SectionToTitle ===" +Write-Host "Resolvable (section guid -> section number via link text): $($resolvableFromHyperlinks.Count)" +foreach ($r in ($resolvableFromHyperlinks | Select-Object -First 10)) { + Write-Host " $($r.Guid) -> $($r.MatchedSection) (text: $($r.Text))" +} +if ($resolvableFromHyperlinks.Count -gt 10) { + Write-Host " ... and $($resolvableFromHyperlinks.Count - 10) more" +} +Write-Host "Unresolvable section GUIDs (no SectionToTitle match): $(($unresolvableSectionGuids | Select-Object -Unique).Count)" +Write-Host "Unresolvable glossary GUIDs: $(($unresolvableGtGuids | Select-Object -Unique).Count)" + +$reportPath = Join-Path $OutputDir "analysis-report.txt" +@" +Docx: $docxFile +Generated: $(Get-Date -Format 'o') + +Metadata: $metaPath + +GuidToSection: $($meta.GuidToSection.Count) +SectionToTitle: $($meta.SectionToTitle.Count) +GuidToGlossarySlug: $($meta.GuidToGlossarySlug.Count) +InternalHyperlinks: $(@($meta.InternalHyperlinks).Count) + +Strategy A - Resolvable from InternalHyperlinks: $($resolvableFromHyperlinks.Count) +Unresolvable section GUIDs: $(($unresolvableSectionGuids | Select-Object -Unique).Count) +Unresolvable glossary GUIDs: $(($unresolvableGtGuids | Select-Object -Unique).Count) +"@ | Set-Content -LiteralPath $reportPath -Encoding UTF8 +Write-Host "`nReport: $reportPath" diff --git a/scripts/Build-Publish.ps1 b/scripts/Build-Publish.ps1 index ed442ccf..096b8529 100644 --- a/scripts/Build-Publish.ps1 +++ b/scripts/Build-Publish.ps1 @@ -57,9 +57,9 @@ try { } if ($patterns.Count -gt 0) { $catalog = $catalog | Where-Object { - $pid = $_.ProtocolId + $protocolId = $_.ProtocolId foreach ($p in $patterns) { - if ($pid -like $p) { return $true } + if ($protocolId -like $p) { return $true } } return $false } @@ -99,6 +99,19 @@ try { } } + $legalSource = Join-Path (Join-Path $convPath '_legal') 'LEGAL.md' + $legalFallback = Join-Path (Join-Path (Join-Path $root 'scripts') 'templates') 'LEGAL.md' + $legalDest = Join-Path $pubPath 'LEGAL.md' + if (Test-Path -LiteralPath $legalSource) { + Copy-Item -LiteralPath $legalSource -Destination $legalDest -Force + } + elseif (Test-Path -LiteralPath $legalFallback) { + Copy-Item -LiteralPath $legalFallback -Destination $legalDest -Force + } + else { + Write-Warning 'LEGAL.md was not created (filter may have excluded MS-DTYP). Add scripts/templates/LEGAL.md as fallback.' + } + Write-Host 'Updating index (README.md)...' Update-OpenSpecIndex -Path $pubPath -Title $IndexTitle diff --git a/scripts/templates/LEGAL.md b/scripts/templates/LEGAL.md new file mode 100644 index 00000000..1514da1f --- /dev/null +++ b/scripts/templates/LEGAL.md @@ -0,0 +1,74 @@ +# Intellectual Property Rights Notice for Open Specifications Documentation + +Intellectual Property Rights Notice for Open Specifications Documentation + +- **Technical Documentation.** Microsoft publishes Open Specifications documentation ("this documentation") for protocols, file formats, data portability, computer languages, and standards support. Additionally, overview documents cover inter-protocol relationships and interactions. +- **Copyrights**. This documentation is covered by Microsoft copyrights. Regardless of any other terms that are contained in the terms of use for the Microsoft website that hosts this documentation, you can make copies of it in order to develop implementations of the technologies that are described in this documentation and can distribute portions of it in your implementations that use these technologies or in your documentation as necessary to properly document the implementation. You can also distribute in your implementation, with or without modification, any schemas, IDLs, or code samples that are included in the documentation. This permission also applies to any documents that are referenced in the Open Specifications documentation. +- **No Trade Secrets**. Microsoft does not claim any trade secret rights in this documentation. +- **Patents**. Microsoft has patents that might cover your implementations of the technologies described in the Open Specifications documentation. Neither this notice nor Microsoft's delivery of this documentation grants any licenses under those patents or any other Microsoft patents. However, a given Open Specifications document might be covered by the Microsoft [Open Specifications Promise](https://go.microsoft.com/fwlink/?LinkId=214445) or the [Microsoft Community Promise](https://go.microsoft.com/fwlink/?LinkId=214448). If you would prefer a written license, or if the technologies described in this documentation are not covered by the Open Specifications Promise or Community Promise, as applicable, patent licenses are available by contacting [iplg@microsoft.com](mailto:iplg@microsoft.com). +- **License Programs**. To see all of the protocols in scope under a specific license program and the associated patents, visit the [Patent Map](https://aka.ms/AA9ufj8). +- **Trademarks**. The names of companies and products contained in this documentation might be covered by trademarks or similar intellectual property rights. This notice does not grant any licenses under those rights. For a list of Microsoft trademarks, visit [www.microsoft.com/trademarks](https://www.microsoft.com/trademarks). +- **Fictitious Names**. The example companies, organizations, products, domain names, email addresses, logos, people, places, and events that are depicted in this documentation are fictitious. No association with any real company, organization, product, domain name, email address, logo, person, place, or event is intended or should be inferred. +**Reservation of Rights**. All other rights are reserved, and this notice does not grant any rights other than as specifically described above, whether by implication, estoppel, or otherwise. + +**Tools**. The Open Specifications documentation does not require the use of Microsoft programming tools or programming environments in order for you to develop an implementation. If you have access to Microsoft programming tools and environments, you are free to take advantage of them. Certain Open Specifications documents are intended for use in conjunction with publicly available standards specifications and network programming art and, as such, assume that the reader either is familiar with the aforementioned material or has immediate access to it. + +**Support.** For questions and support, please contact [dochelp@microsoft.com](mailto:dochelp@microsoft.com). + +**Revision Summary** + +| Date | Revision History | Revision Class | Comments | +| --- | --- | --- | --- | +| 2/14/2008 | 3.1.2 | Editorial | Changed language and formatting in the technical content. | +| 3/14/2008 | 4.0 | Major | Updated and revised the technical content. | +| 6/20/2008 | 5.0 | Major | Updated and revised the technical content. | +| 7/25/2008 | 6.0 | Major | Updated and revised the technical content. | +| 8/29/2008 | 7.0 | Major | Updated and revised the technical content. | +| 10/24/2008 | 8.0 | Major | Updated and revised the technical content. | +| 12/5/2008 | 9.0 | Major | Updated and revised the technical content. | +| 1/16/2009 | 9.0.1 | Editorial | Changed language and formatting in the technical content. | +| 2/27/2009 | 10.0 | Major | Updated and revised the technical content. | +| 4/10/2009 | 10.1 | Minor | Clarified the meaning of the technical content. | +| 5/22/2009 | 11.0 | Major | Updated and revised the technical content. | +| 7/2/2009 | 11.1 | Minor | Clarified the meaning of the technical content. | +| 8/14/2009 | 11.2 | Minor | Clarified the meaning of the technical content. | +| 9/25/2009 | 12.0 | Major | Updated and revised the technical content. | +| 11/6/2009 | 12.1 | Minor | Clarified the meaning of the technical content. | +| 12/18/2009 | 12.2 | Minor | Clarified the meaning of the technical content. | +| 1/29/2010 | 13.0 | Major | Updated and revised the technical content. | +| 3/12/2010 | 13.1 | Minor | Clarified the meaning of the technical content. | +| 4/23/2010 | 13.2 | Minor | Clarified the meaning of the technical content. | +| 6/4/2010 | 14.0 | Major | Updated and revised the technical content. | +| 7/16/2010 | 15.0 | Major | Updated and revised the technical content. | +| 8/27/2010 | 16.0 | Major | Updated and revised the technical content. | +| 10/8/2010 | 17.0 | Major | Updated and revised the technical content. | +| 11/19/2010 | 18.0 | Major | Updated and revised the technical content. | +| 1/7/2011 | 19.0 | Major | Updated and revised the technical content. | +| 2/11/2011 | 20.0 | Major | Updated and revised the technical content. | +| 3/25/2011 | 21.0 | Major | Updated and revised the technical content. | +| 5/6/2011 | 21.1 | Minor | Clarified the meaning of the technical content. | +| 6/17/2011 | 22.0 | Major | Updated and revised the technical content. | +| 9/23/2011 | 22.0 | None | No changes to the meaning, language, or formatting of the technical content. | +| 12/16/2011 | 23.0 | Major | Updated and revised the technical content. | +| 3/30/2012 | 24.0 | Major | Updated and revised the technical content. | +| 7/12/2012 | 24.0 | None | No changes to the meaning, language, or formatting of the technical content. | +| 10/25/2012 | 25.0 | Major | Updated and revised the technical content. | +| 1/31/2013 | 25.1 | Minor | Clarified the meaning of the technical content. | +| 8/8/2013 | 26.0 | Major | Updated and revised the technical content. | +| 11/14/2013 | 27.0 | Major | Updated and revised the technical content. | +| 2/13/2014 | 27.1 | Minor | Clarified the meaning of the technical content. | +| 5/15/2014 | 28.0 | Major | Updated and revised the technical content. | +| 6/30/2015 | 29.0 | Major | Significantly changed the technical content. | +| 10/16/2015 | 30.0 | Major | Significantly changed the technical content. | +| 7/14/2016 | 31.0 | Major | Significantly changed the technical content. | +| 6/1/2017 | 32.0 | Major | Significantly changed the technical content. | +| 9/15/2017 | 33.0 | Major | Significantly changed the technical content. | +| 12/1/2017 | 34.0 | Major | Significantly changed the technical content. | +| 9/12/2018 | 35.0 | Major | Significantly changed the technical content. | +| 4/7/2021 | 36.0 | Major | Significantly changed the technical content. | +| 6/25/2021 | 37.0 | Major | Significantly changed the technical content. | +| 4/29/2022 | 38.0 | Major | Significantly changed the technical content. | +| 4/4/2023 | 39.0 | Major | Significantly changed the technical content. | +| 8/28/2023 | 40.0 | Major | Significantly changed the technical content. | +| 12/12/2023 | 41.0 | Major | Significantly changed the technical content. | +| 11/19/2024 | 42.0 | Major | Significantly changed the technical content. | From e98d99b5263121945b542c842e1033bf99ec8a4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Moreau?= <marcandre.moreau@gmail.com> Date: Sun, 15 Feb 2026 20:09:27 -0500 Subject: [PATCH 2/2] Fix revision history: remove duplicate heading, optimize table for GFM - ConvertTo-OpenSpecGfmRevisionTable: strip **Revision Summary**/**Revision History** line (heading comes from section) - Rename table header column 'Revision History' to 'Version' for clarity - LEGAL.md template: revision history removed (per-doc history at end of each spec) Co-authored-by: Cursor <cursoragent@cursor.com> --- .../Invoke-OpenSpecMarkdownCleanup.ps1 | 79 ++++++++++++++++--- scripts/templates/LEGAL.md | 58 -------------- 2 files changed, 68 insertions(+), 69 deletions(-) diff --git a/AwakeCoding.OpenSpecs/Private/Invoke-OpenSpecMarkdownCleanup.ps1 b/AwakeCoding.OpenSpecs/Private/Invoke-OpenSpecMarkdownCleanup.ps1 index 13380789..2baf01dc 100644 --- a/AwakeCoding.OpenSpecs/Private/Invoke-OpenSpecMarkdownCleanup.ps1 +++ b/AwakeCoding.OpenSpecs/Private/Invoke-OpenSpecMarkdownCleanup.ps1 @@ -146,7 +146,12 @@ function Invoke-OpenSpecMarkdownCleanup { }) } - $legalResult = Add-LegalNoticeLinkAfterToc -Markdown $result + $extractedRev = $frontMatterResult.ExtractedRevisionHistory + if ($extractedRev) { + $result = Add-OpenSpecRevisionHistorySectionAtEnd -Markdown $result -RevisionHistory $extractedRev + } + + $legalResult = Add-LegalNoticeLinkAfterToc -Markdown $result -LastUpdated $frontMatterResult.LastUpdated -HasRevisionHistory:($null -ne $extractedRev) $result = $legalResult.Markdown $newLine = [Environment]::NewLine @@ -942,15 +947,40 @@ function ConvertTo-OpenSpecGitHubFriendlyToc { } } +function Add-OpenSpecRevisionHistorySectionAtEnd { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$Markdown, + [Parameter(Mandatory)] + [string]$RevisionHistory + ) + $newLine = [Environment]::NewLine + $section = $newLine + $newLine + "<a id=`"revision-history`"></a>" + $newLine + $newLine + "## Revision History" + $newLine + $newLine + $RevisionHistory.Trim() + return $Markdown.TrimEnd() + $section +} + function Add-LegalNoticeLinkAfterToc { [CmdletBinding()] param( [Parameter(Mandatory)] - [string]$Markdown + [string]$Markdown, + [Parameter()] + [string]$LastUpdated, + [Parameter()] + [switch]$HasRevisionHistory ) $newLine = [Environment]::NewLine - $legalLine = "For the legal notice and IP terms, see [LEGAL.md](../LEGAL.md)." + $lines = New-Object System.Collections.Generic.List[string] + $lines.Add("For the legal notice and IP terms, see [LEGAL.md](../LEGAL.md).") + if ($LastUpdated) { + $lines.Add("Last updated: $LastUpdated.") + } + if ($HasRevisionHistory) { + $lines.Add("See [Revision History](#revision-history) for full version history.") + } + $legalBlock = $lines -join $newLine $sectionAnchorRegex = [regex]::new('<a\s+id="Section_\d', [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) $firstSectionMatch = $sectionAnchorRegex.Match($Markdown) @@ -978,7 +1008,7 @@ function Add-LegalNoticeLinkAfterToc { $before = $Markdown.Substring(0, $insertEnd) $after = $Markdown.Substring($insertEnd) - $insertion = $trailingNewlines + $legalLine + $newLine + $newLine + $insertion = $trailingNewlines + $legalBlock + $newLine + $newLine $result = $before + $insertion + $after [pscustomobject]@{ @@ -1438,6 +1468,9 @@ function Remove-OpenSpecFrontMatterBoilerplate { $result = $Markdown $removed = $false $newLine = [Environment]::NewLine + $blockContent = $null + $extractedRevisionHistory = $null + $lastUpdated = $null # Block from "Intellectual Property Rights Notice" (or similar) through the revision table, ending before "Table of Contents". $blockRegex = [regex]::new( @@ -1447,20 +1480,24 @@ function Remove-OpenSpecFrontMatterBoilerplate { $match = $blockRegex.Match($result) if ($match.Success) { $blockContent = $match.Groups[2].Value - $lastUpdated = $null $dateRowRegex = [regex]::new('\|\s*(\d{1,2}/\d{1,2}/\d{4})\s*\|') $dateMatches = $dateRowRegex.Matches($blockContent) if ($dateMatches.Count -gt 0) { $lastMatch = $dateMatches[$dateMatches.Count - 1] $lastUpdated = $lastMatch.Groups[1].Value } - $replacement = $match.Groups[1].Value - if ($lastUpdated) { - $replacement += "Last updated: $lastUpdated" + $newLine + $newLine - } else { - $replacement += $match.Groups[3].Value + + # Split into legal part (for LEGAL.md) and revision history (for end of document). + $revisionStartRegex = [regex]::new('\*\*Revision Summary\*\*', [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) + $revMatch = $revisionStartRegex.Match($blockContent) + if ($revMatch.Success) { + $legalPart = $blockContent.Substring(0, $revMatch.Index).TrimEnd() + $revisionPart = $blockContent.Substring($revMatch.Index).Trim() + $extractedRevisionHistory = ConvertTo-OpenSpecGfmRevisionTable -RevisionMarkdown $revisionPart + $blockContent = $legalPart } - $replacement += $match.Groups[4].Value + + $replacement = $match.Groups[1].Value + $match.Groups[4].Value $result = $result.Substring(0, $match.Index) + $replacement + $result.Substring($match.Index + $match.Length) $removed = $true } @@ -1472,9 +1509,29 @@ function Remove-OpenSpecFrontMatterBoilerplate { if ($removed -and $blockContent) { Add-Member -InputObject $out -NotePropertyName 'ExtractedBoilerplate' -NotePropertyValue $blockContent } + if ($extractedRevisionHistory) { + Add-Member -InputObject $out -NotePropertyName 'ExtractedRevisionHistory' -NotePropertyValue $extractedRevisionHistory + } + if ($lastUpdated) { + Add-Member -InputObject $out -NotePropertyName 'LastUpdated' -NotePropertyValue $lastUpdated + } $out } +function ConvertTo-OpenSpecGfmRevisionTable { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$RevisionMarkdown + ) + $result = $RevisionMarkdown.Trim() + # Remove standalone **Revision Summary** or **Revision History** line (heading comes from Add-OpenSpecRevisionHistorySectionAtEnd). + $result = $result -replace '(?im)^\s*\*\*Revision (?:Summary|History)\*\*\s*\r?\n', '' + # Rename table header column "Revision History" to "Version" for GFM clarity (version numbers column). + $result = $result -replace '(?m)^(\|\s*Date\s*\|)\s*Revision History\s*(\|\s*Revision Class\s*\|)', '$1 Version $2' + return $result.Trim() +} + function Add-OpenSpecSectionAnchors { [CmdletBinding()] param( diff --git a/scripts/templates/LEGAL.md b/scripts/templates/LEGAL.md index 1514da1f..02b0de12 100644 --- a/scripts/templates/LEGAL.md +++ b/scripts/templates/LEGAL.md @@ -14,61 +14,3 @@ Intellectual Property Rights Notice for Open Specifications Documentation **Tools**. The Open Specifications documentation does not require the use of Microsoft programming tools or programming environments in order for you to develop an implementation. If you have access to Microsoft programming tools and environments, you are free to take advantage of them. Certain Open Specifications documents are intended for use in conjunction with publicly available standards specifications and network programming art and, as such, assume that the reader either is familiar with the aforementioned material or has immediate access to it. **Support.** For questions and support, please contact [dochelp@microsoft.com](mailto:dochelp@microsoft.com). - -**Revision Summary** - -| Date | Revision History | Revision Class | Comments | -| --- | --- | --- | --- | -| 2/14/2008 | 3.1.2 | Editorial | Changed language and formatting in the technical content. | -| 3/14/2008 | 4.0 | Major | Updated and revised the technical content. | -| 6/20/2008 | 5.0 | Major | Updated and revised the technical content. | -| 7/25/2008 | 6.0 | Major | Updated and revised the technical content. | -| 8/29/2008 | 7.0 | Major | Updated and revised the technical content. | -| 10/24/2008 | 8.0 | Major | Updated and revised the technical content. | -| 12/5/2008 | 9.0 | Major | Updated and revised the technical content. | -| 1/16/2009 | 9.0.1 | Editorial | Changed language and formatting in the technical content. | -| 2/27/2009 | 10.0 | Major | Updated and revised the technical content. | -| 4/10/2009 | 10.1 | Minor | Clarified the meaning of the technical content. | -| 5/22/2009 | 11.0 | Major | Updated and revised the technical content. | -| 7/2/2009 | 11.1 | Minor | Clarified the meaning of the technical content. | -| 8/14/2009 | 11.2 | Minor | Clarified the meaning of the technical content. | -| 9/25/2009 | 12.0 | Major | Updated and revised the technical content. | -| 11/6/2009 | 12.1 | Minor | Clarified the meaning of the technical content. | -| 12/18/2009 | 12.2 | Minor | Clarified the meaning of the technical content. | -| 1/29/2010 | 13.0 | Major | Updated and revised the technical content. | -| 3/12/2010 | 13.1 | Minor | Clarified the meaning of the technical content. | -| 4/23/2010 | 13.2 | Minor | Clarified the meaning of the technical content. | -| 6/4/2010 | 14.0 | Major | Updated and revised the technical content. | -| 7/16/2010 | 15.0 | Major | Updated and revised the technical content. | -| 8/27/2010 | 16.0 | Major | Updated and revised the technical content. | -| 10/8/2010 | 17.0 | Major | Updated and revised the technical content. | -| 11/19/2010 | 18.0 | Major | Updated and revised the technical content. | -| 1/7/2011 | 19.0 | Major | Updated and revised the technical content. | -| 2/11/2011 | 20.0 | Major | Updated and revised the technical content. | -| 3/25/2011 | 21.0 | Major | Updated and revised the technical content. | -| 5/6/2011 | 21.1 | Minor | Clarified the meaning of the technical content. | -| 6/17/2011 | 22.0 | Major | Updated and revised the technical content. | -| 9/23/2011 | 22.0 | None | No changes to the meaning, language, or formatting of the technical content. | -| 12/16/2011 | 23.0 | Major | Updated and revised the technical content. | -| 3/30/2012 | 24.0 | Major | Updated and revised the technical content. | -| 7/12/2012 | 24.0 | None | No changes to the meaning, language, or formatting of the technical content. | -| 10/25/2012 | 25.0 | Major | Updated and revised the technical content. | -| 1/31/2013 | 25.1 | Minor | Clarified the meaning of the technical content. | -| 8/8/2013 | 26.0 | Major | Updated and revised the technical content. | -| 11/14/2013 | 27.0 | Major | Updated and revised the technical content. | -| 2/13/2014 | 27.1 | Minor | Clarified the meaning of the technical content. | -| 5/15/2014 | 28.0 | Major | Updated and revised the technical content. | -| 6/30/2015 | 29.0 | Major | Significantly changed the technical content. | -| 10/16/2015 | 30.0 | Major | Significantly changed the technical content. | -| 7/14/2016 | 31.0 | Major | Significantly changed the technical content. | -| 6/1/2017 | 32.0 | Major | Significantly changed the technical content. | -| 9/15/2017 | 33.0 | Major | Significantly changed the technical content. | -| 12/1/2017 | 34.0 | Major | Significantly changed the technical content. | -| 9/12/2018 | 35.0 | Major | Significantly changed the technical content. | -| 4/7/2021 | 36.0 | Major | Significantly changed the technical content. | -| 6/25/2021 | 37.0 | Major | Significantly changed the technical content. | -| 4/29/2022 | 38.0 | Major | Significantly changed the technical content. | -| 4/4/2023 | 39.0 | Major | Significantly changed the technical content. | -| 8/28/2023 | 40.0 | Major | Significantly changed the technical content. | -| 12/12/2023 | 41.0 | Major | Significantly changed the technical content. | -| 11/19/2024 | 42.0 | Major | Significantly changed the technical content. |