aw-spec-cli 0.1.4 → 0.1.5
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.
- package/assets/.agents/agent.json +28 -0
- package/assets/.agents/skills/aw-spec-orch/SKILL.md +56 -0
- package/assets/.agents/skills/aw-spec-orch-demo/SKILL.md +59 -0
- package/assets/.aw-spec/DESIGN-FORMAT.md +151 -0
- package/assets/.aw-spec/ORCH-DIALECT.md +156 -0
- package/assets/.aw-spec/RULE-PARTITION.md +126 -0
- package/assets/.aw-spec/extensions/agent-context/extension.yml +12 -9
- package/assets/.aw-spec/extensions.yml +12 -19
- package/assets/.aw-spec/integrations/aw-spec.manifest.json +1 -9
- package/assets/.aw-spec/interpreter/run-skill.py +1158 -0
- package/assets/.aw-spec/orchestrator/AGENT-PROFILE.md +94 -0
- package/assets/.aw-spec/orchestrator/README.md +300 -0
- package/assets/.aw-spec/orchestrator/adapters/claude_agent_sdk.py +74 -0
- package/assets/.aw-spec/orchestrator/adapters/google_antigravity.py +74 -0
- package/assets/.aw-spec/orchestrator/modules/delta-design.orch.yaml +105 -0
- package/assets/.aw-spec/orchestrator/modules/delta-spec.orch.yaml +79 -0
- package/assets/.aw-spec/orchestrator/modules/domain-select.orch.yaml +81 -0
- package/assets/.aw-spec/orchestrator/modules/tech-select.orch.yaml +75 -0
- package/assets/.aw-spec/orchestrator/orchestrator.py +1721 -0
- package/assets/.aw-spec/orchestrator/rev-templates/ai-rules.reference.md +40 -0
- package/assets/.aw-spec/orchestrator/rev-templates/comandrulemap.skeleton.md +59 -0
- package/assets/.aw-spec/orchestrator/rev-templates/commandtechrulemap.skeleton.md +97 -0
- package/assets/.aw-spec/orchestrator/rev-templates/domain-constitution.skeleton.md +61 -0
- package/assets/.aw-spec/orchestrator/rev-templates/domain-requirements.skeleton.md +48 -0
- package/assets/.aw-spec/orchestrator/rev-templates/domain-rule-file.skeleton.md +39 -0
- package/assets/.aw-spec/orchestrator/rev-templates/technical-constitution.skeleton.md +106 -0
- package/assets/.aw-spec/orchestrator/rev-templates/technical-rule-file.skeleton.md +54 -0
- package/assets/.aw-spec/orchestrator/workflows/_smoke.orch.yaml +160 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-add-component.orch.yaml +244 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-adopt.orch.yaml +775 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-analyze-delta-design.orch.yaml +248 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-analyze-design.orch.yaml +261 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-analyze-slice.orch.yaml +425 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-build.orch.yaml +188 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-characterize.orch.yaml +474 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-close-delta.orch.yaml +181 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-constitution.orch.yaml +284 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-demo.orch.yaml +103 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-dependency-tree.orch.yaml +69 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-deploy.orch.yaml +85 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-design.orch.yaml +382 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-finalize-delta.orch.yaml +382 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-foundation.orch.yaml +246 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-implement-delta.orch.yaml +231 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-implement.orch.yaml +258 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-inventory.orch.yaml +236 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-onboard.orch.yaml +949 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-parity.orch.yaml +293 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-run.orch.yaml +177 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-select-domain.orch.yaml +79 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-select-feature.orch.yaml +128 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-specify.orch.yaml +412 -0
- package/assets/.aw-spec/scripts/bash/aw-inventory.sh +14 -0
- package/assets/.aw-spec/scripts/bash/aw-parity.sh +13 -0
- package/assets/.aw-spec/scripts/bash/aw-rev-constitution.sh +13 -0
- package/assets/.aw-spec/scripts/bash/aw-spec-actions.sh +1899 -0
- package/assets/.aw-spec/scripts/bash/aw-xref.sh +15 -0
- package/assets/.aw-spec/scripts/bash/common.sh +11 -60
- package/assets/.aw-spec/scripts/powershell/aw-inventory.ps1 +14 -0
- package/assets/.aw-spec/scripts/powershell/aw-parity.ps1 +13 -0
- package/assets/.aw-spec/scripts/powershell/aw-rev-constitution.ps1 +13 -0
- package/assets/.aw-spec/scripts/powershell/aw-spec-actions.ps1 +1563 -0
- package/assets/.aw-spec/scripts/powershell/aw-xref.ps1 +15 -0
- package/assets/.aw-spec/scripts/python/aw-inventory.py +683 -0
- package/assets/.aw-spec/scripts/python/aw-parity.py +380 -0
- package/assets/.aw-spec/scripts/python/aw-rev-constitution.py +312 -0
- package/assets/.aw-spec/scripts/python/aw-xref.py +1330 -0
- package/assets/.aw-spec/scripts/python/validate_okf.py +349 -0
- package/assets/.aw-spec/templates/constitution-domain-template.md +75 -0
- package/assets/.aw-spec/templates/constitution-technical-template.md +70 -0
- package/assets/.aw-spec/templates/delta-design-template.md +69 -0
- package/assets/.aw-spec/templates/delta-spec-template.md +60 -0
- package/assets/.aw-spec/templates/design-template.md +383 -0
- package/assets/.aw-spec/templates/okf-concept-template.md +103 -0
- package/assets/.aw-spec/templates/parity-manifest-template.json +49 -0
- package/assets/.aw-spec/templates/source-map-template.md +85 -0
- package/assets/.aw-spec/templates/spec-template.md +192 -71
- package/assets/.claude/agent.json +27 -0
- package/assets/.claude/skills/aw-spec-orch/SKILL.md +55 -0
- package/assets/.claude/skills/aw-spec-orch-demo/SKILL.md +59 -0
- package/assets/.kilocode/agent.json +28 -0
- package/assets/.kilocode/workflows/aw-spec-orch-demo.md +59 -0
- package/assets/.kilocode/workflows/aw-spec-orch.md +55 -0
- package/assets/docs/architecture/clean-architecture.md +1 -1
- package/assets/docs/architecture/cqrs.md +1 -1
- package/assets/docs/architecture/eventing.md +1 -1
- package/assets/docs/architecture/folder-structure.md +1 -1
- package/assets/docs/architecture/modular-monolith.md +1 -1
- package/assets/docs/architecture/persistence.md +1 -1
- package/assets/docs/architecture/testing.md +1 -1
- package/assets/docs/architecture/vertical-slices.md +1 -1
- package/assets/docs/components/Email/README.md +1 -1
- package/assets/docs/components/Jobs/README.md +1 -1
- package/assets/docs/components/QrGenerator/README.md +1 -1
- package/assets/docs/components/README.md +1 -1
- package/assets/docs/domain/domain-driven-design.md +1 -1
- package/assets/docs/domainspecs/healthcare/api-versioning.domain.md +1 -1
- package/assets/docs/domainspecs/healthcare/auditing.domain.md +86 -18
- package/assets/docs/domainspecs/healthcare/authorization.domain.md +1 -1
- package/assets/docs/domainspecs/healthcare/clinical-terms.md +1 -1
- package/assets/docs/domainspecs/healthcare/coding-standards.domain.md +1 -1
- package/assets/docs/domainspecs/healthcare/comandrulemap.md +58 -124
- package/assets/docs/domainspecs/healthcare/commandtechrulemap.md +119 -0
- package/assets/docs/domainspecs/healthcare/consent.md +1 -1
- package/assets/docs/domainspecs/healthcare/cqrs.domain.md +1 -1
- package/assets/docs/domainspecs/healthcare/data-classification.domain.md +1 -1
- package/assets/docs/domainspecs/healthcare/dependency-policy.domain.md +1 -1
- package/assets/docs/domainspecs/healthcare/dicom.md +1 -1
- package/assets/docs/domainspecs/healthcare/domain-driven-design.domain.md +1 -1
- package/assets/docs/domainspecs/healthcare/eventing.domain.md +1 -1
- package/assets/docs/domainspecs/healthcare/fhir.md +1 -1
- package/assets/docs/domainspecs/healthcare/folder-structure.domain.md +1 -1
- package/assets/docs/domainspecs/healthcare/hipaa.md +1 -1
- package/assets/docs/domainspecs/healthcare/hl7.md +1 -1
- package/assets/docs/domainspecs/healthcare/integration-terms.md +1 -1
- package/assets/docs/domainspecs/healthcare/persistence.domain.md +1 -1
- package/assets/docs/domainspecs/healthcare/terminology.md +1 -1
- package/assets/docs/domainspecs/healthcare/testing.domain.md +1 -1
- package/assets/docs/domainspecs/healthcare/vertical-slices.domain.md +1 -1
- package/assets/docs/domainspecs/healthcare/zero-trust.domain.md +1 -1
- package/assets/docs/domainspecs/pharma/api-versioning.domain.md +1 -1
- package/assets/docs/domainspecs/pharma/atc.md +1 -1
- package/assets/docs/domainspecs/pharma/auditing.domain.md +80 -16
- package/assets/docs/domainspecs/pharma/authorization.domain.md +1 -1
- package/assets/docs/domainspecs/pharma/coding-standards.domain.md +1 -1
- package/assets/docs/domainspecs/pharma/comandrulemap.md +64 -142
- package/assets/docs/domainspecs/pharma/commandtechrulemap.md +119 -0
- package/assets/docs/domainspecs/pharma/cqrs.domain.md +1 -1
- package/assets/docs/domainspecs/pharma/data-classification.domain.md +1 -1
- package/assets/docs/domainspecs/pharma/data-provenance.md +1 -1
- package/assets/docs/domainspecs/pharma/dependency-policy.domain.md +1 -1
- package/assets/docs/domainspecs/pharma/domain-driven-design.domain.md +1 -1
- package/assets/docs/domainspecs/pharma/eventing.domain.md +1 -1
- package/assets/docs/domainspecs/pharma/fhir.md +1 -1
- package/assets/docs/domainspecs/pharma/folder-structure.domain.md +1 -1
- package/assets/docs/domainspecs/pharma/hl7.md +1 -1
- package/assets/docs/domainspecs/pharma/idmp.md +1 -1
- package/assets/docs/domainspecs/pharma/integration-terms.md +1 -1
- package/assets/docs/domainspecs/pharma/ndc.md +1 -1
- package/assets/docs/domainspecs/pharma/persistence.domain.md +1 -1
- package/assets/docs/domainspecs/pharma/pharma-terms.md +1 -1
- package/assets/docs/domainspecs/pharma/product-lifecycle.domain.md +1 -1
- package/assets/docs/domainspecs/pharma/regulatory-compliance.md +1 -1
- package/assets/docs/domainspecs/pharma/terminology.md +1 -1
- package/assets/docs/domainspecs/pharma/testing.domain.md +1 -1
- package/assets/docs/domainspecs/pharma/vertical-slices.domain.md +1 -1
- package/assets/docs/domainspecs/pharma/zero-trust.domain.md +1 -1
- package/assets/docs/governance/api-versioning.md +1 -1
- package/assets/docs/governance/coding-standards.md +1 -1
- package/assets/docs/governance/dependency-policy.md +1 -1
- package/assets/docs/requirement-spec.txt +27 -0
- package/assets/docs/security/auditing.md +54 -11
- package/assets/docs/security/authorization.md +1 -1
- package/assets/docs/security/data-classification.md +1 -1
- package/assets/docs/security/zero-trust.md +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +7 -54
- package/dist/commands/init.js.map +1 -1
- package/dist/index.js +0 -2
- package/dist/index.js.map +1 -1
- package/dist/utils/paths.js +2 -2
- package/dist/utils/paths.js.map +1 -1
- package/package.json +1 -1
- package/assets/.agents/skills/aw-spec-addcomponent/SKILL.md +0 -129
- package/assets/.agents/skills/aw-spec-agent-context-update/SKILL.md +0 -31
- package/assets/.agents/skills/aw-spec-analyze/SKILL.md +0 -296
- package/assets/.agents/skills/aw-spec-checklist/SKILL.md +0 -374
- package/assets/.agents/skills/aw-spec-clarify/SKILL.md +0 -320
- package/assets/.agents/skills/aw-spec-constitution/SKILL.md +0 -194
- package/assets/.agents/skills/aw-spec-fast-implement/SKILL.md +0 -268
- package/assets/.agents/skills/aw-spec-foundation/SKILL.md +0 -358
- package/assets/.agents/skills/aw-spec-implement/SKILL.md +0 -279
- package/assets/.agents/skills/aw-spec-minimal-implementation/SKILL.md +0 -209
- package/assets/.agents/skills/aw-spec-plan/SKILL.md +0 -221
- package/assets/.agents/skills/aw-spec-select-domain/SKILL.md +0 -73
- package/assets/.agents/skills/aw-spec-specify/SKILL.md +0 -382
- package/assets/.agents/skills/aw-spec-tasks/SKILL.md +0 -259
- package/assets/.agents/skills/aw-spec-taskstoissues/SKILL.md +0 -107
- package/assets/.aw-spec/integrations/claude.manifest.json +0 -17
- package/assets/.aw-spec/scripts/bash/check-prerequisites.sh +0 -189
- package/assets/.aw-spec/scripts/bash/create-new-feature.sh +0 -299
- package/assets/.aw-spec/scripts/bash/setup-plan.sh +0 -84
- package/assets/.aw-spec/scripts/bash/setup-tasks.sh +0 -91
- package/assets/.aw-spec/templates/checklist-template.md +0 -40
- package/assets/.aw-spec/templates/constitution-template.md +0 -57
- package/assets/.aw-spec/templates/plan-template.md +0 -113
- package/assets/.aw-spec/templates/tasks-template.md +0 -252
- package/assets/.aw-spec/workflows/aw-spec/workflow.yml +0 -77
- package/assets/.aw-spec/workflows/workflow-registry.json +0 -13
- package/assets/.claude/skills/aw-spec-addcomponent/SKILL.md +0 -129
- package/assets/.claude/skills/aw-spec-agent-context-update/SKILL.md +0 -31
- package/assets/.claude/skills/aw-spec-analyze/SKILL.md +0 -296
- package/assets/.claude/skills/aw-spec-checklist/SKILL.md +0 -374
- package/assets/.claude/skills/aw-spec-clarify/SKILL.md +0 -320
- package/assets/.claude/skills/aw-spec-constitution/SKILL.md +0 -194
- package/assets/.claude/skills/aw-spec-fast-implement/SKILL.md +0 -268
- package/assets/.claude/skills/aw-spec-foundation/SKILL.md +0 -358
- package/assets/.claude/skills/aw-spec-implement/SKILL.md +0 -279
- package/assets/.claude/skills/aw-spec-minimal-implementation/SKILL.md +0 -209
- package/assets/.claude/skills/aw-spec-plan/SKILL.md +0 -221
- package/assets/.claude/skills/aw-spec-select-domain/SKILL.md +0 -73
- package/assets/.claude/skills/aw-spec-specify/SKILL.md +0 -382
- package/assets/.claude/skills/aw-spec-tasks/SKILL.md +0 -259
- package/assets/.claude/skills/aw-spec-taskstoissues/SKILL.md +0 -107
- package/assets/.kilocode/rules/specify-rules.md +0 -4
- package/assets/.kilocode/workflows/aw-spec-addcomponent.md +0 -129
- package/assets/.kilocode/workflows/aw-spec-agent-context.update.md +0 -31
- package/assets/.kilocode/workflows/aw-spec-analyze.md +0 -296
- package/assets/.kilocode/workflows/aw-spec-checklist.md +0 -374
- package/assets/.kilocode/workflows/aw-spec-clarify.md +0 -320
- package/assets/.kilocode/workflows/aw-spec-constitution.md +0 -194
- package/assets/.kilocode/workflows/aw-spec-fast-implement.md +0 -263
- package/assets/.kilocode/workflows/aw-spec-foundation.md +0 -358
- package/assets/.kilocode/workflows/aw-spec-implement.md +0 -279
- package/assets/.kilocode/workflows/aw-spec-minimal-implementation.md +0 -204
- package/assets/.kilocode/workflows/aw-spec-plan.md +0 -221
- package/assets/.kilocode/workflows/aw-spec-select-domain.md +0 -73
- package/assets/.kilocode/workflows/aw-spec-specify.md +0 -382
- package/assets/.kilocode/workflows/aw-spec-tasks.md +0 -259
- package/assets/.kilocode/workflows/aw-spec-taskstoissues.md +0 -107
- package/assets/docs/UI.Visualization/PACKAGING.md +0 -191
- package/assets/docs/UI.Visualization/UI.Visualization.Reference.pack.txt +0 -11768
- package/assets/docs/UI.Visualization/UI.Visualization.pack.txt +0 -11214
- package/assets/docs/UI.Visualization/pack-foundation.ps1 +0 -165
- package/assets/docs/UI.Visualization/pack-foundation.sh +0 -206
- package/assets/docs/UI.Visualization/pack-reference-foundation.ps1 +0 -159
- package/assets/docs/UI.Visualization/pack-reference-foundation.sh +0 -205
- package/assets/docs/UI.Visualization/unpack-foundation.ps1 +0 -126
- package/assets/docs/UI.Visualization/unpack-foundation.sh +0 -217
- package/assets/docs/UI.Visualization/unpack-reference-foundation.ps1 +0 -129
- package/assets/docs/UI.Visualization/unpack-reference-foundation.sh +0 -218
- package/assets/docs/command.txt +0 -295
- package/assets/docs/domainspecs/UIVisualization/chart-bubble.md +0 -70
- package/assets/docs/domainspecs/UIVisualization/chart-dimred.md +0 -58
- package/assets/docs/domainspecs/UIVisualization/chart-heatmap.md +0 -62
- package/assets/docs/domainspecs/UIVisualization/chart-network.md +0 -69
- package/assets/docs/domainspecs/UIVisualization/chart-parallel.md +0 -75
- package/assets/docs/domainspecs/UIVisualization/chart-scurve.md +0 -66
- package/assets/docs/domainspecs/UIVisualization/chart-survival.md +0 -69
- package/assets/docs/domainspecs/UIVisualization/chart-trellis.md +0 -68
- package/assets/docs/domainspecs/UIVisualization/chart-violin.md +0 -61
- package/assets/docs/domainspecs/UIVisualization/chart-volcano.md +0 -63
- package/assets/docs/domainspecs/UIVisualization/charting-standards.domain.md +0 -80
- package/assets/docs/domainspecs/UIVisualization/clean-architecture.domain.md +0 -87
- package/assets/docs/domainspecs/UIVisualization/coding-standards.domain.md +0 -47
- package/assets/docs/domainspecs/UIVisualization/comandrulemap.md +0 -130
- package/assets/docs/domainspecs/UIVisualization/component-contracts.domain.md +0 -67
- package/assets/docs/domainspecs/UIVisualization/data-source.domain.md +0 -68
- package/assets/docs/domainspecs/UIVisualization/domain-requirements.md +0 -110
- package/assets/docs/domainspecs/UIVisualization/folder-structure.domain.md +0 -55
- package/assets/docs/domainspecs/UIVisualization/state-management.domain.md +0 -67
- package/assets/docs/domainspecs/UIVisualization/testing.domain.md +0 -52
- package/assets/docs/domainspecs/UIVisualization/ui-viz-terms.md +0 -39
- package/assets/docs/pharma-constitution.my-ref.md +0 -86
|
@@ -0,0 +1,1563 @@
|
|
|
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
|
+
}
|