aw-spec-cli 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. package/assets/.agents/agent.json +1 -1
  2. package/assets/.agents/skills/{aw-spec-orch → appweaver}/SKILL.md +10 -10
  3. package/assets/.agents/skills/{aw-spec-orch-demo → appweaver-demo}/SKILL.md +9 -9
  4. package/assets/.claude/agent.json +1 -1
  5. package/assets/.claude/skills/{aw-spec-orch → appweaver}/SKILL.md +10 -10
  6. package/assets/.claude/skills/{aw-spec-orch-demo → appweaver-demo}/SKILL.md +9 -9
  7. package/assets/.kilocode/agent.json +1 -1
  8. package/assets/.kilocode/workflows/{aw-spec-orch-demo.md → appweaver-demo.md} +9 -9
  9. package/assets/.kilocode/workflows/{aw-spec-orch.md → appweaver.md} +10 -10
  10. package/assets/docs/pharma-constitution.my-ref.md +86 -0
  11. package/dist/utils/paths.js +1 -1
  12. package/dist/utils/paths.js.map +1 -1
  13. package/package.json +1 -1
  14. package/assets/.aw-spec/DESIGN-FORMAT.md +0 -151
  15. package/assets/.aw-spec/ORCH-DIALECT.md +0 -156
  16. package/assets/.aw-spec/RULE-PARTITION.md +0 -126
  17. package/assets/.aw-spec/domain.json +0 -3
  18. package/assets/.aw-spec/extensions/.registry +0 -19
  19. package/assets/.aw-spec/extensions/agent-context/README.md +0 -57
  20. package/assets/.aw-spec/extensions/agent-context/agent-context-config.yml +0 -4
  21. package/assets/.aw-spec/extensions/agent-context/commands/aw-spec.agent-context.update.md +0 -26
  22. package/assets/.aw-spec/extensions/agent-context/extension.yml +0 -37
  23. package/assets/.aw-spec/extensions/agent-context/scripts/bash/update-agent-context.sh +0 -201
  24. package/assets/.aw-spec/extensions/agent-context/scripts/powershell/update-agent-context.ps1 +0 -237
  25. package/assets/.aw-spec/extensions.yml +0 -16
  26. package/assets/.aw-spec/feature.json +0 -3
  27. package/assets/.aw-spec/init-options.json +0 -9
  28. package/assets/.aw-spec/integration.json +0 -15
  29. package/assets/.aw-spec/integrations/aw-spec.manifest.json +0 -9
  30. package/assets/.aw-spec/interpreter/run-skill.py +0 -1158
  31. package/assets/.aw-spec/memory/constitution.md +0 -50
  32. package/assets/.aw-spec/orchestrator/AGENT-PROFILE.md +0 -94
  33. package/assets/.aw-spec/orchestrator/README.md +0 -300
  34. package/assets/.aw-spec/orchestrator/adapters/claude_agent_sdk.py +0 -74
  35. package/assets/.aw-spec/orchestrator/adapters/google_antigravity.py +0 -74
  36. package/assets/.aw-spec/orchestrator/modules/delta-design.orch.yaml +0 -105
  37. package/assets/.aw-spec/orchestrator/modules/delta-spec.orch.yaml +0 -79
  38. package/assets/.aw-spec/orchestrator/modules/domain-select.orch.yaml +0 -81
  39. package/assets/.aw-spec/orchestrator/modules/tech-select.orch.yaml +0 -75
  40. package/assets/.aw-spec/orchestrator/orchestrator.py +0 -1721
  41. package/assets/.aw-spec/orchestrator/rev-templates/ai-rules.reference.md +0 -40
  42. package/assets/.aw-spec/orchestrator/rev-templates/comandrulemap.skeleton.md +0 -59
  43. package/assets/.aw-spec/orchestrator/rev-templates/commandtechrulemap.skeleton.md +0 -97
  44. package/assets/.aw-spec/orchestrator/rev-templates/domain-constitution.skeleton.md +0 -61
  45. package/assets/.aw-spec/orchestrator/rev-templates/domain-requirements.skeleton.md +0 -48
  46. package/assets/.aw-spec/orchestrator/rev-templates/domain-rule-file.skeleton.md +0 -39
  47. package/assets/.aw-spec/orchestrator/rev-templates/technical-constitution.skeleton.md +0 -106
  48. package/assets/.aw-spec/orchestrator/rev-templates/technical-rule-file.skeleton.md +0 -54
  49. package/assets/.aw-spec/orchestrator/workflows/_smoke.orch.yaml +0 -160
  50. package/assets/.aw-spec/orchestrator/workflows/aw-spec-add-component.orch.yaml +0 -244
  51. package/assets/.aw-spec/orchestrator/workflows/aw-spec-adopt.orch.yaml +0 -775
  52. package/assets/.aw-spec/orchestrator/workflows/aw-spec-analyze-delta-design.orch.yaml +0 -248
  53. package/assets/.aw-spec/orchestrator/workflows/aw-spec-analyze-design.orch.yaml +0 -261
  54. package/assets/.aw-spec/orchestrator/workflows/aw-spec-analyze-slice.orch.yaml +0 -425
  55. package/assets/.aw-spec/orchestrator/workflows/aw-spec-build.orch.yaml +0 -188
  56. package/assets/.aw-spec/orchestrator/workflows/aw-spec-characterize.orch.yaml +0 -474
  57. package/assets/.aw-spec/orchestrator/workflows/aw-spec-close-delta.orch.yaml +0 -181
  58. package/assets/.aw-spec/orchestrator/workflows/aw-spec-constitution.orch.yaml +0 -284
  59. package/assets/.aw-spec/orchestrator/workflows/aw-spec-demo.orch.yaml +0 -103
  60. package/assets/.aw-spec/orchestrator/workflows/aw-spec-dependency-tree.orch.yaml +0 -69
  61. package/assets/.aw-spec/orchestrator/workflows/aw-spec-deploy.orch.yaml +0 -85
  62. package/assets/.aw-spec/orchestrator/workflows/aw-spec-design.orch.yaml +0 -382
  63. package/assets/.aw-spec/orchestrator/workflows/aw-spec-finalize-delta.orch.yaml +0 -382
  64. package/assets/.aw-spec/orchestrator/workflows/aw-spec-foundation.orch.yaml +0 -246
  65. package/assets/.aw-spec/orchestrator/workflows/aw-spec-implement-delta.orch.yaml +0 -231
  66. package/assets/.aw-spec/orchestrator/workflows/aw-spec-implement.orch.yaml +0 -258
  67. package/assets/.aw-spec/orchestrator/workflows/aw-spec-inventory.orch.yaml +0 -236
  68. package/assets/.aw-spec/orchestrator/workflows/aw-spec-onboard.orch.yaml +0 -949
  69. package/assets/.aw-spec/orchestrator/workflows/aw-spec-parity.orch.yaml +0 -293
  70. package/assets/.aw-spec/orchestrator/workflows/aw-spec-run.orch.yaml +0 -177
  71. package/assets/.aw-spec/orchestrator/workflows/aw-spec-select-domain.orch.yaml +0 -79
  72. package/assets/.aw-spec/orchestrator/workflows/aw-spec-select-feature.orch.yaml +0 -128
  73. package/assets/.aw-spec/orchestrator/workflows/aw-spec-specify.orch.yaml +0 -412
  74. package/assets/.aw-spec/scripts/bash/aw-inventory.sh +0 -14
  75. package/assets/.aw-spec/scripts/bash/aw-parity.sh +0 -13
  76. package/assets/.aw-spec/scripts/bash/aw-rev-constitution.sh +0 -13
  77. package/assets/.aw-spec/scripts/bash/aw-spec-actions.sh +0 -1899
  78. package/assets/.aw-spec/scripts/bash/aw-xref.sh +0 -15
  79. package/assets/.aw-spec/scripts/bash/common.sh +0 -533
  80. package/assets/.aw-spec/scripts/powershell/aw-inventory.ps1 +0 -14
  81. package/assets/.aw-spec/scripts/powershell/aw-parity.ps1 +0 -13
  82. package/assets/.aw-spec/scripts/powershell/aw-rev-constitution.ps1 +0 -13
  83. package/assets/.aw-spec/scripts/powershell/aw-spec-actions.ps1 +0 -1563
  84. package/assets/.aw-spec/scripts/powershell/aw-xref.ps1 +0 -15
  85. package/assets/.aw-spec/scripts/python/aw-inventory.py +0 -683
  86. package/assets/.aw-spec/scripts/python/aw-parity.py +0 -380
  87. package/assets/.aw-spec/scripts/python/aw-rev-constitution.py +0 -312
  88. package/assets/.aw-spec/scripts/python/aw-xref.py +0 -1330
  89. package/assets/.aw-spec/scripts/python/validate_okf.py +0 -349
  90. package/assets/.aw-spec/templates/constitution-domain-template.md +0 -75
  91. package/assets/.aw-spec/templates/constitution-technical-template.md +0 -70
  92. package/assets/.aw-spec/templates/delta-design-template.md +0 -69
  93. package/assets/.aw-spec/templates/delta-spec-template.md +0 -60
  94. package/assets/.aw-spec/templates/design-template.md +0 -383
  95. package/assets/.aw-spec/templates/okf-concept-template.md +0 -103
  96. package/assets/.aw-spec/templates/parity-manifest-template.json +0 -49
  97. package/assets/.aw-spec/templates/source-map-template.md +0 -85
  98. package/assets/.aw-spec/templates/spec-template.md +0 -252
  99. package/assets/docs/ModularMonolith/ModularMonolith.Reference.pack.txt +0 -25248
  100. package/assets/docs/ModularMonolith/ModularMonolith.pack.txt +0 -5726
  101. package/assets/docs/ModularMonolith/pack-foundation.ps1 +0 -145
  102. package/assets/docs/ModularMonolith/pack-foundation.sh +0 -196
  103. package/assets/docs/ModularMonolith/pack-reference-foundation.ps1 +0 -157
  104. package/assets/docs/ModularMonolith/pack-reference-foundation.sh +0 -209
  105. package/assets/docs/ModularMonolith/unpack-foundation.ps1 +0 -126
  106. package/assets/docs/ModularMonolith/unpack-foundation.sh +0 -217
  107. package/assets/docs/ModularMonolith/unpack-reference-foundation.ps1 +0 -129
  108. package/assets/docs/ModularMonolith/unpack-reference-foundation.sh +0 -218
  109. package/assets/docs/README.md +0 -106
  110. package/assets/docs/VerticalSlice/PACKAGING.md +0 -185
  111. package/assets/docs/VerticalSlice/VerticalSlice.Reference.pack.txt +0 -19318
  112. package/assets/docs/VerticalSlice/VerticalSlice.pack.txt +0 -2833
  113. package/assets/docs/VerticalSlice/pack-foundation.ps1 +0 -145
  114. package/assets/docs/VerticalSlice/pack-foundation.sh +0 -196
  115. package/assets/docs/VerticalSlice/pack-reference-foundation.ps1 +0 -157
  116. package/assets/docs/VerticalSlice/pack-reference-foundation.sh +0 -209
  117. package/assets/docs/VerticalSlice/unpack-foundation.ps1 +0 -126
  118. package/assets/docs/VerticalSlice/unpack-foundation.sh +0 -217
  119. package/assets/docs/VerticalSlice/unpack-reference-foundation.ps1 +0 -129
  120. package/assets/docs/VerticalSlice/unpack-reference-foundation.sh +0 -218
  121. package/assets/docs/architecture/clean-architecture.md +0 -71
  122. package/assets/docs/architecture/cqrs.md +0 -141
  123. package/assets/docs/architecture/eventing.md +0 -83
  124. package/assets/docs/architecture/folder-structure.md +0 -61
  125. package/assets/docs/architecture/modular-monolith.md +0 -47
  126. package/assets/docs/architecture/persistence.md +0 -269
  127. package/assets/docs/architecture/testing.md +0 -55
  128. package/assets/docs/architecture/vertical-slices.md +0 -201
  129. package/assets/docs/components/Email/README.md +0 -119
  130. package/assets/docs/components/Email/ReferenceEndpoints/SendEmailEndpoint.cs +0 -84
  131. package/assets/docs/components/Email/integration.md +0 -300
  132. package/assets/docs/components/Jobs/README.md +0 -113
  133. package/assets/docs/components/Jobs/integration.md +0 -259
  134. package/assets/docs/components/QrGenerator/README.md +0 -85
  135. package/assets/docs/components/QrGenerator/ReferenceEndpoints/GenerateQrEndpoint.cs +0 -57
  136. package/assets/docs/components/QrGenerator/integration.md +0 -230
  137. package/assets/docs/components/README.md +0 -74
  138. package/assets/docs/domain/domain-driven-design.md +0 -212
  139. package/assets/docs/domainspecs/healthcare/ai-rules.domain.md +0 -34
  140. package/assets/docs/domainspecs/healthcare/api-versioning.domain.md +0 -17
  141. package/assets/docs/domainspecs/healthcare/auditing.domain.md +0 -97
  142. package/assets/docs/domainspecs/healthcare/authorization.domain.md +0 -26
  143. package/assets/docs/domainspecs/healthcare/clinical-terms.md +0 -20
  144. package/assets/docs/domainspecs/healthcare/coding-standards.domain.md +0 -26
  145. package/assets/docs/domainspecs/healthcare/comandrulemap.md +0 -100
  146. package/assets/docs/domainspecs/healthcare/commandtechrulemap.md +0 -119
  147. package/assets/docs/domainspecs/healthcare/consent.md +0 -19
  148. package/assets/docs/domainspecs/healthcare/cqrs.domain.md +0 -12
  149. package/assets/docs/domainspecs/healthcare/data-classification.domain.md +0 -28
  150. package/assets/docs/domainspecs/healthcare/dependency-policy.domain.md +0 -11
  151. package/assets/docs/domainspecs/healthcare/dicom.md +0 -15
  152. package/assets/docs/domainspecs/healthcare/domain-driven-design.domain.md +0 -28
  153. package/assets/docs/domainspecs/healthcare/domain-requirements.md +0 -55
  154. package/assets/docs/domainspecs/healthcare/eventing.domain.md +0 -18
  155. package/assets/docs/domainspecs/healthcare/fhir.md +0 -36
  156. package/assets/docs/domainspecs/healthcare/folder-structure.domain.md +0 -23
  157. package/assets/docs/domainspecs/healthcare/hipaa.md +0 -34
  158. package/assets/docs/domainspecs/healthcare/hl7.md +0 -15
  159. package/assets/docs/domainspecs/healthcare/integration-terms.md +0 -21
  160. package/assets/docs/domainspecs/healthcare/persistence.domain.md +0 -20
  161. package/assets/docs/domainspecs/healthcare/terminology.md +0 -30
  162. package/assets/docs/domainspecs/healthcare/testing.domain.md +0 -15
  163. package/assets/docs/domainspecs/healthcare/vertical-slices.domain.md +0 -18
  164. package/assets/docs/domainspecs/healthcare/zero-trust.domain.md +0 -12
  165. package/assets/docs/domainspecs/pharma/ai-rules.domain.md +0 -32
  166. package/assets/docs/domainspecs/pharma/api-versioning.domain.md +0 -20
  167. package/assets/docs/domainspecs/pharma/atc.md +0 -22
  168. package/assets/docs/domainspecs/pharma/auditing.domain.md +0 -90
  169. package/assets/docs/domainspecs/pharma/authorization.domain.md +0 -25
  170. package/assets/docs/domainspecs/pharma/coding-standards.domain.md +0 -28
  171. package/assets/docs/domainspecs/pharma/comandrulemap.md +0 -108
  172. package/assets/docs/domainspecs/pharma/commandtechrulemap.md +0 -119
  173. package/assets/docs/domainspecs/pharma/cqrs.domain.md +0 -16
  174. package/assets/docs/domainspecs/pharma/data-classification.domain.md +0 -24
  175. package/assets/docs/domainspecs/pharma/data-provenance.md +0 -21
  176. package/assets/docs/domainspecs/pharma/dependency-policy.domain.md +0 -11
  177. package/assets/docs/domainspecs/pharma/domain-driven-design.domain.md +0 -36
  178. package/assets/docs/domainspecs/pharma/domain-requirements.md +0 -99
  179. package/assets/docs/domainspecs/pharma/eventing.domain.md +0 -18
  180. package/assets/docs/domainspecs/pharma/fhir.md +0 -31
  181. package/assets/docs/domainspecs/pharma/folder-structure.domain.md +0 -24
  182. package/assets/docs/domainspecs/pharma/hl7.md +0 -17
  183. package/assets/docs/domainspecs/pharma/idmp.md +0 -26
  184. package/assets/docs/domainspecs/pharma/integration-terms.md +0 -21
  185. package/assets/docs/domainspecs/pharma/ndc.md +0 -28
  186. package/assets/docs/domainspecs/pharma/persistence.domain.md +0 -23
  187. package/assets/docs/domainspecs/pharma/pharma-terms.md +0 -27
  188. package/assets/docs/domainspecs/pharma/product-lifecycle.domain.md +0 -30
  189. package/assets/docs/domainspecs/pharma/regulatory-compliance.md +0 -19
  190. package/assets/docs/domainspecs/pharma/terminology.md +0 -32
  191. package/assets/docs/domainspecs/pharma/testing.domain.md +0 -19
  192. package/assets/docs/domainspecs/pharma/vertical-slices.domain.md +0 -17
  193. package/assets/docs/domainspecs/pharma/zero-trust.domain.md +0 -12
  194. package/assets/docs/glossary/ai-rules.md +0 -59
  195. package/assets/docs/governance/api-versioning.md +0 -99
  196. package/assets/docs/governance/coding-standards.md +0 -87
  197. package/assets/docs/governance/dependency-policy.md +0 -40
  198. package/assets/docs/interop/ihe-conformance.md +0 -31
  199. package/assets/docs/requirement-spec.txt +0 -27
  200. package/assets/docs/security/auditing.md +0 -160
  201. package/assets/docs/security/authorization-matrix.md +0 -14
  202. package/assets/docs/security/authorization.md +0 -87
  203. package/assets/docs/security/data-classification.md +0 -38
  204. package/assets/docs/security/zero-trust.md +0 -76
  205. package/assets/docs/technology-constitution.md +0 -172
