From 8e757985072638ff83bb7181c2852e55005f7f24 Mon Sep 17 00:00:00 2001 From: Jens Reinemann Date: Mon, 18 May 2026 09:32:57 +0200 Subject: [PATCH] feat(genome): Phase 1 - Extraction Script MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implementiert genome-extract.ps1: - Trait-Erkennung (Skills/Agents/Prompts/Instructions) - Verbund-Erkennung für Prompt-Router + Sub-Prompts - Git-Log-Scanning mit Zeitspanne - Mutation-Typ-Klassifizierung (content-change/member-added/member-removed) - Strukturierte Markdown-Ausgabe mit Diffs --- .github/genome/genome-extract.ps1 | 296 ++++++++++++++++++++++++++++++ 1 file changed, 296 insertions(+) create mode 100644 .github/genome/genome-extract.ps1 diff --git a/.github/genome/genome-extract.ps1 b/.github/genome/genome-extract.ps1 new file mode 100644 index 0000000..92e17d3 --- /dev/null +++ b/.github/genome/genome-extract.ps1 @@ -0,0 +1,296 @@ +<# +.SYNOPSIS + Genome Engine – Phase 1: Extraction + Extrahiert Mutations aus der Git-History für Copilot-Customization-Dateien. + +.DESCRIPTION + Scannt git log für Änderungen im Genome-Scope (.github/skills, agents, prompts, instructions). + Gruppiert Diffs nach Trait und gibt strukturiertes Markdown aus. + +.PARAMETER Since + Zeitspanne für git log (z.B. "4 days ago", "2 weeks ago"). Default: "7 days ago" + +.PARAMETER RepoPath + Pfad zum Repository. Default: aktuelles Verzeichnis. + +.PARAMETER OutputPath + Pfad für die Ausgabedatei. Default: .github/genome/output/raw-mutations.md + +.EXAMPLE + .\.github\genome\genome-extract.ps1 -Since "4 days ago" +#> + +param( + [string]$Since = "7 days ago", + [string]$RepoPath = ".", + [string]$OutputPath = "" +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +# --- Konfiguration --- + +$GenomeScopes = @( + ".github/skills/" + ".github/agents/" + ".github/prompts/" + ".github/copilot-instructions.md" + ".github/kotlin-conventions.instructions.md" +) + +# --- Funktionen --- + +function Get-TraitKey { + <# + .SYNOPSIS + Leitet den Trait-Key aus einem Dateipfad ab. + #> + param([string]$FilePath) + + # Skills: skill/ + if ($FilePath -match "^\.github/skills/([^/]+)/") { + return "skill/$($Matches[1])" + } + + # Agents: agent/ + if ($FilePath -match "^\.github/agents/(.+)\.agent\.md$") { + return "agent/$($Matches[1])" + } + + # Prompts: Standalone oder Verbund + if ($FilePath -match "^\.github/prompts/(.+)\.prompt\.md$") { + $name = $Matches[1] + + # Prüfe ob es ein Sub-Prompt ist (enthält Bindestrich und Router existiert) + # Verbund-Erkennung: -.prompt.md → trait des Routers + # Wir suchen den längsten Präfix, der als Router existieren könnte + $parts = $name -split "-" + if ($parts.Count -gt 1) { + # Versuche progressiv kürzere Präfixe als Router-Name + for ($i = $parts.Count - 1; $i -ge 1; $i--) { + $candidate = ($parts[0..($i-1)] -join "-") + $routerPath = ".github/prompts/$candidate.prompt.md" + $fullRouterPath = Join-Path $RepoPath $routerPath + if (Test-Path $fullRouterPath) { + return "prompt/$candidate" + } + } + } + + # Standalone-Prompt + return "prompt/$name" + } + + # Instructions + if ($FilePath -match "^\.github/(.+)\.instructions\.md$") { + return "instructions/$($Matches[1])" + } + if ($FilePath -match "^\.github/copilot-instructions\.md$") { + return "instructions/copilot-instructions" + } + + return $null +} + +function Get-MutationType { + <# + .SYNOPSIS + Bestimmt den Mutation-Typ aus dem Git diff-filter Status. + #> + param( + [string]$Status # A, M, D, R, etc. + ) + + switch -Regex ($Status) { + "^A" { return "member-added" } + "^D" { return "member-removed" } + default { return "content-change" } + } +} + +function Test-InGenomeScope { + <# + .SYNOPSIS + Prüft ob ein Dateipfad im Genome-Scope liegt. + #> + param([string]$FilePath) + + foreach ($scope in $GenomeScopes) { + if ($scope.EndsWith("/")) { + if ($FilePath.StartsWith($scope)) { return $true } + } else { + if ($FilePath -eq $scope) { return $true } + } + } + return $false +} + +# --- Hauptlogik --- + +Push-Location $RepoPath +try { + # Output-Pfad bestimmen + if (-not $OutputPath) { + $OutputPath = Join-Path $RepoPath ".github/genome/output/raw-mutations.md" + } + + Write-Host "Genome Extract: Scanning commits since '$Since'..." -ForegroundColor Cyan + + # Git-Log abrufen: Commits die Genome-Scope-Dateien betreffen + $logFormat = "--format=%H|%aI|%an|%s" + $commits = git log $logFormat --since="$Since" -- $GenomeScopes 2>&1 + + if (-not $commits -or $LASTEXITCODE -ne 0) { + Write-Host "Keine Commits im Genome-Scope seit '$Since' gefunden." -ForegroundColor Yellow + $commits = @() + } + + # Commits parsen + $mutations = @{} # Key: trait → Value: Liste von Mutations + + foreach ($line in $commits) { + if (-not $line -or $line -notmatch "\|") { continue } + + $parts = $line -split "\|", 4 + if ($parts.Count -lt 4) { continue } + + $hash = $parts[0] + $date = $parts[1] + $author = $parts[2] + $message = $parts[3] + + # Geänderte Dateien für diesen Commit abrufen + $diffFiles = git diff-tree --no-commit-id -r --name-status $hash 2>&1 + + foreach ($diffLine in $diffFiles) { + if (-not $diffLine -or $diffLine -notmatch "^\w") { continue } + + $diffParts = $diffLine -split "\t", 3 + $status = $diffParts[0] + $filePath = $diffParts[1] + + # Bei Renames: Zielpfad verwenden + if ($status -match "^R" -and $diffParts.Count -ge 3) { + $filePath = $diffParts[2] + } + + # Normalisieren (Backslash → Forward Slash) + $filePath = $filePath -replace "\\", "/" + + # Prüfe ob im Genome-Scope + if (-not (Test-InGenomeScope $filePath)) { continue } + + # Trait-Key ableiten + $traitKey = Get-TraitKey $filePath + if (-not $traitKey) { continue } + + # Mutation-Typ bestimmen + $mutationType = Get-MutationType $status + + # Diff für diese Datei holen + $diff = git show --format="" --no-color $hash -- $filePath 2>&1 + if ($LASTEXITCODE -ne 0) { + # Fallback: diff-tree + $diff = git diff-tree -p $hash -- $filePath 2>&1 + } + $diffText = ($diff | Out-String).Trim() + + # Mutation speichern + if (-not $mutations.ContainsKey($traitKey)) { + $mutations[$traitKey] = @() + } + + $mutations[$traitKey] += @{ + Hash = $hash.Substring(0, [Math]::Min(8, $hash.Length)) + Date = $date + Author = $author + Message = $message + File = $filePath + Type = $mutationType + Diff = $diffText + } + } + } + + # --- Output generieren --- + + $sb = [System.Text.StringBuilder]::new() + [void]$sb.AppendLine("# Raw Mutations") + [void]$sb.AppendLine("") + [void]$sb.AppendLine("**Extrahiert:** $(Get-Date -Format 'yyyy-MM-dd HH:mm')") + [void]$sb.AppendLine("**Zeitraum:** seit $Since") + [void]$sb.AppendLine("**Repository:** $(Split-Path $RepoPath -Leaf)") + [void]$sb.AppendLine("**Traits mit Mutations:** $($mutations.Count)") + [void]$sb.AppendLine("") + [void]$sb.AppendLine("---") + [void]$sb.AppendLine("") + + if ($mutations.Count -eq 0) { + [void]$sb.AppendLine("*Keine Mutations im angegebenen Zeitraum gefunden.*") + } else { + # Sortiert nach Trait-Key ausgeben + foreach ($traitKey in ($mutations.Keys | Sort-Object)) { + $traitMutations = $mutations[$traitKey] + + [void]$sb.AppendLine("## Trait: ``$traitKey``") + [void]$sb.AppendLine("") + [void]$sb.AppendLine("| Mutations | Dateien |") + [void]$sb.AppendLine("|-----------|---------|") + + $uniqueFiles = ($traitMutations | ForEach-Object { $_.File } | Sort-Object -Unique) -join ", " + [void]$sb.AppendLine("| $($traitMutations.Count) | $uniqueFiles |") + [void]$sb.AppendLine("") + + # Gruppiert nach Commit (Hash) + $byCommit = $traitMutations | Group-Object -Property Hash + + foreach ($commitGroup in $byCommit) { + $first = $commitGroup.Group[0] + [void]$sb.AppendLine("### [$($first.Hash)] $($first.Message)") + [void]$sb.AppendLine("") + [void]$sb.AppendLine("- **Datum:** $($first.Date)") + [void]$sb.AppendLine("- **Autor:** $($first.Author)") + [void]$sb.AppendLine("") + + foreach ($mutation in $commitGroup.Group) { + $header = "#### " + '`' + $mutation.Type + '`' + " - " + $mutation.File + [void]$sb.AppendLine($header) + [void]$sb.AppendLine("") + + if ($mutation.Diff) { + # Diff auf max 80 Zeilen begrenzen + $diffLines = $mutation.Diff -split [Environment]::NewLine + if ($diffLines.Count -gt 80) { + $truncMsg = "... ($($diffLines.Count - 80) weitere Zeilen)" + $diffLines = $diffLines[0..79] + @($truncMsg) + } + [void]$sb.AppendLine('```diff') + [void]$sb.AppendLine(($diffLines -join [Environment]::NewLine)) + [void]$sb.AppendLine('```') + } + [void]$sb.AppendLine("") + } + } + + [void]$sb.AppendLine("---") + [void]$sb.AppendLine("") + } + } + + # Datei schreiben + $outputDir = Split-Path $OutputPath -Parent + if (-not (Test-Path $outputDir)) { + New-Item -ItemType Directory -Path $outputDir -Force | Out-Null + } + + $sb.ToString() | Set-Content -Path $OutputPath -Encoding UTF8 + Write-Host "" + Write-Host "Extraction abgeschlossen:" -ForegroundColor Green + Write-Host " Traits: $($mutations.Count)" -ForegroundColor White + Write-Host " Mutations: $(($mutations.Values | ForEach-Object { $_.Count } | Measure-Object -Sum).Sum)" -ForegroundColor White + Write-Host " Output: $OutputPath" -ForegroundColor White + +} finally { + Pop-Location +}