build-edges.ps1 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. # Edge builder & validator for cockpit-point-locate-and-device-link
  2. # Written by AI agent. Runs in PowerShell (Windows sandbox safe).
  3. #
  4. # 本脚本是本 effort「关联到 map 的每个 ticket」的**原生边唯一来源**:
  5. # 1. 按下面的声明把 `Blocked by:` 字段行写进每张子票 —— 本仓库(本地 Markdown 后端)
  6. # 的原生依赖边就是这一行(面板按它渲染地图视图的依赖分层);
  7. # 2. 校验子票数量 == 声明数量 == map 的 `## 任务清单` 条目数;
  8. # 3. 校验 map 的任务清单**逐条引用**了每张子票(关联关系的落点);
  9. # 4. 校验每条 `Blocked by:` 引用都解析得到真实子票。
  10. # 任一校验对不上就非零退出,并把 expected / actual 都打印出来 —— 对不上就是失败,不要当成成功。
  11. param(
  12. [string]$EffortDir = ""
  13. )
  14. if (-not $EffortDir) {
  15. $EffortDir = Split-Path $PSScriptRoot -Parent
  16. }
  17. $TicketsDir = Join-Path $EffortDir "issues"
  18. $MapPath = Join-Path $EffortDir "map.md"
  19. Write-Host "Building native `Blocked by:` edges and validating ticket count..."
  20. Write-Host " effort: $EffortDir"
  21. # Declare the expected tickets and their blocking edges (map 任务清单 的 01–14).
  22. $ExpectedTickets = [ordered]@{
  23. "01-research-device-position-data-and-rendering-primitives.md" = "None"
  24. "02-research-device-status-effect-mechanisms.md" = "None"
  25. "03-locate-camera-view-contract.md" = "None"
  26. "04-panel-anchor-following-form.md" = "None"
  27. "05-device-point-layer-trigger-and-lifecycle.md" = "None"
  28. "06-panel-to-scene-device-correspondence.md" = "05"
  29. "07-device-status-effect-visual-language.md" = "02, 05"
  30. "08-prototype-anchor-panel-and-device-effects.md" = "04, 05, 07"
  31. "09-task-backend-device-coordinate-contract.md" = "01"
  32. "10-device-model-asset-definition.md" = "01, 05"
  33. "11-overlapping-device-points.md" = "05"
  34. "12-panel-anchor-following-spec.md" = "04"
  35. "13-mode-layer-visibility-matrix.md" = "05"
  36. "14-associated-monitoring-device-layer-spec.md" = "05"
  37. }
  38. $ExpectedTicketCount = $ExpectedTickets.Count
  39. $ExpectedTaskListCount = 14
  40. $Success = $true
  41. # ---- 1. build edges -------------------------------------------------------------------
  42. function Fix-Edge {
  43. param([string]$TicketPath)
  44. $TicketName = Split-Path $TicketPath -Leaf
  45. if (-not $ExpectedTickets.Contains($TicketName)) {
  46. Write-Host " Unknown ticket (not declared): $TicketName"
  47. return $false
  48. }
  49. $EdgeLine = "Blocked by: " + $ExpectedTickets[$TicketName]
  50. $Content = Get-Content $TicketPath -Raw
  51. if ($Content -match "(?m)^Blocked by: .*$") {
  52. $Content = $Content -replace "(?m)^Blocked by: .*$", $EdgeLine
  53. } else {
  54. $Content = $EdgeLine + "`n`n" + $Content
  55. }
  56. $Content | Set-Content $TicketPath -Encoding UTF8 -NoNewline
  57. Write-Host " Fixed $TicketName : $EdgeLine"
  58. return $true
  59. }
  60. foreach ($Ticket in Get-ChildItem $TicketsDir -Filter "*.md") {
  61. if (-not (Fix-Edge $Ticket.FullName)) { $Success = $false }
  62. }
  63. # ---- 2. expected vs actual ticket count -----------------------------------------------
  64. $ActualTicketCount = (Get-ChildItem $TicketsDir -Filter "*.md").Count
  65. Write-Host "expected tickets: $ExpectedTicketCount / actual: $ActualTicketCount"
  66. if ($ActualTicketCount -ne $ExpectedTicketCount) {
  67. Write-Host " Ticket count mismatch."
  68. $Success = $false
  69. }
  70. # ---- 3. map task list must reference every ticket, one line each -----------------------
  71. if (-not (Test-Path $MapPath)) {
  72. Write-Host " Missing map.md at $MapPath"
  73. $Success = $false
  74. } else {
  75. $MapContent = Get-Content $MapPath -Raw
  76. $TaskSection = ""
  77. if ($MapContent -match "(?ms)^## 任务清单\s*(.*?)^## ") {
  78. $TaskSection = $Matches[1]
  79. } else {
  80. Write-Host " map.md has no `## 任务清单` section"
  81. $Success = $false
  82. }
  83. $ListedRefs = @()
  84. if ($TaskSection) {
  85. foreach ($Line in ($TaskSection -split "`n")) {
  86. if ($Line -match '^\s*-\s*\[[ x]\]\s*\[\d{2}\s*:\s*[^\]]*\]\(issues/([^)]+)\)') {
  87. $ListedRefs += $Matches[1].Trim()
  88. }
  89. }
  90. }
  91. Write-Host "expected map task-list items: $ExpectedTaskListCount / actual: $($ListedRefs.Count)"
  92. if ($ListedRefs.Count -ne $ExpectedTaskListCount) {
  93. Write-Host " Task list item count mismatch."
  94. $Success = $false
  95. }
  96. foreach ($TicketName in $ExpectedTickets.Keys) {
  97. if ($ListedRefs -notcontains $TicketName) {
  98. Write-Host " Ticket not referenced from map task list: $TicketName"
  99. $Success = $false
  100. }
  101. }
  102. foreach ($Ref in $ListedRefs) {
  103. if (-not $ExpectedTickets.Contains($Ref)) {
  104. Write-Host " Map task list references an undeclared ticket: $Ref"
  105. $Success = $false
  106. }
  107. }
  108. }
  109. # ---- 4. every `Blocked by:` reference must resolve ------------------------------------
  110. foreach ($Ticket in Get-ChildItem $TicketsDir -Filter "*.md") {
  111. $Content = Get-Content $Ticket.FullName -Raw
  112. if ($Content -match "(?m)^Blocked by: (.*?)\s*$") {
  113. $Edge = $Matches[1]
  114. foreach ($Ref in ($Edge -split "," | ForEach-Object { $_.Trim() })) {
  115. if ($Ref -eq "None" -or $Ref -eq "") { continue }
  116. if (-not (Get-ChildItem $TicketsDir -Filter "$Ref-*.md")) {
  117. Write-Host " Invalid reference in $($Ticket.Name): $Ref"
  118. $Success = $false
  119. }
  120. }
  121. } else {
  122. Write-Host " Missing `Blocked by:` in $($Ticket.Name)"
  123. $Success = $false
  124. }
  125. }
  126. if ($Success) {
  127. Write-Host "All edges built and validated successfully. Total tickets: $ActualTicketCount (expected $ExpectedTicketCount); map task-list items: $($ExpectedTaskListCount)."
  128. exit 0
  129. } else {
  130. Write-Host "Validation failed. Please fix above issues."
  131. exit 1
  132. }