microsoft/hve-core

Public

mirrored fromhttps://github.com/microsoft/hve-coreAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
feat/context-working

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

scripts/collections/Modules/CollectionHelpers.psm1

743lines · modecode

1# Copyright (c) Microsoft Corporation.
2# SPDX-License-Identifier: MIT
3
4# CollectionHelpers.psm1
5#
6# Purpose: Collection helpers - YAML parsing, validation, and shared collection utilities.
7# Author: HVE Core Team
8
9#Requires -Version 7.0
10#Requires -Modules @{ ModuleName='PowerShell-Yaml'; RequiredVersion='0.4.7' }
11
12# ---------------------------------------------------------------------------
13# Marker Constants (shared across collection scripts)
14# ---------------------------------------------------------------------------
15$script:CollectionMdBeginMarker = '<!-- BEGIN AUTO-GENERATED ARTIFACTS -->'
16$script:CollectionMdEndMarker = '<!-- END AUTO-GENERATED ARTIFACTS -->'
17
18# ---------------------------------------------------------------------------
19# Internal Utilities
20# ---------------------------------------------------------------------------
21
22function Set-ContentIfChanged {
23 <#
24 .SYNOPSIS
25 Writes content to a file only when the content has changed.
26 .DESCRIPTION
27 Compares the provided value against the existing file content using
28 case-sensitive ordinal comparison. Writes only when the file does not
29 exist or content differs, preserving the git stat cache for unchanged files.
30 .PARAMETER Path
31 The file path to write.
32 .PARAMETER Value
33 The content to write.
34 .OUTPUTS
35 [bool] True if the file was written, false if skipped.
36 #>
37 [CmdletBinding()]
38 [OutputType([bool])]
39 param(
40 [Parameter(Mandatory)]
41 [string]$Path,
42
43 [Parameter(Mandatory)]
44 [AllowEmptyString()]
45 [string]$Value
46 )
47
48 if (Test-Path -LiteralPath $Path) {
49 $existing = Get-Content -LiteralPath $Path -Raw -Encoding utf8
50 if ([string]::Equals($existing, $Value, [System.StringComparison]::Ordinal)) {
51 return $false
52 }
53 }
54 $parentDir = Split-Path -Path $Path -Parent
55 if ($parentDir -and -not (Test-Path -LiteralPath $parentDir)) {
56 New-Item -ItemType Directory -Path $parentDir -Force | Out-Null
57 }
58 Set-Content -LiteralPath $Path -Value $Value -Encoding utf8NoBOM -NoNewline
59 return $true
60}
61
62# ---------------------------------------------------------------------------
63# Pure Functions (no file system side effects)
64# ---------------------------------------------------------------------------
65
66function Test-DeprecatedPath {
67 <#
68 .SYNOPSIS
69 Checks whether a file path contains a deprecated directory segment.
70
71 .DESCRIPTION
72 Returns true when the path contains a /deprecated/ or \deprecated\ segment,
73 indicating the artifact resides in a deprecated directory tree.
74
75 .PARAMETER Path
76 File path to check (absolute or relative, any slash style).
77
78 .OUTPUTS
79 [bool] True when the path contains a deprecated segment.
80 #>
81 [CmdletBinding()]
82 [OutputType([bool])]
83 param(
84 [Parameter(Mandatory = $true)]
85 [ValidateNotNullOrEmpty()]
86 [string]$Path
87 )
88
89 return ($Path -match '[/\\]deprecated[/\\]')
90}
91
92function Test-HveCoreRepoSpecificPath {
93 <#
94 .SYNOPSIS
95 Checks whether a type-relative path is a root-level repo-specific artifact.
96
97 .DESCRIPTION
98 Returns true when the type-relative path has no subdirectory component,
99 indicating it is a root-level repo-specific artifact not intended for
100 distribution. Collection-scoped artifacts reside in subdirectories.
101
102 .PARAMETER RelativePath
103 Type-relative path (relative to the agents/, prompts/, instructions/, or skills/ directory).
104
105 .OUTPUTS
106 [bool] True when the path is repo-specific.
107 #>
108 [CmdletBinding()]
109 [OutputType([bool])]
110 param(
111 [Parameter(Mandatory = $true)]
112 [ValidateNotNullOrEmpty()]
113 [string]$RelativePath
114 )
115
116 return ($RelativePath -notlike '*/*')
117}
118
119function Test-HveCoreRepoRelativePath {
120 <#
121 .SYNOPSIS
122 Checks whether a repo-relative path is a root-level repo-specific artifact.
123
124 .DESCRIPTION
125 Returns true when the repo-relative path is directly under a .github type
126 directory (agents, instructions, prompts, skills) with no subdirectory,
127 indicating it is a root-level repo-specific artifact not intended for distribution.
128
129 .PARAMETER Path
130 Repo-relative path (e.g., .github/instructions/workflows.instructions.md).
131
132 .OUTPUTS
133 [bool] True when the path is a root-level repo-specific artifact.
134 #>
135 [CmdletBinding()]
136 [OutputType([bool])]
137 param(
138 [Parameter(Mandatory = $true)]
139 [ValidateNotNullOrEmpty()]
140 [string]$Path
141 )
142
143 return ($Path -match '^\.github/(agents|instructions|prompts|skills)/[^/]+$')
144}
145
146function Get-CollectionManifest {
147 <#
148 .SYNOPSIS
149 Loads a collection manifest from a YAML or JSON file.
150
151 .DESCRIPTION
152 Reads and parses a collection manifest file that defines collection-based
153 artifact filtering rules. Supports both YAML (.yml/.yaml) and JSON (.json)
154 formats.
155
156 .PARAMETER CollectionPath
157 Path to the collection manifest file (YAML or JSON).
158
159 .OUTPUTS
160 [hashtable] Parsed collection manifest with id, name, displayName, description, items, and optional include/exclude.
161 #>
162 [CmdletBinding()]
163 [OutputType([hashtable])]
164 param(
165 [Parameter(Mandatory = $true)]
166 [ValidateNotNullOrEmpty()]
167 [string]$CollectionPath
168 )
169
170 if (-not (Test-Path $CollectionPath)) {
171 throw "Collection manifest not found: $CollectionPath"
172 }
173
174 $extension = [System.IO.Path]::GetExtension($CollectionPath).ToLowerInvariant()
175 if ($extension -in @('.yml', '.yaml')) {
176 $content = Get-Content -Path $CollectionPath -Raw
177 return ConvertFrom-Yaml -Yaml $content
178 }
179
180 $content = Get-Content -Path $CollectionPath -Raw
181 return $content | ConvertFrom-Json -AsHashtable
182}
183
184function Get-CollectionArtifactKey {
185 <#
186 .SYNOPSIS
187 Extracts a unique key from an artifact path based on its kind.
188
189 .DESCRIPTION
190 Produces the same key that extension packaging uses for deduplication.
191 Agents and prompts use the filename only; instructions use the
192 type-relative path; skills use the directory name.
193
194 .PARAMETER Kind
195 The artifact kind (agent, prompt, instruction, skill).
196
197 .PARAMETER Path
198 The repo-relative artifact path.
199
200 .OUTPUTS
201 [string] The artifact key.
202 #>
203 [CmdletBinding()]
204 [OutputType([string])]
205 param(
206 [Parameter(Mandatory = $true)]
207 [string]$Kind,
208
209 [Parameter(Mandatory = $true)]
210 [string]$Path
211 )
212
213 switch ($Kind) {
214 'agent' {
215 return ([System.IO.Path]::GetFileName($Path) -replace '\.agent\.md$', '')
216 }
217 'prompt' {
218 return ([System.IO.Path]::GetFileName($Path) -replace '\.prompt\.md$', '')
219 }
220 'instruction' {
221 return ($Path -replace '^\.github/instructions/', '' -replace '\.instructions\.md$', '')
222 }
223 'skill' {
224 return [System.IO.Path]::GetFileName($Path.TrimEnd('/'))
225 }
226 default {
227 if ($Path -match "\.$([regex]::Escape($Kind))\.md$") {
228 return ([System.IO.Path]::GetFileName($Path) -replace "\.$([regex]::Escape($Kind))\.md$", '')
229 }
230
231 if ($Path -like '*.md') {
232 return [System.IO.Path]::GetFileNameWithoutExtension($Path)
233 }
234
235 return [System.IO.Path]::GetFileName($Path)
236 }
237 }
238}
239
240function Get-ArtifactFrontmatter {
241 <#
242 .SYNOPSIS
243 Extracts YAML frontmatter from a markdown file.
244
245 .DESCRIPTION
246 Parses the YAML frontmatter block delimited by --- markers at the start
247 of a markdown file. Returns a hashtable with description.
248
249 .PARAMETER FilePath
250 Path to the markdown file to parse.
251
252 .PARAMETER FallbackDescription
253 Default description if none found in frontmatter.
254
255 .OUTPUTS
256 [hashtable] With description key.
257 #>
258 [CmdletBinding()]
259 [OutputType([hashtable])]
260 param(
261 [Parameter(Mandatory = $true)]
262 [string]$FilePath,
263
264 [Parameter(Mandatory = $false)]
265 [string]$FallbackDescription = ''
266 )
267
268 $content = Get-Content -Path $FilePath -Raw
269 $description = ''
270
271 if ($content -match '(?s)^---\s*\r?\n(.*?)\r?\n---') {
272 $yamlContent = $Matches[1] -replace '\r\n', "`n" -replace '\r', "`n"
273 try {
274 $data = ConvertFrom-Yaml -Yaml $yamlContent
275 if ($data.ContainsKey('description')) {
276 $description = $data.description
277 }
278 }
279 catch {
280 Write-Warning "Failed to parse YAML frontmatter in $(Split-Path -Leaf $FilePath): $_"
281 }
282 }
283
284 return @{
285 description = if ($description) { $description } else { $FallbackDescription }
286 }
287}
288
289function Resolve-CollectionItemMaturity {
290 <#
291 .SYNOPSIS
292 Resolves effective maturity from collection item metadata.
293
294 .DESCRIPTION
295 Returns stable when maturity is omitted; otherwise returns the provided
296 maturity string.
297
298 .PARAMETER Maturity
299 Optional maturity value from a collection item.
300
301 .OUTPUTS
302 [string] Effective maturity value.
303 #>
304 [CmdletBinding()]
305 [OutputType([string])]
306 param(
307 [Parameter()]
308 [AllowNull()]
309 [AllowEmptyString()]
310 [string]$Maturity
311 )
312
313 if ([string]::IsNullOrWhiteSpace($Maturity)) {
314 return 'stable'
315 }
316
317 return $Maturity
318}
319
320function Get-AllCollections {
321 <#
322 .SYNOPSIS
323 Discovers and parses all .collection.yml files in a directory.
324
325 .DESCRIPTION
326 Scans the specified directory for files matching *.collection.yml and
327 parses each one into a hashtable via Get-CollectionManifest.
328
329 .PARAMETER CollectionsDir
330 Path to the directory containing .collection.yml files.
331
332 .OUTPUTS
333 [hashtable[]] Array of parsed collection manifests.
334 #>
335 [CmdletBinding()]
336 [OutputType([hashtable[]])]
337 param(
338 [Parameter(Mandatory = $true)]
339 [string]$CollectionsDir
340 )
341
342 $files = Get-ChildItem -Path $CollectionsDir -Filter '*.collection.yml' -File
343 $collections = @()
344
345 foreach ($file in $files) {
346 $manifest = Get-CollectionManifest -CollectionPath $file.FullName
347 $collections += $manifest
348 }
349
350 return $collections
351}
352
353# ---------------------------------------------------------------------------
354# I/O Functions (file system operations)
355# ---------------------------------------------------------------------------
356
357function Get-ArtifactFiles {
358 <#
359 .SYNOPSIS
360 Discovers all artifact files from .github/ directories.
361
362 .DESCRIPTION
363 Scans .github/agents/, .github/prompts/, .github/instructions/ (recursively),
364 and .github/skills/ to build a complete list of collection items. Returns
365 repo-relative paths with forward slashes.
366
367 .PARAMETER RepoRoot
368 Absolute path to the repository root directory.
369
370 .OUTPUTS
371 [hashtable[]] Array of hashtables with path and kind keys.
372 #>
373 [CmdletBinding()]
374 [OutputType([hashtable[]])]
375 param(
376 [Parameter(Mandatory = $true)]
377 [ValidateNotNullOrEmpty()]
378 [string]$RepoRoot
379 )
380
381 $items = @()
382
383 # AI artifacts discovered by .<kind>.md suffix under .github/
384 # Keep explicit suffix mapping only where naming differs from manifest kind values.
385 $gitHubDir = Join-Path -Path $RepoRoot -ChildPath '.github'
386 if (Test-Path -Path $gitHubDir) {
387 $suffixToKind = @{
388 instructions = 'instruction'
389 }
390
391 $artifactFiles = Get-ChildItem -Path $gitHubDir -Filter '*.*.md' -File -Recurse
392 foreach ($file in $artifactFiles) {
393 if ($file.Name -notmatch '\.(?<suffix>[^.]+)\.md$') {
394 continue
395 }
396
397 $suffix = $Matches['suffix'].ToLowerInvariant()
398 $kind = if ($suffixToKind.ContainsKey($suffix)) { $suffixToKind[$suffix] } else { $suffix }
399 $relativePath = [System.IO.Path]::GetRelativePath($RepoRoot, $file.FullName) -replace '\\', '/'
400
401 if (Test-HveCoreRepoRelativePath -Path $relativePath) {
402 continue
403 }
404 if (Test-DeprecatedPath -Path $relativePath) {
405 continue
406 }
407 $items += @{ path = $relativePath; kind = $kind }
408 }
409 }
410
411 # Skills (directories containing SKILL.md)
412 $skillsDir = Join-Path -Path $RepoRoot -ChildPath '.github/skills'
413 if (Test-Path -Path $skillsDir) {
414 $skillMdFiles = Get-ChildItem -Path $skillsDir -Filter 'SKILL.md' -File -Recurse
415 foreach ($skillFile in $skillMdFiles) {
416 $dir = $skillFile.Directory
417 $relativePath = [System.IO.Path]::GetRelativePath($RepoRoot, $dir.FullName) -replace '\\', '/'
418
419 if (Test-DeprecatedPath -Path $relativePath) {
420 continue
421 }
422 if (Test-HveCoreRepoRelativePath -Path $relativePath) {
423 continue
424 }
425
426 $items += @{ path = $relativePath; kind = 'skill' }
427 }
428 }
429
430 return $items
431}
432
433function Test-ArtifactDeprecated {
434 <#
435 .SYNOPSIS
436 Checks whether an artifact has maturity deprecated in collection metadata.
437
438 .DESCRIPTION
439 Reads maturity from the provided collection item metadata value and
440 returns $true when the effective value equals deprecated.
441
442 .PARAMETER Maturity
443 Optional maturity value from collection item metadata.
444
445 .OUTPUTS
446 [bool] True when the artifact is deprecated.
447 #>
448 [CmdletBinding()]
449 [OutputType([bool])]
450 param(
451 [Parameter()]
452 [AllowNull()]
453 [AllowEmptyString()]
454 [string]$Maturity
455 )
456
457 return ((Resolve-CollectionItemMaturity -Maturity $Maturity) -eq 'deprecated')
458}
459
460function Update-HveCoreAllCollection {
461 <#
462 .SYNOPSIS
463 Auto-updates hve-core-all.collection.yml with all non-deprecated artifacts.
464
465 .DESCRIPTION
466 Discovers all artifacts from .github/ directories, excludes deprecated items,
467 and rewrites the hve-core-all collection manifest. Preserves existing
468 metadata fields (id, name, description, tags, display).
469
470 .PARAMETER RepoRoot
471 Absolute path to the repository root directory.
472
473 .PARAMETER DryRun
474 When specified, logs changes without writing to disk.
475
476 .OUTPUTS
477 [hashtable] With ItemCount, AddedCount, RemovedCount, and DeprecatedCount keys.
478 #>
479 [CmdletBinding()]
480 [OutputType([hashtable])]
481 param(
482 [Parameter(Mandatory = $true)]
483 [ValidateNotNullOrEmpty()]
484 [string]$RepoRoot,
485
486 [Parameter(Mandatory = $false)]
487 [switch]$DryRun
488 )
489
490 $collectionPath = Join-Path -Path $RepoRoot -ChildPath 'collections/hve-core-all.collection.yml'
491
492 # Read existing manifest to preserve metadata
493 $existing = Get-CollectionManifest -CollectionPath $collectionPath
494 $existingPaths = @($existing.items | ForEach-Object { $_.path })
495
496 # Discover all artifacts
497 $allItems = Get-ArtifactFiles -RepoRoot $RepoRoot
498
499 # Exclude deprecated items by path (independent of maturity metadata)
500 $allItems = @($allItems | Where-Object { -not (Test-DeprecatedPath -Path $_.path) })
501
502 # Filter deprecated based on existing collection item maturity metadata
503 $existingItemMaturities = @{}
504 foreach ($existingItem in $existing.items) {
505 $existingKey = "$($existingItem.kind)|$($existingItem.path)"
506 $existingItemMaturities[$existingKey] = Resolve-CollectionItemMaturity -Maturity $existingItem.maturity
507 }
508
509 # Propagate authoritative maturities from source collections so tombstones
510 # (maturity: removed) and deprecations declared in any source manifest
511 # carry into the aggregated hve-core-all collection. Strictest maturity
512 # wins: removed > deprecated > preview > stable.
513 $maturityRank = @{ 'stable' = 0; 'preview' = 1; 'deprecated' = 2; 'removed' = 3 }
514 $collectionsDir = Join-Path -Path $RepoRoot -ChildPath 'collections'
515 $sourceCollections = Get-ChildItem -Path $collectionsDir -Filter '*.collection.yml' -File -ErrorAction SilentlyContinue |
516 Where-Object { $_.Name -ne 'hve-core-all.collection.yml' }
517 foreach ($sourceFile in $sourceCollections) {
518 $sourceManifest = Get-CollectionManifest -CollectionPath $sourceFile.FullName
519 if ($null -eq $sourceManifest -or $null -eq $sourceManifest.items) { continue }
520 foreach ($sourceItem in $sourceManifest.items) {
521 $sourceKey = "$($sourceItem.kind)|$($sourceItem.path)"
522 $sourceMaturity = Resolve-CollectionItemMaturity -Maturity $sourceItem.maturity
523 $currentMaturity = if ($existingItemMaturities.ContainsKey($sourceKey)) { $existingItemMaturities[$sourceKey] } else { 'stable' }
524 if ($maturityRank[$sourceMaturity] -gt $maturityRank[$currentMaturity]) {
525 $existingItemMaturities[$sourceKey] = $sourceMaturity
526 }
527 }
528 }
529
530 $deprecatedCount = 0
531 $filteredItems = @()
532 foreach ($item in $allItems) {
533 $itemKey = "$($item.kind)|$($item.path)"
534 $itemMaturity = 'stable'
535 if ($existingItemMaturities.ContainsKey($itemKey)) {
536 $itemMaturity = $existingItemMaturities[$itemKey]
537 }
538
539 if ($itemMaturity -eq 'removed') {
540 Write-Verbose "Excluding removed: $($item.path)"
541 continue
542 }
543
544 if (Test-ArtifactDeprecated -Maturity $itemMaturity) {
545 $deprecatedCount++
546 Write-Verbose "Excluding deprecated: $($item.path)"
547 continue
548 }
549
550 $filteredItems += @{
551 path = $item.path
552 kind = $item.kind
553 maturity = $itemMaturity
554 }
555 }
556
557 # Sort: known kinds first, then any additional kinds, then by path
558 $kindOrder = @{ 'agent' = 0; 'prompt' = 1; 'instruction' = 2; 'skill' = 3 }
559 $sortedItems = $filteredItems | Sort-Object `
560 { if ($kindOrder.ContainsKey($_.kind)) { $kindOrder[$_.kind] } else { 100 } }, `
561 { $_.kind }, `
562 { $_.path }
563
564 # Build new items array as ordered hashtables for clean YAML output
565 $newItems = @()
566 foreach ($item in $sortedItems) {
567 $newItem = [ordered]@{
568 path = $item.path
569 kind = $item.kind
570 }
571
572 if ((Resolve-CollectionItemMaturity -Maturity $item.maturity) -ne 'stable') {
573 $newItem['maturity'] = $item.maturity
574 }
575
576 $newItems += $newItem
577 }
578
579 # Compute diff
580 $newPaths = @($sortedItems | ForEach-Object { $_.path })
581 $added = @($newPaths | Where-Object { $_ -notin $existingPaths })
582 $removed = @($existingPaths | Where-Object { $_ -notin $newPaths })
583
584 Write-Host "`n--- hve-core-all Auto-Update ---" -ForegroundColor Cyan
585 Write-Host " Discovered: $($allItems.Count) artifacts"
586 Write-Host " Deprecated: $deprecatedCount (excluded)"
587 Write-Host " Final: $($newItems.Count) items"
588 if ($added.Count -gt 0) {
589 Write-Host " Added: $($added -join ', ')" -ForegroundColor Green
590 }
591 if ($removed.Count -gt 0) {
592 Write-Host " Removed: $($removed -join ', ')" -ForegroundColor Yellow
593 }
594
595 if ($DryRun) {
596 Write-Host ' [DRY RUN] No changes written' -ForegroundColor Yellow
597 }
598 else {
599 # Rebuild manifest preserving metadata
600 $displayOrdered = [ordered]@{}
601 if ($existing.display.Contains('featured')) {
602 $displayOrdered['featured'] = $existing.display['featured']
603 }
604 if ($existing.display.Contains('ordering')) {
605 $displayOrdered['ordering'] = $existing.display['ordering']
606 }
607 $manifest = [ordered]@{
608 id = $existing.id
609 name = $existing.name
610 description = $existing.description
611 tags = $existing.tags
612 items = $newItems
613 display = $displayOrdered
614 }
615
616 $yaml = ConvertTo-Yaml -Data $manifest
617 Set-ContentIfChanged -Path $collectionPath -Value $yaml | Out-Null
618 Write-Verbose "Updated $collectionPath"
619 }
620
621 return @{
622 ItemCount = $newItems.Count
623 AddedCount = $added.Count
624 RemovedCount = $removed.Count
625 DeprecatedCount = $deprecatedCount
626 }
627}
628
629function Split-CollectionMdByMarkers {
630 <#
631 .SYNOPSIS
632 Splits collection markdown content at auto-generation markers.
633 .DESCRIPTION
634 Locates the BEGIN and END auto-generated-artifact markers in the
635 supplied markdown string and returns the intro (before), footer (after),
636 and a flag indicating whether markers were found.
637 .PARAMETER Content
638 The full text content of a collection.md file.
639 .OUTPUTS
640 [hashtable] with keys HasMarkers ([bool]), Intro ([string]),
641 and Footer ([string]).
642 .NOTES
643 Returns the entire content as Intro with HasMarkers = $false when
644 markers are missing or mis-ordered.
645 #>
646 [CmdletBinding()]
647 [OutputType([hashtable])]
648 param(
649 [Parameter(Mandatory)]
650 [ValidateNotNullOrEmpty()]
651 [string]$Content
652 )
653
654 $beginIdx = $Content.IndexOf($script:CollectionMdBeginMarker)
655 $endIdx = $Content.IndexOf($script:CollectionMdEndMarker)
656
657 if ($beginIdx -lt 0 -or $endIdx -lt 0 -or $endIdx -le $beginIdx) {
658 return @{
659 HasMarkers = $false
660 Intro = $Content
661 Footer = ''
662 }
663 }
664
665 $intro = $Content.Substring(0, $beginIdx).TrimEnd()
666 $endMarkerEnd = $endIdx + $script:CollectionMdEndMarker.Length
667 $footer = if ($endMarkerEnd -lt $Content.Length) {
668 $Content.Substring($endMarkerEnd).TrimStart("`r", "`n")
669 } else { '' }
670
671 return @{
672 HasMarkers = $true
673 Intro = $intro
674 Footer = $footer
675 }
676}
677
678function Get-ArtifactDescription {
679 <#
680 .SYNOPSIS
681 Reads the description from an artifact file's YAML frontmatter.
682 .DESCRIPTION
683 Parses the YAML frontmatter block at the top of a markdown file and
684 returns the description field value. Returns an empty string when the
685 file is missing, has no frontmatter, or lacks a description field.
686 Strips the common " - Brought to you by microsoft/hve-core" suffix.
687 .PARAMETER FilePath
688 Absolute path to the artifact markdown file.
689 .OUTPUTS
690 [string] Description text, or empty string if unavailable.
691 #>
692 [CmdletBinding()]
693 [OutputType([string])]
694 param(
695 [Parameter(Mandatory = $true)]
696 [string]$FilePath
697 )
698
699 if (-not (Test-Path $FilePath)) {
700 return ''
701 }
702
703 $content = Get-Content -Path $FilePath -Raw
704 if ($content -match '(?s)^---\s*\r?\n(.*?)\r?\n---') {
705 $yamlBlock = $Matches[1]
706 try {
707 $frontmatter = ConvertFrom-Yaml -Yaml $yamlBlock
708 if ($frontmatter -is [hashtable] -and $frontmatter.ContainsKey('description')) {
709 $desc = [string]$frontmatter.description
710 # Strip the common branding suffix
711 $desc = $desc -replace '\s*-\s*Brought to you by microsoft/hve-core$', ''
712 return $desc.Trim()
713 }
714 }
715 catch {
716 Write-Verbose "Failed to parse frontmatter from $FilePath`: $_"
717 }
718 }
719
720 return ''
721}
722
723Export-ModuleMember -Function @(
724 'Get-AllCollections',
725 'Get-ArtifactDescription',
726 'Get-ArtifactFiles',
727 'Get-ArtifactFrontmatter',
728 'Get-CollectionArtifactKey',
729 'Get-CollectionManifest',
730 'Resolve-CollectionItemMaturity',
731 'Set-ContentIfChanged',
732 'Split-CollectionMdByMarkers',
733 'Test-ArtifactDeprecated',
734 'Test-DeprecatedPath',
735 'Test-HveCoreRepoRelativePath',
736 'Test-HveCoreRepoSpecificPath',
737 'Update-HveCoreAllCollection'
738)
739
740Export-ModuleMember -Variable @(
741 'CollectionMdBeginMarker',
742 'CollectionMdEndMarker'
743)
744