@@ -1,1563 +0,0 @@
1
- #!/usr/bin/env pwsh
2
- #
3
- # aw-spec-actions.ps1 — common action dispatcher for declarative aw-spec skills
4
- # (Windows / PowerShell mirror of aw-spec-actions.sh).
5
- #
6
- # ONE shared script for every integration (.claude / .agents / .kilocode). The
7
- # declarative skill.yaml files reference these subcommands via `execute_script`.
8
- # Each subcommand performs deterministic, governance-safe work and prints a single
9
- # JSON object on stdout. NONE of these actions author spec/code content — content
10
- # generation is the LLM's job (the skill's single `llm_generate` step).
11
- #
12
- # Usage: aw-spec-actions.ps1 <subcommand> [options]
13
- # resolve-domain -Command <name>
14
- # resolve-tech -Command <name>
15
- # derive-names <feature description...>
16
- # scaffold -FeatureDir <path> [-AllowExisting]
17
- # resolve-hooks -Phase <before_specify|after_specify>
18
- # create-checklist -FeatureDir <path> [-FeatureName <name>]
19
-
20
- $ErrorActionPreference = 'Stop'
21
-
22
- # ─────────────────────────── shared helpers ────────────────────────────────
23
- function Get-RepoRoot {
24
- $dir = (Get-Location).Path
25
- while ($true) {
26
- if (Test-Path (Join-Path $dir '.aw-spec')) { return $dir }
27
- $parent = Split-Path $dir -Parent
28
- if ([string]::IsNullOrEmpty($parent) -or $parent -eq $dir) { break }
29
- $dir = $parent
30
- }
31
- return (Resolve-Path (Join-Path $PSScriptRoot '..' '..' '..')).Path
32
- }
33
-
34
- function ConvertTo-RepoRelative {
35
- param([string]$Path, [string]$RepoRoot)
36
- $prefix = $RepoRoot.TrimEnd([System.IO.Path]::DirectorySeparatorChar) + [System.IO.Path]::DirectorySeparatorChar
37
- if ($Path.StartsWith($prefix)) { $Path = $Path.Substring($prefix.Length) }
38
- return ($Path -replace '\\', '/')
39
- }
40
-
41
- function Resolve-SpecTemplate {
42
- param([string]$RepoRoot)
43
- $base = Join-Path $RepoRoot '.aw-spec/templates'
44
- $name = 'spec-template.md'
45
- $override = Join-Path $base "overrides/$name"
46
- if (Test-Path $override) { return $override }
47
- $presets = Join-Path $RepoRoot '.aw-spec/presets'
48
- if (Test-Path $presets) {
49
- $hit = Get-ChildItem -Directory $presets -ErrorAction SilentlyContinue |
50
- ForEach-Object { Join-Path $_.FullName "templates/$name" } |
51
- Where-Object { Test-Path $_ } | Select-Object -First 1
52
- if ($hit) { return $hit }
53
- }
54
- $exts = Join-Path $RepoRoot '.aw-spec/extensions'
55
- if (Test-Path $exts) {
56
- $hit = Get-ChildItem -Directory $exts -ErrorAction SilentlyContinue |
57
- Where-Object { -not $_.Name.StartsWith('.') } |
58
- ForEach-Object { Join-Path $_.FullName "templates/$name" } |
59
- Where-Object { Test-Path $_ } | Select-Object -First 1
60
- if ($hit) { return $hit }
61
- }
62
- $core = Join-Path $base $name
63
- if (Test-Path $core) { return $core }
64
- return $null
65
- }
66
-
67
- function Write-Json { param($Obj) $Obj | ConvertTo-Json -Compress -Depth 6 }
68
-
69
- # ─────────────────────────── resolve-domain ────────────────────────────────
70
- function Invoke-ResolveDomain {
71
- param([string[]]$Rest)
72
- $commandName = 'aw-spec-specify'
73
- for ($i = 0; $i -lt $Rest.Count; $i++) {
74
- switch ($Rest[$i]) {
75
- '--command' { $commandName = $Rest[++$i] }
76
- '-Command' { $commandName = $Rest[++$i] }
77
- }
78
- }
79
- $repo = Get-RepoRoot
80
- $domainJson = Join-Path $repo '.aw-spec/domain.json'
81
- $domain = ''
82
- if (Test-Path $domainJson) {
83
- try { $domain = (Get-Content $domainJson -Raw | ConvertFrom-Json).domain } catch { $domain = '' }
84
- if (-not $domain) { $domain = '' }
85
- }
86
- $rulemap = Join-Path $repo "docs/domainspecs/$domain/comandrulemap.md"
87
- $rulemapExists = ($domain -ne '') -and (Test-Path $rulemap)
88
-
89
- $candidates = @()
90
- if ($rulemapExists) {
91
- $lines = Get-Content $rulemap
92
- $heading = "## /$commandName"
93
- $grab = $false; $found = @()
94
- foreach ($line in $lines) {
95
- if ($line -eq $heading) { $grab = $true; continue }
96
- if ($line -match '^## ') { $grab = $false }
97
- if ($grab -and $line -match '^\s*-\s+(.+?)\s*$') { $found += $Matches[1] }
98
- }
99
- if ($found.Count -gt 0) { $candidates = $found }
100
- }
101
-
102
- Write-Json ([ordered]@{
103
- domain = $domain
104
- rulemap_exists = $rulemapExists
105
- candidates = @($candidates)
106
- })
107
- }
108
-
109
- # ─────────────────────────── resolve-tech ───────────────────────────────────
110
- # Technical rule-file mapping — parallel to resolve-domain, but reads
111
- # docs/domainspecs/<domain>/commandtechrulemap.md. The technical rule files (the
112
- # docs/architecture · docs/security · docs/interop · docs/governance set formerly
113
- # hardcoded in each workflow's `standards:` block) are configured per-command here.
114
- # The technical CONSTITUTION (constitution.technical.md) is NOT mapped — each
115
- # consumer still refers to it directly, as-is.
116
- function Invoke-ResolveTech {
117
- param([string[]]$Rest)
118
- $commandName = 'aw-spec-design'
119
- for ($i = 0; $i -lt $Rest.Count; $i++) {
120
- switch ($Rest[$i]) {
121
- '--command' { $commandName = $Rest[++$i] }
122
- '-Command' { $commandName = $Rest[++$i] }
123
- }
124
- }
125
- $repo = Get-RepoRoot
126
- $domainJson = Join-Path $repo '.aw-spec/domain.json'
127
- $domain = ''
128
- if (Test-Path $domainJson) {
129
- try { $domain = (Get-Content $domainJson -Raw | ConvertFrom-Json).domain } catch { $domain = '' }
130
- if (-not $domain) { $domain = '' }
131
- }
132
- $techmap = Join-Path $repo "docs/domainspecs/$domain/commandtechrulemap.md"
133
- $techmapExists = ($domain -ne '') -and (Test-Path $techmap)
134
-
135
- $candidates = @()
136
- if ($techmapExists) {
137
- $lines = Get-Content $techmap
138
- foreach ($heading in @("## /$commandName", "## /$($commandName -replace 'decl-','')")) {
139
- $grab = $false; $found = @()
140
- foreach ($line in $lines) {
141
- if ($line -eq $heading) { $grab = $true; continue }
142
- if ($line -match '^## ') { $grab = $false }
143
- if ($grab -and $line -match '^\s*-\s+(.+?)\s*$') { $found += $Matches[1] }
144
- }
145
- if ($found.Count -gt 0) { $candidates = $found; break }
146
- }
147
- }
148
-
149
- Write-Json ([ordered]@{
150
- domain = $domain
151
- techrulemap_exists = $techmapExists
152
- candidates = @($candidates)
153
- })
154
- }
155
-
156
- # ─────────────────────────── derive-names ──────────────────────────────────
157
- function ConvertTo-Pascal {
158
- param([string]$In)
159
- ($In -replace '-', ' ').Split(' ') | Where-Object { $_ } |
160
- ForEach-Object { $_.Substring(0,1).ToUpper() + $_.Substring(1) } | Join-String
161
- }
162
-
163
- function Get-ShortName {
164
- param([string]$Description)
165
- $stop = 'i','a','an','the','to','for','of','in','on','at','by','with','from','is','are','was','were',
166
- 'be','been','being','have','has','had','do','does','did','will','would','should','could','can',
167
- 'may','might','must','shall','this','that','these','those','my','your','our','their','want','need','add','get','set'
168
- $clean = ($Description.ToLower() -replace '[^a-z0-9]', ' ')
169
- $words = @()
170
- foreach ($w in ($clean.Split(' ') | Where-Object { $_ })) {
171
- if (($stop -notcontains $w) -and $w.Length -ge 3) { $words += $w }
172
- }
173
- $max = if ($words.Count -eq 4) { 4 } else { 3 }
174
- $result = ($words | Select-Object -First $max) -join '-'
175
- if (-not $result) { $result = 'feature' }
176
- return $result
177
- }
178
-
179
-
180
- # ─────────────────────────── scaffold ──────────────────────────────────────
181
- function Invoke-Scaffold {
182
- param([string[]]$Rest)
183
- $featureDirArg = ''; $allowExisting = $false
184
- for ($i = 0; $i -lt $Rest.Count; $i++) {
185
- switch ($Rest[$i]) {
186
- { $_ -in '--feature-dir','-FeatureDir' } { $featureDirArg = $Rest[++$i] }
187
- { $_ -in '--allow-existing','-AllowExisting' } { $allowExisting = $true }
188
- }
189
- }
190
- if (-not $featureDirArg) { Write-Error 'scaffold: --feature-dir is required'; exit 1 }
191
-
192
- $repo = Get-RepoRoot
193
- $featureDir = if ([System.IO.Path]::IsPathRooted($featureDirArg)) { $featureDirArg } else { Join-Path $repo $featureDirArg }
194
- $specFile = Join-Path $featureDir 'spec.md'
195
-
196
- $dirExisted = Test-Path $featureDir
197
- if ($dirExisted -and -not $allowExisting) { Write-Error "scaffold: '$featureDir' already exists (use --allow-existing)"; exit 1 }
198
- New-Item -ItemType Directory -Force -Path $featureDir | Out-Null
199
- $created = -not $dirExisted
200
-
201
- $template = $null; $specCreated = $false
202
- if (-not (Test-Path $specFile)) {
203
- $template = Resolve-SpecTemplate -RepoRoot $repo
204
- if ($template -and (Test-Path $template)) { Copy-Item $template $specFile } else { New-Item -ItemType File -Force -Path $specFile | Out-Null }
205
- $specCreated = $true
206
- }
207
-
208
- $featureJson = Join-Path $repo '.aw-spec/feature.json'
209
- New-Item -ItemType Directory -Force -Path (Join-Path $repo '.aw-spec') | Out-Null
210
- @{ feature_directory = (ConvertTo-RepoRelative -Path $featureDir -RepoRoot $repo) } |
211
- ConvertTo-Json -Compress | Set-Content -Path $featureJson -Encoding UTF8
212
-
213
- Write-Json ([ordered]@{
214
- feature_dir = $featureDir
215
- spec_file = $specFile
216
- template = if ($template) { $template } else { '' }
217
- created = $created
218
- spec_created = $specCreated
219
- })
220
- }
221
-
222
- # ─────────────────────────── resolve-hooks ─────────────────────────────────
223
- # Native indentation parser for the known extensions.yml shape (no YAML module).
224
- # Accepts multiple --phase; merges + dedupes by command.
225
- function Get-HookIndent([string]$s) { return ($s.Length - $s.TrimStart(' ').Length) }
226
-
227
- function Get-PhaseHooks {
228
- param([string[]]$Lines, [string]$Phase)
229
- $hooks = @(); $n = $Lines.Count; $i = 0
230
- while ($i -lt $n) {
231
- if ($Lines[$i].TrimEnd() -eq 'hooks:') {
232
- $base = Get-HookIndent $Lines[$i]; $i++
233
- while ($i -lt $n -and ($Lines[$i].Trim() -eq '' -or (Get-HookIndent $Lines[$i]) -gt $base)) {
234
- $st0 = $Lines[$i].Trim()
235
- if ($st0.TrimEnd(':') -eq $Phase -and $st0.EndsWith(':') -and -not $st0.StartsWith('- ')) {
236
- $phInd = Get-HookIndent $Lines[$i]; $i++
237
- $cur = $null
238
- while ($i -lt $n) {
239
- $l = $Lines[$i]
240
- if ($l.Trim() -eq '') { $i++; continue }
241
- $ind = Get-HookIndent $l; $st = $l.Trim()
242
- if ($ind -lt $phInd) { break }
243
- if ($ind -eq $phInd -and $st.EndsWith(':') -and -not $st.StartsWith('- ')) { break }
244
- if ($st.StartsWith('- ')) {
245
- if ($null -ne $cur) { $hooks += ,$cur }
246
- $cur = @{}; $st = $st.Substring(2)
247
- }
248
- if ($null -ne $cur -and $st.Contains(':') -and -not $st.StartsWith('- ')) {
249
- $k = $st.Substring(0, $st.IndexOf(':')).Trim()
250
- $v = $st.Substring($st.IndexOf(':') + 1).Trim()
251
- $cur[$k] = $v
252
- }
253
- $i++
254
- }
255
- if ($null -ne $cur) { $hooks += ,$cur }
256
- break
257
- } else { $i++ }
258
- }
259
- break
260
- }
261
- $i++
262
- }
263
- return ,$hooks
264
- }
265
-
266
- function Invoke-ResolveHooks {
267
- param([string[]]$Rest)
268
- $phases = @()
269
- for ($i = 0; $i -lt $Rest.Count; $i++) {
270
- if ($Rest[$i] -in '--phase','-Phase') { $phases += $Rest[++$i] }
271
- }
272
- if ($phases.Count -eq 0) { Write-Error 'resolve-hooks: at least one --phase is required'; exit 1 }
273
-
274
- $repo = Get-RepoRoot
275
- $ext = Join-Path $repo '.aw-spec/extensions.yml'
276
- if (-not (Test-Path $ext)) { Write-Json ([ordered]@{ phases = @($phases); hooks = @() }); return }
277
- $lines = Get-Content $ext
278
-
279
- $out = @(); $seen = @{}
280
- foreach ($phase in $phases) {
281
- foreach ($h in (Get-PhaseHooks -Lines $lines -Phase $phase)) {
282
- if ("$($h['enabled'])".ToLower() -eq 'false') { continue }
283
- $cond = $h['condition']
284
- if ($null -ne $cond -and "$cond".Trim().ToLower() -notin @('', 'null', '~')) { continue }
285
- $cmd = [string]$h['command']
286
- if ($seen.ContainsKey($cmd)) { continue }
287
- $seen[$cmd] = $true
288
- $out += [ordered]@{
289
- extension = [string]$h['extension']
290
- command = $cmd
291
- slash = if ($cmd) { '/' + ($cmd -replace '\.', '-') } else { '' }
292
- optional = ("$($h['optional'])".ToLower() -eq 'true')
293
- description = [string]$h['description']
294
- prompt = [string]$h['prompt']
295
- }
296
- }
297
- }
298
- Write-Json ([ordered]@{ phases = @($phases); hooks = @($out) })
299
- }
300
-
301
- # ─────────────────────────── create-checklist ──────────────────────────────
302
- function Invoke-CreateChecklist {
303
- param([string[]]$Rest)
304
- $featureDirArg = ''; $featureName = ''; $kind = 'master'; $changeDirArg = ''
305
- for ($i = 0; $i -lt $Rest.Count; $i++) {
306
- switch ($Rest[$i]) {
307
- { $_ -in '--feature-dir','-FeatureDir' } { $featureDirArg = $Rest[++$i] }
308
- { $_ -in '--feature-name','-FeatureName' } { $featureName = $Rest[++$i] }
309
- { $_ -in '--kind','-Kind' } { $kind = $Rest[++$i] }
310
- { $_ -in '--change-dir','-ChangeDir' } { $changeDirArg = $Rest[++$i] }
311
- }
312
- }
313
- if (-not $featureDirArg) { Write-Error 'create-checklist: --feature-dir is required'; exit 1 }
314
- if ($kind -notin 'master','delta') { Write-Error "create-checklist: --kind must be 'master' or 'delta' (got '$kind')"; exit 1 }
315
-
316
- $repo = Get-RepoRoot
317
- $featureDir = if ([System.IO.Path]::IsPathRooted($featureDirArg)) { $featureDirArg } else { Join-Path $repo $featureDirArg }
318
- if (-not $featureName) { $featureName = Split-Path $featureDir -Leaf }
319
-
320
- # WHERE the checklist lives and WHAT it grades are decided together here, and both
321
- # are reported back, so a caller binds the pair from ONE call and can never point a
322
- # checklist at the wrong document. master -> the feature's spec.md; delta -> the
323
- # change's delta-spec.md (checklist sits beside it, inside changes/<cr-id>/).
324
- $crId = ''
325
- if ($kind -eq 'delta') {
326
- if (-not $changeDirArg) { Write-Error 'create-checklist: --change-dir is required with --kind delta'; exit 1 }
327
- $changeDir = if ([System.IO.Path]::IsPathRooted($changeDirArg)) { $changeDirArg } else { Join-Path $repo $changeDirArg }
328
- $crId = Split-Path $changeDir -Leaf
329
- $checklistDir = Join-Path $changeDir 'checklists'
330
- $targetFile = Join-Path $changeDir 'delta-spec.md'
331
- } else {
332
- $checklistDir = Join-Path $featureDir 'checklists'
333
- $targetFile = Join-Path $featureDir 'spec.md'
334
- }
335
- $checklistFile = Join-Path $checklistDir 'requirements.md'
336
- New-Item -ItemType Directory -Force -Path $checklistDir | Out-Null
337
- $created = -not (Test-Path $checklistFile)
338
- $today = Get-Date -Format 'yyyy-MM-dd'
339
-
340
- $content = if ($kind -eq 'delta') { @"
341
- # Delta-Spec Quality Checklist: $featureName — $crId
342
-
343
- **Purpose**: Validate the CHANGE's delta-spec before proceeding to design
344
- **Created**: $today
345
- **Change**: [delta-spec.md](../delta-spec.md)
346
-
347
- ## Content Quality
348
-
349
- - [ ] No implementation mechanics (languages, frameworks, code/adapter design; interop obligations named as contracts are fine)
350
- - [ ] Focused on user value and business needs
351
- - [ ] Written for non-technical stakeholders
352
- - [ ] Change summary states what is changing and why
353
-
354
- ## Delta Completeness
355
-
356
- - [ ] Every item sits under ADDED, MODIFIED or REMOVED (an empty section states None)
357
- - [ ] MODIFIED items state both sides (was -> now)
358
- - [ ] New behaviour is a user story (US-n) with priority, Given/When/Then acceptance, and an independent test
359
- - [ ] New or changed system requirements keep EARS form
360
- - [ ] Requirements are testable and unambiguous
361
- - [ ] No [NEEDS CLARIFICATION] markers remain
362
-
363
- ## Domain Lift (Constraints / Classification / Contracts)
364
-
365
- - [ ] Constraint / NFR changes are captured as CN- items, or the section states None
366
- - [ ] Data sensitivity or exposure changes are captured as DC- items, or the section states None
367
- - [ ] External interoperability changes are captured as IC- items at contract level, or the section states None
368
-
369
- ## Merge Readiness
370
-
371
- - [ ] Header is complete: aggregate, change type, base version
372
- - [ ] Impact lists the affected ids (FR / BR / CN / DC / IC / US / SC)
373
- - [ ] Regression risk names the existing behaviour that must still hold
374
- - [ ] Scoped to THIS change — the unchanged master is neither restated here nor edited
375
-
376
- ## Notes
377
-
378
- - This checklist grades the DELTA DOCUMENT only. Accuracy against the master spec
379
- (id continuity, conflicts with existing requirements) is checked downstream by
380
- /aw-spec-orch analyze-delta-design.
381
- - Items marked incomplete require delta-spec updates before /aw-spec-orch design
382
- "@ } else { @"
383
- # Specification Quality Checklist: $featureName
384
-
385
- **Purpose**: Validate specification completeness and quality before proceeding to design
386
- **Created**: $today
387
- **Feature**: [spec.md](../spec.md)
388
-
389
- ## Content Quality
390
-
391
- - [ ] No implementation mechanics (languages, frameworks, code/adapter design; interop obligations named as contracts are fine)
392
- - [ ] Focused on user value and business needs
393
- - [ ] Written for non-technical stakeholders
394
- - [ ] All mandatory sections completed
395
-
396
- ## Requirement Completeness
397
-
398
- - [ ] No [NEEDS CLARIFICATION] markers remain
399
- - [ ] Requirements are testable and unambiguous
400
- - [ ] Success criteria are measurable
401
- - [ ] Success criteria are technology-agnostic (no implementation details)
402
- - [ ] All acceptance scenarios are defined
403
- - [ ] Edge cases are identified
404
- - [ ] Scope is clearly bounded
405
- - [ ] Dependencies and assumptions identified
406
-
407
- ## Domain Lift (Constraints / Classification / Contracts)
408
-
409
- - [ ] Domain-derived constraints are captured in Constraints & NFRs (CN-), or the section states None
410
- - [ ] Data sensitivity and exposure rules are captured in Data Classification (DC-), or the section states None
411
- - [ ] External interoperability obligations are captured as Integration Contracts (IC-) at contract level, or the section states None
412
-
413
- ## Feature Readiness
414
-
415
- - [ ] All functional requirements have clear acceptance criteria
416
- - [ ] User scenarios cover primary flows
417
- - [ ] Feature meets measurable outcomes defined in Success Criteria
418
- - [ ] No implementation details leak into specification
419
-
420
- ## Notes
421
-
422
- - Items marked incomplete require spec updates before /aw-spec-orch design
423
- "@ }
424
- Set-Content -Path $checklistFile -Value $content -Encoding UTF8
425
- Write-Json ([ordered]@{ checklist_file = $checklistFile; kind = $kind; target = $targetFile; created = $created })
426
- }
427
-
428
- # ─────────────────────────── list-domains ──────────────────────────────────
429
- function Invoke-ListDomains {
430
- param([string[]]$Rest)
431
- $repo = Get-RepoRoot
432
- $base = Join-Path $repo 'docs/domainspecs'
433
- $domains = @()
434
- if (Test-Path $base) {
435
- $domains = @(Get-ChildItem -Directory $base -ErrorAction SilentlyContinue | ForEach-Object { $_.Name })
436
- }
437
- Write-Json ([ordered]@{ domains = @($domains) })
438
- }
439
-
440
- # ─────────────────────────── set-domain ────────────────────────────────────
441
- function Invoke-SetDomain {
442
- param([string[]]$Rest)
443
- $domain = ''
444
- for ($i = 0; $i -lt $Rest.Count; $i++) {
445
- if ($Rest[$i] -in '--domain','-Domain') { $domain = $Rest[++$i] }
446
- }
447
- if (-not $domain) { Write-Error 'set-domain: --domain is required'; exit 1 }
448
-
449
- $repo = Get-RepoRoot
450
- $mapRel = "docs/domainspecs/$domain/comandrulemap.md"
451
- $mapExists = Test-Path (Join-Path $repo $mapRel)
452
-
453
- New-Item -ItemType Directory -Force -Path (Join-Path $repo '.aw-spec') | Out-Null
454
- @{ domain = $domain } | ConvertTo-Json -Compress |
455
- Set-Content -Path (Join-Path $repo '.aw-spec/domain.json') -Encoding UTF8
456
-
457
- Write-Json ([ordered]@{
458
- domain = $domain
459
- map_path = $mapRel
460
- map_exists = $mapExists
461
- written = $true
462
- })
463
- }
464
-
465
- # ─────────────────────────── list-features ─────────────────────────────────
466
- # Twin of bash cmd_list_features: MERGE two feature sources, deduped by name
467
- # (case-insensitive) — (1) specs/ sub-folders (already scaffolded/adopted) and
468
- # (2) the OKF bundle's Aggregate concepts (the brownfield feature map, resolved by
469
- # the SAME shared python core the bash twin calls). Empty source 2 on greenfield.
470
- function Invoke-ListFeatures {
471
- param([string[]]$Rest)
472
- $preselect = ''; $bundle = ''
473
- for ($i = 0; $i -lt $Rest.Count; $i++) {
474
- if ($Rest[$i] -in '--preselect','-Preselect') { $preselect = $Rest[++$i] }
475
- elseif ($Rest[$i] -in '--bundle','-Bundle') { $bundle = $Rest[++$i] }
476
- }
477
- $repo = Get-RepoRoot
478
- $base = Join-Path $repo 'specs'
479
- $sentinel = '+ Create a new feature'
480
- if (-not $bundle) { $bundle = 'knowledge' }
481
- if (-not [System.IO.Path]::IsPathRooted($bundle)) { $bundle = Join-Path $repo $bundle }
482
-
483
- # Best-effort OKF aggregate list (brownfield feature map). Empty on greenfield.
484
- $okfAggs = @()
485
- $py = Get-Py; $invpy = Join-Path $PSScriptRoot '..' 'python' 'aw-inventory.py'
486
- if ($py -and (Test-Path $bundle) -and (Test-Path $invpy)) {
487
- try {
488
- $okfRaw = & $py $invpy list-aggregates --bundle $bundle 2>$null
489
- if ($okfRaw) { $okfAggs = @((($okfRaw | ConvertFrom-Json).aggregates)) }
490
- } catch { $okfAggs = @() }
491
- }
492
-
493
- $features = @(); $names = @(); $seen = @{}
494
- if (Test-Path $base) {
495
- foreach ($d in (Get-ChildItem -Directory $base -ErrorAction SilentlyContinue | Sort-Object Name)) {
496
- $ver = ''; $activeChange = ''
497
- $avf = Join-Path $d.FullName 'active_version.json'
498
- if (Test-Path $avf) { try { $av = Get-Content $avf -Raw | ConvertFrom-Json; $ver = "$($av.version)"; $activeChange = "$($av.active_change)" } catch { $ver = '' } }
499
- $names += $d.Name; $seen[$d.Name.ToLower()] = $true
500
- $features += [ordered]@{
501
- name = $d.Name
502
- dir = "specs/$($d.Name)"
503
- source = 'spec'
504
- has_spec = (Test-Path (Join-Path $d.FullName 'spec.md'))
505
- # 'design*.md', NOT 'design.*.md': the second dot made this miss the single
506
- # `design.md` the framework actually produces (it only matched the retired
507
- # design.domain.md / design.technical.md family). Informational count only.
508
- # -like, not -Filter: -Filter delegates to the platform's filesystem matcher
509
- # (which on Windows also honours 8.3 short names), so it is not guaranteed to
510
- # agree with the bash twin's glob. -like is PowerShell's own engine — same
511
- # result on every OS, which is what keeps the two scripts provably in parity.
512
- design_docs = @(Get-ChildItem -Path $d.FullName -File -ErrorAction SilentlyContinue |
513
- Where-Object { $_.Name -like 'design*.md' }).Count
514
- version = $ver
515
- active_change = $activeChange
516
- }
517
- }
518
- }
519
- # Merge OKF-discovered aggregates not already scaffolded under specs/.
520
- foreach ($agg in $okfAggs) {
521
- $nm = "$($agg.name)".Trim()
522
- if (-not $nm -or $seen.ContainsKey($nm.ToLower())) { continue }
523
- $names += $nm; $seen[$nm.ToLower()] = $true
524
- $features += [ordered]@{
525
- name = $nm
526
- dir = "specs/$nm"
527
- source = 'okf'
528
- has_spec = $false
529
- adopted = $false
530
- concept_path = "$($agg.concept_path)"
531
- design_docs = 0
532
- version = ''
533
- active_change = ''
534
- }
535
- }
536
- # select_options = merged names + (injected new --preselect) + sentinel.
537
- $opts = @($names)
538
- if ($preselect -and (-not $seen.ContainsKey($preselect.ToLower()))) { $opts += $preselect }
539
- $opts += $sentinel
540
- Write-Json ([ordered]@{
541
- features = @($features)
542
- names = @($names)
543
- count = $names.Count
544
- select_options = @($opts)
545
- sentinel = $sentinel
546
- })
547
- }
548
-
549
- # ─────────────────────────── set-feature ───────────────────────────────────
550
- function Invoke-SetFeature {
551
- param([string[]]$Rest)
552
- $raw = ''
553
- for ($i = 0; $i -lt $Rest.Count; $i++) {
554
- if ($Rest[$i] -in '--feature','-Feature') { $raw = $Rest[++$i] }
555
- }
556
- if (-not $raw) { Write-Error 'set-feature: --feature is required'; exit 1 }
557
-
558
- # Normalize to a PascalCase aggregate identifier (feature = aggregate).
559
- $cleaned = ($raw -replace '[^A-Za-z0-9]+', ' ').Trim()
560
- $feature = ConvertTo-Pascal -In $cleaned
561
- if ($feature -notmatch '^[A-Za-z][A-Za-z0-9]*$') {
562
- Write-Error "set-feature: '$raw' is not a valid feature name (need a letter-led alphanumeric aggregate, e.g. Order)"; exit 1
563
- }
564
-
565
- $repo = Get-RepoRoot
566
- $featureDirRel = "specs/$feature"
567
- $featureDir = Join-Path $repo $featureDirRel
568
- $existed = Test-Path $featureDir
569
- New-Item -ItemType Directory -Force -Path $featureDir | Out-Null
570
- New-Item -ItemType Directory -Force -Path (Join-Path $repo '.aw-spec') | Out-Null
571
- @{ feature_directory = $featureDirRel } | ConvertTo-Json -Compress |
572
- Set-Content -Path (Join-Path $repo '.aw-spec/feature.json') -Encoding UTF8
573
-
574
- Write-Json ([ordered]@{
575
- feature = $feature
576
- feature_dir = $featureDirRel
577
- spec_file = "$featureDirRel/spec.md"
578
- existed = [bool]$existed
579
- created = (-not $existed)
580
- spec_exists = (Test-Path (Join-Path $featureDir 'spec.md'))
581
- })
582
- }
583
-
584
- # ═══════════════════════════ active_version.json ════════════════════════════
585
- # Per-feature version state. Shells out to the SAME python as the bash twin so the
586
- # bump/timestamp logic is byte-identical across shells. (See bash _active_version.)
587
- $PyActiveVersion = @'
588
- import json, os, re, datetime, sys
589
- fd = os.environ["AW_FD"]; op = os.environ["AW_OP"]; cr = os.environ.get("AW_CR", "")
590
- force = os.environ.get("AW_FORCE", "")
591
- path = os.path.join(fd, "active_version.json")
592
- feature = os.path.basename(fd.rstrip("/"))
593
- def now(): return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
594
- def load():
595
- if os.path.isfile(path):
596
- try: return json.load(open(path, encoding="utf-8")), True
597
- except Exception: pass
598
- return {"feature": feature, "version": "v1.0", "active_change": "", "updated": now()}, False
599
- def save(d):
600
- d["updated"] = now()
601
- open(path, "w", encoding="utf-8").write(json.dumps(d, indent=2) + "\n")
602
- def bump(v):
603
- m = re.match(r'v?(\d+)(?:\.(\d+))?', (v or "v1.0").strip())
604
- maj = int(m.group(1)) if m else 1
605
- mnr = int(m.group(2)) if (m and m.group(2)) else 0
606
- return "v%d.%d" % (maj, mnr + 1)
607
-
608
- d, existed = load()
609
- d.setdefault("feature", feature); d.setdefault("version", "v1.0"); d.setdefault("active_change", "")
610
- out = {"feature": d["feature"], "version": d["version"], "active_change": d.get("active_change", ""),
611
- "exists": existed, "path": "active_version.json", "ok": True}
612
- if op == "read":
613
- pass
614
- elif op == "init":
615
- if not existed: save(d)
616
- out["created"] = (not existed)
617
- elif op == "open":
618
- cur = d.get("active_change", "")
619
- if cur and cur != cr and force != "1": # a DIFFERENT change is already open -> refuse
620
- out["ok"] = False
621
- out["error"] = ("change '%s' is already open on this feature; finish it "
622
- "(finalize-delta -> analyze-slice -> close-delta) before opening "
623
- "'%s', or pass --force" % (cur, cr))
624
- out["conflict"] = cur; out["requested"] = cr; out["has_change"] = True
625
- print(json.dumps(out)); sys.exit(3)
626
- d["active_change"] = cr; save(d)
627
- out["active_change"] = cr; out["opened"] = cr
628
- if cur and cur == cr: out["reopened"] = True # idempotent re-open of the same CR
629
- elif op == "close":
630
- prev = d["version"]; closed = d.get("active_change", "")
631
- if closed: # a real change was open -> materialized -> bump
632
- d["version"] = bump(prev)
633
- d["active_change"] = ""; save(d)
634
- out["version"] = d["version"]; out["active_change"] = ""
635
- out["previous"] = prev; out["closed_change"] = closed
636
- out["has_change"] = bool(out.get("active_change", "")) # clean branch signal for design
637
- # What a close from THIS state would produce. One bump() authority, read by BOTH
638
- # finalize-delta (to write the right number into the Change History rows it appends)
639
- # and close-delta (which actually performs the bump and stamps the two headers), so
640
- # the two stages can never disagree about the target. Equals `version` when no change
641
- # is open, mirroring close's "only bump if a change was really open" guard.
642
- out["next_version"] = bump(out["version"]) if out.get("active_change", "") else out["version"]
643
- print(json.dumps(out))
644
- '@
645
-
646
- function Invoke-ActiveVersion {
647
- param([string]$FeatureDir, [string]$Op, [string]$CrId = '', [string]$Force = '')
648
- $py = Get-Py; if (-not $py) { Write-Error "version-$Op`: python3 required"; exit 1 }
649
- $env:AW_FD = $FeatureDir; $env:AW_OP = $Op; $env:AW_CR = $CrId; $env:AW_FORCE = $Force
650
- $PyActiveVersion | & $py - # sets $LASTEXITCODE=3 when op=open is refused (guard)
651
- }
652
-
653
- function Invoke-VersionState { # op in read|init (feature-dir only)
654
- param([string]$Op, [string[]]$Rest)
655
- $fdArg = ''
656
- for ($i = 0; $i -lt $Rest.Count; $i++) { if ($Rest[$i] -in '--feature-dir','-FeatureDir') { $fdArg = $Rest[++$i] } }
657
- $repo = Get-RepoRoot
658
- $fd = Resolve-FeatureDir -Arg $fdArg -RepoRoot $repo
659
- if (-not $fd) { Write-Error "version-$Op`: no feature set and no --feature-dir"; exit 1 }
660
- Invoke-ActiveVersion -FeatureDir $fd -Op $Op
661
- }
662
-
663
-
664
-
665
- # ─────────────────────────── feature-paths ─────────────────────────────────
666
- function Invoke-FeaturePaths {
667
- param([string[]]$Rest)
668
- $repo = Get-RepoRoot
669
- $fj = Join-Path $repo '.aw-spec/feature.json'
670
- $fd = ''
671
- if (Test-Path $fj) {
672
- try { $fd = (Get-Content $fj -Raw | ConvertFrom-Json).feature_directory } catch { $fd = '' }
673
- }
674
- if (-not $fd) { Write-Error 'feature-paths: no feature set (run /aw-spec-orch select-feature first)'; exit 1 }
675
- if (-not [System.IO.Path]::IsPathRooted($fd)) { $fd = Join-Path $repo $fd }
676
- $feature = Split-Path $fd -Leaf # the aggregate name (feature = specs/<Feature>)
677
- # The two-document model: spec.md + design.md ONLY. plan.md / tasks.md belonged to
678
- # the retired /aw-spec-plan + /aw-spec-tasks phases and had zero consumers.
679
- $spec = Join-Path $fd 'spec.md'
680
- $design = Join-Path $fd 'design.md'
681
- Write-Json ([ordered]@{
682
- feature = $feature
683
- feature_dir = $fd
684
- spec_file = $spec
685
- design_file = $design
686
- spec_exists = (Test-Path $spec)
687
- design_exists = (Test-Path $design)
688
- })
689
- }
690
-
691
- # ────────────────────────── checklist-read / -apply ────────────────────────
692
- function Get-CheckboxLines {
693
- param([string[]]$Lines)
694
- $fence = $false; $n = 0; $items = @()
695
- for ($i = 0; $i -lt $Lines.Count; $i++) {
696
- $line = [string]$Lines[$i]
697
- if ($line.Trim().StartsWith('```')) { $fence = -not $fence; continue }
698
- if ($fence) { continue }
699
- $m = [regex]::Match($line, '^\s*- \[([ xX])\]\s?(.*)$')
700
- if ($m.Success) {
701
- $n++
702
- $items += [pscustomobject]@{
703
- n = $n; index = $i
704
- checked = ($m.Groups[1].Value -match '[xX]')
705
- text = $m.Groups[2].Value.Trim()
706
- }
707
- }
708
- }
709
- return ,$items
710
- }
711
-
712
- function Invoke-ChecklistRead {
713
- param([string[]]$Rest)
714
- $file = ''
715
- for ($i = 0; $i -lt $Rest.Count; $i++) { if ($Rest[$i] -eq '--file') { $file = $Rest[++$i] } }
716
- if (-not $file) { Write-Error 'checklist-read: --file required'; exit 1 }
717
- if (-not (Test-Path $file)) { Write-Json ([ordered]@{ exists = $false; total = 0; checked = 0; items = @() }); return }
718
- $lines = Get-Content $file
719
- $items = Get-CheckboxLines -Lines $lines
720
- Write-Json ([ordered]@{
721
- exists = $true; total = $items.Count
722
- checked = @($items | Where-Object { $_.checked }).Count
723
- items = @($items | ForEach-Object { [ordered]@{ n = $_.n; checked = $_.checked; text = $_.text } })
724
- })
725
- }
726
-
727
- function Invoke-ChecklistApply {
728
- param([string[]]$Rest)
729
- $file = ''; $resultsRaw = '{}'
730
- for ($i = 0; $i -lt $Rest.Count; $i++) {
731
- switch ($Rest[$i]) { '--file' { $file = $Rest[++$i] } '--results' { $resultsRaw = $Rest[++$i] } }
732
- }
733
- if (-not $file) { Write-Error 'checklist-apply: --file required'; exit 1 }
734
- if (-not (Test-Path $file)) { Write-Json ([ordered]@{ exists = $false; before_checked = 0; after_checked = 0; total = 0; newly_passing = @(); regressions = @() }); return }
735
- $results = @{}
736
- try { (ConvertFrom-Json $resultsRaw).PSObject.Properties | ForEach-Object { $results[[int]$_.Name] = [bool]$_.Value } } catch {}
737
- $lines = Get-Content $file
738
- $items = Get-CheckboxLines -Lines $lines
739
- $before = @($items | Where-Object { $_.checked }).Count
740
- $newly = @(); $regress = @(); $after = 0
741
- foreach ($it in $items) {
742
- $desired = if ($results.ContainsKey($it.n)) { $results[$it.n] } else { $it.checked }
743
- if ($desired -ne $it.checked) { if ($desired) { $newly += $it.n } else { $regress += $it.n } }
744
- if ($desired) { $after++ }
745
- $marker = if ($desired) { 'x' } else { ' ' }
746
- $lines[$it.index] = $lines[$it.index] -replace '^(\s*- \[)[ xX](\].*)$', "`$1$marker`$2"
747
- }
748
- Set-Content -Path $file -Value $lines -Encoding UTF8
749
- Write-Json ([ordered]@{ exists = $true; before_checked = $before; after_checked = $after; total = $items.Count; newly_passing = @($newly); regressions = @($regress) })
750
- }
751
-
752
-
753
-
754
- # ─────────────────────────────── detect-project ────────────────────────────
755
- function Invoke-DetectProject {
756
- param([string[]]$Rest)
757
- $repo = Get-RepoRoot
758
- $markers = @()
759
- $prune = @('.aw-spec', 'docs', 'specs', '.git', 'node_modules')
760
- Get-ChildItem -Path $repo -Recurse -File -Include *.sln, *.csproj -ErrorAction SilentlyContinue |
761
- Where-Object { $rel = $_.FullName.Substring($repo.Length).TrimStart('\','/'); -not ($prune | Where-Object { $rel -like "$_*" -or $rel -like "$_/*" -or $rel -like "$_\*" }) } |
762
- Select-Object -First 20 | ForEach-Object { $markers += ($_.FullName.Substring($repo.Length).TrimStart('\','/') -replace '\\','/') }
763
- foreach ($f in @('package.json','pom.xml','build.gradle','build.gradle.kts','go.mod','Cargo.toml','pyproject.toml')) {
764
- if (Test-Path (Join-Path $repo $f)) { $markers += $f }
765
- }
766
- Write-Json ([ordered]@{ scaffolded = ($markers.Count -gt 0); markers = @($markers) })
767
- }
768
-
769
-
770
- # ─────────────────────────── foundation-locate ─────────────────────────────
771
- # ──────────────────────────── foundation-list ──────────────────────────────
772
- # List available foundations (docs/<Name>/ with a real <Name>.pack.txt) for the
773
- # user to PICK — deterministic, no LLM. Optional --args fast path mirrors bash.
774
- function Invoke-FoundationList {
775
- param([string[]]$Rest)
776
- $raw = ''
777
- for ($i = 0; $i -lt $Rest.Count; $i++) { switch ($Rest[$i]) { '--args' { $raw = $Rest[++$i] } } }
778
- $repo = Get-RepoRoot
779
- $foundations = @()
780
- $docs = Join-Path $repo 'docs'
781
- if (Test-Path $docs) {
782
- foreach ($d in (Get-ChildItem -Directory $docs -ErrorAction SilentlyContinue)) {
783
- if (Test-Path (Join-Path $d.FullName "$($d.Name).pack.txt")) { $foundations += $d.Name }
784
- }
785
- }
786
- $preName = ''; $preApp = ''; $outDir = '.'
787
- if ($raw) { $t = $raw -split '\s+' | Where-Object { $_ }; if ($t.Count -ge 1) { $preName = $t[0] }; if ($t.Count -ge 2) { $preApp = $t[1] }; if ($t.Count -ge 3) { $outDir = $t[2] } }
788
- $match = if ($foundations -contains $preName) { $preName } else { '' }
789
- Write-Json ([ordered]@{
790
- foundations = @($foundations); count = $foundations.Count
791
- preselect_name = $match; preselect_app = $preApp; out_dir = $outDir
792
- })
793
- }
794
-
795
- function Invoke-FoundationLocate {
796
- param([string[]]$Rest)
797
- $name = ''; $app = ''; $out = '.'; $raw = ''
798
- for ($i = 0; $i -lt $Rest.Count; $i++) { switch ($Rest[$i]) { '--name' { $name = $Rest[++$i] } '--app' { $app = $Rest[++$i] } '--out' { $out = $Rest[++$i] } '--args' { $raw = $Rest[++$i] } } }
799
- if ($raw) { $t = $raw -split '\s+' | Where-Object { $_ }; if ($t.Count -ge 1) { $name = $t[0] }; if ($t.Count -ge 2) { $app = $t[1] }; if ($t.Count -ge 3) { $out = $t[2] } }
800
- $repo = Get-RepoRoot
801
- $available = @()
802
- $docs = Join-Path $repo 'docs'
803
- if (Test-Path $docs) {
804
- foreach ($d in (Get-ChildItem -Directory $docs -ErrorAction SilentlyContinue)) {
805
- if (Get-ChildItem $d.FullName -Filter *.pack.txt -ErrorAction SilentlyContinue | Select-Object -First 1) { $available += $d.Name }
806
- }
807
- }
808
- function _fail($r) { Write-Json ([ordered]@{ ok = $false; reason = $r; available = @($available) }) }
809
- if (-not $name -or -not $app) { _fail 'Usage: /aw-spec-foundation <FoundationName> <AppName> [OutputDirectory]'; return }
810
- $fdir = Join-Path $repo "docs/$name"
811
- if (-not (Test-Path $fdir)) { _fail "Foundation '$name' not found (expected docs/$name/)"; return }
812
- $pack = Join-Path $fdir "$name.pack.txt"
813
- if (-not (Test-Path $pack)) { _fail "Foundation '$name' has no bundle yet (expected $pack). Build it via docs/$name/PACKAGING.md."; return }
814
- $runner = ''; $unpacker = ''
815
- if ((Get-Command pwsh -ErrorAction SilentlyContinue) -and (Test-Path (Join-Path $fdir 'unpack-foundation.ps1'))) { $runner = 'pwsh'; $unpacker = "docs/$name/unpack-foundation.ps1" }
816
- elseif (Test-Path (Join-Path $fdir 'unpack-foundation.sh')) { $runner = 'sh'; $unpacker = "docs/$name/unpack-foundation.sh" }
817
- else { _fail "No unpacker found in docs/$name/ (unpack-foundation.ps1 or .sh)"; return }
818
- $header = (Get-Content $pack -TotalCount 30 | Where-Object { $_ -like '#*' }) -join "`n"
819
- $sentinel = ''
820
- $sm = [regex]::Match($header, '(?i)sentinel[^A-Za-z0-9_.]*([A-Za-z0-9_.]+)')
821
- if ($sm.Success) { $sentinel = $sm.Groups[1].Value }
822
- $fc = 0; $fm = [regex]::Match($header, '(?i)file[^0-9]*([0-9]+)'); if ($fm.Success) { $fc = [int]$fm.Groups[1].Value }
823
- Write-Json ([ordered]@{
824
- ok = $true; reason = ''; name = $name; app = $app; out_dir = $out
825
- pack_file = "docs/$name/$name.pack.txt"; unpacker = $unpacker; runner = $runner
826
- sentinel = $sentinel; file_count = $fc; available = @($available)
827
- })
828
- }
829
-
830
- # ─────────────────────────── foundation-unpack ─────────────────────────────
831
- function Invoke-FoundationUnpack {
832
- param([string[]]$Rest)
833
- $name = ''; $app = ''; $out = '.'; $dry = $false; $force = 'no'; $reference = $false
834
- for ($i = 0; $i -lt $Rest.Count; $i++) {
835
- switch ($Rest[$i]) { '--name' { $name = $Rest[++$i] } '--app' { $app = $Rest[++$i] } '--out' { $out = $Rest[++$i] } '--dry-run' { $dry = $true } '--force' { $force = $Rest[++$i] } '--reference' { $reference = $true } }
836
- }
837
- if (-not $name -or -not $app) { Write-Error 'foundation-unpack: --name and --app required'; exit 1 }
838
- $repo = Get-RepoRoot
839
- $fdir = Join-Path $repo "docs/$name"
840
- $packbase = if ($reference) { "$name.Reference" } else { $name }
841
- $stem = if ($reference) { 'unpack-reference-foundation' } else { 'unpack-foundation' }
842
- $pack = Join-Path $fdir "$packbase.pack.txt"
843
- if (-not (Test-Path $pack)) { Write-Json ([ordered]@{ ok = $false; mode = $(if ($dry) { 'dry' } else { 'real' }); written = 0; has_collisions = $false; stdout = "pack not found: $packbase.pack.txt" }); return }
844
- $cmd = $null; $cargs = @()
845
- if ((Get-Command pwsh -ErrorAction SilentlyContinue) -and (Test-Path (Join-Path $fdir "$stem.ps1"))) {
846
- $cmd = 'pwsh'; $cargs = @('-NoProfile','-File',(Join-Path $fdir "$stem.ps1"),'-PackFile',$pack,'-TargetAppName',$app,'-OutDir',$out)
847
- if ($dry) { $cargs += '-DryRun' }
848
- if ($force -eq 'yes' -or $reference) { $cargs += '-Force' }
849
- } elseif (Test-Path (Join-Path $fdir "$stem.sh")) {
850
- $cmd = 'bash'; $cargs = @((Join-Path $fdir "$stem.sh"),'--pack-file',$pack,'--target-app-name',$app,'--out-dir',$out)
851
- if ($dry) { $cargs += '--dry-run' }
852
- if ($force -eq 'yes' -or $reference) { $cargs += '--force' }
853
- } else { Write-Json ([ordered]@{ ok = $false; mode = 'real'; written = 0; has_collisions = $false; stdout = "no unpacker script for $name" }); return }
854
- $outp = & $cmd @cargs 2>&1 | Out-String
855
- $rc = $LASTEXITCODE
856
- # unpackers say "Unpacked N file(s) to …" / "DryRun: N file(s) would be written …"
857
- $written = 0; $wm = [regex]::Match($outp, '(?i)([0-9]+)\s+file'); if ($wm.Success) { $written = [int]$wm.Groups[1].Value }
858
- $hasColl = ($dry -and $rc -ne 0)
859
- Write-Json ([ordered]@{ ok = ($rc -eq 0); mode = $(if ($dry) { 'dry' } else { 'real' }); written = $written; has_collisions = $hasColl; stdout = ($outp.Substring([Math]::Max(0,$outp.Length-800))) })
860
- }
861
-
862
- # ─────────────────────── foundation-stash-dbcontext ────────────────────────
863
- function Invoke-FoundationStashDbContext {
864
- param([string[]]$Rest)
865
- $out = '.'
866
- for ($i = 0; $i -lt $Rest.Count; $i++) { if ($Rest[$i] -eq '--out') { $out = $Rest[++$i] } }
867
- $repo = Get-RepoRoot
868
- if (-not [System.IO.Path]::IsPathRooted($out)) { $out = Join-Path $repo $out }
869
- $ctx = Get-ChildItem -Path (Join-Path $out 'src') -Recurse -Filter 'ApplicationDbContext.cs' -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty FullName
870
- if (-not $ctx) { Write-Json ([ordered]@{ found = $false; dbcontext = ''; backup = '' }); return }
871
- $bak = "$ctx.foundation.bak"
872
- Copy-Item $ctx $bak
873
- Write-Json ([ordered]@{ found = $true; dbcontext = $ctx; backup = $bak })
874
- }
875
-
876
- # ─────────────────────────── foundation-verify ─────────────────────────────
877
- function Invoke-FoundationVerify {
878
- param([string[]]$Rest)
879
- $out = '.'; $sentinel = ''
880
- for ($i = 0; $i -lt $Rest.Count; $i++) { switch ($Rest[$i]) { '--out' { $out = $Rest[++$i] } '--sentinel' { $sentinel = $Rest[++$i] } } }
881
- $repo = Get-RepoRoot
882
- if (-not [System.IO.Path]::IsPathRooted($out)) { $out = Join-Path $repo $out }
883
- $residue = @()
884
- if ($sentinel -and (Test-Path $out)) {
885
- # scan the generated tree only — exclude bundle/framework dirs
886
- $residue = @(Get-ChildItem $out -Recurse -File -ErrorAction SilentlyContinue |
887
- Where-Object { $_.FullName -notmatch '[/\\](\.git|docs|\.aw-spec|specs|node_modules)[/\\]' } |
888
- Where-Object { try { Select-String -Path $_.FullName -SimpleMatch $sentinel -Quiet -ErrorAction SilentlyContinue } catch { $false } } |
889
- Select-Object -First 50 | ForEach-Object { $_.FullName.Substring($repo.Length).TrimStart('\','/') -replace '\\','/' })
890
- }
891
- Write-Json ([ordered]@{ clean = ($residue.Count -eq 0); residue = @($residue) })
892
- }
893
-
894
- # ─────────────────────────────── setup-constitution ────────────────────────
895
- # Ensure the split constitution files exist (copy from templates if missing):
896
- # .aw-spec/memory/constitution.technical.md (from constitution-technical-template)
897
- # .aw-spec/memory/constitution.domain.md (from constitution-domain-template)
898
- # The composed constitution.md is produced by compose-constitution after authoring.
899
- function Invoke-SetupConstitution {
900
- param([string[]]$Rest)
901
- $repo = Get-RepoRoot
902
- New-Item -ItemType Directory -Force -Path (Join-Path $repo '.aw-spec/memory') | Out-Null
903
- $base = Join-Path $repo '.aw-spec/templates'
904
- $tech = Join-Path $repo '.aw-spec/memory/constitution.technical.md'
905
- $dom = Join-Path $repo '.aw-spec/memory/constitution.domain.md'
906
- $techCreated = $false; $domCreated = $false
907
- if (-not (Test-Path $tech)) {
908
- $tpl = $null
909
- $ov = Join-Path $base 'overrides/constitution-technical-template.md'; $core = Join-Path $base 'constitution-technical-template.md'
910
- if (Test-Path $ov) { $tpl = $ov } elseif (Test-Path $core) { $tpl = $core }
911
- if ($tpl) { Copy-Item $tpl $tech } else { New-Item -ItemType File -Force -Path $tech | Out-Null }
912
- $techCreated = $true
913
- }
914
- if (-not (Test-Path $dom)) {
915
- $tpl = $null
916
- $ov = Join-Path $base 'overrides/constitution-domain-template.md'; $core = Join-Path $base 'constitution-domain-template.md'
917
- if (Test-Path $ov) { $tpl = $ov } elseif (Test-Path $core) { $tpl = $core }
918
- if ($tpl) { Copy-Item $tpl $dom } else { New-Item -ItemType File -Force -Path $dom | Out-Null }
919
- $domCreated = $true
920
- }
921
- Write-Json ([ordered]@{ constitution_technical_file = $tech; constitution_domain_file = $dom; technical_created = $techCreated; domain_created = $domCreated })
922
- }
923
-
924
- # ─────────────────────────────── compose-constitution ──────────────────────
925
- # Deterministically concatenate the technical + domain halves into the composed
926
- # constitution.md (human/compat view; NOT loaded by the pipeline phases). If the
927
- # domain half is absent/empty, the composed file mirrors the technical half.
928
- function Invoke-ComposeConstitution {
929
- param([string[]]$Rest)
930
- $repo = Get-RepoRoot
931
- $tech = Join-Path $repo '.aw-spec/memory/constitution.technical.md'
932
- $dom = Join-Path $repo '.aw-spec/memory/constitution.domain.md'
933
- $out = Join-Path $repo '.aw-spec/memory/constitution.md'
934
- if (-not (Test-Path $tech)) {
935
- Write-Json ([ordered]@{ composed_file = $out; ok = $false; domain_included = $false; issues = @('technical half not found') }); return
936
- }
937
- $header = @(
938
- '<!-- COMPOSED — generated by `aw-spec-actions compose-constitution`. Do not edit here.',
939
- ' Source of truth: constitution.technical.md + constitution.domain.md (edit those).',
940
- ' Pipeline phases load the halves directly; this composed view is for humans/compat. -->',
941
- ''
942
- ) -join "`n"
943
- $body = $header + "`n" + (Get-Content -Raw $tech)
944
- # Include the domain half only if authored — non-empty AND free of residual
945
- # [ALL_CAPS] template placeholders (a technical-only project leaves it a raw template).
946
- $includeDomain = $false
947
- if ((Test-Path $dom) -and ((Get-Item $dom).Length -gt 0)) {
948
- if (-not (Select-String -Path $dom -Pattern '\[[A-Z0-9]+(_[A-Z0-9]+)+\]' -Quiet)) { $includeDomain = $true }
949
- }
950
- if ($includeDomain) {
951
- $body += "`n`n---`n`n" + (Get-Content -Raw $dom)
952
- }
953
- Set-Content -Path $out -Value $body -NoNewline
954
- Write-Json ([ordered]@{ composed_file = $out; ok = $true; domain_included = $includeDomain; issues = @() })
955
- }
956
-
957
- # ─────────────────────────── constitution-verify ───────────────────────────
958
- # Deterministic lint of a generated constitution (NO LLM): residual
959
- # [ALL_CAPS_WITH_UNDERSCORE] placeholder tokens, body-vs-Sync-Impact-Report version
960
- # agreement, and non-ISO dates. Consumed by the constitution skill's bounded
961
- # self-correction loop. ok=false => the loop asks the model to patch.
962
- function Get-LastLabeledVersion {
963
- param([string]$Text)
964
- $m = [regex]::Match($Text, '(?im)version[^0-9\n]{0,20}v?(\d+\.\d+\.\d+)')
965
- if ($m.Success) { return $m.Groups[1].Value } else { return $null }
966
- }
967
- function Invoke-ConstitutionVerify {
968
- param([string[]]$Rest)
969
- $file = ''
970
- for ($i = 0; $i -lt $Rest.Count; $i++) { if ($Rest[$i] -eq '--file') { $file = $Rest[++$i] } }
971
- if (-not $file) { Write-Error 'constitution-verify: --file required'; exit 1 }
972
- if (-not (Test-Path $file)) {
973
- Write-Json ([ordered]@{ ok = $false; exists = $false; issues = @('constitution file not found');
974
- placeholders = @(); version_body = $null; version_report = $null; version_match = $null; bad_dates = @() })
975
- return
976
- }
977
- $text = Get-Content -Path $file -Raw
978
- # Strip HTML comments (template guidance + Sync Impact Report); keep the first as the report.
979
- $cm = [regex]::Match($text, '(?s)<!--.*?-->')
980
- $report = if ($cm.Success) { $cm.Value } else { '' }
981
- $body = [regex]::Replace($text, '(?s)<!--.*?-->', '')
982
- # 1. Residual ALL_CAPS_WITH_UNDERSCORE placeholder tokens.
983
- $placeholders = @([regex]::Matches($body, '\[([A-Z0-9]+(?:_[A-Z0-9]+)+)\]') |
984
- ForEach-Object { $_.Groups[1].Value } | Select-Object -Unique | Sort-Object)
985
- # 2. Version: labeled body version vs the report's "new" version.
986
- $versionBody = Get-LastLabeledVersion $body
987
- $arrow = [regex]::Match($report, '(\d+\.\d+\.\d+)\s*(?:->|→|=>)\s*(\d+\.\d+\.\d+)')
988
- $versionReport = if ($arrow.Success) { $arrow.Groups[2].Value } else { Get-LastLabeledVersion $report }
989
- $versionMatch = $null
990
- if ($versionBody -and $versionReport) { $versionMatch = ($versionBody -eq $versionReport) }
991
- # 3. Obviously non-ISO dates on ratification/amended/date lines (ISO never matches these).
992
- $bad = @()
993
- foreach ($line in ($body -split "`n")) {
994
- if ($line -match '(?i)ratif|amend|\bdate\b') {
995
- foreach ($mm in [regex]::Matches($line, '\b\d{1,2}/\d{1,2}/\d{2,4}\b')) { $bad += $mm.Value }
996
- foreach ($mm in [regex]::Matches($line, '\b\d{1,2}-\d{1,2}-\d{4}\b')) { $bad += $mm.Value }
997
- foreach ($mm in [regex]::Matches($line, '\b(?:January|February|March|April|May|June|July|August|September|October|November|December)\s+\d{1,2},?\s+\d{4}\b')) { $bad += $mm.Value }
998
- }
999
- }
1000
- $bad = @($bad | Select-Object -Unique | Sort-Object)
1001
- $issues = @()
1002
- if ($placeholders.Count -gt 0) { $issues += ("{0} unfilled placeholder(s): {1}" -f $placeholders.Count, ($placeholders -join ', ')) }
1003
- if ($versionMatch -eq $false) { $issues += ("version mismatch: body {0} vs Sync Impact Report {1}" -f $versionBody, $versionReport) }
1004
- if ($bad.Count -gt 0) { $issues += ("non-ISO date(s) (use YYYY-MM-DD): {0}" -f ($bad -join ', ')) }
1005
- $ok = ($placeholders.Count -eq 0) -and ($versionMatch -ne $false) -and ($bad.Count -eq 0)
1006
- Write-Json ([ordered]@{ ok = $ok; exists = $true; issues = @($issues);
1007
- placeholders = @($placeholders); version_body = $versionBody; version_report = $versionReport;
1008
- version_match = $versionMatch; bad_dates = @($bad) })
1009
- }
1010
-
1011
- # ────────────────────────── agent-context-update ───────────────────────────
1012
- function Invoke-AgentContextUpdate {
1013
- param([string[]]$Rest)
1014
- $plan = ''
1015
- for ($i = 0; $i -lt $Rest.Count; $i++) { if ($Rest[$i] -eq '--plan') { $plan = $Rest[++$i] } }
1016
- $repo = Get-RepoRoot
1017
- $script = Join-Path $repo '.aw-spec/extensions/agent-context/scripts/powershell/update-agent-context.ps1'
1018
- if (-not (Test-Path $script)) {
1019
- Write-Json ([ordered]@{ ok = $true; ran = $false; message = 'agent-context extension not installed; nothing to do' }); return
1020
- }
1021
- if ($plan) { $out = & pwsh -NoProfile -File $script $plan 2>&1 | Out-String }
1022
- else { $out = & pwsh -NoProfile -File $script 2>&1 | Out-String }
1023
- $rc = $LASTEXITCODE
1024
- Write-Json ([ordered]@{ ok = ($rc -eq 0); ran = $true; message = ($out.Substring([Math]::Max(0,$out.Length-400))).Trim() })
1025
- }
1026
-
1027
-
1028
-
1029
- # ─────────────────────────────── list-components ───────────────────────────
1030
- function Invoke-ListComponents {
1031
- param([string[]]$Rest)
1032
- $repo = Get-RepoRoot
1033
- $base = Join-Path $repo 'docs/components'
1034
- $comps = @()
1035
- if (Test-Path $base) {
1036
- foreach ($d in (Get-ChildItem -Directory $base -ErrorAction SilentlyContinue)) {
1037
- if (Test-Path (Join-Path $d.FullName 'integration.md')) { $comps += $d.Name }
1038
- }
1039
- }
1040
- Write-Json ([ordered]@{ components = @($comps) })
1041
- }
1042
-
1043
- # ─────────────────────────────── find-host-project ─────────────────────────
1044
- function Invoke-FindHostProject {
1045
- param([string[]]$Rest)
1046
- $repo = Get-RepoRoot
1047
- $prog = Get-ChildItem -Path (Join-Path $repo 'src') -Recurse -Filter 'Program.cs' -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty FullName
1048
- if (-not $prog) {
1049
- $prog = Get-ChildItem -Path $repo -Recurse -Filter 'Program.cs' -ErrorAction SilentlyContinue |
1050
- Where-Object { $_.FullName -notmatch '[/\\](\.git|docs|specs|\.aw-spec|obj|bin)[/\\]' } |
1051
- Select-Object -First 1 -ExpandProperty FullName
1052
- }
1053
- if (-not $prog) { Write-Json ([ordered]@{ found = $false; program_cs = ''; project_dir = ''; csproj = '' }); return }
1054
- $pdir = Split-Path $prog -Parent
1055
- $csproj = Get-ChildItem -Path $pdir -Filter *.csproj -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty FullName
1056
- Write-Json ([ordered]@{ found = $true; program_cs = $prog; project_dir = $pdir; csproj = $(if ($csproj) { $csproj } else { '' }) })
1057
- }
1058
-
1059
- # ─────────────────────────────── component-catalog ──────────────────────────
1060
- # Deterministic parse of a component's integration.md "Implementation catalog"
1061
- # markdown table (NO LLM). Returns {integrable, impl_keys[], implementations[{key,desc}]}.
1062
- function Invoke-ComponentCatalog {
1063
- param([string[]]$Rest)
1064
- $file = ''
1065
- for ($i = 0; $i -lt $Rest.Count; $i++) { if ($Rest[$i] -eq '--file') { $file = $Rest[++$i] } }
1066
- if (-not $file) { Write-Error 'component-catalog: --file required'; exit 1 }
1067
- $repo = Get-RepoRoot
1068
- if (-not [System.IO.Path]::IsPathRooted($file)) { $file = Join-Path $repo $file }
1069
- if (-not (Test-Path $file)) { Write-Json ([ordered]@{ integrable = $false; impl_keys = @(); implementations = @() }); return }
1070
- $lines = Get-Content $file
1071
- $start = -1
1072
- for ($i = 0; $i -lt $lines.Count; $i++) {
1073
- if ($lines[$i] -match '^#{1,6}\s' -and $lines[$i] -match '(?i)implementation catalog|selection list') { $start = $i + 1; break }
1074
- }
1075
- $rows = @()
1076
- if ($start -ge 0) {
1077
- for ($i = $start; $i -lt $lines.Count; $i++) {
1078
- $ln = [string]$lines[$i]
1079
- if ($ln -match '^#{1,6}\s') { break }
1080
- $s = $ln.Trim()
1081
- if ($s.StartsWith('|')) { $rows += $s }
1082
- elseif ($rows.Count -gt 0) { break }
1083
- }
1084
- }
1085
- function Split-Cells([string]$row) { return ($row.Trim().Trim('|') -split '\|') | ForEach-Object { $_.Trim() } }
1086
- $implKeys = @(); $impls = @()
1087
- if ($rows.Count -ge 2) {
1088
- $header = @(Split-Cells $rows[0])
1089
- $kidx = 0
1090
- for ($j = 0; $j -lt $header.Count; $j++) { if ($header[$j].ToLower() -eq 'key') { $kidx = $j; break } }
1091
- for ($r = 2; $r -lt $rows.Count; $r++) {
1092
- $c = @(Split-Cells $rows[$r])
1093
- $isSep = $true; foreach ($x in $c) { if ($x -notmatch '^:?-{2,}:?$') { $isSep = $false; break } }
1094
- if ($isSep) { continue }
1095
- if ($kidx -ge $c.Count) { continue }
1096
- $key = $c[$kidx].Trim('`').Trim()
1097
- if (-not $key) { continue }
1098
- $parts = @()
1099
- for ($j = 0; $j -lt $c.Count; $j++) {
1100
- if ($j -eq $kidx -or -not $c[$j]) { continue }
1101
- $h = if ($j -lt $header.Count) { $header[$j] } else { '' }
1102
- $parts += if ($h) { "{0}: {1}" -f $h, $c[$j] } else { $c[$j] }
1103
- }
1104
- $implKeys += $key
1105
- $impls += [ordered]@{ key = $key; desc = ($parts -join ' · ') }
1106
- }
1107
- }
1108
- Write-Json ([ordered]@{ integrable = ($implKeys.Count -gt 0); impl_keys = @($implKeys); implementations = @($impls) })
1109
- }
1110
-
1111
- # ─────────────────────────────── component-verify ───────────────────────────
1112
- # Post-wiring verification (NO LLM decision): dotnet build (graceful skip if absent) +
1113
- # R0.2a dual-registration guard. Returns {ok, ran, build_ok, errors[],
1114
- # dual_registration_ok, fix_target, fix_reason, can_fix}.
1115
- function Get-DefiningFiles {
1116
- param([string]$Method, [string]$Root)
1117
- $hits = @()
1118
- if (-not $Root -or -not (Test-Path $Root)) { return , $hits }
1119
- $pat = '\b' + [regex]::Escape($Method) + '\s*\('
1120
- Get-ChildItem $Root -Recurse -File -Filter *.cs -ErrorAction SilentlyContinue |
1121
- Where-Object { $_.FullName -notmatch '[/\\](bin|obj|\.git)[/\\]' } |
1122
- ForEach-Object {
1123
- $txt = Get-Content $_.FullName -Raw -ErrorAction SilentlyContinue
1124
- if ($txt -and ($txt -match $pat)) { $hits += [pscustomobject]@{ path = $_.FullName; text = $txt } }
1125
- }
1126
- return , $hits
1127
- }
1128
- function Invoke-ComponentVerify {
1129
- param([string[]]$Rest)
1130
- $hostDir = ''; $hostCsproj = ''; $marker = ''
1131
- for ($i = 0; $i -lt $Rest.Count; $i++) {
1132
- switch ($Rest[$i]) { '--host-dir' { $hostDir = $Rest[++$i] } '--host-csproj' { $hostCsproj = $Rest[++$i] } '--marker' { $marker = $Rest[++$i] } }
1133
- }
1134
- $repo = Get-RepoRoot
1135
- $marker = $marker.Trim()
1136
- function RelP($p) { if ($p -and $repo) { try { return ([System.IO.Path]::GetRelativePath($repo, $p) -replace '\\', '/') } catch { return $p } } else { return $p } }
1137
- # --- (1) build ---
1138
- $ran = $false; $buildOk = $true; $errors = @(); $primary = ''
1139
- if (Get-Command dotnet -ErrorAction SilentlyContinue) {
1140
- $target = if ($hostCsproj) { $hostCsproj } else { $hostDir }
1141
- if ($target -and (Test-Path $target)) {
1142
- $ran = $true
1143
- $out = (& dotnet build $target -nologo 2>&1 | Out-String)
1144
- $code = $LASTEXITCODE
1145
- foreach ($line in ($out -split "`n")) { if ($line -match ':\s*error\s') { $errors += $line.Trim() } }
1146
- $errors = @($errors | Select-Object -Unique | Select-Object -First 20)
1147
- $buildOk = ($code -eq 0)
1148
- if (-not $buildOk -and $errors.Count -gt 0) {
1149
- $m = [regex]::Match($errors[0], '^(.*?)\(\d+,\d+\)'); if ($m.Success) { $primary = RelP ($m.Groups[1].Value.Trim()) }
1150
- }
1151
- if (-not $buildOk -and -not $primary -and $hostCsproj) { $primary = RelP $hostCsproj }
1152
- }
1153
- }
1154
- # --- (2) R0.2a dual-registration guard ---
1155
- $searchRoot = if ($hostDir -and (Test-Path $hostDir)) { $hostDir } else { $repo }
1156
- $infra = @(Get-DefiningFiles 'AddInfrastructure' $searchRoot)
1157
- $aspire = @(Get-DefiningFiles 'AddAspireInfrastructure' $searchRoot)
1158
- $dualApp = ($infra.Count -gt 0 -and $aspire.Count -gt 0)
1159
- $dualOk = $true; $dualNote = ''; $dualFix = ''
1160
- function Pick-Fix($hits) {
1161
- $nonProg = @($hits | Where-Object { [System.IO.Path]::GetFileName($_.path) -ne 'Program.cs' })
1162
- if ($nonProg.Count -gt 0) { return RelP $nonProg[0].path } elseif ($hits.Count -gt 0) { return RelP $hits[0].path } else { return '' }
1163
- }
1164
- if (-not $dualApp) { $dualNote = 'single registration path (R0.2) — dual-registration check N/A' }
1165
- elseif (-not $marker) { $dualNote = 'both AddInfrastructure and AddAspireInfrastructure exist; no marker provided to verify the component is in both' }
1166
- else {
1167
- $inInfra = @($infra | Where-Object { $_.text.Contains($marker) }).Count -gt 0
1168
- $inAspire = @($aspire | Where-Object { $_.text.Contains($marker) }).Count -gt 0
1169
- $dualOk = ($inInfra -and $inAspire)
1170
- if (-not $dualOk) {
1171
- $missing = if (-not $inInfra) { 'AddInfrastructure' } else { 'AddAspireInfrastructure' }
1172
- $dualFix = Pick-Fix ($(if (-not $inInfra) { $infra } else { $aspire }))
1173
- $dualNote = "marker '$marker' missing from $missing (R0.2a: register in BOTH paths)"
1174
- }
1175
- else { $dualNote = "marker '$marker' present in both registration paths" }
1176
- }
1177
- $ok = ($buildOk -and $dualOk)
1178
- $fixTarget = ''; $fixReason = ''
1179
- if (-not $buildOk) { $fixTarget = if ($primary) { $primary } elseif ($hostCsproj) { RelP $hostCsproj } else { '' }; $fixReason = 'build failed: ' + ((@($errors) | Select-Object -First 5) -join ' | ') }
1180
- elseif (-not $dualOk) { $fixTarget = $dualFix; $fixReason = $dualNote }
1181
- Write-Json ([ordered]@{ ok = $ok; ran = $ran; build_ok = $buildOk; errors = @($errors); primary_error_file = $primary;
1182
- dual_applicable = $dualApp; dual_registration_ok = $dualOk; dual_note = $dualNote; dual_fix_file = $dualFix;
1183
- fix_target = $fixTarget; fix_reason = $fixReason; can_fix = [bool]$fixTarget })
1184
- }
1185
-
1186
- # ═════════════════ spec-anchored design pipeline (parity with bash) ═════════
1187
- # Mirror of the design/build/analyze-slice/run/deploy/cr subcommands. The fence parsers
1188
- # (extract/reconcile/paths) shell out to the SAME python3 as bash so the logic
1189
- # is byte-identical; the rest is native. See .aw-spec/DESIGN-FORMAT.md.
1190
- # NOTE: authored for Windows parity; smoke-test under pwsh before relying on it.
1191
-
1192
- function Get-Py {
1193
- if (Get-Command python3 -ErrorAction SilentlyContinue) { return 'python3' }
1194
- if (Get-Command python -ErrorAction SilentlyContinue) { return 'python' }
1195
- return $null
1196
- }
1197
-
1198
- function Read-FeatureDir {
1199
- param([string]$RepoRoot)
1200
- $fj = Join-Path $RepoRoot '.aw-spec/feature.json'
1201
- if (-not (Test-Path $fj)) { return '' }
1202
- try { return (Get-Content $fj -Raw | ConvertFrom-Json).feature_directory } catch { return '' }
1203
- }
1204
-
1205
- function Resolve-FeatureDir {
1206
- param([string]$Arg, [string]$RepoRoot)
1207
- $fd = $Arg
1208
- if (-not $fd) { $fd = Read-FeatureDir -RepoRoot $RepoRoot }
1209
- if (-not $fd) { return $null }
1210
- if (-not [System.IO.Path]::IsPathRooted($fd)) { $fd = Join-Path $RepoRoot $fd }
1211
- return $fd
1212
- }
1213
-
1214
- function Resolve-NamedTemplate {
1215
- param([string]$Name, [string]$RepoRoot)
1216
- $base = Join-Path $RepoRoot '.aw-spec/templates'
1217
- $file = "$Name.md"
1218
- $override = Join-Path $base "overrides/$file"
1219
- if (Test-Path $override) { return $override }
1220
- $presets = Join-Path $RepoRoot '.aw-spec/presets'
1221
- if (Test-Path $presets) {
1222
- $hit = Get-ChildItem -Directory $presets -ErrorAction SilentlyContinue |
1223
- ForEach-Object { Join-Path $_.FullName "templates/$file" } |
1224
- Where-Object { Test-Path $_ } | Select-Object -First 1
1225
- if ($hit) { return $hit }
1226
- }
1227
- $exts = Join-Path $RepoRoot '.aw-spec/extensions'
1228
- if (Test-Path $exts) {
1229
- $hit = Get-ChildItem -Directory $exts -ErrorAction SilentlyContinue |
1230
- Where-Object { -not $_.Name.StartsWith('.') } |
1231
- ForEach-Object { Join-Path $_.FullName "templates/$file" } |
1232
- Where-Object { Test-Path $_ } | Select-Object -First 1
1233
- if ($hit) { return $hit }
1234
- }
1235
- $core = Join-Path $base $file
1236
- if (Test-Path $core) { return $core }
1237
- return $null
1238
- }
1239
-
1240
- # The former path-tagged helpers (design-extract / -reconcile / -paths) are GONE, in
1241
- # lockstep with the bash twin. They belonged to a retired four-document model in which
1242
- # design.{domain,application,api,infra}.md carried the source inside ```lang path=…
1243
- # fences. Nothing emits that fence any more, and their glob (`design.*.md`) could not
1244
- # match a single `design.md` even if something did — so all three returned nothing,
1245
- # silently, at exit 0. "Which files is this slice made of" is now an OKF query:
1246
- # aw-xref concept-paths to read, concept-upsert --merge to write.
1247
- function Invoke-DesignScaffold {
1248
- param([string[]]$Rest)
1249
- $fdArg = ''; $tplName = 'design-template'
1250
- # --template <name>: which template to scaffold (default `design-template`).
1251
- # In a brownfield repo, /aw-spec-orch onboard GENERATES a code-derived
1252
- # `design-template` override, so the default already yields the as-built structure
1253
- # for both forward `design` and adopt (no per-call --template needed).
1254
- for ($i = 0; $i -lt $Rest.Count; $i++) { switch ($Rest[$i]) { { $_ -in '--feature-dir','-FeatureDir' } { $fdArg = $Rest[++$i] } { $_ -in '--template','-Template' } { $tplName = $Rest[++$i] } } }
1255
- $repo = Get-RepoRoot
1256
- $fd = Resolve-FeatureDir -Arg $fdArg -RepoRoot $repo
1257
- if (-not $fd) { Write-Error 'design-scaffold: no feature set (run /aw-spec-orch select-feature first) and no --feature-dir'; exit 1 }
1258
- New-Item -ItemType Directory -Force -Path $fd | Out-Null
1259
- $target = Join-Path $fd 'design.md'
1260
- if (Test-Path $target) { $created = $false }
1261
- else {
1262
- $tpl = Resolve-NamedTemplate -Name $tplName -RepoRoot $repo
1263
- if ($tpl -and (Test-Path $tpl)) { Copy-Item $tpl $target } else { New-Item -ItemType File -Force -Path $target | Out-Null }
1264
- $created = $true
1265
- }
1266
- # override_active: is a PROJECT-level override installed for this template name?
1267
- # Computed ALWAYS (a stable signal on bootstrap AND delta). onboard installs the
1268
- # code-derived brownfield template as the `design-template` override, so
1269
- # override_active=true means the forward design phase should author brownfield.
1270
- $overrideActive = Test-Path (Join-Path $repo (".aw-spec/templates/overrides/" + $tplName + ".md"))
1271
- $templateSource = Resolve-NamedTemplate -Name $tplName -RepoRoot $repo
1272
- $tsOut = if ($templateSource) { $templateSource } else { '' }
1273
- Write-Json ([ordered]@{ feature_dir = $fd; design_doc = $target; created = $created; override_active = $overrideActive; template = $tplName; template_source = $tsOut })
1274
- }
1275
-
1276
- function Invoke-BuildCompile {
1277
- param([string[]]$Rest)
1278
- $repo = Get-RepoRoot
1279
- $tool = 'none'; $ran = $false; $ok = $true; $cmd = ''; $log = ''
1280
- $hasSln = @(Get-ChildItem -Path $repo -Filter *.sln -File -ErrorAction SilentlyContinue).Count -gt 0
1281
- $hasCsproj = @(Get-ChildItem -Path (Join-Path $repo 'src') -Filter *.csproj -Recurse -ErrorAction SilentlyContinue).Count -gt 0
1282
- if ($hasSln -or $hasCsproj) {
1283
- $tool = 'dotnet'
1284
- if (Get-Command dotnet -ErrorAction SilentlyContinue) {
1285
- $cmd = 'dotnet build --nologo'; $ran = $true
1286
- Push-Location $repo; try { $log = (dotnet build --nologo 2>&1 | Out-String); $ok = ($LASTEXITCODE -eq 0) } finally { Pop-Location }
1287
- } else { $ok = $false }
1288
- } elseif ((Test-Path (Join-Path $repo 'package.json')) -and (Get-Command npm -ErrorAction SilentlyContinue)) {
1289
- $tool = 'npm'; $cmd = 'npm run build --if-present'; $ran = $true
1290
- Push-Location $repo; try { $log = (npm run build --if-present 2>&1 | Out-String); $ok = ($LASTEXITCODE -eq 0) } finally { Pop-Location }
1291
- }
1292
- $tail = if ($log.Length -gt 1400) { $log.Substring($log.Length - 1400) } else { $log }
1293
- Write-Json ([ordered]@{ ok = $ok; tool = $tool; ran = $ran; cmd = $cmd; log_tail = $tail })
1294
- }
1295
-
1296
- function Invoke-TestRun {
1297
- param([string[]]$Rest)
1298
- # --scope repo (default): run the whole suite — the historic behaviour, every existing
1299
- # caller is unaffected.
1300
- # --scope slice : run ONLY the tests whose FullyQualifiedName contains the
1301
- # feature's aggregate name (feature-dir basename) plus any --also referrer
1302
- # names — the per-feature integrity scope. .NET only; npm/none ignore it
1303
- # and run the full suite. (Kept byte-for-byte in step with the bash cmd_test_run.)
1304
- $featureDir = ''; $scope = 'repo'; $also = ''
1305
- for ($i = 0; $i -lt $Rest.Count; $i++) {
1306
- switch ($Rest[$i]) {
1307
- { $_ -in '--feature-dir','-FeatureDir' } { $featureDir = $Rest[++$i] }
1308
- { $_ -in '--scope','-Scope' } { $scope = $Rest[++$i] }
1309
- { $_ -in '--also','-Also' } { $also = $Rest[++$i] }
1310
- }
1311
- }
1312
- $repo = Get-RepoRoot
1313
-
1314
- # For scope=slice, build the token set = feature aggregate name + --also referrers, each
1315
- # sanitised to an identifier (VSTest filter operators |&()!=~ are illegal) and de-duped.
1316
- # A surviving token becomes a `FullyQualifiedName~<tok>` clause; substring match is
1317
- # deliberate — it over-includes (safe for an integrity fence) rather than under-scoping.
1318
- $slices = @()
1319
- if ($scope -eq 'slice') {
1320
- $names = @()
1321
- if ($featureDir) { $names += (Split-Path -Leaf ($featureDir.TrimEnd('/','\'))) }
1322
- if ($also) { $names += ($also -split ',') }
1323
- foreach ($name in $names) {
1324
- $tok = ($name -replace '[^A-Za-z0-9._-]','')
1325
- if ($tok -notmatch '^[A-Za-z]') { continue } # must start with a letter
1326
- if ($slices -notcontains $tok) { $slices += $tok } # de-dup
1327
- }
1328
- }
1329
-
1330
- $tool = 'none'; $ran = $false; $ok = $true; $cmd = ''; $log = ''
1331
- $matched = $false; $scoped = $false; $fenceGreen = $false; $filter = ''
1332
- $hasSln = @(Get-ChildItem -Path $repo -Filter *.sln -File -ErrorAction SilentlyContinue).Count -gt 0
1333
- $hasCsproj = @(Get-ChildItem -Path $repo -Filter *.csproj -Recurse -ErrorAction SilentlyContinue).Count -gt 0
1334
- if ($hasSln -or $hasCsproj) {
1335
- $tool = 'dotnet'
1336
- if (Get-Command dotnet -ErrorAction SilentlyContinue) {
1337
- if ($scope -eq 'slice') {
1338
- if ($slices.Count -eq 0) {
1339
- # scope=slice but nothing valid to filter on — fail loudly, never
1340
- # silently fall back to a repo-wide run.
1341
- $log = "scope=slice but no valid filter tokens from --feature-dir/--also`n"
1342
- $ok = $false; $ran = $false; $cmd = '(no filter tokens)'
1343
- } else {
1344
- $filter = ($slices | ForEach-Object { "FullyQualifiedName~$_" }) -join '|'
1345
- $scoped = $true; $cmd = "dotnet test --nologo --filter `"$filter`""; $ran = $true
1346
- Push-Location $repo; try { $log = (dotnet test --nologo --filter $filter 2>&1 | Out-String); $ok = ($LASTEXITCODE -eq 0) } finally { Pop-Location }
1347
- }
1348
- } else {
1349
- $cmd = 'dotnet test --nologo'; $ran = $true
1350
- Push-Location $repo; try { $log = (dotnet test --nologo 2>&1 | Out-String); $ok = ($LASTEXITCODE -eq 0) } finally { Pop-Location }
1351
- }
1352
- } else { $ok = $false }
1353
- } elseif ((Test-Path (Join-Path $repo 'package.json')) -and (Get-Command npm -ErrorAction SilentlyContinue)) {
1354
- $tool = 'npm'; $cmd = 'npm test'; $ran = $true
1355
- Push-Location $repo; try { $log = (npm test 2>&1 | Out-String); $ok = ($LASTEXITCODE -eq 0) } finally { Pop-Location }
1356
- }
1357
-
1358
- # matched = at least one test was actually selected + run. Read from the LOG, never the
1359
- # exit code: a .NET --filter that selects ZERO tests still EXITS 0, so trusting the exit
1360
- # code would report an empty scoped run as green (the vacuous-pass trap).
1361
- if ($log -match 'Total:\s*[1-9]') { $matched = $true }
1362
-
1363
- # fence_green — a green that actually proves something ran.
1364
- # scoped run → ok AND matched (guards the zero-match vacuous pass)
1365
- # everything else (repo suite, npm, no test tool, the loud no-token fail) → ok.
1366
- if ($scoped) { $fenceGreen = ($ok -and $matched) } else { $fenceGreen = $ok }
1367
-
1368
- $tail = if ($log.Length -gt 1400) { $log.Substring($log.Length - 1400) } else { $log }
1369
- Write-Json ([ordered]@{ ok = $ok; tool = $tool; ran = $ran; scope = $scope; slices = ($slices -join ' '); scoped = $scoped; matched = $matched; fence_green = $fenceGreen; cmd = $cmd; log_tail = $tail })
1370
- }
1371
-
1372
- function Invoke-RunMigrate {
1373
- param([string[]]$Rest)
1374
- $repo = Get-RepoRoot
1375
- $tool = 'none'; $ran = $false; $ok = $true; $cmd = ''; $log = ''
1376
- $hasSln = @(Get-ChildItem -Path $repo -Filter *.sln -File -ErrorAction SilentlyContinue).Count -gt 0
1377
- $hasCsproj = @(Get-ChildItem -Path (Join-Path $repo 'src') -Filter *.csproj -Recurse -ErrorAction SilentlyContinue).Count -gt 0
1378
- $efOk = $false
1379
- if (($hasSln -or $hasCsproj) -and (Get-Command dotnet -ErrorAction SilentlyContinue)) {
1380
- Push-Location $repo; try { dotnet ef --version *> $null; $efOk = ($LASTEXITCODE -eq 0) } catch { $efOk = $false } finally { Pop-Location }
1381
- }
1382
- if ($efOk) {
1383
- # Prefer *Service.csproj (API host with ApplicationDbContext); fall back to first
1384
- # non-AppHost/Web/ServiceDefaults/test project under src/.
1385
- $project = Get-ChildItem -Path (Join-Path $repo 'src') -Filter '*.Service.csproj' -Recurse -ErrorAction SilentlyContinue |
1386
- Select-Object -First 1 -ExpandProperty FullName
1387
- if (-not $project) {
1388
- $project = Get-ChildItem -Path (Join-Path $repo 'src') -Filter '*.csproj' -Recurse -ErrorAction SilentlyContinue |
1389
- Where-Object { $_.FullName -notmatch 'AppHost|ServiceDefaults|\.Web\.|Tests' } |
1390
- Select-Object -First 1 -ExpandProperty FullName
1391
- }
1392
- if ($project) {
1393
- $tool = 'dotnet-ef'
1394
- $cmd = "dotnet ef database update --project $project --context ApplicationDbContext"
1395
- $ran = $true
1396
- Push-Location $repo
1397
- try {
1398
- $log = (dotnet ef database update --project $project --context ApplicationDbContext 2>&1 | Out-String)
1399
- $ok = ($LASTEXITCODE -eq 0)
1400
- } finally { Pop-Location }
1401
- } else {
1402
- $tool = 'dotnet-ef'; $cmd = 'dotnet ef database update'; $ran = $true
1403
- Push-Location $repo; try { $log = (dotnet ef database update 2>&1 | Out-String); $ok = ($LASTEXITCODE -eq 0) } finally { Pop-Location }
1404
- }
1405
- }
1406
- $tail = if ($log.Length -gt 1400) { $log.Substring($log.Length - 1400) } else { $log }
1407
- Write-Json ([ordered]@{ ok = $ok; tool = $tool; ran = $ran; cmd = $cmd; log_tail = $tail })
1408
- }
1409
-
1410
- function Invoke-DeployDispatch {
1411
- param([string[]]$Rest)
1412
- $repo = Get-RepoRoot; $cfg = Join-Path $repo '.aw-spec/deploy.json'
1413
- $configured = $false; $ran = $false; $ok = $true; $cmd = ''; $log = ''
1414
- if (Test-Path $cfg) { try { $cmd = (Get-Content $cfg -Raw | ConvertFrom-Json).command } catch { $cmd = '' } }
1415
- if ($cmd) {
1416
- $configured = $true; $ran = $true
1417
- Push-Location $repo; try { $log = (Invoke-Expression $cmd 2>&1 | Out-String); $ok = ($LASTEXITCODE -eq 0) } catch { $ok = $false; $log = $_.ToString() } finally { Pop-Location }
1418
- }
1419
- $tail = if ($log.Length -gt 1400) { $log.Substring($log.Length - 1400) } else { $log }
1420
- Write-Json ([ordered]@{ ok = $ok; configured = $configured; ran = $ran; cmd = $cmd; log_tail = $tail })
1421
- }
1422
-
1423
- function Invoke-CrSetup {
1424
- param([string[]]$Rest)
1425
- $crId = ''; $fdArg = ''; $title = ''; $force = ''
1426
- for ($i = 0; $i -lt $Rest.Count; $i++) {
1427
- switch ($Rest[$i]) {
1428
- { $_ -in '--cr-id','-CrId' } { $crId = $Rest[++$i] }
1429
- { $_ -in '--feature-dir','-FeatureDir' } { $fdArg = $Rest[++$i] }
1430
- { $_ -in '--title','-Title' } { $title = $Rest[++$i] }
1431
- { $_ -in '--force','-Force' } { $force = '1' }
1432
- }
1433
- }
1434
- if (-not $crId) { $crId = 'CR-' + (Get-Date -Format 'yyyyMMdd-HHmmss') }
1435
- $repo = Get-RepoRoot; $fd = Resolve-FeatureDir -Arg $fdArg -RepoRoot $repo
1436
- if (-not $fd) { Write-Error 'cr-setup: no feature set and no --feature-dir'; exit 1 }
1437
- # Single-active-change guard: open the change FIRST so a second concurrent CR on the
1438
- # same aggregate is refused BEFORE any changes/<cr>/ dir is scaffolded (no orphan
1439
- # left behind). Re-opening the same cr_id is a no-op. --force overrides.
1440
- $avOpen = Invoke-ActiveVersion -FeatureDir $fd -Op 'open' -CrId $crId -Force $force
1441
- if ($LASTEXITCODE -and $LASTEXITCODE -ne 0) {
1442
- Write-Error "cr-setup: refusing to open '$crId' — a change is already open on this feature; finish it (finalize-delta -> analyze-slice -> close-delta) first, or pass --force."
1443
- $avOpen
1444
- exit 1
1445
- }
1446
- $crDir = Join-Path $fd "changes/$crId"
1447
- New-Item -ItemType Directory -Force -Path $crDir | Out-Null
1448
- $dspec = Join-Path $crDir 'delta-spec.md'; $ddesign = Join-Path $crDir 'delta-design.md'; $created = $false
1449
- if (-not (Test-Path $dspec)) { $t = Resolve-NamedTemplate -Name 'delta-spec-template' -RepoRoot $repo; if ($t) { Copy-Item $t $dspec } else { New-Item -ItemType File -Force -Path $dspec | Out-Null }; $created = $true }
1450
- if (-not (Test-Path $ddesign)) { $t = Resolve-NamedTemplate -Name 'delta-design-template' -RepoRoot $repo; if ($t) { Copy-Item $t $ddesign } else { New-Item -ItemType File -Force -Path $ddesign | Out-Null }; $created = $true }
1451
- Write-Json ([ordered]@{ cr_id = $crId; cr_dir = $crDir; delta_spec = $dspec; delta_design = $ddesign; title = $title; created = $created })
1452
- }
1453
-
1454
- function Invoke-CrArchive {
1455
- param([string[]]$Rest)
1456
- $crId = ''; $fdArg = ''
1457
- for ($i = 0; $i -lt $Rest.Count; $i++) {
1458
- switch ($Rest[$i]) {
1459
- { $_ -in '--cr-id','-CrId' } { $crId = $Rest[++$i] }
1460
- { $_ -in '--feature-dir','-FeatureDir' } { $fdArg = $Rest[++$i] }
1461
- }
1462
- }
1463
- if (-not $crId) { Write-Error 'cr-archive: --cr-id required'; exit 1 }
1464
- $repo = Get-RepoRoot; $fd = Resolve-FeatureDir -Arg $fdArg -RepoRoot $repo
1465
- if (-not $fd) { Write-Error 'cr-archive: no feature set and no --feature-dir'; exit 1 }
1466
- $crDir = Join-Path $fd "changes/$crId"; $archRoot = Join-Path $fd 'changes/archive'; $archived = ''
1467
- if (Test-Path $crDir) {
1468
- New-Item -ItemType Directory -Force -Path $archRoot | Out-Null
1469
- $dest = Join-Path $archRoot $crId
1470
- if (Test-Path $dest) { Remove-Item -Recurse -Force $dest }
1471
- Move-Item $crDir $dest; $archived = $dest
1472
- }
1473
- # Close the change in active_version.json (single version source): bump + clear.
1474
- $av = Invoke-ActiveVersion -FeatureDir $fd -Op 'close' | ConvertFrom-Json
1475
- Write-Json ([ordered]@{ ok = $true; archived_dir = $archived; version = $av.version; previous = $av.previous; closed_change = $av.closed_change })
1476
- }
1477
-
1478
- # Stamp a version into the MASTER documents' `**Version**:` headers — the
1479
- # deterministic twin of cr-archive's active_version.json bump. close-delta calls
1480
- # this with the version cr-archive just computed, so all THREE carriers of the
1481
- # number (spec.md, design.md, active_version.json) move in ONE stage by ONE
1482
- # mechanism. Before this, design.md's header was bumped by the MODEL at
1483
- # finalize-delta — a stage early, and guessing the target since the number was
1484
- # never passed into the merge prompt — while spec.md's header was bumped by
1485
- # nobody at all and sat at v1.0 forever.
1486
- # Idempotent: re-stamping the same version reports `unchanged`, writes nothing.
1487
- # Only the value moves; any trailing inline comment on the header line is kept.
1488
- function Invoke-VersionStamp {
1489
- param([string[]]$Rest)
1490
- $ver = ''; $fdArg = ''
1491
- for ($i = 0; $i -lt $Rest.Count; $i++) {
1492
- switch ($Rest[$i]) {
1493
- { $_ -in '--version','-Version' } { $ver = $Rest[++$i] }
1494
- { $_ -in '--feature-dir','-FeatureDir' } { $fdArg = $Rest[++$i] }
1495
- }
1496
- }
1497
- if (-not $ver) { Write-Error 'version-stamp: --version required'; exit 1 }
1498
- $repo = Get-RepoRoot; $fd = Resolve-FeatureDir -Arg $fdArg -RepoRoot $repo
1499
- if (-not $fd) { Write-Error 'version-stamp: no feature set and no --feature-dir'; exit 1 }
1500
- $ver = $ver.Trim()
1501
- [string[]]$stamped = @(); [string[]]$unchanged = @(); [string[]]$missing = @(); [string[]]$noHeader = @()
1502
- # First `**Version**: <value>` line only — that is the header. The Change History
1503
- # table's `| Version |` column head does not match this pattern.
1504
- foreach ($name in @('spec.md', 'design.md')) {
1505
- $p = Join-Path $fd $name
1506
- if (-not (Test-Path $p)) { $missing += $name; continue }
1507
- $src = Get-Content -Raw -Encoding UTF8 $p
1508
- $m = [regex]::Match($src, '(?m)^(\*\*Version\*\*:[ \t]*)(\S+)(.*)$')
1509
- if (-not $m.Success) { $noHeader += $name; continue }
1510
- if ($m.Groups[2].Value -eq $ver) { $unchanged += $name; continue }
1511
- $out = $src.Substring(0, $m.Index) + $m.Groups[1].Value + $ver + $m.Groups[3].Value +
1512
- $src.Substring($m.Index + $m.Length)
1513
- [System.IO.File]::WriteAllText($p, $out)
1514
- $stamped += $name
1515
- }
1516
- Write-Json ([ordered]@{ ok = $true; version = $ver; stamped = $stamped; unchanged = $unchanged;
1517
- missing = $missing; no_header = $noHeader; count = $stamped.Count })
1518
- }
1519
-
1520
- # ─────────────────────────────── main ──────────────────────────────────────
1521
- if ($args.Count -lt 1) { Write-Output 'Usage: aw-spec-actions.ps1 <subcommand> [options]'; exit 0 }
1522
- $sub = $args[0]
1523
- $rest = @(); if ($args.Count -gt 1) { $rest = $args[1..($args.Count - 1)] }
1524
-
1525
- switch ($sub) {
1526
- 'resolve-domain' { Invoke-ResolveDomain -Rest $rest }
1527
- 'resolve-tech' { Invoke-ResolveTech -Rest $rest }
1528
- 'scaffold' { Invoke-Scaffold -Rest $rest }
1529
- 'resolve-hooks' { Invoke-ResolveHooks -Rest $rest }
1530
- 'create-checklist' { Invoke-CreateChecklist -Rest $rest }
1531
- 'list-domains' { Invoke-ListDomains -Rest $rest }
1532
- 'set-domain' { Invoke-SetDomain -Rest $rest }
1533
- 'list-features' { Invoke-ListFeatures -Rest $rest }
1534
- 'set-feature' { Invoke-SetFeature -Rest $rest }
1535
- 'version-read' { Invoke-VersionState 'read' $rest }
1536
- 'version-init' { Invoke-VersionState 'init' $rest }
1537
- 'feature-paths' { Invoke-FeaturePaths -Rest $rest }
1538
- 'design-scaffold' { Invoke-DesignScaffold -Rest $rest }
1539
- 'build-compile' { Invoke-BuildCompile -Rest $rest }
1540
- 'test-run' { Invoke-TestRun -Rest $rest }
1541
- 'run-migrate' { Invoke-RunMigrate -Rest $rest }
1542
- 'deploy-dispatch' { Invoke-DeployDispatch -Rest $rest }
1543
- 'cr-setup' { Invoke-CrSetup -Rest $rest }
1544
- 'cr-archive' { Invoke-CrArchive -Rest $rest }
1545
- 'version-stamp' { Invoke-VersionStamp -Rest $rest }
1546
- 'checklist-read' { Invoke-ChecklistRead -Rest $rest }
1547
- 'checklist-apply' { Invoke-ChecklistApply -Rest $rest }
1548
- 'detect-project' { Invoke-DetectProject -Rest $rest }
1549
- 'foundation-list' { Invoke-FoundationList -Rest $rest }
1550
- 'foundation-locate' { Invoke-FoundationLocate -Rest $rest }
1551
- 'foundation-unpack' { Invoke-FoundationUnpack -Rest $rest }
1552
- 'foundation-verify' { Invoke-FoundationVerify -Rest $rest }
1553
- 'foundation-stash-dbcontext' { Invoke-FoundationStashDbContext -Rest $rest }
1554
- 'list-components' { Invoke-ListComponents -Rest $rest }
1555
- 'component-catalog' { Invoke-ComponentCatalog -Rest $rest }
1556
- 'component-verify' { Invoke-ComponentVerify -Rest $rest }
1557
- 'find-host-project' { Invoke-FindHostProject -Rest $rest }
1558
- 'setup-constitution' { Invoke-SetupConstitution -Rest $rest }
1559
- 'compose-constitution' { Invoke-ComposeConstitution -Rest $rest }
1560
- 'constitution-verify' { Invoke-ConstitutionVerify -Rest $rest }
1561
- 'agent-context-update' { Invoke-AgentContextUpdate -Rest $rest }
1562
- default { Write-Error "Unknown subcommand: $sub"; exit 1 }
1563
- }