microsoft/hve-core

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
feat/1873-devcontainer

Branches

Tags

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

Clone

HTTPS

Download ZIP

scripts/evals/Modules/ArtifactDetection.psm1

206lines · modecode

1# Copyright (c) Microsoft Corporation.
2# SPDX-License-Identifier: MIT
3
4# ArtifactDetection.psm1
5#
6# Purpose: Classify repository paths as AI customization artifacts
7# (agent / prompt / instruction / skill) for eval coverage tooling.
8# Author: HVE Core Team
9
10#Requires -Version 7.0
11
12Set-StrictMode -Version Latest
13
14$script:ArtifactPatterns = @(
15 [pscustomobject]@{
16 Kind = 'agent'
17 Pattern = '^\.github/agents/(?:.+/)?(?<slug>[^/]+)\.agent\.md$'
18 }
19 [pscustomobject]@{
20 Kind = 'prompt'
21 Pattern = '^\.github/prompts/(?:.+/)?(?<slug>[^/]+)\.prompt\.md$'
22 }
23 [pscustomobject]@{
24 Kind = 'instruction'
25 Pattern = '^\.github/instructions/(?:.+/)?(?<slug>[^/]+)\.instructions\.md$'
26 }
27 [pscustomobject]@{
28 Kind = 'skill'
29 Pattern = '^\.github/skills/(?:.+/)?(?<slug>[^/]+)/SKILL\.md$'
30 }
31)
32
33function ConvertTo-NormalizedArtifactPath {
34 <#
35 .SYNOPSIS
36 Normalizes a workspace path by stripping leading separators and collapsing backslashes to forward slashes.
37 #>
38 [CmdletBinding()]
39 [OutputType([string])]
40 param(
41 [Parameter(Mandatory = $true)]
42 [AllowEmptyString()]
43 [string]$Path
44 )
45
46 if ([string]::IsNullOrWhiteSpace($Path)) {
47 return ''
48 }
49
50 return ($Path -replace '\\', '/').TrimStart('/')
51}
52
53function Get-ArtifactDescriptor {
54 <#
55 .SYNOPSIS
56 Classifies a workspace path as an AI customization artifact when it matches one of the known kinds.
57
58 .DESCRIPTION
59 Tests `Path` against the agent / prompt / instruction / skill path patterns and returns a
60 descriptor describing the detected artifact, or `$null` when the path is not an AI artifact.
61
62 .PARAMETER Path
63 Workspace-relative path (forward or backslash separators accepted).
64
65 .OUTPUTS
66 [hashtable] When matched, returns `@{ kind; path; artifactId }`. Returns `$null` otherwise.
67 #>
68 [CmdletBinding()]
69 [OutputType([hashtable])]
70 param(
71 [Parameter(Mandatory = $true)]
72 [AllowEmptyString()]
73 [string]$Path
74 )
75
76 $normalized = ConvertTo-NormalizedArtifactPath -Path $Path
77 if ([string]::IsNullOrEmpty($normalized)) {
78 return $null
79 }
80
81 foreach ($entry in $script:ArtifactPatterns) {
82 $match = [regex]::Match($normalized, $entry.Pattern)
83 if ($match.Success) {
84 return @{
85 kind = $entry.Kind
86 path = $normalized
87 artifactId = $match.Groups['slug'].Value
88 }
89 }
90 }
91
92 return $null
93}
94
95function ConvertFrom-GitDiffNameStatus {
96 <#
97 .SYNOPSIS
98 Parses output of `git diff --name-status` into change records.
99
100 .DESCRIPTION
101 Each input line is parsed into `@{ status; path; previousPath }`. Status codes:
102 A = added, M = modified, D = deleted, T = type-changed,
103 R = renamed (score suffix stripped), C = copied (score suffix stripped).
104 Rename and copy entries include the destination as `path` and the source as `previousPath`.
105
106 .PARAMETER Lines
107 Lines emitted by `git diff --name-status` (tab-separated).
108 #>
109 [CmdletBinding()]
110 [OutputType([hashtable[]])]
111 param(
112 [Parameter(Mandatory = $false)]
113 [AllowNull()]
114 [AllowEmptyCollection()]
115 [string[]]$Lines
116 )
117
118 $records = [System.Collections.Generic.List[hashtable]]::new()
119 if ($null -eq $Lines) { return ,@() }
120
121 foreach ($line in $Lines) {
122 if ([string]::IsNullOrWhiteSpace($line)) { continue }
123
124 $parts = $line -split "`t"
125 if ($parts.Count -lt 2) { continue }
126
127 $rawStatus = $parts[0].Trim()
128 if ([string]::IsNullOrWhiteSpace($rawStatus)) { continue }
129
130 $statusLetter = $rawStatus.Substring(0, 1).ToUpperInvariant()
131 $record = @{
132 status = $statusLetter
133 path = ''
134 previousPath = $null
135 }
136
137 if ($statusLetter -in @('R', 'C')) {
138 if ($parts.Count -lt 3) { continue }
139 $record.previousPath = ConvertTo-NormalizedArtifactPath -Path $parts[1]
140 $record.path = ConvertTo-NormalizedArtifactPath -Path $parts[2]
141 }
142 else {
143 $record.path = ConvertTo-NormalizedArtifactPath -Path $parts[1]
144 }
145
146 if ([string]::IsNullOrEmpty($record.path)) { continue }
147 $records.Add($record)
148 }
149
150 return ,$records.ToArray()
151}
152
153function Get-ChangedArtifactRecord {
154 <#
155 .SYNOPSIS
156 Converts a parsed git change record into an AI-artifact change record.
157
158 .DESCRIPTION
159 Filters non-artifact paths and emits `@{ kind; path; artifactId; status; previousPath }` when the
160 primary path is an AI artifact. For renames where only the source path was an artifact, the
161 record falls back to the source path so deletions of artifacts via rename are still reported.
162
163 .PARAMETER Change
164 A change record produced by `ConvertFrom-GitDiffNameStatus`.
165
166 .OUTPUTS
167 [hashtable] Artifact change record, or `$null` when neither path is an artifact.
168 #>
169 [CmdletBinding()]
170 [OutputType([hashtable])]
171 param(
172 [Parameter(Mandatory = $true)]
173 [hashtable]$Change
174 )
175
176 $descriptor = Get-ArtifactDescriptor -Path $Change.path
177 if ($null -eq $descriptor -and -not [string]::IsNullOrEmpty([string]$Change.previousPath)) {
178 $descriptor = Get-ArtifactDescriptor -Path $Change.previousPath
179 if ($null -ne $descriptor) {
180 return @{
181 kind = $descriptor.kind
182 path = $descriptor.path
183 artifactId = $descriptor.artifactId
184 status = 'D'
185 previousPath = $null
186 }
187 }
188 }
189
190 if ($null -eq $descriptor) { return $null }
191
192 return @{
193 kind = $descriptor.kind
194 path = $descriptor.path
195 artifactId = $descriptor.artifactId
196 status = $Change.status
197 previousPath = $Change.previousPath
198 }
199}
200
201Export-ModuleMember -Function @(
202 'Get-ArtifactDescriptor',
203 'ConvertFrom-GitDiffNameStatus',
204 'Get-ChangedArtifactRecord',
205 'ConvertTo-NormalizedArtifactPath'
206)
207