【整合】 框架使用规则
This commit is contained in:
@@ -1,89 +1,59 @@
|
||||
# Hook 共享层
|
||||
# 共享检查
|
||||
|
||||
> 本目录存放 UnityAI / StrayFog 框架知识库共享的 hook 脚本和规则。
|
||||
>
|
||||
> 各 AI 工具自己的私有目录只放适配器,不重复维护 hook 逻辑。
|
||||
`hooks/` 提供只读工作流检查和提交信息检查。脚本不自动修改代码、资源或文档。
|
||||
|
||||
---
|
||||
|
||||
## 脚本清单
|
||||
|
||||
| 脚本 | 用途 |
|
||||
|---|---|
|
||||
| [archive-check.ps1](archive-check.ps1) | 读取 git diff 中变更的 `.md` 文件,解析 `## 文档关联` 表,输出需要同步检查的关联文档 |
|
||||
| [sync-doc-associations.ps1](sync-doc-associations.ps1) | Python 脚本的 PowerShell 包装器 |
|
||||
| [sync_doc_associations.py](sync_doc_associations.py) | 在关联文档中追加变更提醒(只追加、不覆盖,执行后需 review) |
|
||||
| [commit-check.ps1](commit-check.ps1) | 提交前检查:推断变更文件所属模块,校验提交信息格式 |
|
||||
| [commit-module-mappings.json](commit-module-mappings.json) | `commit-check.ps1` 使用的目录-模块映射配置 |
|
||||
|
||||
---
|
||||
|
||||
## 归档检查
|
||||
|
||||
所有任务结束后都应执行归档检查,判断是否需要同步更新长期文档。
|
||||
## 工作流检查
|
||||
|
||||
```powershell
|
||||
# PowerShell
|
||||
.\docs\hooks\archive-check.ps1
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File hooks/workflow-check.ps1 `
|
||||
[-UnityProjectRoot <path>] [-DocsRoot <path>]
|
||||
```
|
||||
|
||||
该脚本只输出建议,不会自动修改任何文档。最终是否更新关联文档,需要用户确认。
|
||||
项目定位顺序:
|
||||
|
||||
---
|
||||
1. `-UnityProjectRoot`
|
||||
2. `UNITY_PROJECT_ROOT`
|
||||
3. 当前目录本身是 Unity 项目
|
||||
4. UnityAI 同级唯一 Unity 项目
|
||||
5. 未找到时只检查文档仓库
|
||||
|
||||
## 文档关联同步
|
||||
检查内容:
|
||||
|
||||
如需在关联文档中自动留下变更提醒,可运行:
|
||||
- UnityAI 与 Unity 项目的 tracked、staged、unstaged 和 untracked 文件。
|
||||
- 中文路径和未跟踪 Markdown。
|
||||
- ESF/General 源码与 CopyTo 生成目标是否同时变化。
|
||||
- 改动路径对应的建议构建、Unity 和平台验证。
|
||||
- 可能需要复核的模块文档。
|
||||
- 本次修改 Markdown 中的相对链接。
|
||||
|
||||
```powershell
|
||||
.\docs\hooks\sync-doc-associations.ps1
|
||||
```
|
||||
|
||||
该脚本只追加、不覆盖,执行后必须 review。
|
||||
|
||||
---
|
||||
`archive-check.ps1` 是兼容包装,新接入统一调用 `workflow-check.ps1`。
|
||||
|
||||
## 提交检查
|
||||
|
||||
提交前可运行:
|
||||
|
||||
```powershell
|
||||
.\docs\hooks\commit-check.ps1 -Message "[entity][docs]: 补充实体池说明"
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File hooks/commit-check.ps1 `
|
||||
-RepositoryRoot <path> `
|
||||
-Message "[模块][类型]: 描述"
|
||||
```
|
||||
|
||||
检查项包括:
|
||||
脚本优先检查 staged 文件;没有 staged 文件时检查整个工作树。支持 UnityAI 和目标 Unity 项目,路径映射维护在 `commit-module-mappings.json`。
|
||||
|
||||
- 提交信息是否符合 `[模块][类型]: 描述` 格式
|
||||
- 变更文件是否能映射到已知模块
|
||||
- 模块映射配置来自 [commit-module-mappings.json](commit-module-mappings.json)
|
||||
允许类型:`feat`、`fix`、`docs`、`refactor`、`test`、`chore`。
|
||||
|
||||
---
|
||||
提交和推送仍然只在用户明确要求时执行。检查通过不代表可以自动提交。
|
||||
|
||||
## 触发方式
|
||||
## 维护规则
|
||||
|
||||
| 触发方式 | 命令/路径 | 说明 |
|
||||
|---|---|---|
|
||||
| Codex Stop hook | `.codex/hooks/archive-reminder.ps1` | 若已配置,每次任务结束自动调用 `hooks/archive-check.ps1` |
|
||||
| Claude Code 手动 | 直接运行脚本 | 任务结束时手动调用 `hooks/archive-check.ps1` |
|
||||
| 手动脚本 | `hooks/archive-check.ps1` | 直接运行 PowerShell 脚本 |
|
||||
|
||||
---
|
||||
|
||||
## 新增 Hook 时
|
||||
|
||||
1. 把脚本放入 `hooks/`。
|
||||
2. 在本文件中登记。
|
||||
3. 在 [ai/AI接入入口.md](../ai/AI接入入口.md) 中说明触发方式。
|
||||
4. 如果涉及提交信息格式,同步更新 [rules/40-工作流清单.md](../rules/40-工作流清单.md)。
|
||||
|
||||
---
|
||||
- 新增稳定模块路径时更新工作流与提交映射。
|
||||
- 脚本保持 Windows PowerShell 5.1 可运行,不依赖写入仓库的缓存。
|
||||
- 脚本只输出建议;源码边界警告不自动回退任何文件。
|
||||
- 新增检查后补无改动、中文路径、无 Unity 工程和双仓库场景验证。
|
||||
|
||||
## 文档关联
|
||||
|
||||
> 当本文档发生变更时,应同步检查以下关联文档,避免信息不一致。
|
||||
|
||||
| 文档 | 关联原因 |
|
||||
|---|---|
|
||||
| [ai/AI接入入口.md](../ai/AI接入入口.md) | AI 接入入口引用 hook 共享层 |
|
||||
| [rules/项目规则入口.md](../rules/项目规则入口.md) | 规则入口索引归档触发机制 |
|
||||
| [rules/40-工作流清单.md](../rules/40-工作流清单.md) | 提交前检查与工作流清单互相引用 |
|
||||
| [工作流清单](../rules/40-工作流清单.md) | 检查对应的任务阶段 |
|
||||
| [文档同步规则](../rules/50-文档同步规则.md) | 文档影响判断 |
|
||||
| [开发规则](../rules/20-开发规则.md) | 源码与生成代码约束 |
|
||||
| [AI 接入入口](../ai/AI接入入口.md) | AI 工具调用入口 |
|
||||
|
||||
@@ -1,181 +1,10 @@
|
||||
# hooks/archive-check.ps1
|
||||
# Check changed .md files and suggest related docs to update.
|
||||
# This script only outputs suggestions; it does NOT modify any files.
|
||||
#
|
||||
# Mechanism:
|
||||
# For every changed .md file, parse its "## 文档关联" section and extract
|
||||
# Markdown links. Those links are the documents that may need syncing.
|
||||
param(
|
||||
[string]$UnityProjectRoot = '',
|
||||
[string]$DocsRoot = ''
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "SilentlyContinue"
|
||||
|
||||
$scriptPath = $MyInvocation.MyCommand.Path
|
||||
if (-not [System.IO.Path]::IsPathRooted($scriptPath)) {
|
||||
$scriptPath = Join-Path (Get-Location) $scriptPath
|
||||
}
|
||||
$scriptPath = [System.IO.Path]::GetFullPath($scriptPath)
|
||||
$scriptDir = Split-Path -Parent $scriptPath
|
||||
$projectRoot = [System.IO.Path]::GetFullPath((Join-Path $scriptDir ".."))
|
||||
|
||||
Set-Location $projectRoot
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
function Resolve-RelativePath {
|
||||
param(
|
||||
[string]$BaseFile,
|
||||
[string]$LinkPath
|
||||
)
|
||||
|
||||
# Skip anchors, query strings, and external URLs
|
||||
if ($LinkPath -match '^[a-zA-Z][a-zA-Z0-9+.-]*://') { return $null }
|
||||
if ([string]::IsNullOrWhiteSpace($LinkPath)) { return $null }
|
||||
|
||||
# Drop fragment
|
||||
$pathOnly = ($LinkPath -split '#')[0]
|
||||
if ([string]::IsNullOrWhiteSpace($pathOnly)) { return $null }
|
||||
|
||||
$baseDir = Split-Path -Parent $BaseFile
|
||||
$combined = Join-Path $baseDir $pathOnly
|
||||
try {
|
||||
$resolved = [System.IO.Path]::GetFullPath($combined)
|
||||
} catch {
|
||||
return $null
|
||||
}
|
||||
|
||||
# Convert to relative path from project root, normalizing separators
|
||||
$fullProjectRoot = [System.IO.Path]::GetFullPath($projectRoot)
|
||||
if ($resolved.StartsWith($fullProjectRoot, [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||
$relative = $resolved.Substring($fullProjectRoot.Length).TrimStart('\', '/')
|
||||
return $relative -replace '\\', '/'
|
||||
}
|
||||
return $pathOnly -replace '\\', '/'
|
||||
}
|
||||
|
||||
function Get-DocumentAssociations {
|
||||
param(
|
||||
[string]$MdFile
|
||||
)
|
||||
|
||||
$fullPath = Join-Path $projectRoot $MdFile
|
||||
if (-not (Test-Path $fullPath)) { return @() }
|
||||
|
||||
$lines = [System.IO.File]::ReadAllLines($fullPath, [System.Text.Encoding]::UTF8)
|
||||
$associations = @()
|
||||
$inSection = $false
|
||||
$inCodeBlock = $false
|
||||
|
||||
foreach ($line in $lines) {
|
||||
# Toggle fenced code block state (``` or ~~~)
|
||||
if ($line -match '^\s*```' -or $line -match '^\s*~~~') {
|
||||
$inCodeBlock = -not $inCodeBlock
|
||||
continue
|
||||
}
|
||||
if ($inCodeBlock) { continue }
|
||||
|
||||
# Match exactly '## 文档关联' (not '## 文档关联机制' etc.).
|
||||
if ($line -match "^##\s+文档关联\s*$") {
|
||||
$inSection = $true
|
||||
continue
|
||||
}
|
||||
if ($inSection -and $line -match '^##\s+') {
|
||||
break
|
||||
}
|
||||
if ($inSection) {
|
||||
# Extract markdown links: [text](path)
|
||||
$regex = '\[(?<text>[^\]]*)\]\((?<path>[^\)]+)\)'
|
||||
$matches = [regex]::Matches($line, $regex)
|
||||
foreach ($match in $matches) {
|
||||
$linkPath = $match.Groups['path'].Value.Trim()
|
||||
$resolved = Resolve-RelativePath -BaseFile $MdFile -LinkPath $linkPath
|
||||
if ($resolved) {
|
||||
$associations += $resolved
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $associations | Select-Object -Unique
|
||||
}
|
||||
|
||||
function Test-WildcardMatch {
|
||||
param(
|
||||
[string]$Pattern,
|
||||
[string]$Path
|
||||
)
|
||||
$regex = "^" + ($Pattern -replace "\.", "\." -replace "\*", ".*" -replace "\?", ".") + "$"
|
||||
return $Path -match $regex
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Collect changed .md files (tracked modifications + untracked new files)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
$changedFiles = @(cmd /c "git diff --name-only 2>nul") | Where-Object { $_ }
|
||||
if ($changedFiles.Count -eq 0) {
|
||||
$changedFiles = @(cmd /c "git diff --name-only --cached 2>nul") | Where-Object { $_ }
|
||||
}
|
||||
|
||||
# Also include untracked .md files so new documents are checked too
|
||||
$untrackedFiles = @(cmd /c "git ls-files --others --exclude-standard 2>nul") | Where-Object { $_ }
|
||||
$allChangedFiles = @($changedFiles) + @($untrackedFiles) | Where-Object { $_ } | Select-Object -Unique
|
||||
|
||||
if (-not $allChangedFiles) {
|
||||
Write-Output "[archive-check] No documentation changes detected (git diff is empty and no untracked .md files)."
|
||||
exit 0
|
||||
}
|
||||
|
||||
$changedMdFiles = $allChangedFiles | Where-Object { $_ -like "*.md" } | Select-Object -Unique
|
||||
|
||||
if ($changedMdFiles.Count -eq 0) {
|
||||
Write-Output "[archive-check] No .md documentation changes detected."
|
||||
exit 0
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Build suggestions from each changed file's "## 文档关联" section
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
$suggestions = @{}
|
||||
|
||||
foreach ($changed in $changedMdFiles) {
|
||||
$relatedDocs = Get-DocumentAssociations -MdFile $changed
|
||||
foreach ($related in $relatedDocs) {
|
||||
# Normalize related path
|
||||
$relatedNormalized = $related -replace '\\', '/'
|
||||
if (-not $suggestions.ContainsKey($relatedNormalized)) {
|
||||
$suggestions[$relatedNormalized] = @()
|
||||
}
|
||||
if ($suggestions[$relatedNormalized] -notcontains $changed) {
|
||||
$suggestions[$relatedNormalized] += $changed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Output results
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Write-Output "[archive-check] Changed documentation files:"
|
||||
foreach ($changed in $changedMdFiles | Sort-Object) {
|
||||
Write-Output " - $changed"
|
||||
}
|
||||
|
||||
if ($suggestions.Count -eq 0) {
|
||||
Write-Output ""
|
||||
Write-Output "[archive-check] No associations found in changed files' `"## 文档关联`" sections."
|
||||
exit 0
|
||||
}
|
||||
|
||||
Write-Output ""
|
||||
Write-Output "[archive-check] Related docs that may need updating:"
|
||||
foreach ($related in $suggestions.Keys | Sort-Object) {
|
||||
$sources = $suggestions[$related] | Sort-Object
|
||||
$sourceList = $sources -join ", "
|
||||
Write-Output " - $related (mentioned by: $sourceList)"
|
||||
}
|
||||
|
||||
Write-Output ""
|
||||
Write-Output "[archive-check] Please confirm whether to sync-update the above docs."
|
||||
exit 0
|
||||
Write-Warning 'archive-check.ps1 is a compatibility entry. Use workflow-check.ps1 for new integrations.'
|
||||
& (Join-Path $PSScriptRoot 'workflow-check.ps1') `
|
||||
-UnityProjectRoot $UnityProjectRoot `
|
||||
-DocsRoot $DocsRoot
|
||||
exit $LASTEXITCODE
|
||||
|
||||
@@ -1,111 +1,106 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
提交前共享检查:推断变更文件所属模块,校验提交信息格式。
|
||||
|
||||
.DESCRIPTION
|
||||
读取 git diff 中变更的文件,根据 hooks/commit-module-mappings.json
|
||||
推断模块,然后校验提交信息是否符合 [模块][类型]: 描述 格式。
|
||||
|
||||
.PARAMETER Message
|
||||
待校验的提交信息。
|
||||
|
||||
.EXAMPLE
|
||||
.\docs\hooks\commit-check.ps1 -Message "[entity][docs]: 补充实体池说明"
|
||||
#>
|
||||
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Message
|
||||
[string]$Message,
|
||||
[string]$RepositoryRoot = ''
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($RepositoryRoot)) {
|
||||
$RepositoryRoot = (Get-Location).Path
|
||||
}
|
||||
$RepositoryRoot = [System.IO.Path]::GetFullPath($RepositoryRoot)
|
||||
$gitRoot = & git -C $RepositoryRoot rev-parse --show-toplevel 2>$null | Select-Object -First 1
|
||||
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($gitRoot)) {
|
||||
Write-Error "Git repository was not found: $RepositoryRoot"
|
||||
exit 1
|
||||
}
|
||||
$RepositoryRoot = [System.IO.Path]::GetFullPath($gitRoot)
|
||||
|
||||
$mappingFile = Join-Path $PSScriptRoot 'commit-module-mappings.json'
|
||||
if (-not (Test-Path $mappingFile)) {
|
||||
Write-Error "Module mapping file not found: $mappingFile"
|
||||
$mapping = Get-Content -LiteralPath $mappingFile -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
|
||||
function Get-CommitFiles {
|
||||
param([string]$Root)
|
||||
|
||||
$staged = @(& git -C $Root -c core.quotepath=false diff --cached --name-only --diff-filter=ACMRD)
|
||||
$staged = @($staged | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
|
||||
if ($staged.Count -gt 0) {
|
||||
return $staged
|
||||
}
|
||||
|
||||
$files = @()
|
||||
$statusLines = & git -C $Root -c core.quotepath=false status --porcelain=v1 --untracked-files=all
|
||||
foreach ($line in $statusLines) {
|
||||
if ([string]::IsNullOrWhiteSpace($line) -or $line.Length -lt 4) {
|
||||
continue
|
||||
}
|
||||
$path = $line.Substring(3).Trim()
|
||||
if ($path.Contains(' -> ')) {
|
||||
$path = ($path -split ' -> ')[-1]
|
||||
}
|
||||
$files += $path.Replace('\', '/')
|
||||
}
|
||||
return @($files | Select-Object -Unique)
|
||||
}
|
||||
|
||||
function Get-Modules {
|
||||
param([string[]]$Files)
|
||||
|
||||
$modules = @()
|
||||
foreach ($file in $Files) {
|
||||
foreach ($entry in $mapping.mappings) {
|
||||
if ($file -match $entry.pattern) {
|
||||
$modules += $entry.module
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return @($modules | Select-Object -Unique)
|
||||
}
|
||||
|
||||
if ($Message -notmatch '^\[([^\]]+)\]\[([^\]]+)\]:\s+(.+)$') {
|
||||
Write-Error 'Commit message format must be: [module][type]: description'
|
||||
exit 1
|
||||
}
|
||||
|
||||
$mapping = Get-Content $mappingFile -Raw | ConvertFrom-Json
|
||||
|
||||
function Get-ChangedFiles {
|
||||
$staged = (cmd /c "git diff --cached --name-only 2>nul") -split "`r?`n" | Where-Object { $_ }
|
||||
if (-not $staged) { $staged = @() }
|
||||
|
||||
$unstaged = (cmd /c "git diff --name-only 2>nul") -split "`r?`n" | Where-Object { $_ }
|
||||
if (-not $unstaged) { $unstaged = @() }
|
||||
|
||||
$all = @($staged) + @($unstaged) | Select-Object -Unique | Where-Object { $_ }
|
||||
return $all
|
||||
$messageModule = $Matches[1]
|
||||
$messageType = $Matches[2]
|
||||
$description = $Matches[3]
|
||||
if ([string]::IsNullOrWhiteSpace($description)) {
|
||||
Write-Error 'Commit message description is empty.'
|
||||
exit 1
|
||||
}
|
||||
if ($mapping.validTypes -notcontains $messageType) {
|
||||
Write-Error ("Invalid commit type '{0}'. Allowed types: {1}" -f
|
||||
$messageType, ($mapping.validTypes -join ', '))
|
||||
exit 1
|
||||
}
|
||||
|
||||
function Get-ModuleForFile($file) {
|
||||
foreach ($entry in $mapping.mappings) {
|
||||
if ($file -match $entry.pattern) {
|
||||
return $entry.module
|
||||
}
|
||||
}
|
||||
return $null
|
||||
}
|
||||
$files = @(Get-CommitFiles $RepositoryRoot)
|
||||
$modules = @(Get-Modules $files)
|
||||
|
||||
function Get-ModulesFromChangedFiles($files) {
|
||||
$modules = @()
|
||||
foreach ($file in $files) {
|
||||
$module = Get-ModuleForFile $file
|
||||
if ($module) {
|
||||
$modules += $module
|
||||
}
|
||||
}
|
||||
return $modules | Select-Object -Unique
|
||||
}
|
||||
|
||||
function Test-CommitMessage($message, $modules) {
|
||||
if ($message -notmatch '^\[([^\]]+)\]\[([^\]]+)\]:\s*(.+)$') {
|
||||
return @{ IsValid = $false; Reason = '提交信息格式应为:[模块][类型]: 描述' }
|
||||
}
|
||||
|
||||
$msgModule = $Matches[1]
|
||||
$msgType = $Matches[2]
|
||||
$msgDesc = $Matches[3]
|
||||
|
||||
if (-not $msgDesc) {
|
||||
return @{ IsValid = $false; Reason = '提交信息缺少描述内容' }
|
||||
}
|
||||
|
||||
if ($mapping.validTypes -notcontains $msgType) {
|
||||
return @{ IsValid = $false; Reason = "无效的类型 '$msgType'。有效类型:$($mapping.validTypes -join ', ')" }
|
||||
}
|
||||
|
||||
if ($modules -and $modules -notcontains $msgModule) {
|
||||
Write-Warning "提交信息中的模块 [$msgModule] 与变更文件推断的模块 [$($modules -join ', ')] 不一致,请确认是否 intentional。"
|
||||
}
|
||||
|
||||
return @{ IsValid = $true; Reason = '' }
|
||||
}
|
||||
|
||||
$files = Get-ChangedFiles
|
||||
if (-not $files) {
|
||||
Write-Host "未检测到变更文件。" -ForegroundColor Yellow
|
||||
exit 0
|
||||
}
|
||||
|
||||
$modules = Get-ModulesFromChangedFiles $files
|
||||
|
||||
Write-Host "变更文件:"
|
||||
$files | ForEach-Object { Write-Host " - $_" }
|
||||
|
||||
if ($modules) {
|
||||
Write-Host "推断模块:$($modules -join ', ')"
|
||||
Write-Output ("Repository: {0}" -f $RepositoryRoot)
|
||||
if ($files.Count -eq 0) {
|
||||
Write-Warning 'No staged, unstaged, or untracked files were found. Only the message was validated.'
|
||||
}
|
||||
else {
|
||||
Write-Warning "未能从变更文件推断出模块,请检查提交信息是否手动指定了正确模块。"
|
||||
Write-Output 'Commit scope files:'
|
||||
foreach ($file in $files) {
|
||||
Write-Output (" - {0}" -f $file)
|
||||
}
|
||||
}
|
||||
|
||||
$result = Test-CommitMessage $Message $modules
|
||||
if (-not $result.IsValid) {
|
||||
Write-Error $result.Reason
|
||||
exit 1
|
||||
if ($modules.Count -gt 0) {
|
||||
Write-Output ("Detected modules: {0}" -f ($modules -join ', '))
|
||||
if ($modules -notcontains $messageModule) {
|
||||
Write-Warning ("Message module [{0}] does not match detected modules [{1}]. Review the commit scope." -f
|
||||
$messageModule, ($modules -join ', '))
|
||||
}
|
||||
}
|
||||
else {
|
||||
Write-Warning 'No path mapping matched. Review the commit module manually.'
|
||||
}
|
||||
|
||||
Write-Host "提交信息格式正确。" -ForegroundColor Green
|
||||
Write-Output 'Commit message is valid.'
|
||||
exit 0
|
||||
|
||||
@@ -1,81 +1,38 @@
|
||||
{
|
||||
{
|
||||
"mappings": [
|
||||
{
|
||||
"pattern": "rules/.*",
|
||||
"module": "rules"
|
||||
},
|
||||
{
|
||||
"pattern": "modules/frameworks/实体系统.*",
|
||||
"module": "entity"
|
||||
},
|
||||
{
|
||||
"pattern": "modules/frameworks/UI窗口系统.*",
|
||||
"module": "ui"
|
||||
},
|
||||
{
|
||||
"pattern": "modules/frameworks/事件系统.*",
|
||||
"module": "event"
|
||||
},
|
||||
{
|
||||
"pattern": "modules/frameworks/资源管理.*|modules/frameworks/通用框架.*",
|
||||
"module": "resource"
|
||||
},
|
||||
{
|
||||
"pattern": "modules/frameworks/内存对象管理.*",
|
||||
"module": "pool"
|
||||
},
|
||||
{
|
||||
"pattern": "modules/frameworks/相机系统.*",
|
||||
"module": "camera"
|
||||
},
|
||||
{
|
||||
"pattern": "modules/frameworks/场景系统.*",
|
||||
"module": "scene"
|
||||
},
|
||||
{
|
||||
"pattern": "modules/frameworks/网络系统.*",
|
||||
"module": "network"
|
||||
},
|
||||
{
|
||||
"pattern": "modules/frameworks/数据配置框架.*",
|
||||
"module": "config"
|
||||
},
|
||||
{
|
||||
"pattern": "modules/frameworks/技能系统.*",
|
||||
"module": "skill"
|
||||
},
|
||||
{
|
||||
"pattern": "modules/frameworks/Hotfix启动.*",
|
||||
"module": "hotfix"
|
||||
},
|
||||
{
|
||||
"pattern": "modules/frameworks/编辑器工具.*|modules/frameworks/第三方插件.*",
|
||||
"module": "editor"
|
||||
},
|
||||
{
|
||||
"pattern": "hooks/.*",
|
||||
"module": "hooks"
|
||||
},
|
||||
{
|
||||
"pattern": "ai/.*|AGENTS\\.md|CLAUDE\\.md|RULES\\.md",
|
||||
"module": "ai"
|
||||
},
|
||||
{
|
||||
"pattern": "specs/.*|plans/.*",
|
||||
"module": "spec"
|
||||
},
|
||||
{
|
||||
"pattern": "modules/frameworks/编辑器工具/UI工具/.*",
|
||||
"module": "tools"
|
||||
},
|
||||
{
|
||||
"pattern": "business-reference/.*",
|
||||
"module": "business-reference"
|
||||
},
|
||||
{
|
||||
"pattern": "README\\.md",
|
||||
"module": "docs"
|
||||
}
|
||||
{ "pattern": "^rules/", "module": "rules" },
|
||||
{ "pattern": "^ai/|^(AGENTS|CLAUDE|RULES)\\.md$", "module": "ai" },
|
||||
{ "pattern": "^hooks/", "module": "hooks" },
|
||||
{ "pattern": "^Tools/CodexUnityMcp/|^modules/frameworks/编辑器工具/MCP工具/", "module": "mcp" },
|
||||
{ "pattern": "^modules/frameworks/UI窗口系统/|^modules/frameworks/编辑器工具/UI拼接工具\\.md$", "module": "ui" },
|
||||
{ "pattern": "^modules/frameworks/资源管理/", "module": "resource" },
|
||||
{ "pattern": "^modules/frameworks/实体系统/", "module": "entity" },
|
||||
{ "pattern": "^modules/frameworks/事件系统/", "module": "event" },
|
||||
{ "pattern": "^modules/frameworks/内存对象管理/", "module": "pool" },
|
||||
{ "pattern": "^modules/frameworks/相机系统/", "module": "camera" },
|
||||
{ "pattern": "^modules/frameworks/场景系统", "module": "scene" },
|
||||
{ "pattern": "^modules/frameworks/网络系统", "module": "network" },
|
||||
{ "pattern": "^modules/frameworks/数据配置框架", "module": "config" },
|
||||
{ "pattern": "^modules/frameworks/技能系统/", "module": "skill" },
|
||||
{ "pattern": "^modules/frameworks/Hotfix启动", "module": "hotfix" },
|
||||
{ "pattern": "^modules/frameworks/编辑器工具/", "module": "editor" },
|
||||
{ "pattern": "^modules/frameworks/", "module": "framework" },
|
||||
{ "pattern": "^modules/", "module": "docs" },
|
||||
{ "pattern": "^business-reference/", "module": "business-reference" },
|
||||
{ "pattern": "^README\\.md$", "module": "docs" },
|
||||
|
||||
{ "pattern": "^Assets/StrayFog/Editor/CopyToX/CopyToHF/(AssetBundle|SpriteAtlas)/", "module": "resource" },
|
||||
{ "pattern": "^Assets/StrayFog/Core/SFSubPackageV2\\.cs$|^Assets/StrayFog/Editor/Tools/AssestBundle/", "module": "resource" },
|
||||
{ "pattern": "^Assets/StrayFog/Editor/CopyToX/CopyToHF/UIWindowMgr/", "module": "ui" },
|
||||
{ "pattern": "^Assets/StrayFog/Editor/", "module": "editor" },
|
||||
{ "pattern": "^Assets/StrayFog/Core/", "module": "framework" },
|
||||
{ "pattern": "^Assets/Editor/Command/", "module": "mcp" },
|
||||
{ "pattern": "^Assets/Game/GameHFScripte/GameFunction/HFDownLoadSubPackage/|^Assets/Game/GameMono/Scripts/GameDllManager\\.cs$", "module": "resource" },
|
||||
{ "pattern": "^Assets/Game/GameHFScripte/GameFunction/UIWindows/|^Assets/Game/GameMono/Scripts/UIMono/", "module": "ui" },
|
||||
{ "pattern": "^Assets/Game/GameHFScripte/", "module": "gamehf" },
|
||||
{ "pattern": "^Assets/Game/GameGeneralScripte/", "module": "general" },
|
||||
{ "pattern": "^Assets/Game/GameMono/", "module": "mono" },
|
||||
{ "pattern": "^Assets/Game/GameHFResource/", "module": "asset" }
|
||||
],
|
||||
"validTypes": [
|
||||
"feat",
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
# hooks/sync-doc-associations.ps1
|
||||
# Wrapper for sync_doc_associations.py
|
||||
# This script appends change reminders to associated .md files.
|
||||
# It only appends to standard sections; it never overwrites existing content.
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$scriptPath = $MyInvocation.MyCommand.Path
|
||||
if (-not [System.IO.Path]::IsPathRooted($scriptPath)) {
|
||||
$scriptPath = Join-Path (Get-Location) $scriptPath
|
||||
}
|
||||
$scriptPath = [System.IO.Path]::GetFullPath($scriptPath)
|
||||
$scriptDir = Split-Path -Parent $scriptPath
|
||||
$pythonScript = Join-Path $scriptDir "sync_doc_associations.py"
|
||||
|
||||
if (-not (Test-Path $pythonScript)) {
|
||||
Write-Error "Python script not found: $pythonScript"
|
||||
exit 1
|
||||
}
|
||||
|
||||
python "$pythonScript"
|
||||
exit $LASTEXITCODE
|
||||
@@ -1,218 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# hooks/sync_doc_associations.py
|
||||
#
|
||||
# 当 md 文件变更时,自动在关联文档的标准位置追加变更提醒:
|
||||
# - 有 ## 变更记录 表格的模块文档:追加一行变更记录
|
||||
# - 有 ## 下一步 清单的 progress/tasks 文档:追加一个待检查项
|
||||
# - 其他文档:追加 ## 最近关联变更 章节
|
||||
#
|
||||
# 本脚本只追加、不覆盖正文;执行后请通过 git diff 人工确认。
|
||||
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
|
||||
|
||||
EXCLUDED_PREFIXES = (
|
||||
'.claude/', '.codex/', '.git/',
|
||||
'Library/', 'Temp/', 'Logs/', 'UserSettings/',
|
||||
'obj/', 'bin/', 'HybridCLRData/',
|
||||
'Assets/ThirdPlugins/',
|
||||
'Assets/.WX-WASM-SDK-V2/',
|
||||
'Packages/',
|
||||
)
|
||||
|
||||
|
||||
def run_git(args):
|
||||
result = subprocess.run(
|
||||
["git"] + args,
|
||||
cwd=PROJECT_ROOT,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
return result.stdout.strip(), result.returncode
|
||||
|
||||
|
||||
def get_changed_md_files():
|
||||
"""获取 tracked 改动 + untracked 新 md 文件。"""
|
||||
changed, code = run_git(["diff", "--name-only"])
|
||||
if code != 0 or not changed:
|
||||
changed = ""
|
||||
|
||||
untracked, code = run_git(["ls-files", "--others", "--exclude-standard"])
|
||||
if code == 0 and untracked:
|
||||
changed = (changed + "\n" + untracked).strip()
|
||||
|
||||
if not changed:
|
||||
return []
|
||||
|
||||
files = [line.strip() for line in changed.splitlines() if line.strip().endswith(".md")]
|
||||
files = [f for f in files if not f.startswith(EXCLUDED_PREFIXES)]
|
||||
return sorted(set(files))
|
||||
|
||||
|
||||
def resolve_link(source_file: Path, link: str):
|
||||
"""解析相对链接为项目相对路径。"""
|
||||
if re.match(r"^[a-zA-Z][a-zA-Z0-9+.-]*://", link):
|
||||
return None
|
||||
if link.startswith("#"):
|
||||
return None
|
||||
link = link.split("#")[0]
|
||||
if not link:
|
||||
return None
|
||||
target = (source_file.parent / link).resolve()
|
||||
try:
|
||||
target.relative_to(PROJECT_ROOT)
|
||||
except ValueError:
|
||||
return None
|
||||
rel = target.relative_to(PROJECT_ROOT)
|
||||
return str(rel).replace("\\", "/")
|
||||
|
||||
|
||||
def extract_associations(md_path: Path):
|
||||
"""读取 md 的 ## 文档关联 章节,返回关联文件相对路径列表。"""
|
||||
content = md_path.read_text(encoding="utf-8")
|
||||
in_section = False
|
||||
targets = []
|
||||
for line in content.splitlines():
|
||||
if re.match(r"^##\s+文档关联\s*$", line):
|
||||
in_section = True
|
||||
continue
|
||||
if in_section and re.match(r"^##\s+", line):
|
||||
break
|
||||
if in_section:
|
||||
for _, link in re.findall(r"\[([^\]]*)\]\(([^\)]+)\)", line):
|
||||
resolved = resolve_link(md_path, link)
|
||||
if resolved:
|
||||
targets.append(resolved)
|
||||
return targets
|
||||
|
||||
|
||||
def find_section_range(lines, title_re):
|
||||
"""返回 [start, end) 行号,end 为下一个 ## 标题或文件末尾。"""
|
||||
start = None
|
||||
for i, line in enumerate(lines):
|
||||
if re.match(title_re, line):
|
||||
start = i
|
||||
continue
|
||||
if start is not None and re.match(r"^##\s+", line):
|
||||
return start, i
|
||||
if start is not None:
|
||||
return start, len(lines)
|
||||
return None, None
|
||||
|
||||
|
||||
def append_changelog(target_path: Path, source_rel: str, date_str: str) -> bool:
|
||||
"""在 ## 变更记录 表格追加一行;成功返回 True。"""
|
||||
content = target_path.read_text(encoding="utf-8")
|
||||
lines = content.splitlines()
|
||||
start, end = find_section_range(lines, r"^##\s+变更记录\s*$")
|
||||
if start is None:
|
||||
return False
|
||||
|
||||
# 找到表格最后一行(跳过标题和分隔线)
|
||||
insert_pos = end
|
||||
for i in range(start + 1, end):
|
||||
if lines[i].strip().startswith("|") and not re.match(r"^\|[-:\s|]+\|$", lines[i].strip()):
|
||||
insert_pos = i + 1
|
||||
|
||||
new_line = f"| {date_str} | [{source_rel}](../../{source_rel}) 发生变更 | 待补充影响范围 | 待确认 |"
|
||||
lines.insert(insert_pos, new_line)
|
||||
target_path.write_text("\n".join(lines) + ("\n" if content.endswith("\n") else ""), encoding="utf-8")
|
||||
return True
|
||||
|
||||
|
||||
def append_next_step(target_path: Path, source_rel: str) -> bool:
|
||||
"""在 ## 下一步 清单追加一个待检查项;成功返回 True。"""
|
||||
content = target_path.read_text(encoding="utf-8")
|
||||
lines = content.splitlines()
|
||||
start, end = find_section_range(lines, r"^##\s+下一步\s*$")
|
||||
if start is None:
|
||||
return False
|
||||
|
||||
new_line = f"- [ ] 检查 [{source_rel}](../../{source_rel}) 的变更是否影响本文档"
|
||||
# 避免重复
|
||||
for line in lines[start:end]:
|
||||
if source_rel in line and "变更是否影响本文档" in line:
|
||||
return True # 已存在,视为成功但不写入
|
||||
|
||||
lines.insert(end, new_line)
|
||||
target_path.write_text("\n".join(lines) + ("\n" if content.endswith("\n") else ""), encoding="utf-8")
|
||||
return True
|
||||
|
||||
|
||||
def append_recent_changes_section(target_path: Path, source_rel: str, date_str: str) -> bool:
|
||||
"""在没有 变更记录/下一步 的文档末尾追加 ## 最近关联变更。"""
|
||||
content = target_path.read_text(encoding="utf-8")
|
||||
# 生成从 target 到 source 的相对链接
|
||||
try:
|
||||
link = os.path.relpath(PROJECT_ROOT / source_rel, target_path.parent).replace("\\", "/")
|
||||
except ValueError:
|
||||
link = source_rel
|
||||
|
||||
if "## 最近关联变更" in content:
|
||||
lines = content.splitlines()
|
||||
start, end = find_section_range(lines, r"^##\s+最近关联变更\s*$")
|
||||
if start is not None:
|
||||
new_line = f"- {date_str}:关联文档 [{source_rel}]({link}) 发生变更,需同步检查。"
|
||||
lines.insert(end, new_line)
|
||||
target_path.write_text("\n".join(lines) + ("\n" if content.endswith("\n") else ""), encoding="utf-8")
|
||||
return True
|
||||
|
||||
section = (
|
||||
"\n---\n\n"
|
||||
"## 最近关联变更\n\n"
|
||||
f"- {date_str}:关联文档 [{source_rel}]({link}) 发生变更,需同步检查。\n"
|
||||
)
|
||||
target_path.write_text(content.rstrip() + section, encoding="utf-8")
|
||||
return True
|
||||
|
||||
|
||||
def sync_associations():
|
||||
changed_files = get_changed_md_files()
|
||||
if not changed_files:
|
||||
print("[sync-doc-assoc] No .md changes detected.")
|
||||
return 0
|
||||
|
||||
date_str = datetime.now().strftime("%Y-%m-%d")
|
||||
modified = []
|
||||
|
||||
for source_rel in changed_files:
|
||||
source_path = PROJECT_ROOT / source_rel
|
||||
if not source_path.exists():
|
||||
continue
|
||||
targets = extract_associations(source_path)
|
||||
for target_rel in targets:
|
||||
target_path = PROJECT_ROOT / target_rel
|
||||
if not target_path.is_file():
|
||||
# Skip directories, globs, or missing targets
|
||||
continue
|
||||
if target_rel == source_rel:
|
||||
continue
|
||||
|
||||
done = (
|
||||
append_changelog(target_path, source_rel, date_str)
|
||||
or append_next_step(target_path, source_rel)
|
||||
or append_recent_changes_section(target_path, source_rel, date_str)
|
||||
)
|
||||
if done:
|
||||
modified.append((source_rel, target_rel))
|
||||
|
||||
if not modified:
|
||||
print("[sync-doc-assoc] No associated docs need updating.")
|
||||
return 0
|
||||
|
||||
print("[sync-doc-assoc] Updated associated docs:")
|
||||
for source, target in modified:
|
||||
print(f" - {source} -> {target}")
|
||||
print("\n[sync-doc-assoc] Please review the changes with git diff before committing.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(sync_associations())
|
||||
311
hooks/workflow-check.ps1
Normal file
311
hooks/workflow-check.ps1
Normal file
@@ -0,0 +1,311 @@
|
||||
param(
|
||||
[string]$UnityProjectRoot = '',
|
||||
[string]$DocsRoot = ''
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Get-FullPath {
|
||||
param([string]$Path, [string]$BasePath)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($Path)) {
|
||||
return $null
|
||||
}
|
||||
if (-not [System.IO.Path]::IsPathRooted($Path)) {
|
||||
$Path = Join-Path $BasePath $Path
|
||||
}
|
||||
return [System.IO.Path]::GetFullPath($Path)
|
||||
}
|
||||
|
||||
function Test-UnityProject {
|
||||
param([string]$Path)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($Path)) {
|
||||
return $false
|
||||
}
|
||||
return (Test-Path -LiteralPath (Join-Path $Path 'Assets')) -and
|
||||
(Test-Path -LiteralPath (Join-Path $Path 'ProjectSettings/ProjectVersion.txt'))
|
||||
}
|
||||
|
||||
function Resolve-UnityProject {
|
||||
param([string]$ExplicitPath, [string]$DocumentationRoot)
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($ExplicitPath)) {
|
||||
$resolved = Get-FullPath $ExplicitPath (Get-Location).Path
|
||||
if (-not (Test-UnityProject $resolved)) {
|
||||
throw "Unity project was not found: $resolved"
|
||||
}
|
||||
return $resolved
|
||||
}
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($env:UNITY_PROJECT_ROOT)) {
|
||||
$resolved = Get-FullPath $env:UNITY_PROJECT_ROOT (Get-Location).Path
|
||||
if (-not (Test-UnityProject $resolved)) {
|
||||
throw "UNITY_PROJECT_ROOT is not a Unity project: $resolved"
|
||||
}
|
||||
return $resolved
|
||||
}
|
||||
|
||||
$current = [System.IO.Path]::GetFullPath((Get-Location).Path)
|
||||
if (Test-UnityProject $current) {
|
||||
return $current
|
||||
}
|
||||
|
||||
$parent = Split-Path -Parent $DocumentationRoot
|
||||
$candidates = @()
|
||||
if (Test-Path -LiteralPath $parent) {
|
||||
$directories = Get-ChildItem -LiteralPath $parent -Directory -ErrorAction SilentlyContinue
|
||||
foreach ($directory in $directories) {
|
||||
if (Test-UnityProject $directory.FullName) {
|
||||
$candidates += $directory.FullName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($candidates.Count -eq 1) {
|
||||
return [System.IO.Path]::GetFullPath($candidates[0])
|
||||
}
|
||||
if ($candidates.Count -gt 1) {
|
||||
Write-Warning 'Multiple sibling Unity projects were found. Use -UnityProjectRoot to select one.'
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Get-GitRoot {
|
||||
param([string]$Path)
|
||||
|
||||
$result = & git -C $Path rev-parse --show-toplevel 2>$null
|
||||
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($result)) {
|
||||
return $null
|
||||
}
|
||||
return [System.IO.Path]::GetFullPath(($result | Select-Object -First 1))
|
||||
}
|
||||
|
||||
function Get-GitChanges {
|
||||
param([string]$RepositoryRoot, [string]$RepositoryName)
|
||||
|
||||
$result = @()
|
||||
if ([string]::IsNullOrWhiteSpace($RepositoryRoot)) {
|
||||
return $result
|
||||
}
|
||||
|
||||
$lines = & git -C $RepositoryRoot -c core.quotepath=false status --porcelain=v1 --untracked-files=all
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Could not read git status: $RepositoryRoot"
|
||||
}
|
||||
|
||||
foreach ($line in $lines) {
|
||||
if ([string]::IsNullOrWhiteSpace($line) -or $line.Length -lt 4) {
|
||||
continue
|
||||
}
|
||||
$path = $line.Substring(3).Trim()
|
||||
if ($path.Contains(' -> ')) {
|
||||
$path = ($path -split ' -> ')[-1]
|
||||
}
|
||||
$result += [pscustomobject]@{
|
||||
Repository = $RepositoryName
|
||||
Root = $RepositoryRoot
|
||||
Status = $line.Substring(0, 2)
|
||||
Path = $path.Replace('\', '/')
|
||||
}
|
||||
}
|
||||
return $result
|
||||
}
|
||||
|
||||
function Show-Changes {
|
||||
param([string]$Title, [object[]]$Changes)
|
||||
|
||||
Write-Output ""
|
||||
Write-Output "[$Title]"
|
||||
if ($Changes.Count -eq 0) {
|
||||
Write-Output ' Clean'
|
||||
return
|
||||
}
|
||||
foreach ($change in $Changes) {
|
||||
Write-Output (" {0} {1}" -f $change.Status, $change.Path)
|
||||
}
|
||||
}
|
||||
|
||||
function Get-MarkdownLinkFailures {
|
||||
param([string]$DocumentationRoot, [object[]]$Changes)
|
||||
|
||||
$failures = @()
|
||||
$markdownChanges = $Changes | Where-Object { $_.Path -like '*.md' -and $_.Status -notmatch 'D' }
|
||||
foreach ($change in $markdownChanges) {
|
||||
$fullPath = Join-Path $DocumentationRoot $change.Path
|
||||
if (-not (Test-Path -LiteralPath $fullPath)) {
|
||||
continue
|
||||
}
|
||||
$content = [System.IO.File]::ReadAllText($fullPath, [System.Text.Encoding]::UTF8)
|
||||
$matches = [regex]::Matches($content, '!?(?:\[[^\]]*\])\((?<path>[^\)]+)\)')
|
||||
foreach ($match in $matches) {
|
||||
$link = $match.Groups['path'].Value.Trim()
|
||||
if ([string]::IsNullOrWhiteSpace($link) -or $link.StartsWith('#') -or
|
||||
$link -match '^[a-zA-Z][a-zA-Z0-9+.-]*://' -or $link -match '^mailto:') {
|
||||
continue
|
||||
}
|
||||
if ($link.StartsWith('<') -and $link.EndsWith('>')) {
|
||||
$link = $link.Substring(1, $link.Length - 2)
|
||||
}
|
||||
$pathOnly = ($link -split '#')[0]
|
||||
if ([string]::IsNullOrWhiteSpace($pathOnly)) {
|
||||
continue
|
||||
}
|
||||
$decoded = [System.Uri]::UnescapeDataString($pathOnly)
|
||||
$target = Get-FullPath $decoded (Split-Path -Parent $fullPath)
|
||||
if (-not (Test-Path -LiteralPath $target)) {
|
||||
$failures += [pscustomobject]@{
|
||||
Source = $change.Path
|
||||
Link = $link
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $failures
|
||||
}
|
||||
|
||||
$scriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
if ([string]::IsNullOrWhiteSpace($DocsRoot)) {
|
||||
$DocsRoot = Split-Path -Parent $scriptRoot
|
||||
}
|
||||
$DocsRoot = Get-FullPath $DocsRoot (Get-Location).Path
|
||||
$docsGitRoot = Get-GitRoot $DocsRoot
|
||||
if ([string]::IsNullOrWhiteSpace($docsGitRoot)) {
|
||||
throw "Documentation git repository was not found: $DocsRoot"
|
||||
}
|
||||
$DocsRoot = $docsGitRoot
|
||||
|
||||
$resolvedUnityRoot = Resolve-UnityProject $UnityProjectRoot $DocsRoot
|
||||
$unityGitRoot = $null
|
||||
if (-not [string]::IsNullOrWhiteSpace($resolvedUnityRoot)) {
|
||||
$unityGitRoot = Get-GitRoot $resolvedUnityRoot
|
||||
if ([string]::IsNullOrWhiteSpace($unityGitRoot)) {
|
||||
throw "Unity project is not a git repository: $resolvedUnityRoot"
|
||||
}
|
||||
}
|
||||
|
||||
$docsChanges = @(Get-GitChanges $DocsRoot 'UnityAI')
|
||||
$unityChanges = @()
|
||||
if (-not [string]::IsNullOrWhiteSpace($unityGitRoot)) {
|
||||
$unityChanges = @(Get-GitChanges $unityGitRoot (Split-Path -Leaf $resolvedUnityRoot))
|
||||
}
|
||||
|
||||
Write-Output '[workflow-check] Read-only StrayFog workflow check'
|
||||
Write-Output ("Documentation repository: {0}" -f $DocsRoot)
|
||||
if ($resolvedUnityRoot) {
|
||||
Write-Output ("Unity project: {0}" -f $resolvedUnityRoot)
|
||||
$versionFile = Join-Path $resolvedUnityRoot 'ProjectSettings/ProjectVersion.txt'
|
||||
$versionLine = Get-Content -LiteralPath $versionFile -Encoding UTF8 |
|
||||
Where-Object { $_ -match '^m_EditorVersion:' } | Select-Object -First 1
|
||||
if ($versionLine) {
|
||||
Write-Output ("Unity version: {0}" -f (($versionLine -split ':', 2)[1].Trim()))
|
||||
}
|
||||
}
|
||||
else {
|
||||
Write-Warning 'Unity project was not found. Running documentation-only checks.'
|
||||
}
|
||||
|
||||
Show-Changes 'UnityAI changes' $docsChanges
|
||||
Show-Changes 'Unity project changes' $unityChanges
|
||||
|
||||
$unityPaths = @($unityChanges | ForEach-Object { $_.Path })
|
||||
$copyRelationships = @(
|
||||
[pscustomobject]@{
|
||||
Name = 'CopyToHF'
|
||||
SourcePattern = '^Assets/StrayFog/Editor/CopyToX/CopyToHF/'
|
||||
TargetPattern = '^Assets/Game/GameHFScripte/CopyToHF/'
|
||||
GeneratedName = 'HF'
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = 'CopyToGeneral'
|
||||
SourcePattern = '^Assets/StrayFog/Editor/CopyToX/CopyToGeneral/'
|
||||
TargetPattern = '^Assets/Game/GameGeneralScripte/CopyToGeneral/'
|
||||
GeneratedName = 'General'
|
||||
}
|
||||
)
|
||||
|
||||
Write-Output ''
|
||||
Write-Output '[Source and generation boundary]'
|
||||
$relationshipDetected = $false
|
||||
foreach ($relationship in $copyRelationships) {
|
||||
$hasSource = @($unityPaths | Where-Object {
|
||||
$_ -match $relationship.SourcePattern
|
||||
}).Count -gt 0
|
||||
$hasTarget = @($unityPaths | Where-Object {
|
||||
$_ -match $relationship.TargetPattern
|
||||
}).Count -gt 0
|
||||
|
||||
if (-not $hasSource -and -not $hasTarget) {
|
||||
continue
|
||||
}
|
||||
$relationshipDetected = $true
|
||||
if ($hasSource -and -not $hasTarget) {
|
||||
Write-Warning ("{0} source changed but generated target did not change. Run CopyToX and inspect the generated diff." -f
|
||||
$relationship.Name)
|
||||
}
|
||||
elseif ($hasTarget -and -not $hasSource) {
|
||||
Write-Warning ("Generated {0} files changed without {1} source changes. Confirm that generated files were not edited directly." -f
|
||||
$relationship.GeneratedName, $relationship.Name)
|
||||
}
|
||||
else {
|
||||
Write-Output (" Source and generated target changes were both detected for {0}." -f
|
||||
$relationship.Name)
|
||||
}
|
||||
}
|
||||
if (-not $relationshipDetected) {
|
||||
Write-Output ' No CopyToX source/target relationship was detected.'
|
||||
}
|
||||
|
||||
$mappingPath = Join-Path $scriptRoot 'workflow-path-mappings.json'
|
||||
$mapping = Get-Content -LiteralPath $mappingPath -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
$documents = @{}
|
||||
$verifications = @{}
|
||||
foreach ($path in $unityPaths) {
|
||||
foreach ($entry in $mapping.mappings) {
|
||||
if ($path -match $entry.pattern) {
|
||||
$documents[$entry.document] = $true
|
||||
$verifications[$entry.verification] = $true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Write-Output ''
|
||||
Write-Output '[Suggested verification]'
|
||||
if ($verifications.Count -eq 0) {
|
||||
Write-Output ' No Unity verification suggestions for the current diff.'
|
||||
}
|
||||
else {
|
||||
foreach ($item in $verifications.Keys | Sort-Object) {
|
||||
Write-Output (" - {0}" -f $item)
|
||||
}
|
||||
}
|
||||
|
||||
Write-Output ''
|
||||
Write-Output '[Documentation impact]'
|
||||
if ($documents.Count -eq 0) {
|
||||
Write-Output ' No module document mapping matched the Unity diff.'
|
||||
}
|
||||
else {
|
||||
foreach ($document in $documents.Keys | Sort-Object) {
|
||||
$changed = @($docsChanges | Where-Object { $_.Path -eq $document }).Count -gt 0
|
||||
$marker = if ($changed) { 'updated' } else { 'review if long-term behavior changed' }
|
||||
Write-Output (" - {0} ({1})" -f $document, $marker)
|
||||
}
|
||||
}
|
||||
|
||||
$linkFailures = @(Get-MarkdownLinkFailures $DocsRoot $docsChanges)
|
||||
Write-Output ''
|
||||
Write-Output '[Markdown links]'
|
||||
if ($linkFailures.Count -eq 0) {
|
||||
Write-Output ' Changed Markdown links are valid.'
|
||||
}
|
||||
else {
|
||||
foreach ($failure in $linkFailures) {
|
||||
Write-Error ("Broken link in {0}: {1}" -f $failure.Source, $failure.Link)
|
||||
}
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Output ''
|
||||
Write-Output '[workflow-check] Completed without writing files.'
|
||||
exit 0
|
||||
59
hooks/workflow-path-mappings.json
Normal file
59
hooks/workflow-path-mappings.json
Normal file
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"mappings": [
|
||||
{
|
||||
"pattern": "^Assets/StrayFog/Editor/CopyToX/CopyToHF/(AssetBundle|SpriteAtlas)/",
|
||||
"document": "modules/frameworks/资源管理/README.md",
|
||||
"verification": "执行 CopyToX,检查 HF 生成差异,构建 GameHF,并验证 Unity Console。"
|
||||
},
|
||||
{
|
||||
"pattern": "^Assets/StrayFog/Core/SFSubPackageV2\\.cs$|^Assets/StrayFog/Editor/Tools/AssestBundle/",
|
||||
"document": "modules/frameworks/资源管理/README.md",
|
||||
"verification": "构建 StrayFogCore 或 SFEditor,并验证 V2 清单、MD5、状态和目标平台下载。"
|
||||
},
|
||||
{
|
||||
"pattern": "^Assets/Game/GameHFScripte/GameFunction/HFDownLoadSubPackage/|^Assets/Game/GameMono/Scripts/GameDllManager\\.cs$",
|
||||
"document": "modules/frameworks/资源管理/使用指南.md",
|
||||
"verification": "构建 GameHF 或 SFMono,验证 Bootstrap 门禁、差异回调和普通资源更新。"
|
||||
},
|
||||
{
|
||||
"pattern": "^Assets/StrayFog/Editor/CopyToX/CopyToHF/UIWindowMgr/|^Assets/Game/GameHFScripte/GameFunction/UIWindows/|^Assets/Game/GameMono/Scripts/UIMono/",
|
||||
"document": "modules/frameworks/UI窗口系统/README.md",
|
||||
"verification": "构建 GameHF/SFMono,刷新 Unity,检查 Prefab 层级、列表回收和实际显示。"
|
||||
},
|
||||
{
|
||||
"pattern": "^Assets/Editor/Command/|^Tools/CodexUnityMcp/",
|
||||
"document": "modules/frameworks/编辑器工具/MCP工具/README.md",
|
||||
"verification": "检查 Unity Editor 编译、unity_health、目标命令和 Console 日志。"
|
||||
},
|
||||
{
|
||||
"pattern": "^Assets/Game/GameHFResource/.+\\.prefab$",
|
||||
"document": "modules/frameworks/UI窗口系统/界面拼接.md",
|
||||
"verification": "刷新 Unity,检查 RectTransform、组件状态、资源引用和运行时视觉结果。"
|
||||
},
|
||||
{
|
||||
"pattern": "^Assets/StrayFog/Core/",
|
||||
"document": "modules/frameworks/通用框架/README.md",
|
||||
"verification": "构建 StrayFogCore,并检查 Unity Console。"
|
||||
},
|
||||
{
|
||||
"pattern": "^Assets/StrayFog/Editor/",
|
||||
"document": "modules/frameworks/编辑器工具/README.md",
|
||||
"verification": "构建 SFEditor 或触发 Unity 编译,并验证对应菜单或生成器。"
|
||||
},
|
||||
{
|
||||
"pattern": "^Assets/Game/GameGeneralScripte/",
|
||||
"document": "modules/frameworks/通用框架/README.md",
|
||||
"verification": "构建 GameGeneral;生成目录变化时确认 CopyToGeneral 源码同步。"
|
||||
},
|
||||
{
|
||||
"pattern": "^Assets/Game/GameHFScripte/",
|
||||
"document": "modules/模块索引.md",
|
||||
"verification": "构建 GameHF,并检查 Unity Console。"
|
||||
},
|
||||
{
|
||||
"pattern": "^Assets/Game/GameMono/",
|
||||
"document": "modules/模块索引.md",
|
||||
"verification": "构建 SFMono,并检查 Unity Console。"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user