aw-spec-cli 0.1.7 → 0.1.9
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/.appweaver/DESIGN-FORMAT.md +200 -0
- package/assets/.appweaver/ORCH-DIALECT.md +156 -0
- package/assets/.appweaver/RULE-PARTITION.md +175 -0
- package/assets/.appweaver/STACK-PROFILES.md +461 -0
- package/assets/.appweaver/ai-rules.md +180 -0
- package/assets/.appweaver/domain.json +1 -0
- package/assets/.appweaver/extensions/.registry +19 -0
- package/assets/.appweaver/extensions/agent-context/README.md +57 -0
- package/assets/.appweaver/extensions/agent-context/agent-context-config.yml +4 -0
- package/assets/.appweaver/extensions/agent-context/commands/aw-spec.agent-context.update.md +26 -0
- package/assets/.appweaver/extensions/agent-context/extension.yml +37 -0
- package/assets/.appweaver/extensions/agent-context/scripts/bash/update-agent-context.sh +201 -0
- package/assets/.appweaver/extensions/agent-context/scripts/powershell/update-agent-context.ps1 +237 -0
- package/assets/.appweaver/extensions.yml +16 -0
- package/assets/.appweaver/feature.json +3 -0
- package/assets/.appweaver/init-options.json +9 -0
- package/assets/.appweaver/integration.json +15 -0
- package/assets/.appweaver/integrations/aw-spec.manifest.json +9 -0
- package/assets/.appweaver/interpreter/run-skill.py +1176 -0
- package/assets/.appweaver/memory/constitution.md +50 -0
- package/assets/.appweaver/orchestrator/.smoke-out/smoke.md +6 -0
- package/assets/.appweaver/orchestrator/AGENT-PROFILE.md +94 -0
- package/assets/.appweaver/orchestrator/README.md +300 -0
- package/assets/.appweaver/orchestrator/adapters/claude_agent_sdk.py +74 -0
- package/assets/.appweaver/orchestrator/adapters/google_antigravity.py +74 -0
- package/assets/.appweaver/orchestrator/modules/delta-design.orch.yaml +105 -0
- package/assets/.appweaver/orchestrator/modules/delta-spec.orch.yaml +101 -0
- package/assets/.appweaver/orchestrator/modules/domain-select.orch.yaml +90 -0
- package/assets/.appweaver/orchestrator/modules/lane-gate.orch.yaml +126 -0
- package/assets/.appweaver/orchestrator/modules/tech-select.orch.yaml +123 -0
- package/assets/.appweaver/orchestrator/orchestrator.py +1721 -0
- package/assets/.appweaver/orchestrator/rev-templates/ai-rules.reference.md +40 -0
- package/assets/.appweaver/orchestrator/rev-templates/delta-design-template.skeleton.md +101 -0
- package/assets/.appweaver/orchestrator/rev-templates/design-template.skeleton.md +245 -0
- package/assets/.appweaver/orchestrator/rev-templates/domain-constitution.skeleton.md +63 -0
- package/assets/.appweaver/orchestrator/rev-templates/domain-requirements.skeleton.md +48 -0
- package/assets/.appweaver/orchestrator/rev-templates/domain-rule-bindings.skeleton.md +59 -0
- package/assets/.appweaver/orchestrator/rev-templates/domain-rule-file.skeleton.md +39 -0
- package/assets/.appweaver/orchestrator/rev-templates/frontend-rule-file.skeleton.md +63 -0
- package/assets/.appweaver/orchestrator/rev-templates/tech-rule-bindings.skeleton.md +116 -0
- package/assets/.appweaver/orchestrator/rev-templates/technical-constitution.skeleton.md +106 -0
- package/assets/.appweaver/orchestrator/rev-templates/technical-rule-file.skeleton.md +54 -0
- package/assets/.appweaver/orchestrator/rev-templates/ui-constitution.skeleton.md +125 -0
- package/assets/.appweaver/orchestrator/rev-templates/ui-delta-design-template.skeleton.md +123 -0
- package/assets/.appweaver/orchestrator/rev-templates/ui-design-template.skeleton.md +294 -0
- package/assets/.appweaver/orchestrator/workflows/_smoke.orch.yaml +160 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-add-component.orch.yaml +270 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-adopt.orch.yaml +934 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-delta-design.orch.yaml +251 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-design.orch.yaml +265 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice.orch.yaml +437 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-build.orch.yaml +197 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-characterize.orch.yaml +475 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-close-delta.orch.yaml +181 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-constitution.orch.yaml +727 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-demo.orch.yaml +100 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-dependency-tree.orch.yaml +69 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-deploy.orch.yaml +85 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-design-ui.orch.yaml +386 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-design.orch.yaml +382 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-finalize-delta.orch.yaml +462 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-foundation.orch.yaml +265 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-implement-delta.orch.yaml +253 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-implement-ui.orch.yaml +267 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-implement.orch.yaml +287 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-inventory.orch.yaml +238 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-onboard.orch.yaml +1822 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-parity.orch.yaml +293 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-run.orch.yaml +194 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-select-domain.orch.yaml +83 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-select-feature.orch.yaml +128 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-select-stack.orch.yaml +296 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-specify.orch.yaml +439 -0
- package/assets/.appweaver/scripts/bash/aw-inventory.sh +17 -0
- package/assets/.appweaver/scripts/bash/aw-parity.sh +13 -0
- package/assets/.appweaver/scripts/bash/aw-rev-constitution.sh +13 -0
- package/assets/.appweaver/scripts/bash/aw-spec-actions.sh +2626 -0
- package/assets/.appweaver/scripts/bash/aw-xref.sh +15 -0
- package/assets/.appweaver/scripts/bash/common.sh +533 -0
- package/assets/.appweaver/scripts/powershell/aw-inventory.ps1 +17 -0
- package/assets/.appweaver/scripts/powershell/aw-parity.ps1 +13 -0
- package/assets/.appweaver/scripts/powershell/aw-rev-constitution.ps1 +13 -0
- package/assets/.appweaver/scripts/powershell/aw-spec-actions.ps1 +2232 -0
- package/assets/.appweaver/scripts/powershell/aw-xref.ps1 +15 -0
- package/assets/.appweaver/scripts/python/aw-component-catalog.py +121 -0
- package/assets/.appweaver/scripts/python/aw-inventory.py +1180 -0
- package/assets/.appweaver/scripts/python/aw-parity.py +380 -0
- package/assets/.appweaver/scripts/python/aw-rev-constitution.py +329 -0
- package/assets/.appweaver/scripts/python/aw-stack.py +1251 -0
- package/assets/.appweaver/scripts/python/aw-xref.py +1330 -0
- package/assets/.appweaver/scripts/python/validate_okf.py +352 -0
- package/assets/.appweaver/stack-profiles/angular.json +16 -0
- package/assets/.appweaver/stack-profiles/blazor-wasm.json +17 -0
- package/assets/.appweaver/stack-profiles/dotnet-webapi.json +15 -0
- package/assets/.appweaver/stack-profiles/go-chi.json +14 -0
- package/assets/.appweaver/stack-profiles/java-spring.json +16 -0
- package/assets/.appweaver/stack-profiles/nextjs.json +16 -0
- package/assets/.appweaver/stack-profiles/node-express.json +15 -0
- package/assets/.appweaver/stack-profiles/python-fastapi.json +16 -0
- package/assets/.appweaver/stack-profiles/react-vite.json +17 -0
- package/assets/.appweaver/stack-profiles/sveltekit.json +16 -0
- package/assets/.appweaver/stack-profiles/vue-vite.json +17 -0
- package/assets/.appweaver/stacks.json +3 -0
- package/assets/.appweaver/templates/constitution-domain-template.md +116 -0
- package/assets/.appweaver/templates/constitution-technical-template.md +108 -0
- package/assets/.appweaver/templates/constitution-ui-template.md +116 -0
- package/assets/.appweaver/templates/delta-design-template.md +69 -0
- package/assets/.appweaver/templates/delta-spec-template.md +63 -0
- package/assets/.appweaver/templates/design-template.md +383 -0
- package/assets/.appweaver/templates/okf-concept-template.md +114 -0
- package/assets/.appweaver/templates/parity-manifest-template.json +49 -0
- package/assets/.appweaver/templates/source-map-template.md +85 -0
- package/assets/.appweaver/templates/spec-template.md +309 -0
- package/assets/.appweaver/templates/ui-design-template.md +273 -0
- package/assets/foundations/dotnet/ModularMonolith/ModularMonolith.Reference.pack.txt +25248 -0
- package/assets/foundations/dotnet/ModularMonolith/ModularMonolith.pack.txt +5726 -0
- package/assets/foundations/dotnet/ModularMonolith/pack-foundation.ps1 +145 -0
- package/assets/foundations/dotnet/ModularMonolith/pack-foundation.sh +196 -0
- package/assets/foundations/dotnet/ModularMonolith/pack-reference-foundation.ps1 +157 -0
- package/assets/foundations/dotnet/ModularMonolith/pack-reference-foundation.sh +209 -0
- package/assets/foundations/dotnet/ModularMonolith/unpack-foundation.ps1 +126 -0
- package/assets/foundations/dotnet/ModularMonolith/unpack-foundation.sh +217 -0
- package/assets/foundations/dotnet/ModularMonolith/unpack-reference-foundation.ps1 +129 -0
- package/assets/foundations/dotnet/ModularMonolith/unpack-reference-foundation.sh +218 -0
- package/assets/foundations/dotnet/VerticalSlice/VerticalSlice.Reference.pack.txt +19318 -0
- package/assets/foundations/dotnet/VerticalSlice/VerticalSlice.pack.txt +2833 -0
- package/assets/foundations/dotnet/VerticalSlice/pack-foundation.ps1 +145 -0
- package/assets/foundations/dotnet/VerticalSlice/pack-foundation.sh +196 -0
- package/assets/foundations/dotnet/VerticalSlice/pack-reference-foundation.ps1 +157 -0
- package/assets/foundations/dotnet/VerticalSlice/pack-reference-foundation.sh +209 -0
- package/assets/foundations/dotnet/VerticalSlice/unpack-foundation.ps1 +126 -0
- package/assets/foundations/dotnet/VerticalSlice/unpack-foundation.sh +217 -0
- package/assets/foundations/dotnet/VerticalSlice/unpack-reference-foundation.ps1 +129 -0
- package/assets/foundations/dotnet/VerticalSlice/unpack-reference-foundation.sh +218 -0
- package/assets/foundations/dotnet/components/Email/README.md +119 -0
- package/assets/foundations/dotnet/components/Email/ReferenceEndpoints/SendEmailEndpoint.cs +84 -0
- package/assets/foundations/dotnet/components/Email/integration.md +300 -0
- package/assets/foundations/dotnet/components/Jobs/README.md +113 -0
- package/assets/foundations/dotnet/components/Jobs/integration.md +259 -0
- package/assets/foundations/dotnet/components/QrGenerator/README.md +85 -0
- package/assets/foundations/dotnet/components/QrGenerator/ReferenceEndpoints/GenerateQrEndpoint.cs +57 -0
- package/assets/foundations/dotnet/components/QrGenerator/integration.md +230 -0
- package/assets/foundations/dotnet/components/README.md +74 -0
- package/assets/guardrails/README.md +222 -0
- package/assets/guardrails/domain/healthcare/ai-rules.domain.md +34 -0
- package/assets/guardrails/domain/healthcare/api-versioning.domain.md +17 -0
- package/assets/guardrails/domain/healthcare/auditing.domain.md +97 -0
- package/assets/guardrails/domain/healthcare/authorization.domain.md +26 -0
- package/assets/guardrails/domain/healthcare/clinical-terms.md +20 -0
- package/assets/guardrails/domain/healthcare/coding-standards.domain.md +26 -0
- package/assets/guardrails/domain/healthcare/consent.md +19 -0
- package/assets/guardrails/domain/healthcare/cqrs.domain.md +12 -0
- package/assets/guardrails/domain/healthcare/data-classification.domain.md +28 -0
- package/assets/guardrails/domain/healthcare/dependency-policy.domain.md +11 -0
- package/assets/guardrails/domain/healthcare/dicom.md +15 -0
- package/assets/guardrails/domain/healthcare/domain-constitution.md +119 -0
- package/assets/guardrails/domain/healthcare/domain-driven-design.domain.md +28 -0
- package/assets/guardrails/domain/healthcare/eventing.domain.md +18 -0
- package/assets/guardrails/domain/healthcare/fhir.md +36 -0
- package/assets/guardrails/domain/healthcare/folder-structure.domain.md +25 -0
- package/assets/guardrails/domain/healthcare/hipaa.md +34 -0
- package/assets/guardrails/domain/healthcare/hl7.md +15 -0
- package/assets/guardrails/domain/healthcare/ihe-conformance.md +31 -0
- package/assets/guardrails/domain/healthcare/integration-terms.md +21 -0
- package/assets/guardrails/domain/healthcare/persistence.domain.md +20 -0
- package/assets/guardrails/domain/healthcare/terminology.md +30 -0
- package/assets/guardrails/domain/healthcare/testing.domain.md +15 -0
- package/assets/guardrails/domain/healthcare/ux/accessibility-standards.md +36 -0
- package/assets/guardrails/domain/healthcare/ux/content-design.md +32 -0
- package/assets/guardrails/domain/healthcare/ux/interaction-states.md +44 -0
- package/assets/guardrails/domain/healthcare/ux/journey-mapping.md +40 -0
- package/assets/guardrails/domain/healthcare/vertical-slices.domain.md +18 -0
- package/assets/guardrails/domain/healthcare/zero-trust.domain.md +12 -0
- package/assets/guardrails/domain/pharma/ai-rules.domain.md +32 -0
- package/assets/guardrails/domain/pharma/api-versioning.domain.md +20 -0
- package/assets/guardrails/domain/pharma/atc.md +22 -0
- package/assets/guardrails/domain/pharma/auditing.domain.md +90 -0
- package/assets/guardrails/domain/pharma/authorization.domain.md +25 -0
- package/assets/guardrails/domain/pharma/coding-standards.domain.md +28 -0
- package/assets/guardrails/domain/pharma/cqrs.domain.md +16 -0
- package/assets/guardrails/domain/pharma/data-classification.domain.md +24 -0
- package/assets/guardrails/domain/pharma/data-provenance.md +21 -0
- package/assets/guardrails/domain/pharma/dependency-policy.domain.md +11 -0
- package/assets/guardrails/domain/pharma/domain-constitution.md +113 -0
- package/assets/guardrails/domain/pharma/domain-driven-design.domain.md +36 -0
- package/assets/guardrails/domain/pharma/domain-requirements.sample.md +103 -0
- package/assets/guardrails/domain/pharma/eventing.domain.md +18 -0
- package/assets/guardrails/domain/pharma/fhir.md +31 -0
- package/assets/guardrails/domain/pharma/folder-structure.domain.md +26 -0
- package/assets/guardrails/domain/pharma/hl7.md +17 -0
- package/assets/guardrails/domain/pharma/idmp.md +26 -0
- package/assets/guardrails/domain/pharma/integration-terms.md +21 -0
- package/assets/guardrails/domain/pharma/ndc.md +28 -0
- package/assets/guardrails/domain/pharma/persistence.domain.md +23 -0
- package/assets/guardrails/domain/pharma/pharma-terms.md +27 -0
- package/assets/guardrails/domain/pharma/product-lifecycle.domain.md +30 -0
- package/assets/guardrails/domain/pharma/regulatory-compliance.md +19 -0
- package/assets/guardrails/domain/pharma/terminology.md +32 -0
- package/assets/guardrails/domain/pharma/testing.domain.md +19 -0
- package/assets/guardrails/domain/pharma/vertical-slices.domain.md +17 -0
- package/assets/guardrails/domain/pharma/zero-trust.domain.md +12 -0
- package/assets/guardrails/projects/dotnet-default-project/domain/domain-requirements.md +58 -0
- package/assets/guardrails/projects/dotnet-default-project/domain-rule-bindings.md +113 -0
- package/assets/guardrails/projects/dotnet-default-project/frontend/ui-requirements.md +43 -0
- package/assets/guardrails/projects/dotnet-default-project/service/technical-requirements.md +40 -0
- package/assets/guardrails/projects/dotnet-default-project/tech-rule-bindings.md +263 -0
- package/assets/guardrails/requirement-spec.txt +27 -0
- package/assets/guardrails/technology/dotnet-webapi/PACKAGING.md +184 -0
- package/assets/guardrails/technology/dotnet-webapi/api-versioning.md +44 -0
- package/assets/guardrails/technology/dotnet-webapi/auditing.md +96 -0
- package/assets/guardrails/technology/dotnet-webapi/authorization-matrix.md +14 -0
- package/assets/guardrails/technology/dotnet-webapi/authorization.md +39 -0
- package/assets/guardrails/technology/dotnet-webapi/clean-architecture.md +54 -0
- package/assets/guardrails/technology/dotnet-webapi/coding-standards.md +87 -0
- package/assets/guardrails/technology/dotnet-webapi/cqrs.md +63 -0
- package/assets/guardrails/technology/dotnet-webapi/data-classification.md +38 -0
- package/assets/guardrails/technology/dotnet-webapi/dependency-policy.md +40 -0
- package/assets/guardrails/technology/dotnet-webapi/domain-driven-design.md +91 -0
- package/assets/guardrails/technology/dotnet-webapi/eventing.md +57 -0
- package/assets/guardrails/technology/dotnet-webapi/examples/api-versioning.md +75 -0
- package/assets/guardrails/technology/dotnet-webapi/examples/auditing.md +84 -0
- package/assets/guardrails/technology/dotnet-webapi/examples/authorization.md +68 -0
- package/assets/guardrails/technology/dotnet-webapi/examples/clean-architecture.md +37 -0
- package/assets/guardrails/technology/dotnet-webapi/examples/cqrs.md +97 -0
- package/assets/guardrails/technology/dotnet-webapi/examples/domain-driven-design.md +141 -0
- package/assets/guardrails/technology/dotnet-webapi/examples/eventing.md +46 -0
- package/assets/guardrails/technology/dotnet-webapi/examples/persistence.md +207 -0
- package/assets/guardrails/technology/dotnet-webapi/examples/vertical-slices.md +151 -0
- package/assets/guardrails/technology/dotnet-webapi/examples/zero-trust.md +48 -0
- package/assets/guardrails/technology/dotnet-webapi/folder-structure.md +61 -0
- package/assets/guardrails/technology/dotnet-webapi/modular-monolith.md +47 -0
- package/assets/guardrails/technology/dotnet-webapi/persistence.md +82 -0
- package/assets/guardrails/technology/dotnet-webapi/technology-constitution.md +162 -0
- package/assets/guardrails/technology/dotnet-webapi/testing.md +55 -0
- package/assets/guardrails/technology/dotnet-webapi/vertical-slices.md +70 -0
- package/assets/guardrails/technology/dotnet-webapi/zero-trust.md +48 -0
- package/assets/guardrails/technology/java-spring/README.md +62 -0
- package/assets/guardrails/technology/nextjs/accessibility-implementation.md +56 -0
- package/assets/guardrails/technology/nextjs/api-client.md +56 -0
- package/assets/guardrails/technology/nextjs/api-integration.dotnet-webapi.md +174 -0
- package/assets/guardrails/technology/nextjs/api-integration.java-spring.md +203 -0
- package/assets/guardrails/technology/nextjs/auth-session.md +68 -0
- package/assets/guardrails/technology/nextjs/client-security.md +69 -0
- package/assets/guardrails/technology/nextjs/component-architecture.md +60 -0
- package/assets/guardrails/technology/nextjs/component-conventions.md +58 -0
- package/assets/guardrails/technology/nextjs/design-system.md +60 -0
- package/assets/guardrails/technology/nextjs/error-handling.md +72 -0
- package/assets/guardrails/technology/nextjs/forms-and-validation.md +75 -0
- package/assets/guardrails/technology/nextjs/frontend-testing.md +94 -0
- package/assets/guardrails/technology/nextjs/i18n.md +57 -0
- package/assets/guardrails/technology/nextjs/observability.md +67 -0
- package/assets/guardrails/technology/nextjs/performance-budgets.md +66 -0
- package/assets/guardrails/technology/nextjs/routing-navigation.md +52 -0
- package/assets/guardrails/technology/nextjs/state-management.md +70 -0
- package/assets/guardrails/technology/nextjs/ui-constitution.md +166 -0
- package/dist/commands/init.js +2 -2
- package/dist/commands/init.js.map +1 -1
- package/dist/utils/copy-assets.js +8 -8
- package/dist/utils/copy-assets.js.map +1 -1
- package/package.json +1 -1
- package/assets/docs/pharma-constitution.my-ref.md +0 -86
|
@@ -0,0 +1,2232 @@
|
|
|
1
|
+
#!/usr/bin/env pwsh
|
|
2
|
+
#
|
|
3
|
+
# aw-spec-actions.ps1 — common action dispatcher for the aw-spec orchestrator
|
|
4
|
+
# (Windows / PowerShell mirror of aw-spec-actions.sh).
|
|
5
|
+
#
|
|
6
|
+
# ONE shared script for every integration (.claude / .agents / .kilocode). The
|
|
7
|
+
# orchestrator workflows reach these subcommands through `execute_script` steps.
|
|
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, in the workflow's agent_* steps.
|
|
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 '.appweaver')) { 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 '.appweaver/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 '.appweaver/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 '.appweaver/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 '.appweaver/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 "guardrails/projects/$domain/domain-rule-bindings.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
|
+
# guardrails/projects/<project>/tech-rule-bindings.md. The TECHNICAL rule files live in the
|
|
112
|
+
# shared per-stack corpora at guardrails/technology/<stack>/ (rules) and
|
|
113
|
+
# guardrails/technology/<stack>/examples/ (the worked sample for a rule file), and are
|
|
114
|
+
# configured per-command in this map, so they can be tuned per project WITHOUT editing the
|
|
115
|
+
# workflows and a second project can map the same corpus. The technical CONSTITUTION
|
|
116
|
+
# (constitution.technical.md) is NOT mapped — it is still referred to directly by
|
|
117
|
+
# each consumer, as-is.
|
|
118
|
+
# A bullet may name its stack with a PLACEHOLDER — {{stack}} / {{backend_stack}} /
|
|
119
|
+
# {{frontend_stack}} / {{backend_ecosystem}} / {{frontend_ecosystem}} — resolved from
|
|
120
|
+
# the lanes in .appweaver/stacks.json. That is what lets one map serve a Spring repo and
|
|
121
|
+
# a .NET repo without an edit. A bullet that instead spells a stack directory out is kept
|
|
122
|
+
# and USED (it may be a deliberate cross-stack reference) but reported through
|
|
123
|
+
# stack_mismatch when it is not a stack this repo declares, because a map pinned to the
|
|
124
|
+
# wrong corpus is how a Java project ends up being handed C# rules.
|
|
125
|
+
function Invoke-ResolveTech {
|
|
126
|
+
param([string[]]$Rest)
|
|
127
|
+
$commandName = 'aw-spec-design'
|
|
128
|
+
for ($i = 0; $i -lt $Rest.Count; $i++) {
|
|
129
|
+
switch ($Rest[$i]) {
|
|
130
|
+
'--command' { $commandName = $Rest[++$i] }
|
|
131
|
+
'-Command' { $commandName = $Rest[++$i] }
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
$repo = Get-RepoRoot
|
|
135
|
+
$domainJson = Join-Path $repo '.appweaver/domain.json'
|
|
136
|
+
$domain = ''
|
|
137
|
+
if (Test-Path $domainJson) {
|
|
138
|
+
try { $domain = (Get-Content $domainJson -Raw | ConvertFrom-Json).domain } catch { $domain = '' }
|
|
139
|
+
if (-not $domain) { $domain = '' }
|
|
140
|
+
}
|
|
141
|
+
$techmap = Join-Path $repo "guardrails/projects/$domain/tech-rule-bindings.md"
|
|
142
|
+
$techmapExists = ($domain -ne '') -and (Test-Path $techmap)
|
|
143
|
+
|
|
144
|
+
# The declared stacks, so one map can be written for any of them.
|
|
145
|
+
$subs = [ordered]@{
|
|
146
|
+
'backend_stack' = (Get-LaneField -Field 'backend_profile')
|
|
147
|
+
'frontend_stack' = (Get-LaneField -Field 'frontend_profile')
|
|
148
|
+
'backend_ecosystem' = (Get-LaneField -Field 'backend_ecosystem')
|
|
149
|
+
'frontend_ecosystem' = (Get-LaneField -Field 'frontend_ecosystem')
|
|
150
|
+
}
|
|
151
|
+
$subs['stack'] = $subs['backend_stack'] # the unqualified form means the service lane
|
|
152
|
+
$declared = @($subs.Values | Where-Object { $_ } | Select-Object -Unique)
|
|
153
|
+
|
|
154
|
+
$bullets = @()
|
|
155
|
+
if ($techmapExists) {
|
|
156
|
+
$lines = Get-Content $techmap
|
|
157
|
+
foreach ($heading in @("## /$commandName", "## /$($commandName -replace 'decl-','')")) {
|
|
158
|
+
$grab = $false; $found = @()
|
|
159
|
+
foreach ($line in $lines) {
|
|
160
|
+
if ($line -eq $heading) { $grab = $true; continue }
|
|
161
|
+
if ($line -match '^## ') { $grab = $false }
|
|
162
|
+
if ($grab -and $line -match '^\s*-\s+(.+?)\s*$') { $found += $Matches[1] }
|
|
163
|
+
}
|
|
164
|
+
if ($found.Count -gt 0) { $bullets = $found; break }
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
$candidates = @(); $missing = @(); $skipped = @(); $mismatched = @()
|
|
169
|
+
foreach ($b in $bullets) {
|
|
170
|
+
$holes = @([regex]::Matches($b, '\{\{\s*([a-z_]+)\s*\}\}') | ForEach-Object { $_.Groups[1].Value })
|
|
171
|
+
$why = ''
|
|
172
|
+
foreach ($h in $holes) {
|
|
173
|
+
if (-not $subs.Contains($h)) { $why = "unknown placeholder {{$h}}"; break }
|
|
174
|
+
# A frontend placeholder in a backend-only repo is not an error: the lane
|
|
175
|
+
# genuinely does not exist, so its rules genuinely do not apply.
|
|
176
|
+
if (-not $subs[$h]) { $why = "no lane declared for {{$h}}"; break }
|
|
177
|
+
}
|
|
178
|
+
if ($why) { $skipped += [ordered]@{ bullet = $b; why = $why }; continue }
|
|
179
|
+
$path = [regex]::Replace($b, '\{\{\s*([a-z_]+)\s*\}\}', { param($m) $subs[$m.Groups[1].Value] })
|
|
180
|
+
if (Test-Path (Join-Path $repo $path)) { $candidates += $path } else { $missing += $path }
|
|
181
|
+
# A LITERAL stack directory that is not one this repo declares. Reported, never
|
|
182
|
+
# dropped: the file exists and the project chose it, so it may be a deliberate
|
|
183
|
+
# cross-stack reference — but it is also exactly how a Java repo ends up being
|
|
184
|
+
# handed C# rules, and that must not be invisible.
|
|
185
|
+
$m2 = [regex]::Match($path, '^guardrails/technology/([^/]+)/')
|
|
186
|
+
if ($m2.Success -and $declared.Count -gt 0 -and ($declared -notcontains $m2.Groups[1].Value) -and ($b -notmatch '\{\{')) {
|
|
187
|
+
if ($mismatched -notcontains $m2.Groups[1].Value) { $mismatched += $m2.Groups[1].Value }
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
Write-Json ([ordered]@{
|
|
192
|
+
domain = $domain
|
|
193
|
+
techrulemap_exists = $techmapExists
|
|
194
|
+
candidates = @($candidates)
|
|
195
|
+
missing = @($missing)
|
|
196
|
+
skipped = @($skipped)
|
|
197
|
+
declared_stacks = @($declared | Sort-Object)
|
|
198
|
+
stack_mismatch = ($mismatched.Count -gt 0)
|
|
199
|
+
mismatched_stacks = @($mismatched)
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
# ─────────────────────────── derive-names ──────────────────────────────────
|
|
204
|
+
function ConvertTo-Pascal {
|
|
205
|
+
param([string]$In)
|
|
206
|
+
($In -replace '-', ' ').Split(' ') | Where-Object { $_ } |
|
|
207
|
+
ForEach-Object { $_.Substring(0,1).ToUpper() + $_.Substring(1) } | Join-String
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function Get-ShortName {
|
|
211
|
+
param([string]$Description)
|
|
212
|
+
$stop = 'i','a','an','the','to','for','of','in','on','at','by','with','from','is','are','was','were',
|
|
213
|
+
'be','been','being','have','has','had','do','does','did','will','would','should','could','can',
|
|
214
|
+
'may','might','must','shall','this','that','these','those','my','your','our','their','want','need','add','get','set'
|
|
215
|
+
$clean = ($Description.ToLower() -replace '[^a-z0-9]', ' ')
|
|
216
|
+
$words = @()
|
|
217
|
+
foreach ($w in ($clean.Split(' ') | Where-Object { $_ })) {
|
|
218
|
+
if (($stop -notcontains $w) -and $w.Length -ge 3) { $words += $w }
|
|
219
|
+
}
|
|
220
|
+
$max = if ($words.Count -eq 4) { 4 } else { 3 }
|
|
221
|
+
$result = ($words | Select-Object -First $max) -join '-'
|
|
222
|
+
if (-not $result) { $result = 'feature' }
|
|
223
|
+
return $result
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
# ─────────────────────────── scaffold ──────────────────────────────────────
|
|
228
|
+
function Invoke-Scaffold {
|
|
229
|
+
param([string[]]$Rest)
|
|
230
|
+
$featureDirArg = ''; $allowExisting = $false
|
|
231
|
+
for ($i = 0; $i -lt $Rest.Count; $i++) {
|
|
232
|
+
switch ($Rest[$i]) {
|
|
233
|
+
{ $_ -in '--feature-dir','-FeatureDir' } { $featureDirArg = $Rest[++$i] }
|
|
234
|
+
{ $_ -in '--allow-existing','-AllowExisting' } { $allowExisting = $true }
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (-not $featureDirArg) { Write-Error 'scaffold: --feature-dir is required'; exit 1 }
|
|
238
|
+
|
|
239
|
+
$repo = Get-RepoRoot
|
|
240
|
+
$featureDir = if ([System.IO.Path]::IsPathRooted($featureDirArg)) { $featureDirArg } else { Join-Path $repo $featureDirArg }
|
|
241
|
+
$specFile = Join-Path $featureDir 'spec.md'
|
|
242
|
+
|
|
243
|
+
$dirExisted = Test-Path $featureDir
|
|
244
|
+
if ($dirExisted -and -not $allowExisting) { Write-Error "scaffold: '$featureDir' already exists (use --allow-existing)"; exit 1 }
|
|
245
|
+
New-Item -ItemType Directory -Force -Path $featureDir | Out-Null
|
|
246
|
+
$created = -not $dirExisted
|
|
247
|
+
|
|
248
|
+
$template = $null; $specCreated = $false
|
|
249
|
+
if (-not (Test-Path $specFile)) {
|
|
250
|
+
$template = Resolve-SpecTemplate -RepoRoot $repo
|
|
251
|
+
if ($template -and (Test-Path $template)) { Copy-Item $template $specFile } else { New-Item -ItemType File -Force -Path $specFile | Out-Null }
|
|
252
|
+
$specCreated = $true
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
$featureJson = Join-Path $repo '.appweaver/feature.json'
|
|
256
|
+
New-Item -ItemType Directory -Force -Path (Join-Path $repo '.appweaver') | Out-Null
|
|
257
|
+
@{ feature_directory = (ConvertTo-RepoRelative -Path $featureDir -RepoRoot $repo) } |
|
|
258
|
+
ConvertTo-Json -Compress | Set-Content -Path $featureJson -Encoding UTF8
|
|
259
|
+
|
|
260
|
+
Write-Json ([ordered]@{
|
|
261
|
+
feature_dir = $featureDir
|
|
262
|
+
spec_file = $specFile
|
|
263
|
+
template = if ($template) { $template } else { '' }
|
|
264
|
+
created = $created
|
|
265
|
+
spec_created = $specCreated
|
|
266
|
+
})
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
# ─────────────────────────── resolve-hooks ─────────────────────────────────
|
|
270
|
+
# Native indentation parser for the known extensions.yml shape (no YAML module).
|
|
271
|
+
# Accepts multiple --phase; merges + dedupes by command.
|
|
272
|
+
function Get-HookIndent([string]$s) { return ($s.Length - $s.TrimStart(' ').Length) }
|
|
273
|
+
|
|
274
|
+
function Get-PhaseHooks {
|
|
275
|
+
param([string[]]$Lines, [string]$Phase)
|
|
276
|
+
$hooks = @(); $n = $Lines.Count; $i = 0
|
|
277
|
+
while ($i -lt $n) {
|
|
278
|
+
if ($Lines[$i].TrimEnd() -eq 'hooks:') {
|
|
279
|
+
$base = Get-HookIndent $Lines[$i]; $i++
|
|
280
|
+
while ($i -lt $n -and ($Lines[$i].Trim() -eq '' -or (Get-HookIndent $Lines[$i]) -gt $base)) {
|
|
281
|
+
$st0 = $Lines[$i].Trim()
|
|
282
|
+
if ($st0.TrimEnd(':') -eq $Phase -and $st0.EndsWith(':') -and -not $st0.StartsWith('- ')) {
|
|
283
|
+
$phInd = Get-HookIndent $Lines[$i]; $i++
|
|
284
|
+
$cur = $null
|
|
285
|
+
while ($i -lt $n) {
|
|
286
|
+
$l = $Lines[$i]
|
|
287
|
+
if ($l.Trim() -eq '') { $i++; continue }
|
|
288
|
+
$ind = Get-HookIndent $l; $st = $l.Trim()
|
|
289
|
+
if ($ind -lt $phInd) { break }
|
|
290
|
+
if ($ind -eq $phInd -and $st.EndsWith(':') -and -not $st.StartsWith('- ')) { break }
|
|
291
|
+
if ($st.StartsWith('- ')) {
|
|
292
|
+
if ($null -ne $cur) { $hooks += ,$cur }
|
|
293
|
+
$cur = @{}; $st = $st.Substring(2)
|
|
294
|
+
}
|
|
295
|
+
if ($null -ne $cur -and $st.Contains(':') -and -not $st.StartsWith('- ')) {
|
|
296
|
+
$k = $st.Substring(0, $st.IndexOf(':')).Trim()
|
|
297
|
+
$v = $st.Substring($st.IndexOf(':') + 1).Trim()
|
|
298
|
+
$cur[$k] = $v
|
|
299
|
+
}
|
|
300
|
+
$i++
|
|
301
|
+
}
|
|
302
|
+
if ($null -ne $cur) { $hooks += ,$cur }
|
|
303
|
+
break
|
|
304
|
+
} else { $i++ }
|
|
305
|
+
}
|
|
306
|
+
break
|
|
307
|
+
}
|
|
308
|
+
$i++
|
|
309
|
+
}
|
|
310
|
+
return ,$hooks
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function Invoke-ResolveHooks {
|
|
314
|
+
param([string[]]$Rest)
|
|
315
|
+
$phases = @()
|
|
316
|
+
for ($i = 0; $i -lt $Rest.Count; $i++) {
|
|
317
|
+
if ($Rest[$i] -in '--phase','-Phase') { $phases += $Rest[++$i] }
|
|
318
|
+
}
|
|
319
|
+
if ($phases.Count -eq 0) { Write-Error 'resolve-hooks: at least one --phase is required'; exit 1 }
|
|
320
|
+
|
|
321
|
+
$repo = Get-RepoRoot
|
|
322
|
+
$ext = Join-Path $repo '.appweaver/extensions.yml'
|
|
323
|
+
if (-not (Test-Path $ext)) { Write-Json ([ordered]@{ phases = @($phases); hooks = @() }); return }
|
|
324
|
+
$lines = Get-Content $ext
|
|
325
|
+
|
|
326
|
+
$out = @(); $seen = @{}
|
|
327
|
+
foreach ($phase in $phases) {
|
|
328
|
+
foreach ($h in (Get-PhaseHooks -Lines $lines -Phase $phase)) {
|
|
329
|
+
if ("$($h['enabled'])".ToLower() -eq 'false') { continue }
|
|
330
|
+
$cond = $h['condition']
|
|
331
|
+
if ($null -ne $cond -and "$cond".Trim().ToLower() -notin @('', 'null', '~')) { continue }
|
|
332
|
+
$cmd = [string]$h['command']
|
|
333
|
+
if ($seen.ContainsKey($cmd)) { continue }
|
|
334
|
+
$seen[$cmd] = $true
|
|
335
|
+
$out += [ordered]@{
|
|
336
|
+
extension = [string]$h['extension']
|
|
337
|
+
command = $cmd
|
|
338
|
+
slash = if ($cmd) { '/' + ($cmd -replace '\.', '-') } else { '' }
|
|
339
|
+
optional = ("$($h['optional'])".ToLower() -eq 'true')
|
|
340
|
+
description = [string]$h['description']
|
|
341
|
+
prompt = [string]$h['prompt']
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
Write-Json ([ordered]@{ phases = @($phases); hooks = @($out) })
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
# ─────────────────────────── create-checklist ──────────────────────────────
|
|
349
|
+
function Invoke-CreateChecklist {
|
|
350
|
+
param([string[]]$Rest)
|
|
351
|
+
$featureDirArg = ''; $featureName = ''; $kind = 'master'; $changeDirArg = ''
|
|
352
|
+
for ($i = 0; $i -lt $Rest.Count; $i++) {
|
|
353
|
+
switch ($Rest[$i]) {
|
|
354
|
+
{ $_ -in '--feature-dir','-FeatureDir' } { $featureDirArg = $Rest[++$i] }
|
|
355
|
+
{ $_ -in '--feature-name','-FeatureName' } { $featureName = $Rest[++$i] }
|
|
356
|
+
{ $_ -in '--kind','-Kind' } { $kind = $Rest[++$i] }
|
|
357
|
+
{ $_ -in '--change-dir','-ChangeDir' } { $changeDirArg = $Rest[++$i] }
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
if (-not $featureDirArg) { Write-Error 'create-checklist: --feature-dir is required'; exit 1 }
|
|
361
|
+
if ($kind -notin 'master','delta') { Write-Error "create-checklist: --kind must be 'master' or 'delta' (got '$kind')"; exit 1 }
|
|
362
|
+
|
|
363
|
+
$repo = Get-RepoRoot
|
|
364
|
+
$featureDir = if ([System.IO.Path]::IsPathRooted($featureDirArg)) { $featureDirArg } else { Join-Path $repo $featureDirArg }
|
|
365
|
+
if (-not $featureName) { $featureName = Split-Path $featureDir -Leaf }
|
|
366
|
+
|
|
367
|
+
# WHERE the checklist lives and WHAT it grades are decided together here, and both
|
|
368
|
+
# are reported back, so a caller binds the pair from ONE call and can never point a
|
|
369
|
+
# checklist at the wrong document. master -> the feature's spec.md; delta -> the
|
|
370
|
+
# change's delta-spec.md (checklist sits beside it, inside changes/<cr-id>/).
|
|
371
|
+
$crId = ''
|
|
372
|
+
if ($kind -eq 'delta') {
|
|
373
|
+
if (-not $changeDirArg) { Write-Error 'create-checklist: --change-dir is required with --kind delta'; exit 1 }
|
|
374
|
+
$changeDir = if ([System.IO.Path]::IsPathRooted($changeDirArg)) { $changeDirArg } else { Join-Path $repo $changeDirArg }
|
|
375
|
+
$crId = Split-Path $changeDir -Leaf
|
|
376
|
+
$checklistDir = Join-Path $changeDir 'checklists'
|
|
377
|
+
$targetFile = Join-Path $changeDir 'delta-spec.md'
|
|
378
|
+
} else {
|
|
379
|
+
$checklistDir = Join-Path $featureDir 'checklists'
|
|
380
|
+
$targetFile = Join-Path $featureDir 'spec.md'
|
|
381
|
+
}
|
|
382
|
+
$checklistFile = Join-Path $checklistDir 'requirements.md'
|
|
383
|
+
New-Item -ItemType Directory -Force -Path $checklistDir | Out-Null
|
|
384
|
+
$created = -not (Test-Path $checklistFile)
|
|
385
|
+
$today = Get-Date -Format 'yyyy-MM-dd'
|
|
386
|
+
|
|
387
|
+
$content = if ($kind -eq 'delta') { @"
|
|
388
|
+
# Delta-Spec Quality Checklist: $featureName — $crId
|
|
389
|
+
|
|
390
|
+
**Purpose**: Validate the CHANGE's delta-spec before proceeding to design
|
|
391
|
+
**Created**: $today
|
|
392
|
+
**Change**: [delta-spec.md](../delta-spec.md)
|
|
393
|
+
|
|
394
|
+
## Content Quality
|
|
395
|
+
|
|
396
|
+
- [ ] No implementation mechanics (languages, frameworks, code/adapter design; interop obligations named as contracts are fine)
|
|
397
|
+
- [ ] Focused on user value and business needs
|
|
398
|
+
- [ ] Written for non-technical stakeholders
|
|
399
|
+
- [ ] Change summary states what is changing and why
|
|
400
|
+
|
|
401
|
+
## Delta Completeness
|
|
402
|
+
|
|
403
|
+
- [ ] Every item sits under ADDED, MODIFIED or REMOVED (an empty section states None)
|
|
404
|
+
- [ ] MODIFIED items state both sides (was -> now)
|
|
405
|
+
- [ ] New behaviour is a user story (US-n) with priority, Given/When/Then acceptance, and an independent test
|
|
406
|
+
- [ ] New or changed system requirements keep EARS form
|
|
407
|
+
- [ ] Requirements are testable and unambiguous
|
|
408
|
+
- [ ] No [NEEDS CLARIFICATION] markers remain
|
|
409
|
+
|
|
410
|
+
## Domain Lift (Constraints / Classification / Contracts)
|
|
411
|
+
|
|
412
|
+
- [ ] Constraint / NFR changes are captured as CN- items, or the section states None
|
|
413
|
+
- [ ] Data sensitivity or exposure changes are captured as DC- items, or the section states None
|
|
414
|
+
- [ ] External interoperability changes are captured as IC- items at contract level, or the section states None
|
|
415
|
+
|
|
416
|
+
## Experience Lift (UI / UX standards)
|
|
417
|
+
|
|
418
|
+
- [ ] Experience-standard changes are captured as UX- items, or the section states None
|
|
419
|
+
- [ ] Where UX- deltas exist, each states a project-wide standard checkable on any surface — no screen, page, layout, navigation or component
|
|
420
|
+
- [ ] Where UX- deltas exist, a change to THIS aggregate's own interaction behaviour is an FR- delta, not a UX- one
|
|
421
|
+
- [ ] Where UX- deltas exist, a bound the service must meet stays a CN-, not a UX-
|
|
422
|
+
|
|
423
|
+
## Merge Readiness
|
|
424
|
+
|
|
425
|
+
- [ ] Header is complete: aggregate, change type, base version
|
|
426
|
+
- [ ] Impact lists the affected ids (FR / BR / CN / DC / IC / UX / US / SC)
|
|
427
|
+
- [ ] Regression risk names the existing behaviour that must still hold
|
|
428
|
+
- [ ] Scoped to THIS change — the unchanged master is neither restated here nor edited
|
|
429
|
+
|
|
430
|
+
## Notes
|
|
431
|
+
|
|
432
|
+
- This checklist grades the DELTA DOCUMENT only. Accuracy against the master spec
|
|
433
|
+
(id continuity, conflicts with existing requirements) is checked downstream by
|
|
434
|
+
/appweaver analyze-delta-design.
|
|
435
|
+
- Items marked incomplete require delta-spec updates before /appweaver design
|
|
436
|
+
"@ } else { @"
|
|
437
|
+
# Specification Quality Checklist: $featureName
|
|
438
|
+
|
|
439
|
+
**Purpose**: Validate specification completeness and quality before proceeding to design
|
|
440
|
+
**Created**: $today
|
|
441
|
+
**Feature**: [spec.md](../spec.md)
|
|
442
|
+
|
|
443
|
+
## Content Quality
|
|
444
|
+
|
|
445
|
+
- [ ] No implementation mechanics (languages, frameworks, code/adapter design; interop obligations named as contracts are fine)
|
|
446
|
+
- [ ] Focused on user value and business needs
|
|
447
|
+
- [ ] Written for non-technical stakeholders
|
|
448
|
+
- [ ] All mandatory sections completed
|
|
449
|
+
|
|
450
|
+
## Requirement Completeness
|
|
451
|
+
|
|
452
|
+
- [ ] No [NEEDS CLARIFICATION] markers remain
|
|
453
|
+
- [ ] Requirements are testable and unambiguous
|
|
454
|
+
- [ ] Success criteria are measurable
|
|
455
|
+
- [ ] Success criteria are technology-agnostic (no implementation details)
|
|
456
|
+
- [ ] All acceptance scenarios are defined
|
|
457
|
+
- [ ] Edge cases are identified
|
|
458
|
+
- [ ] Scope is clearly bounded
|
|
459
|
+
- [ ] Dependencies and assumptions identified
|
|
460
|
+
|
|
461
|
+
## Domain Lift (Constraints / Classification / Contracts)
|
|
462
|
+
|
|
463
|
+
- [ ] Domain-derived constraints are captured in Constraints & NFRs (CN-), or the section states None
|
|
464
|
+
- [ ] Data sensitivity and exposure rules are captured in Data Classification (DC-), or the section states None
|
|
465
|
+
- [ ] External interoperability obligations are captured as Integration Contracts (IC-) at contract level, or the section states None
|
|
466
|
+
|
|
467
|
+
## Experience Lift (UI / UX standards)
|
|
468
|
+
|
|
469
|
+
- [ ] Experience Scope states the actors, channels and device / browser / assistive-technology floors, or states None
|
|
470
|
+
- [ ] Project-wide experience standards are captured as UX- items, or the section states None
|
|
471
|
+
- [ ] Where UX- items exist, each is checkable on any surface — no screen, page, layout, navigation or component appears
|
|
472
|
+
- [ ] Where UX- items exist, none of them is a per-feature interaction behaviour — those stay FR-
|
|
473
|
+
- [ ] Where UX- items exist, a bound the SERVICE must meet is a CN-, cross-referenced where the obligation binds both lanes
|
|
474
|
+
|
|
475
|
+
## Feature Readiness
|
|
476
|
+
|
|
477
|
+
- [ ] All functional requirements have clear acceptance criteria
|
|
478
|
+
- [ ] User scenarios cover primary flows
|
|
479
|
+
- [ ] Feature meets measurable outcomes defined in Success Criteria
|
|
480
|
+
- [ ] No implementation details leak into specification
|
|
481
|
+
|
|
482
|
+
## Notes
|
|
483
|
+
|
|
484
|
+
- Items marked incomplete require spec updates before /appweaver design
|
|
485
|
+
"@ }
|
|
486
|
+
Set-Content -Path $checklistFile -Value $content -Encoding UTF8
|
|
487
|
+
Write-Json ([ordered]@{ checklist_file = $checklistFile; kind = $kind; target = $targetFile; created = $created })
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
# ─────────────────────────── list-domains ──────────────────────────────────
|
|
491
|
+
# The selectable projects: immediate subdirs of guardrails/projects/ that carry a rule
|
|
492
|
+
# map. The map is what makes a directory a project, exactly as a *.pack.txt is what makes
|
|
493
|
+
# a foundations/ directory a foundation — without that test any stray sibling would be
|
|
494
|
+
# offered as a governance target. EITHER map qualifies, because onboard writes the domain
|
|
495
|
+
# map several steps before the technical one and a half-built project must stay visible.
|
|
496
|
+
function Invoke-ListDomains {
|
|
497
|
+
param([string[]]$Rest)
|
|
498
|
+
$repo = Get-RepoRoot
|
|
499
|
+
$base = Join-Path $repo 'guardrails/projects'
|
|
500
|
+
$domains = @()
|
|
501
|
+
if (Test-Path $base) {
|
|
502
|
+
$domains = @(Get-ChildItem -Directory $base -ErrorAction SilentlyContinue | Where-Object {
|
|
503
|
+
(Test-Path (Join-Path $_.FullName 'tech-rule-bindings.md')) -or
|
|
504
|
+
(Test-Path (Join-Path $_.FullName 'domain-rule-bindings.md'))
|
|
505
|
+
} | ForEach-Object { $_.Name })
|
|
506
|
+
}
|
|
507
|
+
Write-Json ([ordered]@{ domains = @($domains) })
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
# ─────────────────────────── set-domain ────────────────────────────────────
|
|
511
|
+
function Invoke-SetDomain {
|
|
512
|
+
param([string[]]$Rest)
|
|
513
|
+
$domain = ''
|
|
514
|
+
for ($i = 0; $i -lt $Rest.Count; $i++) {
|
|
515
|
+
if ($Rest[$i] -in '--domain','-Domain') { $domain = $Rest[++$i] }
|
|
516
|
+
}
|
|
517
|
+
if (-not $domain) { Write-Error 'set-domain: --domain is required'; exit 1 }
|
|
518
|
+
|
|
519
|
+
$repo = Get-RepoRoot
|
|
520
|
+
$mapRel = "guardrails/projects/$domain/domain-rule-bindings.md"
|
|
521
|
+
$mapExists = Test-Path (Join-Path $repo $mapRel)
|
|
522
|
+
|
|
523
|
+
New-Item -ItemType Directory -Force -Path (Join-Path $repo '.appweaver') | Out-Null
|
|
524
|
+
@{ domain = $domain } | ConvertTo-Json -Compress |
|
|
525
|
+
Set-Content -Path (Join-Path $repo '.appweaver/domain.json') -Encoding UTF8
|
|
526
|
+
|
|
527
|
+
Write-Json ([ordered]@{
|
|
528
|
+
domain = $domain
|
|
529
|
+
map_path = $mapRel
|
|
530
|
+
map_exists = $mapExists
|
|
531
|
+
written = $true
|
|
532
|
+
})
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
# ────── stack-list / stack-detect / lane-declare / lane-resolve / lane-probe ──────
|
|
536
|
+
# The lane model. Twins of the bash cmd_stack_list / cmd_stack_detect /
|
|
537
|
+
# cmd_lane_declare / cmd_lane_resolve / cmd_lane_probe: both shells delegate to the
|
|
538
|
+
# SAME python core, so the JSON contract has one implementation and the pair cannot
|
|
539
|
+
# drift.
|
|
540
|
+
#
|
|
541
|
+
# lane-declare is the SOLE WRITER of .appweaver/stacks.json, with two callers:
|
|
542
|
+
# aw-spec-select-stack, where a person picks, and aw-spec-onboard, which declares
|
|
543
|
+
# what it reverse-engineered (those lanes carry origin: reverse-engineered, and
|
|
544
|
+
# onboard never overwrites a lane that is already declared). stack-detect proposes
|
|
545
|
+
# candidates and writes nothing. lane-resolve can never write — which is why these
|
|
546
|
+
# are separate subcommands rather than one with a mode flag.
|
|
547
|
+
function Invoke-AwStackCore {
|
|
548
|
+
param([string]$Op, [string[]]$Rest)
|
|
549
|
+
$py = Get-Py
|
|
550
|
+
if (-not $py) { Write-Error 'aw-stack: python3 is required for the lane model'; exit 1 }
|
|
551
|
+
$core = Join-Path $PSScriptRoot '..' 'python' 'aw-stack.py'
|
|
552
|
+
if (-not (Test-Path $core)) { Write-Error "aw-stack: core not found at $core"; exit 1 }
|
|
553
|
+
$repo = Get-RepoRoot
|
|
554
|
+
& $py $core '--repo' $repo $Op @Rest
|
|
555
|
+
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function Invoke-StackList {
|
|
559
|
+
param([string[]]$Rest)
|
|
560
|
+
$fwd = @()
|
|
561
|
+
for ($i = 0; $i -lt $Rest.Count; $i++) {
|
|
562
|
+
switch ($Rest[$i]) {
|
|
563
|
+
'--kind' { $fwd += '--kind'; $fwd += $Rest[++$i] }
|
|
564
|
+
'--preselect' { $fwd += '--preselect'; $fwd += $Rest[++$i] }
|
|
565
|
+
'--include-none' { $fwd += '--include-none' }
|
|
566
|
+
'--suggest' { $fwd += '--suggest' }
|
|
567
|
+
default { Write-Error "stack-list: unknown arg '$($Rest[$i])'"; exit 1 }
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
Invoke-AwStackCore -Op 'list' -Rest $fwd
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function Invoke-StackDetect {
|
|
574
|
+
param([string[]]$Rest)
|
|
575
|
+
$fwd = @()
|
|
576
|
+
for ($i = 0; $i -lt $Rest.Count; $i++) {
|
|
577
|
+
switch ($Rest[$i]) {
|
|
578
|
+
'--kind' { $fwd += '--kind'; $fwd += $Rest[++$i] }
|
|
579
|
+
'--exclude-root' { $fwd += '--exclude-root'; $fwd += $Rest[++$i] }
|
|
580
|
+
default { Write-Error "stack-detect: unknown arg '$($Rest[$i])'"; exit 1 }
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
Invoke-AwStackCore -Op 'detect' -Rest $fwd
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function Invoke-LaneDeclare {
|
|
587
|
+
param([string[]]$Rest)
|
|
588
|
+
$fwd = @()
|
|
589
|
+
for ($i = 0; $i -lt $Rest.Count; $i++) {
|
|
590
|
+
switch ($Rest[$i]) {
|
|
591
|
+
# --source-path is the same field as --root; both spellings reach the core.
|
|
592
|
+
{ $_ -in '--profile','--kind','--id','--root','--source-path','--stack','--origin','--build','--test','--run','--prototype-root','--prototype-run' } {
|
|
593
|
+
$fwd += $Rest[$i]; $fwd += $Rest[++$i]
|
|
594
|
+
}
|
|
595
|
+
'--no-prototype' { $fwd += '--no-prototype' }
|
|
596
|
+
'--require-existing-root' { $fwd += '--require-existing-root' }
|
|
597
|
+
default { Write-Error "lane-declare: unknown arg '$($Rest[$i])'"; exit 1 }
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
Invoke-AwStackCore -Op 'declare' -Rest $fwd
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function Invoke-LaneResolve {
|
|
604
|
+
param([string[]]$Rest)
|
|
605
|
+
$fwd = @()
|
|
606
|
+
for ($i = 0; $i -lt $Rest.Count; $i++) {
|
|
607
|
+
switch ($Rest[$i]) {
|
|
608
|
+
{ $_ -in '--kind','--id' } { $fwd += $Rest[$i]; $fwd += $Rest[++$i] }
|
|
609
|
+
default { Write-Error "lane-resolve: unknown arg '$($Rest[$i])'"; exit 1 }
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
Invoke-AwStackCore -Op 'resolve' -Rest $fwd
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
# The gate: is the declaration USABLE for the phase about to run. Twin of bash
|
|
616
|
+
# cmd_lane_verify. --require names a lane kind the phase cannot run without;
|
|
617
|
+
# --allow-missing-root is the greenfield state, where a source path is declared
|
|
618
|
+
# before implement creates it.
|
|
619
|
+
function Invoke-LaneVerify {
|
|
620
|
+
param([string[]]$Rest)
|
|
621
|
+
$fwd = @()
|
|
622
|
+
for ($i = 0; $i -lt $Rest.Count; $i++) {
|
|
623
|
+
switch ($Rest[$i]) {
|
|
624
|
+
'--require' { $fwd += '--require'; $fwd += $Rest[++$i] }
|
|
625
|
+
# --flag=value: --command carries a phase name that starts with '/'.
|
|
626
|
+
'--command' { $fwd += ('--command=' + $Rest[++$i]) }
|
|
627
|
+
'--allow-missing-root' { $fwd += '--allow-missing-root' }
|
|
628
|
+
default { Write-Error "lane-verify: unknown arg '$($Rest[$i])'"; exit 1 }
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
Invoke-AwStackCore -Op 'verify' -Rest $fwd
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
# Evidence that a lane of this kind exists, from the FILE signals only: a lane in
|
|
635
|
+
# stacks.json, and a stack name appearing in free prose. The authoritative signal
|
|
636
|
+
# — whether the active project's technical map carries `## /aw-spec-<command>` for the
|
|
637
|
+
# lane's phases — is resolve-tech's, and callers OR the two together.
|
|
638
|
+
function Invoke-LaneProbe {
|
|
639
|
+
param([string[]]$Rest)
|
|
640
|
+
$fwd = @()
|
|
641
|
+
for ($i = 0; $i -lt $Rest.Count; $i++) {
|
|
642
|
+
switch ($Rest[$i]) {
|
|
643
|
+
# --flag=value, not two tokens: --hint carries raw user prose, and a value
|
|
644
|
+
# that starts with '-' reads as another flag when passed separately.
|
|
645
|
+
{ $_ -in '--kind','--hint' } { $fwd += ($Rest[$i] + '=' + $Rest[++$i]) }
|
|
646
|
+
default { Write-Error "lane-probe: unknown arg '$($Rest[$i])'"; exit 1 }
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
Invoke-AwStackCore -Op 'probe' -Rest $fwd
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
# ─────────────────────────── list-features ─────────────────────────────────
|
|
653
|
+
# Twin of bash cmd_list_features: MERGE two feature sources, deduped by name
|
|
654
|
+
# (case-insensitive) — (1) specs/ sub-folders (already scaffolded/adopted) and
|
|
655
|
+
# (2) the OKF bundle's Aggregate concepts (the brownfield feature map, resolved by
|
|
656
|
+
# the SAME shared python core the bash twin calls). Empty source 2 on greenfield.
|
|
657
|
+
function Invoke-ListFeatures {
|
|
658
|
+
param([string[]]$Rest)
|
|
659
|
+
$preselect = ''; $bundle = ''
|
|
660
|
+
for ($i = 0; $i -lt $Rest.Count; $i++) {
|
|
661
|
+
if ($Rest[$i] -in '--preselect','-Preselect') { $preselect = $Rest[++$i] }
|
|
662
|
+
elseif ($Rest[$i] -in '--bundle','-Bundle') { $bundle = $Rest[++$i] }
|
|
663
|
+
}
|
|
664
|
+
$repo = Get-RepoRoot
|
|
665
|
+
$base = Join-Path $repo 'specs'
|
|
666
|
+
$sentinel = '+ Create a new feature'
|
|
667
|
+
if (-not $bundle) { $bundle = 'knowledge' }
|
|
668
|
+
if (-not [System.IO.Path]::IsPathRooted($bundle)) { $bundle = Join-Path $repo $bundle }
|
|
669
|
+
|
|
670
|
+
# Best-effort OKF aggregate list (brownfield feature map). Empty on greenfield.
|
|
671
|
+
$okfAggs = @()
|
|
672
|
+
$py = Get-Py; $invpy = Join-Path $PSScriptRoot '..' 'python' 'aw-inventory.py'
|
|
673
|
+
if ($py -and (Test-Path $bundle) -and (Test-Path $invpy)) {
|
|
674
|
+
try {
|
|
675
|
+
$okfRaw = & $py $invpy list-aggregates --bundle $bundle 2>$null
|
|
676
|
+
if ($okfRaw) { $okfAggs = @((($okfRaw | ConvertFrom-Json).aggregates)) }
|
|
677
|
+
} catch { $okfAggs = @() }
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
$features = @(); $names = @(); $seen = @{}
|
|
681
|
+
if (Test-Path $base) {
|
|
682
|
+
foreach ($d in (Get-ChildItem -Directory $base -ErrorAction SilentlyContinue | Sort-Object Name)) {
|
|
683
|
+
$ver = ''; $activeChange = ''
|
|
684
|
+
$avf = Join-Path $d.FullName 'active_version.json'
|
|
685
|
+
if (Test-Path $avf) { try { $av = Get-Content $avf -Raw | ConvertFrom-Json; $ver = "$($av.version)"; $activeChange = "$($av.active_change)" } catch { $ver = '' } }
|
|
686
|
+
$names += $d.Name; $seen[$d.Name.ToLower()] = $true
|
|
687
|
+
$features += [ordered]@{
|
|
688
|
+
name = $d.Name
|
|
689
|
+
dir = "specs/$($d.Name)"
|
|
690
|
+
source = 'spec'
|
|
691
|
+
has_spec = (Test-Path (Join-Path $d.FullName 'spec.md'))
|
|
692
|
+
# 'design*.md', NOT 'design.*.md': the second dot made this miss the single
|
|
693
|
+
# `design.md` the framework actually produces (it only matched the retired
|
|
694
|
+
# design.domain.md / design.technical.md family). Informational count only.
|
|
695
|
+
# -like, not -Filter: -Filter delegates to the platform's filesystem matcher
|
|
696
|
+
# (which on Windows also honours 8.3 short names), so it is not guaranteed to
|
|
697
|
+
# agree with the bash twin's glob. -like is PowerShell's own engine — same
|
|
698
|
+
# result on every OS, which is what keeps the two scripts provably in parity.
|
|
699
|
+
design_docs = @(Get-ChildItem -Path $d.FullName -File -ErrorAction SilentlyContinue |
|
|
700
|
+
Where-Object { $_.Name -like 'design*.md' }).Count
|
|
701
|
+
version = $ver
|
|
702
|
+
active_change = $activeChange
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
# Merge OKF-discovered aggregates not already scaffolded under specs/.
|
|
707
|
+
foreach ($agg in $okfAggs) {
|
|
708
|
+
$nm = "$($agg.name)".Trim()
|
|
709
|
+
if (-not $nm -or $seen.ContainsKey($nm.ToLower())) { continue }
|
|
710
|
+
$names += $nm; $seen[$nm.ToLower()] = $true
|
|
711
|
+
$features += [ordered]@{
|
|
712
|
+
name = $nm
|
|
713
|
+
dir = "specs/$nm"
|
|
714
|
+
source = 'okf'
|
|
715
|
+
has_spec = $false
|
|
716
|
+
adopted = $false
|
|
717
|
+
concept_path = "$($agg.concept_path)"
|
|
718
|
+
design_docs = 0
|
|
719
|
+
version = ''
|
|
720
|
+
active_change = ''
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
# select_options = merged names + (injected new --preselect) + sentinel.
|
|
724
|
+
$opts = @($names)
|
|
725
|
+
if ($preselect -and (-not $seen.ContainsKey($preselect.ToLower()))) { $opts += $preselect }
|
|
726
|
+
$opts += $sentinel
|
|
727
|
+
Write-Json ([ordered]@{
|
|
728
|
+
features = @($features)
|
|
729
|
+
names = @($names)
|
|
730
|
+
count = $names.Count
|
|
731
|
+
select_options = @($opts)
|
|
732
|
+
sentinel = $sentinel
|
|
733
|
+
})
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
# ─────────────────────────── set-feature ───────────────────────────────────
|
|
737
|
+
function Invoke-SetFeature {
|
|
738
|
+
param([string[]]$Rest)
|
|
739
|
+
$raw = ''
|
|
740
|
+
for ($i = 0; $i -lt $Rest.Count; $i++) {
|
|
741
|
+
if ($Rest[$i] -in '--feature','-Feature') { $raw = $Rest[++$i] }
|
|
742
|
+
}
|
|
743
|
+
if (-not $raw) { Write-Error 'set-feature: --feature is required'; exit 1 }
|
|
744
|
+
|
|
745
|
+
# Normalize to a PascalCase aggregate identifier (feature = aggregate).
|
|
746
|
+
$cleaned = ($raw -replace '[^A-Za-z0-9]+', ' ').Trim()
|
|
747
|
+
$feature = ConvertTo-Pascal -In $cleaned
|
|
748
|
+
if ($feature -notmatch '^[A-Za-z][A-Za-z0-9]*$') {
|
|
749
|
+
Write-Error "set-feature: '$raw' is not a valid feature name (need a letter-led alphanumeric aggregate, e.g. Order)"; exit 1
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
$repo = Get-RepoRoot
|
|
753
|
+
$featureDirRel = "specs/$feature"
|
|
754
|
+
$featureDir = Join-Path $repo $featureDirRel
|
|
755
|
+
$existed = Test-Path $featureDir
|
|
756
|
+
New-Item -ItemType Directory -Force -Path $featureDir | Out-Null
|
|
757
|
+
New-Item -ItemType Directory -Force -Path (Join-Path $repo '.appweaver') | Out-Null
|
|
758
|
+
@{ feature_directory = $featureDirRel } | ConvertTo-Json -Compress |
|
|
759
|
+
Set-Content -Path (Join-Path $repo '.appweaver/feature.json') -Encoding UTF8
|
|
760
|
+
|
|
761
|
+
Write-Json ([ordered]@{
|
|
762
|
+
feature = $feature
|
|
763
|
+
feature_dir = $featureDirRel
|
|
764
|
+
spec_file = "$featureDirRel/spec.md"
|
|
765
|
+
existed = [bool]$existed
|
|
766
|
+
created = (-not $existed)
|
|
767
|
+
spec_exists = (Test-Path (Join-Path $featureDir 'spec.md'))
|
|
768
|
+
})
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
# ═══════════════════════════ active_version.json ════════════════════════════
|
|
772
|
+
# Per-feature version state. Shells out to the SAME python as the bash twin so the
|
|
773
|
+
# bump/timestamp logic is byte-identical across shells. (See bash _active_version.)
|
|
774
|
+
$PyActiveVersion = @'
|
|
775
|
+
import json, os, re, datetime, sys
|
|
776
|
+
fd = os.environ["AW_FD"]; op = os.environ["AW_OP"]; cr = os.environ.get("AW_CR", "")
|
|
777
|
+
force = os.environ.get("AW_FORCE", "")
|
|
778
|
+
path = os.path.join(fd, "active_version.json")
|
|
779
|
+
feature = os.path.basename(fd.rstrip("/"))
|
|
780
|
+
def now(): return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
781
|
+
def load():
|
|
782
|
+
if os.path.isfile(path):
|
|
783
|
+
try: return json.load(open(path, encoding="utf-8")), True
|
|
784
|
+
except Exception: pass
|
|
785
|
+
return {"feature": feature, "version": "v1.0", "active_change": "", "updated": now()}, False
|
|
786
|
+
def save(d):
|
|
787
|
+
d["updated"] = now()
|
|
788
|
+
open(path, "w", encoding="utf-8").write(json.dumps(d, indent=2) + "\n")
|
|
789
|
+
def bump(v):
|
|
790
|
+
m = re.match(r'v?(\d+)(?:\.(\d+))?', (v or "v1.0").strip())
|
|
791
|
+
maj = int(m.group(1)) if m else 1
|
|
792
|
+
mnr = int(m.group(2)) if (m and m.group(2)) else 0
|
|
793
|
+
return "v%d.%d" % (maj, mnr + 1)
|
|
794
|
+
|
|
795
|
+
d, existed = load()
|
|
796
|
+
d.setdefault("feature", feature); d.setdefault("version", "v1.0"); d.setdefault("active_change", "")
|
|
797
|
+
out = {"feature": d["feature"], "version": d["version"], "active_change": d.get("active_change", ""),
|
|
798
|
+
"exists": existed, "path": "active_version.json", "ok": True}
|
|
799
|
+
if op == "read":
|
|
800
|
+
pass
|
|
801
|
+
elif op == "init":
|
|
802
|
+
if not existed: save(d)
|
|
803
|
+
out["created"] = (not existed)
|
|
804
|
+
elif op == "open":
|
|
805
|
+
cur = d.get("active_change", "")
|
|
806
|
+
if cur and cur != cr and force != "1": # a DIFFERENT change is already open -> refuse
|
|
807
|
+
out["ok"] = False
|
|
808
|
+
out["error"] = ("change '%s' is already open on this feature; finish it "
|
|
809
|
+
"(finalize-delta -> analyze-slice -> close-delta) before opening "
|
|
810
|
+
"'%s', or pass --force" % (cur, cr))
|
|
811
|
+
out["conflict"] = cur; out["requested"] = cr; out["has_change"] = True
|
|
812
|
+
print(json.dumps(out)); sys.exit(3)
|
|
813
|
+
d["active_change"] = cr; save(d)
|
|
814
|
+
out["active_change"] = cr; out["opened"] = cr
|
|
815
|
+
if cur and cur == cr: out["reopened"] = True # idempotent re-open of the same CR
|
|
816
|
+
elif op == "close":
|
|
817
|
+
prev = d["version"]; closed = d.get("active_change", "")
|
|
818
|
+
if closed: # a real change was open -> materialized -> bump
|
|
819
|
+
d["version"] = bump(prev)
|
|
820
|
+
d["active_change"] = ""; save(d)
|
|
821
|
+
out["version"] = d["version"]; out["active_change"] = ""
|
|
822
|
+
out["previous"] = prev; out["closed_change"] = closed
|
|
823
|
+
out["has_change"] = bool(out.get("active_change", "")) # clean branch signal for design
|
|
824
|
+
# What a close from THIS state would produce. One bump() authority, read by BOTH
|
|
825
|
+
# finalize-delta (to write the right number into the Change History rows it appends)
|
|
826
|
+
# and close-delta (which actually performs the bump and stamps the two headers), so
|
|
827
|
+
# the two stages can never disagree about the target. Equals `version` when no change
|
|
828
|
+
# is open, mirroring close's "only bump if a change was really open" guard.
|
|
829
|
+
out["next_version"] = bump(out["version"]) if out.get("active_change", "") else out["version"]
|
|
830
|
+
print(json.dumps(out))
|
|
831
|
+
'@
|
|
832
|
+
|
|
833
|
+
function Invoke-ActiveVersion {
|
|
834
|
+
param([string]$FeatureDir, [string]$Op, [string]$CrId = '', [string]$Force = '')
|
|
835
|
+
$py = Get-Py; if (-not $py) { Write-Error "version-$Op`: python3 required"; exit 1 }
|
|
836
|
+
$env:AW_FD = $FeatureDir; $env:AW_OP = $Op; $env:AW_CR = $CrId; $env:AW_FORCE = $Force
|
|
837
|
+
$PyActiveVersion | & $py - # sets $LASTEXITCODE=3 when op=open is refused (guard)
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
function Invoke-VersionState { # op in read|init (feature-dir only)
|
|
841
|
+
param([string]$Op, [string[]]$Rest)
|
|
842
|
+
$fdArg = ''
|
|
843
|
+
for ($i = 0; $i -lt $Rest.Count; $i++) { if ($Rest[$i] -in '--feature-dir','-FeatureDir') { $fdArg = $Rest[++$i] } }
|
|
844
|
+
$repo = Get-RepoRoot
|
|
845
|
+
$fd = Resolve-FeatureDir -Arg $fdArg -RepoRoot $repo
|
|
846
|
+
if (-not $fd) { Write-Error "version-$Op`: no feature set and no --feature-dir"; exit 1 }
|
|
847
|
+
Invoke-ActiveVersion -FeatureDir $fd -Op $Op
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
|
|
851
|
+
|
|
852
|
+
# ─────────────────────────── feature-paths ─────────────────────────────────
|
|
853
|
+
function Invoke-FeaturePaths {
|
|
854
|
+
param([string[]]$Rest)
|
|
855
|
+
$repo = Get-RepoRoot
|
|
856
|
+
$fj = Join-Path $repo '.appweaver/feature.json'
|
|
857
|
+
$fd = ''
|
|
858
|
+
if (Test-Path $fj) {
|
|
859
|
+
try { $fd = (Get-Content $fj -Raw | ConvertFrom-Json).feature_directory } catch { $fd = '' }
|
|
860
|
+
}
|
|
861
|
+
if (-not $fd) { Write-Error 'feature-paths: no feature set (run /appweaver select-feature first)'; exit 1 }
|
|
862
|
+
if (-not [System.IO.Path]::IsPathRooted($fd)) { $fd = Join-Path $repo $fd }
|
|
863
|
+
$feature = Split-Path $fd -Leaf # the aggregate name (feature = specs/<Feature>)
|
|
864
|
+
# The master documents of a feature: spec.md (WHAT/WHY) + design.md (the service
|
|
865
|
+
# lane's HOW) + design.ui.md (the frontend lane's HOW, where the feature has a UI
|
|
866
|
+
# half). plan.md / tasks.md belonged to the retired /aw-spec-plan + /aw-spec-tasks
|
|
867
|
+
# phases and had zero consumers.
|
|
868
|
+
#
|
|
869
|
+
# ui_design_exists IS THE UI-HALF SIGNAL for the whole CR loop. Every delta stage
|
|
870
|
+
# asks the same question — is there a UI master to keep in sync? — and the master's
|
|
871
|
+
# own presence answers it without re-deriving the lane declaration and the slice's
|
|
872
|
+
# UI file count at each stage. It is also self-correcting: whatever authors
|
|
873
|
+
# design.ui.md (adopt today) enrolls the feature in the UI half of the delta loop
|
|
874
|
+
# by the act of writing the file.
|
|
875
|
+
$spec = Join-Path $fd 'spec.md'
|
|
876
|
+
$design = Join-Path $fd 'design.md'
|
|
877
|
+
$uiDesign = Join-Path $fd 'design.ui.md'
|
|
878
|
+
Write-Json ([ordered]@{
|
|
879
|
+
feature = $feature
|
|
880
|
+
feature_dir = $fd
|
|
881
|
+
spec_file = $spec
|
|
882
|
+
design_file = $design
|
|
883
|
+
ui_design_file = $uiDesign
|
|
884
|
+
spec_exists = (Test-Path $spec)
|
|
885
|
+
design_exists = (Test-Path $design)
|
|
886
|
+
ui_design_exists = (Test-Path $uiDesign)
|
|
887
|
+
})
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
# ────────────────────────── checklist-read / -apply ────────────────────────
|
|
891
|
+
function Get-CheckboxLines {
|
|
892
|
+
param([string[]]$Lines)
|
|
893
|
+
$fence = $false; $n = 0; $items = @()
|
|
894
|
+
for ($i = 0; $i -lt $Lines.Count; $i++) {
|
|
895
|
+
$line = [string]$Lines[$i]
|
|
896
|
+
if ($line.Trim().StartsWith('```')) { $fence = -not $fence; continue }
|
|
897
|
+
if ($fence) { continue }
|
|
898
|
+
$m = [regex]::Match($line, '^\s*- \[([ xX])\]\s?(.*)$')
|
|
899
|
+
if ($m.Success) {
|
|
900
|
+
$n++
|
|
901
|
+
$items += [pscustomobject]@{
|
|
902
|
+
n = $n; index = $i
|
|
903
|
+
checked = ($m.Groups[1].Value -match '[xX]')
|
|
904
|
+
text = $m.Groups[2].Value.Trim()
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
return ,$items
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
function Invoke-ChecklistRead {
|
|
912
|
+
param([string[]]$Rest)
|
|
913
|
+
$file = ''
|
|
914
|
+
for ($i = 0; $i -lt $Rest.Count; $i++) { if ($Rest[$i] -eq '--file') { $file = $Rest[++$i] } }
|
|
915
|
+
if (-not $file) { Write-Error 'checklist-read: --file required'; exit 1 }
|
|
916
|
+
if (-not (Test-Path $file)) { Write-Json ([ordered]@{ exists = $false; total = 0; checked = 0; items = @() }); return }
|
|
917
|
+
$lines = Get-Content $file
|
|
918
|
+
$items = Get-CheckboxLines -Lines $lines
|
|
919
|
+
Write-Json ([ordered]@{
|
|
920
|
+
exists = $true; total = $items.Count
|
|
921
|
+
checked = @($items | Where-Object { $_.checked }).Count
|
|
922
|
+
items = @($items | ForEach-Object { [ordered]@{ n = $_.n; checked = $_.checked; text = $_.text } })
|
|
923
|
+
})
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
function Invoke-ChecklistApply {
|
|
927
|
+
param([string[]]$Rest)
|
|
928
|
+
$file = ''; $resultsRaw = '{}'
|
|
929
|
+
for ($i = 0; $i -lt $Rest.Count; $i++) {
|
|
930
|
+
switch ($Rest[$i]) { '--file' { $file = $Rest[++$i] } '--results' { $resultsRaw = $Rest[++$i] } }
|
|
931
|
+
}
|
|
932
|
+
if (-not $file) { Write-Error 'checklist-apply: --file required'; exit 1 }
|
|
933
|
+
if (-not (Test-Path $file)) { Write-Json ([ordered]@{ exists = $false; before_checked = 0; after_checked = 0; total = 0; newly_passing = @(); regressions = @() }); return }
|
|
934
|
+
$results = @{}
|
|
935
|
+
try { (ConvertFrom-Json $resultsRaw).PSObject.Properties | ForEach-Object { $results[[int]$_.Name] = [bool]$_.Value } } catch {}
|
|
936
|
+
$lines = Get-Content $file
|
|
937
|
+
$items = Get-CheckboxLines -Lines $lines
|
|
938
|
+
$before = @($items | Where-Object { $_.checked }).Count
|
|
939
|
+
$newly = @(); $regress = @(); $after = 0
|
|
940
|
+
foreach ($it in $items) {
|
|
941
|
+
$desired = if ($results.ContainsKey($it.n)) { $results[$it.n] } else { $it.checked }
|
|
942
|
+
if ($desired -ne $it.checked) { if ($desired) { $newly += $it.n } else { $regress += $it.n } }
|
|
943
|
+
if ($desired) { $after++ }
|
|
944
|
+
$marker = if ($desired) { 'x' } else { ' ' }
|
|
945
|
+
$lines[$it.index] = $lines[$it.index] -replace '^(\s*- \[)[ xX](\].*)$', "`$1$marker`$2"
|
|
946
|
+
}
|
|
947
|
+
Set-Content -Path $file -Value $lines -Encoding UTF8
|
|
948
|
+
Write-Json ([ordered]@{ exists = $true; before_checked = $before; after_checked = $after; total = $items.Count; newly_passing = @($newly); regressions = @($regress) })
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
|
|
952
|
+
|
|
953
|
+
# ─────────────────────────────── detect-project ────────────────────────────
|
|
954
|
+
# Project Structure Gate: is there a scaffolded app (not just framework files)?
|
|
955
|
+
#
|
|
956
|
+
# EVERY ecosystem's manifest is searched to the same depth. A project is where its
|
|
957
|
+
# manifest is, not where the repository root is: a Maven service copied into
|
|
958
|
+
# backend/orderservice/ is as scaffolded as a .sln at the top. Searching *.sln
|
|
959
|
+
# recursively while testing pom.xml and package.json only at the root reported
|
|
960
|
+
# every non-.NET layout that nests its project as no-project-at-all, which reads
|
|
961
|
+
# at the gate as "run /appweaver foundation first" — advice a stack with no
|
|
962
|
+
# foundation bundle cannot take.
|
|
963
|
+
#
|
|
964
|
+
# --root scopes the search to one lane's subtree: the directory the calling phase
|
|
965
|
+
# is about to write into, so the answer is about that lane's project rather than
|
|
966
|
+
# about any project anywhere. It falls back to the whole repository when the
|
|
967
|
+
# subtree holds no manifest of its own, which is the ordinary .NET layout — a
|
|
968
|
+
# lane rooted at src/ under a solution file that sits beside it.
|
|
969
|
+
function Get-ProjectMarkers {
|
|
970
|
+
param([string]$Repo, [string]$Start)
|
|
971
|
+
# Anchored at the top level for the framework directories and at any depth for
|
|
972
|
+
# node_modules, matching the twin's -path/-name split.
|
|
973
|
+
$topPrune = @('.appweaver', 'guardrails', 'foundations', 'specs', '.git')
|
|
974
|
+
$names = @('*.sln','*.csproj','package.json','pom.xml','build.gradle','build.gradle.kts','go.mod','Cargo.toml','pyproject.toml')
|
|
975
|
+
$out = @(
|
|
976
|
+
Get-ChildItem -Path $Start -Recurse -File -Include $names -ErrorAction SilentlyContinue |
|
|
977
|
+
ForEach-Object { $_.FullName.Substring($Repo.Length).TrimStart('\','/') -replace '\\','/' } |
|
|
978
|
+
Where-Object {
|
|
979
|
+
$seg = $_.Split('/')
|
|
980
|
+
(-not ($topPrune -contains $seg[0])) -and (-not ($seg -contains 'node_modules'))
|
|
981
|
+
} |
|
|
982
|
+
Sort-Object -Unique -CaseSensitive | Select-Object -First 20
|
|
983
|
+
)
|
|
984
|
+
# Returned unwrapped so the caller's += appends the paths rather than the array.
|
|
985
|
+
return $out
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
function Invoke-DetectProject {
|
|
989
|
+
param([string[]]$Rest)
|
|
990
|
+
$scopeRoot = ''
|
|
991
|
+
for ($i = 0; $i -lt $Rest.Count; $i++) {
|
|
992
|
+
switch ($Rest[$i]) {
|
|
993
|
+
{ $_ -in '--root','--source-path','-Root' } { $scopeRoot = $Rest[++$i] }
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
$repo = Get-RepoRoot
|
|
997
|
+
$scope = 'repo'
|
|
998
|
+
$start = $repo
|
|
999
|
+
# A lane's source path arrives as the value declare stored, so it may carry a
|
|
1000
|
+
# trailing slash; an option label ("path — why") never reaches a script argument.
|
|
1001
|
+
$rel = ($scopeRoot -replace '\\','/').TrimStart('.','/').TrimEnd('/')
|
|
1002
|
+
if ($rel -and (Test-Path -PathType Container (Join-Path $repo $rel))) {
|
|
1003
|
+
$start = (Join-Path $repo $rel); $scope = 'lane'
|
|
1004
|
+
}
|
|
1005
|
+
$markers = @(Get-ProjectMarkers -Repo $repo -Start $start)
|
|
1006
|
+
if ($markers.Count -eq 0 -and $scope -eq 'lane') {
|
|
1007
|
+
$scope = 'repo'
|
|
1008
|
+
$markers = @(Get-ProjectMarkers -Repo $repo -Start $repo)
|
|
1009
|
+
}
|
|
1010
|
+
Write-Json ([ordered]@{ scaffolded = ($markers.Count -gt 0); markers = @($markers); scope = $scope })
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
|
|
1014
|
+
# ───────────────────────── foundation resolution ───────────────────────────
|
|
1015
|
+
# Foundations live at foundations/<stack>/<Name>/, grouped by the technology stack
|
|
1016
|
+
# they scaffold. Names are unique across stacks, so callers pass a bare <Name> and
|
|
1017
|
+
# the lookup searches every stack. A stack also holds siblings that are not foundations
|
|
1018
|
+
# (components/), so carrying a *.pack.txt is what makes a directory one — without that
|
|
1019
|
+
# test they surface as bundle-less foundations in every lookup.
|
|
1020
|
+
function Get-FoundationDirs {
|
|
1021
|
+
param([string]$Repo)
|
|
1022
|
+
$root = Join-Path $Repo 'foundations'
|
|
1023
|
+
$out = @()
|
|
1024
|
+
if (Test-Path $root) {
|
|
1025
|
+
foreach ($s in (Get-ChildItem -Directory $root -ErrorAction SilentlyContinue)) {
|
|
1026
|
+
foreach ($d in (Get-ChildItem -Directory $s.FullName -ErrorAction SilentlyContinue)) {
|
|
1027
|
+
if (-not (Get-ChildItem -Path $d.FullName -Filter '*.pack.txt' -File -ErrorAction SilentlyContinue)) { continue }
|
|
1028
|
+
$out += [pscustomobject]@{ Name = $d.Name; Stack = $s.Name; Dir = $d.FullName; Rel = "foundations/$($s.Name)/$($d.Name)" }
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
# Returned unwrapped: call sites pipe this into Where-Object, and a ,$out wrapper
|
|
1033
|
+
# would hand the pipeline the array itself as one object instead of its elements.
|
|
1034
|
+
return $out
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
# ──────────────────────────── foundation-list ──────────────────────────────
|
|
1038
|
+
# List available foundations (foundations/<stack>/<Name>/ with a real <Name>.pack.txt)
|
|
1039
|
+
# for the user to PICK — deterministic, no LLM. Optional --args fast path mirrors bash.
|
|
1040
|
+
function Invoke-FoundationList {
|
|
1041
|
+
param([string[]]$Rest)
|
|
1042
|
+
$raw = ''
|
|
1043
|
+
for ($i = 0; $i -lt $Rest.Count; $i++) { switch ($Rest[$i]) { '--args' { $raw = $Rest[++$i] } } }
|
|
1044
|
+
$repo = Get-RepoRoot
|
|
1045
|
+
|
|
1046
|
+
# WHICH foundations this repo may be offered. A foundation bundle is a whole
|
|
1047
|
+
# compiling project in one ecosystem's languages and build system, so unpacking a
|
|
1048
|
+
# .NET one into a Spring repo does not produce a scaffold — it produces a second,
|
|
1049
|
+
# foreign project beside the real one. The declared lanes already say which
|
|
1050
|
+
# ecosystems this repo is, so filter to those and let the gate say why when that
|
|
1051
|
+
# leaves nothing. Nothing declared (a bare greenfield repo, before select-stack)
|
|
1052
|
+
# filters nothing: there is no answer yet to filter on.
|
|
1053
|
+
$laneEcos = @(Get-Ecosystems)
|
|
1054
|
+
$foundations = @(); $all = @(); $ecoOf = @()
|
|
1055
|
+
foreach ($f in @(Get-FoundationDirs $repo)) {
|
|
1056
|
+
if (-not (Test-Path (Join-Path $f.Dir "$($f.Name).pack.txt"))) { continue }
|
|
1057
|
+
$all += $f.Name; $ecoOf += $f.Stack
|
|
1058
|
+
if ($laneEcos.Count -eq 0 -or $laneEcos -contains $f.Stack) { $foundations += $f.Name }
|
|
1059
|
+
}
|
|
1060
|
+
$preName = ''; $preApp = ''; $outDir = '.'
|
|
1061
|
+
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] } }
|
|
1062
|
+
$match = if ($foundations -contains $preName) { $preName } else { '' }
|
|
1063
|
+
Write-Json ([ordered]@{
|
|
1064
|
+
foundations = @($foundations); count = $foundations.Count
|
|
1065
|
+
all_foundations = @($all); all_count = $all.Count
|
|
1066
|
+
ecosystems = @($ecoOf); lane_ecosystems = @($laneEcos)
|
|
1067
|
+
filtered = ($laneEcos.Count -gt 0)
|
|
1068
|
+
preselect_name = $match; preselect_app = $preApp; out_dir = $outDir
|
|
1069
|
+
})
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
function Invoke-FoundationLocate {
|
|
1073
|
+
param([string[]]$Rest)
|
|
1074
|
+
$name = ''; $app = ''; $out = '.'; $raw = ''
|
|
1075
|
+
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] } } }
|
|
1076
|
+
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] } }
|
|
1077
|
+
$repo = Get-RepoRoot
|
|
1078
|
+
$dirs = @(Get-FoundationDirs $repo)
|
|
1079
|
+
# available = every foundation dir (Get-FoundationDirs already requires a *.pack.txt)
|
|
1080
|
+
$available = @($dirs | ForEach-Object { $_.Name })
|
|
1081
|
+
function _fail($r) { Write-Json ([ordered]@{ ok = $false; reason = $r; available = @($available) }) }
|
|
1082
|
+
if (-not $name -or -not $app) { _fail 'Usage: /aw-spec-foundation <FoundationName> <AppName> [OutputDirectory]'; return }
|
|
1083
|
+
$found = $dirs | Where-Object { $_.Name -eq $name } | Select-Object -First 1
|
|
1084
|
+
if (-not $found) { _fail "Foundation '$name' not found (expected foundations/<stack>/$name/)"; return }
|
|
1085
|
+
$stack = $found.Stack; $rel = $found.Rel; $fdir = $found.Dir
|
|
1086
|
+
$pack = Join-Path $fdir "$name.pack.txt"
|
|
1087
|
+
if (-not (Test-Path $pack)) { _fail "Foundation '$name' has no bundle yet (expected $pack). Build it via the pack scripts in $rel/."; return }
|
|
1088
|
+
$runner = ''; $unpacker = ''
|
|
1089
|
+
if ((Get-Command pwsh -ErrorAction SilentlyContinue) -and (Test-Path (Join-Path $fdir 'unpack-foundation.ps1'))) { $runner = 'pwsh'; $unpacker = "$rel/unpack-foundation.ps1" }
|
|
1090
|
+
elseif (Test-Path (Join-Path $fdir 'unpack-foundation.sh')) { $runner = 'sh'; $unpacker = "$rel/unpack-foundation.sh" }
|
|
1091
|
+
else { _fail "No unpacker found in $rel/ (unpack-foundation.ps1 or .sh)"; return }
|
|
1092
|
+
$header = (Get-Content $pack -TotalCount 30 | Where-Object { $_ -like '#*' }) -join "`n"
|
|
1093
|
+
$sentinel = ''
|
|
1094
|
+
$sm = [regex]::Match($header, '(?i)sentinel[^A-Za-z0-9_.]*([A-Za-z0-9_.]+)')
|
|
1095
|
+
if ($sm.Success) { $sentinel = $sm.Groups[1].Value }
|
|
1096
|
+
$fc = 0; $fm = [regex]::Match($header, '(?i)file[^0-9]*([0-9]+)'); if ($fm.Success) { $fc = [int]$fm.Groups[1].Value }
|
|
1097
|
+
Write-Json ([ordered]@{
|
|
1098
|
+
ok = $true; reason = ''; name = $name; stack = $stack; app = $app; out_dir = $out
|
|
1099
|
+
pack_file = "$rel/$name.pack.txt"; unpacker = $unpacker; runner = $runner
|
|
1100
|
+
sentinel = $sentinel; file_count = $fc; available = @($available)
|
|
1101
|
+
})
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
# ─────────────────────────── foundation-unpack ─────────────────────────────
|
|
1105
|
+
function Invoke-FoundationUnpack {
|
|
1106
|
+
param([string[]]$Rest)
|
|
1107
|
+
$name = ''; $app = ''; $out = '.'; $dry = $false; $force = 'no'; $reference = $false
|
|
1108
|
+
for ($i = 0; $i -lt $Rest.Count; $i++) {
|
|
1109
|
+
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 } }
|
|
1110
|
+
}
|
|
1111
|
+
if (-not $name -or -not $app) { Write-Error 'foundation-unpack: --name and --app required'; exit 1 }
|
|
1112
|
+
$repo = Get-RepoRoot
|
|
1113
|
+
$found = @(Get-FoundationDirs $repo) | Where-Object { $_.Name -eq $name } | Select-Object -First 1
|
|
1114
|
+
$fdir = if ($found) { $found.Dir } else { Join-Path $repo "foundations/$name" }
|
|
1115
|
+
$packbase = if ($reference) { "$name.Reference" } else { $name }
|
|
1116
|
+
$stem = if ($reference) { 'unpack-reference-foundation' } else { 'unpack-foundation' }
|
|
1117
|
+
$pack = Join-Path $fdir "$packbase.pack.txt"
|
|
1118
|
+
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 }
|
|
1119
|
+
$cmd = $null; $cargs = @()
|
|
1120
|
+
if ((Get-Command pwsh -ErrorAction SilentlyContinue) -and (Test-Path (Join-Path $fdir "$stem.ps1"))) {
|
|
1121
|
+
$cmd = 'pwsh'; $cargs = @('-NoProfile','-File',(Join-Path $fdir "$stem.ps1"),'-PackFile',$pack,'-TargetAppName',$app,'-OutDir',$out)
|
|
1122
|
+
if ($dry) { $cargs += '-DryRun' }
|
|
1123
|
+
if ($force -eq 'yes' -or $reference) { $cargs += '-Force' }
|
|
1124
|
+
} elseif (Test-Path (Join-Path $fdir "$stem.sh")) {
|
|
1125
|
+
$cmd = 'bash'; $cargs = @((Join-Path $fdir "$stem.sh"),'--pack-file',$pack,'--target-app-name',$app,'--out-dir',$out)
|
|
1126
|
+
if ($dry) { $cargs += '--dry-run' }
|
|
1127
|
+
if ($force -eq 'yes' -or $reference) { $cargs += '--force' }
|
|
1128
|
+
} else { Write-Json ([ordered]@{ ok = $false; mode = 'real'; written = 0; has_collisions = $false; stdout = "no unpacker script for $name" }); return }
|
|
1129
|
+
$outp = & $cmd @cargs 2>&1 | Out-String
|
|
1130
|
+
$rc = $LASTEXITCODE
|
|
1131
|
+
# unpackers say "Unpacked N file(s) to …" / "DryRun: N file(s) would be written …"
|
|
1132
|
+
$written = 0; $wm = [regex]::Match($outp, '(?i)([0-9]+)\s+file'); if ($wm.Success) { $written = [int]$wm.Groups[1].Value }
|
|
1133
|
+
$hasColl = ($dry -and $rc -ne 0)
|
|
1134
|
+
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))) })
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
# ─────────────────────── foundation-stash-dbcontext ────────────────────────
|
|
1138
|
+
function Invoke-FoundationStashDbContext {
|
|
1139
|
+
param([string[]]$Rest)
|
|
1140
|
+
$out = '.'
|
|
1141
|
+
for ($i = 0; $i -lt $Rest.Count; $i++) { if ($Rest[$i] -eq '--out') { $out = $Rest[++$i] } }
|
|
1142
|
+
$repo = Get-RepoRoot
|
|
1143
|
+
if (-not [System.IO.Path]::IsPathRooted($out)) { $out = Join-Path $repo $out }
|
|
1144
|
+
$ctx = Get-ChildItem -Path (Join-Path $out 'src') -Recurse -Filter 'ApplicationDbContext.cs' -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty FullName
|
|
1145
|
+
if (-not $ctx) { Write-Json ([ordered]@{ found = $false; dbcontext = ''; backup = '' }); return }
|
|
1146
|
+
$bak = "$ctx.foundation.bak"
|
|
1147
|
+
Copy-Item $ctx $bak
|
|
1148
|
+
Write-Json ([ordered]@{ found = $true; dbcontext = $ctx; backup = $bak })
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
# ─────────────────────────── foundation-verify ─────────────────────────────
|
|
1152
|
+
function Invoke-FoundationVerify {
|
|
1153
|
+
param([string[]]$Rest)
|
|
1154
|
+
$out = '.'; $sentinel = ''
|
|
1155
|
+
for ($i = 0; $i -lt $Rest.Count; $i++) { switch ($Rest[$i]) { '--out' { $out = $Rest[++$i] } '--sentinel' { $sentinel = $Rest[++$i] } } }
|
|
1156
|
+
$repo = Get-RepoRoot
|
|
1157
|
+
if (-not [System.IO.Path]::IsPathRooted($out)) { $out = Join-Path $repo $out }
|
|
1158
|
+
$residue = @()
|
|
1159
|
+
if ($sentinel -and (Test-Path $out)) {
|
|
1160
|
+
# scan the generated tree only — exclude bundle/framework dirs
|
|
1161
|
+
$residue = @(Get-ChildItem $out -Recurse -File -ErrorAction SilentlyContinue |
|
|
1162
|
+
Where-Object { $_.FullName -notmatch '[/\\](\.git|guardrails|foundations|\.appweaver|specs|node_modules)[/\\]' } |
|
|
1163
|
+
Where-Object { try { Select-String -Path $_.FullName -SimpleMatch $sentinel -Quiet -ErrorAction SilentlyContinue } catch { $false } } |
|
|
1164
|
+
Select-Object -First 50 | ForEach-Object { $_.FullName.Substring($repo.Length).TrimStart('\','/') -replace '\\','/' })
|
|
1165
|
+
}
|
|
1166
|
+
Write-Json ([ordered]@{ clean = ($residue.Count -eq 0); residue = @($residue) })
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
# ─────────────────────────────── setup-constitution ────────────────────────
|
|
1170
|
+
# Ensure the split constitution files exist (copy from templates if missing):
|
|
1171
|
+
# .appweaver/memory/constitution.technical.md (from constitution-technical-template)
|
|
1172
|
+
# .appweaver/memory/constitution.domain.md (from constitution-domain-template)
|
|
1173
|
+
# .appweaver/memory/constitution.ui.md (from constitution-ui-template) — ONLY with --ui
|
|
1174
|
+
# The composed constitution.md is produced by compose-constitution after authoring.
|
|
1175
|
+
#
|
|
1176
|
+
# The UI half is opt-in because it is the only one whose ABSENCE is meaningful: a
|
|
1177
|
+
# backend-only project has no frontend lane and must look exactly as it did before
|
|
1178
|
+
# the lane model existed. The other two are always created and left as unauthored
|
|
1179
|
+
# templates when unused, which compose-constitution then skips.
|
|
1180
|
+
function Invoke-SetupConstitution {
|
|
1181
|
+
param([string[]]$Rest)
|
|
1182
|
+
# --ui / --no-technical follow the LANES the project declared. A half is a lane's
|
|
1183
|
+
# standing governance, so a lane the project does not have gets no half — and the
|
|
1184
|
+
# file is not created either, because an empty template left in memory/ is
|
|
1185
|
+
# indistinguishable from a half somebody forgot to author.
|
|
1186
|
+
$wantUi = $false; $wantTech = $true
|
|
1187
|
+
for ($i = 0; $i -lt $Rest.Count; $i++) {
|
|
1188
|
+
switch ($Rest[$i]) {
|
|
1189
|
+
'--ui' { $wantUi = $true }
|
|
1190
|
+
'--no-technical' { $wantTech = $false }
|
|
1191
|
+
# An EMPTY argument is a flag slot the caller left unfilled. The engine builds
|
|
1192
|
+
# a step's `args:` list before it knows which branch it is in, so a workflow
|
|
1193
|
+
# that varies one flag has to choose between duplicating the whole step and
|
|
1194
|
+
# passing '' — and duplicating it is how this call came to recreate the
|
|
1195
|
+
# technical half that the earlier call had just been told to skip.
|
|
1196
|
+
'' { }
|
|
1197
|
+
default { Write-Error "setup-constitution: unknown arg '$($Rest[$i])'"; exit 1 }
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
$repo = Get-RepoRoot
|
|
1201
|
+
New-Item -ItemType Directory -Force -Path (Join-Path $repo '.appweaver/memory') | Out-Null
|
|
1202
|
+
$tech = Join-Path $repo '.appweaver/memory/constitution.technical.md'
|
|
1203
|
+
$dom = Join-Path $repo '.appweaver/memory/constitution.domain.md'
|
|
1204
|
+
$ui = Join-Path $repo '.appweaver/memory/constitution.ui.md'
|
|
1205
|
+
$techCreated = $false; $domCreated = $false; $uiCreated = $false; $uiPath = ''
|
|
1206
|
+
# Full override/preset/extension/core resolution, matching the bash twin — and the
|
|
1207
|
+
# same resolution the placeholder check reads the template's token vocabulary from,
|
|
1208
|
+
# so a preset-supplied template is scored against itself rather than against core.
|
|
1209
|
+
if (-not $wantTech) { $tech = '' }
|
|
1210
|
+
if ($tech -and -not (Test-Path $tech)) {
|
|
1211
|
+
$tpl = Resolve-NamedTemplate -Name 'constitution-technical-template' -RepoRoot $repo
|
|
1212
|
+
if ($tpl) { Copy-Item $tpl $tech } else { New-Item -ItemType File -Force -Path $tech | Out-Null }
|
|
1213
|
+
$techCreated = $true
|
|
1214
|
+
}
|
|
1215
|
+
if (-not (Test-Path $dom)) {
|
|
1216
|
+
$tpl = Resolve-NamedTemplate -Name 'constitution-domain-template' -RepoRoot $repo
|
|
1217
|
+
if ($tpl) { Copy-Item $tpl $dom } else { New-Item -ItemType File -Force -Path $dom | Out-Null }
|
|
1218
|
+
$domCreated = $true
|
|
1219
|
+
}
|
|
1220
|
+
if ($wantUi) {
|
|
1221
|
+
$uiPath = $ui
|
|
1222
|
+
if (-not (Test-Path $ui)) {
|
|
1223
|
+
$tpl = Resolve-NamedTemplate -Name 'constitution-ui-template' -RepoRoot $repo
|
|
1224
|
+
if ($tpl) { Copy-Item $tpl $ui } else { New-Item -ItemType File -Force -Path $ui | Out-Null }
|
|
1225
|
+
$uiCreated = $true
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
Write-Json ([ordered]@{ constitution_technical_file = $tech; constitution_domain_file = $dom; constitution_ui_file = $uiPath; technical_created = $techCreated; domain_created = $domCreated; ui_created = $uiCreated })
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
# ─────────────────────────────── compose-constitution ──────────────────────
|
|
1232
|
+
# Deterministically concatenate the authored halves into the composed
|
|
1233
|
+
# constitution.md (human/compat view; NOT loaded by the pipeline phases). A half
|
|
1234
|
+
# that is absent or still an unauthored template is skipped, so a technical-only
|
|
1235
|
+
# project's composed file mirrors the technical half exactly.
|
|
1236
|
+
#
|
|
1237
|
+
# Order is technical → domain → ui. The domain half's principles CONTINUE the
|
|
1238
|
+
# technical base's roman numerals from wherever that base ended, so those two stay
|
|
1239
|
+
# adjacent; the UI half runs a parallel UX-* series and appends without interrupting
|
|
1240
|
+
# that run.
|
|
1241
|
+
#
|
|
1242
|
+
# NUMBERING CHECK. The boundary between the two roman-numeral halves is derived per
|
|
1243
|
+
# project — the technical half runs to whatever length its mapped technology base
|
|
1244
|
+
# defines — so a base of eighteen principles and a domain half that assumed it could
|
|
1245
|
+
# start at XVI would silently produce two Principle XVIs. Composition is the one place
|
|
1246
|
+
# both halves are read together, so it is where that is caught. A collision sets
|
|
1247
|
+
# numbering_ok=false; the file is still written, because a composed view that shows the
|
|
1248
|
+
# clash is more useful for fixing it than no file at all.
|
|
1249
|
+
$script:RomanValues = @(
|
|
1250
|
+
@(1000, 'M'), @(900, 'CM'), @(500, 'D'), @(400, 'CD'), @(100, 'C'), @(90, 'XC'),
|
|
1251
|
+
@(50, 'L'), @(40, 'XL'), @(10, 'X'), @(9, 'IX'), @(5, 'V'), @(4, 'IV'), @(1, 'I')
|
|
1252
|
+
)
|
|
1253
|
+
|
|
1254
|
+
function ConvertFrom-Roman {
|
|
1255
|
+
param([string]$Roman)
|
|
1256
|
+
$total = 0; $i = 0
|
|
1257
|
+
foreach ($pair in $script:RomanValues) {
|
|
1258
|
+
$val = $pair[0]; $sym = [string]$pair[1]
|
|
1259
|
+
while (($i + $sym.Length) -le $Roman.Length -and $Roman.Substring($i, $sym.Length) -ceq $sym) {
|
|
1260
|
+
$total += $val; $i += $sym.Length
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
if ($i -ne $Roman.Length) { return $null }
|
|
1264
|
+
return $total
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
function ConvertTo-Roman {
|
|
1268
|
+
param([int]$Value)
|
|
1269
|
+
$sb = ''
|
|
1270
|
+
foreach ($pair in $script:RomanValues) {
|
|
1271
|
+
$val = $pair[0]; $sym = [string]$pair[1]
|
|
1272
|
+
while ($Value -ge $val) { $sb += $sym; $Value -= $val }
|
|
1273
|
+
}
|
|
1274
|
+
return $sb
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
# UNFILLED-PLACEHOLDER TEST. "Still a raw template" is decided by intersecting the
|
|
1278
|
+
# half's [TOKEN]s with the TOKENS ITS OWN KIT TEMPLATE DEFINES — never by the shape of
|
|
1279
|
+
# the bracket. A constitution is free to write [HIPAA], [ISO_13485] or a [GDPR](url)
|
|
1280
|
+
# link; those are project content and match no template token, so they can no longer
|
|
1281
|
+
# make a whole ratified half vanish from the composed file.
|
|
1282
|
+
function Get-TemplateTokens {
|
|
1283
|
+
param([string]$Name, [string]$RepoRoot)
|
|
1284
|
+
$tpl = Resolve-NamedTemplate -Name $Name -RepoRoot $RepoRoot
|
|
1285
|
+
if (-not $tpl) { return $null }
|
|
1286
|
+
$set = @{}
|
|
1287
|
+
foreach ($m in [regex]::Matches((Get-Content -Raw $tpl), '\[([A-Z][A-Z0-9_]*)\]')) {
|
|
1288
|
+
$set[$m.Groups[1].Value] = $true
|
|
1289
|
+
}
|
|
1290
|
+
return $set
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
# The template's numbered slots are a series the author is told to extend, so
|
|
1294
|
+
# PRINCIPLE_6_NAME is as much an unfilled slot as PRINCIPLE_1_NAME even though the
|
|
1295
|
+
# template only shows five. Digit runs generalise; nothing else does, which is what keeps
|
|
1296
|
+
# a bracketed regulation code out of the family.
|
|
1297
|
+
function Test-TemplateToken {
|
|
1298
|
+
param([string]$Token, [hashtable]$Known)
|
|
1299
|
+
if ($Known.ContainsKey($Token)) { return $true }
|
|
1300
|
+
if ($Token -notmatch '\d') { return $false }
|
|
1301
|
+
foreach ($k in $Known.Keys) {
|
|
1302
|
+
if ($k -match '\d') {
|
|
1303
|
+
$rx = '\A' + ([regex]::Escape($k) -replace '\d+', '\d+') + '\Z'
|
|
1304
|
+
if ($Token -cmatch $rx) { return $true }
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
return $false
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
# Tokens in the file that its template also defines. Comments are stripped first so a
|
|
1311
|
+
# Sync Impact Report NAMING the placeholders it filled ("Removed: [PROJECT_NAME]")
|
|
1312
|
+
# cannot be mistaken for the placeholders themselves.
|
|
1313
|
+
function Get-UnfilledTokens {
|
|
1314
|
+
param([string]$Path, [hashtable]$Known)
|
|
1315
|
+
$found = @()
|
|
1316
|
+
if (-not (Test-Path $Path)) { return $found }
|
|
1317
|
+
$body = [regex]::Replace((Get-Content -Raw $Path), '(?s)<!--.*?-->', '')
|
|
1318
|
+
foreach ($m in [regex]::Matches($body, '\[([A-Z][A-Z0-9_]*)\]')) {
|
|
1319
|
+
$t = $m.Groups[1].Value
|
|
1320
|
+
if ($found -notcontains $t -and (Test-TemplateToken -Token $t -Known $Known)) { $found += $t }
|
|
1321
|
+
}
|
|
1322
|
+
return $found
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
# A principle heading opens with its numeral: "### VII — Name" or "### VII. Name".
|
|
1326
|
+
# UX-* is a PARALLEL series belonging to the frontend lane and is deliberately not
|
|
1327
|
+
# part of this run, so it is skipped rather than checked against it.
|
|
1328
|
+
function Get-PrincipleNumbers {
|
|
1329
|
+
param([string]$Path)
|
|
1330
|
+
# Callers wrap the result in @(), which is what guarantees array-ness for the
|
|
1331
|
+
# empty and single-item cases; returning ,$found on top of that would nest.
|
|
1332
|
+
$found = @()
|
|
1333
|
+
if (-not (Test-Path $Path)) { return $found }
|
|
1334
|
+
$text = Get-Content -Raw $Path
|
|
1335
|
+
$body = [regex]::Replace($text, '<!--.*?-->', '', 'Singleline')
|
|
1336
|
+
foreach ($m in [regex]::Matches($body, '(?m)^#{2,4}[ \t]+(UX-)?([IVXLCDM]+)[ \t]*[—–\-.:)]')) {
|
|
1337
|
+
if ($m.Groups[1].Success) { continue } # UX- series — parallel, not this sequence
|
|
1338
|
+
$roman = $m.Groups[2].Value
|
|
1339
|
+
$n = ConvertFrom-Roman -Roman $roman
|
|
1340
|
+
# Round-trip + a sane ceiling keep ordinary words made of roman letters
|
|
1341
|
+
# (and stray four-digit numerals) out of the sequence.
|
|
1342
|
+
if ($null -ne $n -and $n -le 100 -and (ConvertTo-Roman -Value $n) -ceq $roman) {
|
|
1343
|
+
# An object, not a nested array: PowerShell unrolls arrays-of-arrays on
|
|
1344
|
+
# return and through the pipeline, which loses the pairing.
|
|
1345
|
+
$found += [PSCustomObject]@{ N = $n; Roman = $roman }
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
return $found
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
function Invoke-ComposeConstitution {
|
|
1352
|
+
param([string[]]$Rest)
|
|
1353
|
+
$repo = Get-RepoRoot
|
|
1354
|
+
$tech = Join-Path $repo '.appweaver/memory/constitution.technical.md'
|
|
1355
|
+
$dom = Join-Path $repo '.appweaver/memory/constitution.domain.md'
|
|
1356
|
+
$ui = Join-Path $repo '.appweaver/memory/constitution.ui.md'
|
|
1357
|
+
$out = Join-Path $repo '.appweaver/memory/constitution.md'
|
|
1358
|
+
# A project that declared no SERVICE lane authors no technical half, and that is a
|
|
1359
|
+
# complete constitution rather than a broken one — a UI-only project's governance is
|
|
1360
|
+
# the UI half plus the domain addenda. What is not composable is a project with
|
|
1361
|
+
# nothing at all, which means the halves were never authored.
|
|
1362
|
+
$includeTech = Test-Path $tech
|
|
1363
|
+
if (-not ($includeTech -or (Test-Path $dom) -or (Test-Path $ui))) {
|
|
1364
|
+
Write-Json ([ordered]@{ composed_file = $out; ok = $false; technical_included = $false; domain_included = $false; ui_included = $false; numbering_ok = $true; numbering_checked = $false; issues = @('no constitution half found — run /appweaver constitution') }); return
|
|
1365
|
+
}
|
|
1366
|
+
$header = @(
|
|
1367
|
+
'<!-- COMPOSED — generated by `aw-spec-actions compose-constitution`. Do not edit here.',
|
|
1368
|
+
' Source of truth: the constitution halves — technical + domain + ui (edit those).',
|
|
1369
|
+
' Pipeline phases load the halves directly; this composed view is for humans/compat. -->',
|
|
1370
|
+
''
|
|
1371
|
+
) -join "`n"
|
|
1372
|
+
$body = $header + "`n"
|
|
1373
|
+
if ($includeTech) { $body += (Get-Content -Raw $tech) }
|
|
1374
|
+
# Include a half only if authored — non-empty AND carrying none of its own kit
|
|
1375
|
+
# template's tokens (an unused half stays a raw template). A half whose template
|
|
1376
|
+
# cannot be resolved is included: never drop one on a guess.
|
|
1377
|
+
$tokDom = Get-TemplateTokens -Name 'constitution-domain-template' -RepoRoot $repo
|
|
1378
|
+
$tokUi = Get-TemplateTokens -Name 'constitution-ui-template' -RepoRoot $repo
|
|
1379
|
+
$includeDomain = $false
|
|
1380
|
+
if ((Test-Path $dom) -and ((Get-Item $dom).Length -gt 0)) {
|
|
1381
|
+
$includeDomain = ($null -eq $tokDom) -or (@(Get-UnfilledTokens -Path $dom -Known $tokDom).Count -eq 0)
|
|
1382
|
+
}
|
|
1383
|
+
$includeUi = $false
|
|
1384
|
+
if ((Test-Path $ui) -and ((Get-Item $ui).Length -gt 0)) {
|
|
1385
|
+
$includeUi = ($null -eq $tokUi) -or (@(Get-UnfilledTokens -Path $ui -Known $tokUi).Count -eq 0)
|
|
1386
|
+
}
|
|
1387
|
+
if ($includeDomain) {
|
|
1388
|
+
$body += "`n`n---`n`n" + (Get-Content -Raw $dom)
|
|
1389
|
+
}
|
|
1390
|
+
if ($includeUi) {
|
|
1391
|
+
$body += "`n`n---`n`n" + (Get-Content -Raw $ui)
|
|
1392
|
+
}
|
|
1393
|
+
Set-Content -Path $out -Value $body -NoNewline
|
|
1394
|
+
|
|
1395
|
+
# ── principle numbering ────────────────────────────────────────────────
|
|
1396
|
+
$techP = if ($includeTech) { @(Get-PrincipleNumbers -Path $tech) } else { @() }
|
|
1397
|
+
$domP = if ($includeDomain) { @(Get-PrincipleNumbers -Path $dom) } else { @() }
|
|
1398
|
+
$issues = @()
|
|
1399
|
+
$numberingOk = $true
|
|
1400
|
+
|
|
1401
|
+
# The cross-half check can only run on halves whose principles it can actually read.
|
|
1402
|
+
# A half numbered some other way (1., 2. / P-01) parses to nothing, and silence there
|
|
1403
|
+
# would look identical to a clean result — so say so instead of reporting all-clear.
|
|
1404
|
+
$numberingForm = "principles must be headed '### I — Title' or '### I. Title' (the UI half runs its own UX-I, UX-II, … series)"
|
|
1405
|
+
$numberingChecked = $true
|
|
1406
|
+
if (-not $includeTech) {
|
|
1407
|
+
# No service lane, so no technical half and no boundary to check: the domain
|
|
1408
|
+
# half opens the roman-numeral sequence at I instead of continuing one. Not an
|
|
1409
|
+
# issue, and not a check that silently passed — it is a check with no subject.
|
|
1410
|
+
$numberingChecked = $false
|
|
1411
|
+
} elseif ($techP.Count -eq 0) {
|
|
1412
|
+
$numberingOk = $false; $numberingChecked = $false
|
|
1413
|
+
$issues += ('no roman-numeral principle headings found in the technical half, so numbering could not be verified — ' + $numberingForm)
|
|
1414
|
+
}
|
|
1415
|
+
if ($includeDomain -and $domP.Count -eq 0) {
|
|
1416
|
+
$numberingOk = $false; $numberingChecked = $false
|
|
1417
|
+
$issues += ('no roman-numeral principle headings found in the domain half, so numbering could not be verified — ' + $numberingForm)
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
foreach ($set in @(@{ Items = $techP; Label = 'technical' }, @{ Items = $domP; Label = 'domain' })) {
|
|
1421
|
+
$seen = @{}; $dup = @()
|
|
1422
|
+
foreach ($p in $set.Items) {
|
|
1423
|
+
if ($seen.ContainsKey($p.N) -and $dup -notcontains $p.Roman) { $dup += $p.Roman }
|
|
1424
|
+
$seen[$p.N] = $true
|
|
1425
|
+
}
|
|
1426
|
+
if ($dup.Count -gt 0) {
|
|
1427
|
+
$numberingOk = $false
|
|
1428
|
+
$issues += ('{0} half reuses principle number(s): {1}' -f $set.Label, ($dup -join ', '))
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
if ($techP.Count -gt 0 -and $domP.Count -gt 0) {
|
|
1433
|
+
$techNums = @($techP | ForEach-Object { $_.N })
|
|
1434
|
+
$domNums = @($domP | ForEach-Object { $_.N })
|
|
1435
|
+
$lastTech = ($techNums | Measure-Object -Maximum).Maximum
|
|
1436
|
+
$firstDom = ($domNums | Measure-Object -Minimum).Minimum
|
|
1437
|
+
$overlap = @($techNums | Where-Object { $domNums -contains $_ } | Sort-Object -Unique)
|
|
1438
|
+
if ($overlap.Count -gt 0) {
|
|
1439
|
+
$numberingOk = $false
|
|
1440
|
+
$issues += ('principle number(s) {0} appear in BOTH the technical and domain halves — the domain half must resume from the number after the technical half''s last principle ({1}), not from a fixed value' -f (($overlap | ForEach-Object { ConvertTo-Roman -Value $_ }) -join ', '), (ConvertTo-Roman -Value $lastTech))
|
|
1441
|
+
}
|
|
1442
|
+
elseif ($firstDom -ne ($lastTech + 1)) {
|
|
1443
|
+
$numberingOk = $false
|
|
1444
|
+
$issues += ('domain half starts at {0} but the technical half ends at {1} — numbering must run unbroken across the two halves (expected {2})' -f (ConvertTo-Roman -Value $firstDom), (ConvertTo-Roman -Value $lastTech), (ConvertTo-Roman -Value ($lastTech + 1)))
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
Write-Json ([ordered]@{
|
|
1449
|
+
composed_file = $out
|
|
1450
|
+
ok = $numberingOk
|
|
1451
|
+
domain_included = $includeDomain
|
|
1452
|
+
ui_included = $includeUi
|
|
1453
|
+
numbering_ok = $numberingOk
|
|
1454
|
+
numbering_checked = $numberingChecked
|
|
1455
|
+
issues = $issues
|
|
1456
|
+
technical_last = $(if ($techP.Count -gt 0) { ConvertTo-Roman -Value ((@($techP | ForEach-Object { $_.N }) | Measure-Object -Maximum).Maximum) } else { $null })
|
|
1457
|
+
domain_first = $(if ($domP.Count -gt 0) { ConvertTo-Roman -Value ((@($domP | ForEach-Object { $_.N }) | Measure-Object -Minimum).Minimum) } else { $null })
|
|
1458
|
+
})
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
# ─────────────────────────── constitution-verify ───────────────────────────
|
|
1462
|
+
# Deterministic lint of a generated constitution (NO LLM): unfilled template
|
|
1463
|
+
# placeholders, body-vs-Sync-Impact-Report version agreement, and non-ISO dates.
|
|
1464
|
+
# Consumed by the constitution skill's bounded self-correction loop. ok=false => the
|
|
1465
|
+
# loop asks the model to patch.
|
|
1466
|
+
#
|
|
1467
|
+
# A placeholder is a token THE KIT TEMPLATE ACTUALLY DEFINES, resolved from the half's
|
|
1468
|
+
# own template and intersected with the file. Bracket SHAPE is not evidence: [HIPAA],
|
|
1469
|
+
# [ISO_13485] and [GDPR](url) are ordinary constitution content in a regulated domain
|
|
1470
|
+
# and must never be reported as unfilled slots. --template overrides the name derived
|
|
1471
|
+
# from the filename.
|
|
1472
|
+
#
|
|
1473
|
+
# The version is read from lines that OPEN with the label, last one wins, so an
|
|
1474
|
+
# "OpenAPI version 3.1.0" inside a principle cannot be mistaken for the document's
|
|
1475
|
+
# version. On a report line the LAST semver is the new version, which also survives an
|
|
1476
|
+
# "old -> new" whose arrow was mangled by an encoding round-trip.
|
|
1477
|
+
function Get-LabeledVersion {
|
|
1478
|
+
param([string]$Text, [switch]$LastOnLine)
|
|
1479
|
+
$vals = @()
|
|
1480
|
+
foreach ($line in ($Text -split "`n")) {
|
|
1481
|
+
if ($line -match '^[\s>*_#-]*version\b') {
|
|
1482
|
+
$sem = @([regex]::Matches($line, '\d+\.\d+\.\d+') | ForEach-Object { $_.Value })
|
|
1483
|
+
if ($sem.Count -gt 0) { $vals += $(if ($LastOnLine) { $sem[-1] } else { $sem[0] }) }
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
if ($vals.Count -gt 0) { return $vals[-1] } else { return $null }
|
|
1487
|
+
}
|
|
1488
|
+
function Invoke-ConstitutionVerify {
|
|
1489
|
+
param([string[]]$Rest)
|
|
1490
|
+
$file = ''; $template = ''
|
|
1491
|
+
for ($i = 0; $i -lt $Rest.Count; $i++) {
|
|
1492
|
+
if ($Rest[$i] -eq '--file') { $file = $Rest[++$i] }
|
|
1493
|
+
elseif ($Rest[$i] -eq '--template') { $template = $Rest[++$i] }
|
|
1494
|
+
}
|
|
1495
|
+
if (-not $file) { Write-Error 'constitution-verify: --file required'; exit 1 }
|
|
1496
|
+
if (-not (Test-Path $file)) {
|
|
1497
|
+
Write-Json ([ordered]@{ ok = $false; exists = $false; issues = @('constitution file not found');
|
|
1498
|
+
placeholders = @(); version_body = $null; version_report = $null; version_match = $null; bad_dates = @() })
|
|
1499
|
+
return
|
|
1500
|
+
}
|
|
1501
|
+
if (-not $template) {
|
|
1502
|
+
switch (Split-Path -Leaf $file) {
|
|
1503
|
+
'constitution.technical.md' { $template = 'constitution-technical-template' }
|
|
1504
|
+
'constitution.domain.md' { $template = 'constitution-domain-template' }
|
|
1505
|
+
'constitution.ui.md' { $template = 'constitution-ui-template' }
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
$known = $null
|
|
1509
|
+
if ($template) { $known = Get-TemplateTokens -Name $template -RepoRoot (Get-RepoRoot) }
|
|
1510
|
+
$text = Get-Content -Path $file -Raw
|
|
1511
|
+
# Strip HTML comments (template guidance + Sync Impact Report); keep the first as the report.
|
|
1512
|
+
$cm = [regex]::Match($text, '(?s)<!--.*?-->')
|
|
1513
|
+
$report = if ($cm.Success) { $cm.Value } else { '' }
|
|
1514
|
+
$body = [regex]::Replace($text, '(?s)<!--.*?-->', '')
|
|
1515
|
+
# 1. Unfilled slots = the file's [TOKEN]s intersected with the template's vocabulary.
|
|
1516
|
+
$templateMissing = ($null -eq $known)
|
|
1517
|
+
$placeholders = @()
|
|
1518
|
+
if (-not $templateMissing) {
|
|
1519
|
+
$placeholders = @([regex]::Matches($body, '\[([A-Z][A-Z0-9_]*)\]') |
|
|
1520
|
+
ForEach-Object { $_.Groups[1].Value } |
|
|
1521
|
+
Where-Object { Test-TemplateToken -Token $_ -Known $known } |
|
|
1522
|
+
Select-Object -Unique | Sort-Object)
|
|
1523
|
+
}
|
|
1524
|
+
# 2. Version: the document's own labelled version line vs the report's "new" version.
|
|
1525
|
+
$versionBody = Get-LabeledVersion $body
|
|
1526
|
+
$arrow = [regex]::Match($report, '(\d+\.\d+\.\d+)\s*(?:->|→|=>)\s*(\d+\.\d+\.\d+)')
|
|
1527
|
+
$versionReport = if ($arrow.Success) { $arrow.Groups[2].Value } else { Get-LabeledVersion $report -LastOnLine }
|
|
1528
|
+
$versionMatch = $null
|
|
1529
|
+
if ($versionBody -and $versionReport) { $versionMatch = ($versionBody -eq $versionReport) }
|
|
1530
|
+
# 3. Obviously non-ISO dates on ratification/amended/date lines (ISO never matches these).
|
|
1531
|
+
$bad = @()
|
|
1532
|
+
foreach ($line in ($body -split "`n")) {
|
|
1533
|
+
if ($line -match '(?i)ratif|amend|\bdate\b') {
|
|
1534
|
+
foreach ($mm in [regex]::Matches($line, '\b\d{1,2}/\d{1,2}/\d{2,4}\b')) { $bad += $mm.Value }
|
|
1535
|
+
foreach ($mm in [regex]::Matches($line, '\b\d{1,2}-\d{1,2}-\d{4}\b')) { $bad += $mm.Value }
|
|
1536
|
+
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 }
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
$bad = @($bad | Select-Object -Unique | Sort-Object)
|
|
1540
|
+
$issues = @()
|
|
1541
|
+
if ($placeholders.Count -gt 0) { $issues += ("{0} unfilled placeholder(s): {1}" -f $placeholders.Count, ($placeholders -join ', ')) }
|
|
1542
|
+
# Reported, not failed: a missing kit template is an install problem, and blocking
|
|
1543
|
+
# the authoring loop over it would help nobody.
|
|
1544
|
+
if ($templateMissing) { $issues += 'placeholder check skipped — no kit template resolved for this half' }
|
|
1545
|
+
if ($versionMatch -eq $false) { $issues += ("version mismatch: body {0} vs Sync Impact Report {1}" -f $versionBody, $versionReport) }
|
|
1546
|
+
if ($bad.Count -gt 0) { $issues += ("non-ISO date(s) (use YYYY-MM-DD): {0}" -f ($bad -join ', ')) }
|
|
1547
|
+
$ok = ($placeholders.Count -eq 0) -and ($versionMatch -ne $false) -and ($bad.Count -eq 0)
|
|
1548
|
+
Write-Json ([ordered]@{ ok = $ok; exists = $true; issues = @($issues);
|
|
1549
|
+
placeholders = @($placeholders); placeholder_check = (-not $templateMissing);
|
|
1550
|
+
version_body = $versionBody; version_report = $versionReport;
|
|
1551
|
+
version_match = $versionMatch; bad_dates = @($bad) })
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
# ────────────────────────── agent-context-update ───────────────────────────
|
|
1555
|
+
function Invoke-AgentContextUpdate {
|
|
1556
|
+
param([string[]]$Rest)
|
|
1557
|
+
$plan = ''
|
|
1558
|
+
for ($i = 0; $i -lt $Rest.Count; $i++) { if ($Rest[$i] -eq '--plan') { $plan = $Rest[++$i] } }
|
|
1559
|
+
$repo = Get-RepoRoot
|
|
1560
|
+
$script = Join-Path $repo '.appweaver/extensions/agent-context/scripts/powershell/update-agent-context.ps1'
|
|
1561
|
+
if (-not (Test-Path $script)) {
|
|
1562
|
+
Write-Json ([ordered]@{ ok = $true; ran = $false; message = 'agent-context extension not installed; nothing to do' }); return
|
|
1563
|
+
}
|
|
1564
|
+
if ($plan) { $out = & pwsh -NoProfile -File $script $plan 2>&1 | Out-String }
|
|
1565
|
+
else { $out = & pwsh -NoProfile -File $script 2>&1 | Out-String }
|
|
1566
|
+
$rc = $LASTEXITCODE
|
|
1567
|
+
Write-Json ([ordered]@{ ok = ($rc -eq 0); ran = $true; message = ($out.Substring([Math]::Max(0,$out.Length-400))).Trim() })
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
|
|
1571
|
+
|
|
1572
|
+
# ─────────────────────────────── list-components ───────────────────────────
|
|
1573
|
+
# Immediate subdirs of foundations/dotnet/components/ that contain an integration.md runbook.
|
|
1574
|
+
# Immediate subdirs of foundations/<ecosystem>/components/ that contain an integration.md
|
|
1575
|
+
# runbook, for the ecosystem the BACKEND lane declares. Components are wired into a host
|
|
1576
|
+
# project by editing its build files and its startup registration, so a component only
|
|
1577
|
+
# exists for the ecosystem whose build files and startup it knows how to edit — the
|
|
1578
|
+
# catalogue is per-ecosystem for the same reason the foundations are.
|
|
1579
|
+
function Invoke-ListComponents {
|
|
1580
|
+
param([string[]]$Rest)
|
|
1581
|
+
$repo = Get-RepoRoot
|
|
1582
|
+
$eco = Get-LaneField -Field 'backend_ecosystem'
|
|
1583
|
+
# No backend lane yet: 'dotnet' is the only ecosystem that ships components, so it is
|
|
1584
|
+
# what an undeclared repo would have been shown before the lane model and stays the
|
|
1585
|
+
# answer until select-stack says otherwise.
|
|
1586
|
+
if (-not $eco) { $eco = 'dotnet' }
|
|
1587
|
+
$base = Join-Path $repo "foundations/$eco/components"
|
|
1588
|
+
$comps = @()
|
|
1589
|
+
if (Test-Path $base) {
|
|
1590
|
+
foreach ($d in (Get-ChildItem -Directory $base -ErrorAction SilentlyContinue)) {
|
|
1591
|
+
if (Test-Path (Join-Path $d.FullName 'integration.md')) { $comps += $d.Name }
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
Write-Json ([ordered]@{ components = @($comps); ecosystem = $eco; dir = "foundations/$eco/components" })
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
# ─────────────────────────────── find-host-project ─────────────────────────
|
|
1598
|
+
# DELIBERATELY .NET-ONLY, and it reports which stack it looked at so a caller can say
|
|
1599
|
+
# so. 'Program.cs' is not a cross-stack concept: a Spring lane's entry point is an
|
|
1600
|
+
# @SpringBootApplication class, an Express lane's is whatever 'main' in package.json
|
|
1601
|
+
# names, and neither has the .csproj this returns. Callers that need a LAUNCH COMMAND
|
|
1602
|
+
# ask the lane instead (its declared 'run'); this exists for the two callers that need
|
|
1603
|
+
# the .NET host DIRECTORY. found:false on a non-.NET lane is the correct answer.
|
|
1604
|
+
function Invoke-FindHostProject {
|
|
1605
|
+
param([string[]]$Rest)
|
|
1606
|
+
$repo = Get-RepoRoot
|
|
1607
|
+
# WHERE the host lives. The service lane's declared source path when there is one,
|
|
1608
|
+
# 'src' when there is not — the same question run-migrate asks, and the same answer.
|
|
1609
|
+
$svcRoot = Get-LaneField -Field 'backend_root'
|
|
1610
|
+
if (-not $svcRoot) { $svcRoot = 'src' }
|
|
1611
|
+
$eco = Get-LaneField -Field 'backend_ecosystem'
|
|
1612
|
+
$searched = $svcRoot.TrimEnd('/')
|
|
1613
|
+
$prog = Get-ChildItem -Path (Join-Path $repo ($svcRoot.TrimEnd('/'))) -Recurse -Filter 'Program.cs' -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty FullName
|
|
1614
|
+
if (-not $prog) {
|
|
1615
|
+
$prog = Get-ChildItem -Path $repo -Recurse -Filter 'Program.cs' -ErrorAction SilentlyContinue |
|
|
1616
|
+
Where-Object { $_.FullName -notmatch '[/\\](\.git|guardrails|foundations|specs|\.appweaver|obj|bin)[/\\]' } |
|
|
1617
|
+
Select-Object -First 1 -ExpandProperty FullName
|
|
1618
|
+
}
|
|
1619
|
+
if (-not $prog) { Write-Json ([ordered]@{ found = $false; program_cs = ''; project_dir = ''; csproj = ''; ecosystem = $eco; searched = $searched }); return }
|
|
1620
|
+
$pdir = Split-Path $prog -Parent
|
|
1621
|
+
$csproj = Get-ChildItem -Path $pdir -Filter *.csproj -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty FullName
|
|
1622
|
+
Write-Json ([ordered]@{ found = $true; program_cs = $prog; project_dir = $pdir; csproj = $(if ($csproj) { $csproj } else { '' }); ecosystem = $eco; searched = $searched })
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
# ─────────────────────────────── component-catalog ──────────────────────────
|
|
1626
|
+
# Deterministic parse of a component's integration.md "Implementation catalog"
|
|
1627
|
+
# markdown table (NO LLM). Returns {integrable, impl_keys[], implementations[{key,desc}]}.
|
|
1628
|
+
# Twin of the bash cmd_component_catalog: both shells delegate to the SAME python
|
|
1629
|
+
# core, so the parse and the JSON contract have one implementation and the pair
|
|
1630
|
+
# cannot drift.
|
|
1631
|
+
function Invoke-ComponentCatalog {
|
|
1632
|
+
param([string[]]$Rest)
|
|
1633
|
+
$file = ''
|
|
1634
|
+
for ($i = 0; $i -lt $Rest.Count; $i++) { if ($Rest[$i] -eq '--file') { $file = $Rest[++$i] } }
|
|
1635
|
+
if (-not $file) { Write-Error 'component-catalog: --file required'; exit 1 }
|
|
1636
|
+
$py = Get-Py
|
|
1637
|
+
if (-not $py) { Write-Error 'component-catalog: python3 is required to parse the catalog'; exit 1 }
|
|
1638
|
+
$core = Join-Path $PSScriptRoot '..' 'python' 'aw-component-catalog.py'
|
|
1639
|
+
if (-not (Test-Path $core)) { Write-Error "component-catalog: core not found at $core"; exit 1 }
|
|
1640
|
+
& $py $core '--repo' (Get-RepoRoot) '--file' $file
|
|
1641
|
+
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
# ─────────────────────────────── component-verify ───────────────────────────
|
|
1645
|
+
# Post-wiring verification (NO LLM decision): dotnet build (graceful skip if absent) +
|
|
1646
|
+
# R0.2a dual-registration guard. Returns {ok, ran, build_ok, errors[],
|
|
1647
|
+
# dual_registration_ok, fix_target, fix_reason, can_fix}.
|
|
1648
|
+
function Get-DefiningFiles {
|
|
1649
|
+
param([string]$Method, [string]$Root)
|
|
1650
|
+
$hits = @()
|
|
1651
|
+
if (-not $Root -or -not (Test-Path $Root)) { return , $hits }
|
|
1652
|
+
$pat = '\b' + [regex]::Escape($Method) + '\s*\('
|
|
1653
|
+
Get-ChildItem $Root -Recurse -File -Filter *.cs -ErrorAction SilentlyContinue |
|
|
1654
|
+
Where-Object { $_.FullName -notmatch '[/\\](bin|obj|\.git)[/\\]' } |
|
|
1655
|
+
ForEach-Object {
|
|
1656
|
+
$txt = Get-Content $_.FullName -Raw -ErrorAction SilentlyContinue
|
|
1657
|
+
if ($txt -and ($txt -match $pat)) { $hits += [pscustomobject]@{ path = $_.FullName; text = $txt } }
|
|
1658
|
+
}
|
|
1659
|
+
return , $hits
|
|
1660
|
+
}
|
|
1661
|
+
function Invoke-ComponentVerify {
|
|
1662
|
+
param([string[]]$Rest)
|
|
1663
|
+
$hostDir = ''; $hostCsproj = ''; $marker = ''
|
|
1664
|
+
for ($i = 0; $i -lt $Rest.Count; $i++) {
|
|
1665
|
+
switch ($Rest[$i]) { '--host-dir' { $hostDir = $Rest[++$i] } '--host-csproj' { $hostCsproj = $Rest[++$i] } '--marker' { $marker = $Rest[++$i] } }
|
|
1666
|
+
}
|
|
1667
|
+
$repo = Get-RepoRoot
|
|
1668
|
+
$marker = $marker.Trim()
|
|
1669
|
+
function RelP($p) { if ($p -and $repo) { try { return ([System.IO.Path]::GetRelativePath($repo, $p) -replace '\\', '/') } catch { return $p } } else { return $p } }
|
|
1670
|
+
# --- (1) build ---
|
|
1671
|
+
$ran = $false; $buildOk = $true; $errors = @(); $primary = ''
|
|
1672
|
+
if (Get-Command dotnet -ErrorAction SilentlyContinue) {
|
|
1673
|
+
$target = if ($hostCsproj) { $hostCsproj } else { $hostDir }
|
|
1674
|
+
if ($target -and (Test-Path $target)) {
|
|
1675
|
+
$ran = $true
|
|
1676
|
+
$out = (& dotnet build $target -nologo 2>&1 | Out-String)
|
|
1677
|
+
$code = $LASTEXITCODE
|
|
1678
|
+
foreach ($line in ($out -split "`n")) { if ($line -match ':\s*error\s') { $errors += $line.Trim() } }
|
|
1679
|
+
$errors = @($errors | Select-Object -Unique | Select-Object -First 20)
|
|
1680
|
+
$buildOk = ($code -eq 0)
|
|
1681
|
+
if (-not $buildOk -and $errors.Count -gt 0) {
|
|
1682
|
+
$m = [regex]::Match($errors[0], '^(.*?)\(\d+,\d+\)'); if ($m.Success) { $primary = RelP ($m.Groups[1].Value.Trim()) }
|
|
1683
|
+
}
|
|
1684
|
+
if (-not $buildOk -and -not $primary -and $hostCsproj) { $primary = RelP $hostCsproj }
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
# --- (2) R0.2a dual-registration guard ---
|
|
1688
|
+
$searchRoot = if ($hostDir -and (Test-Path $hostDir)) { $hostDir } else { $repo }
|
|
1689
|
+
$infra = @(Get-DefiningFiles 'AddInfrastructure' $searchRoot)
|
|
1690
|
+
$aspire = @(Get-DefiningFiles 'AddAspireInfrastructure' $searchRoot)
|
|
1691
|
+
$dualApp = ($infra.Count -gt 0 -and $aspire.Count -gt 0)
|
|
1692
|
+
$dualOk = $true; $dualNote = ''; $dualFix = ''
|
|
1693
|
+
function Pick-Fix($hits) {
|
|
1694
|
+
$nonProg = @($hits | Where-Object { [System.IO.Path]::GetFileName($_.path) -ne 'Program.cs' })
|
|
1695
|
+
if ($nonProg.Count -gt 0) { return RelP $nonProg[0].path } elseif ($hits.Count -gt 0) { return RelP $hits[0].path } else { return '' }
|
|
1696
|
+
}
|
|
1697
|
+
if (-not $dualApp) { $dualNote = 'single registration path (R0.2) — dual-registration check N/A' }
|
|
1698
|
+
elseif (-not $marker) { $dualNote = 'both AddInfrastructure and AddAspireInfrastructure exist; no marker provided to verify the component is in both' }
|
|
1699
|
+
else {
|
|
1700
|
+
$inInfra = @($infra | Where-Object { $_.text.Contains($marker) }).Count -gt 0
|
|
1701
|
+
$inAspire = @($aspire | Where-Object { $_.text.Contains($marker) }).Count -gt 0
|
|
1702
|
+
$dualOk = ($inInfra -and $inAspire)
|
|
1703
|
+
if (-not $dualOk) {
|
|
1704
|
+
$missing = if (-not $inInfra) { 'AddInfrastructure' } else { 'AddAspireInfrastructure' }
|
|
1705
|
+
$dualFix = Pick-Fix ($(if (-not $inInfra) { $infra } else { $aspire }))
|
|
1706
|
+
$dualNote = "marker '$marker' missing from $missing (R0.2a: register in BOTH paths)"
|
|
1707
|
+
}
|
|
1708
|
+
else { $dualNote = "marker '$marker' present in both registration paths" }
|
|
1709
|
+
}
|
|
1710
|
+
$ok = ($buildOk -and $dualOk)
|
|
1711
|
+
$fixTarget = ''; $fixReason = ''
|
|
1712
|
+
if (-not $buildOk) { $fixTarget = if ($primary) { $primary } elseif ($hostCsproj) { RelP $hostCsproj } else { '' }; $fixReason = 'build failed: ' + ((@($errors) | Select-Object -First 5) -join ' | ') }
|
|
1713
|
+
elseif (-not $dualOk) { $fixTarget = $dualFix; $fixReason = $dualNote }
|
|
1714
|
+
Write-Json ([ordered]@{ ok = $ok; ran = $ran; build_ok = $buildOk; errors = @($errors); primary_error_file = $primary;
|
|
1715
|
+
dual_applicable = $dualApp; dual_registration_ok = $dualOk; dual_note = $dualNote; dual_fix_file = $dualFix;
|
|
1716
|
+
fix_target = $fixTarget; fix_reason = $fixReason; can_fix = [bool]$fixTarget })
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
# ═════════════════ spec-anchored design pipeline (parity with bash) ═════════
|
|
1720
|
+
# Mirror of the design/build/analyze-slice/run/deploy/cr subcommands. The fence parsers
|
|
1721
|
+
# (extract/reconcile/paths) shell out to the SAME python3 as bash so the logic
|
|
1722
|
+
# is byte-identical; the rest is native. See .appweaver/DESIGN-FORMAT.md.
|
|
1723
|
+
# NOTE: authored for Windows parity; smoke-test under pwsh before relying on it.
|
|
1724
|
+
|
|
1725
|
+
function Get-Py {
|
|
1726
|
+
if (Get-Command python3 -ErrorAction SilentlyContinue) { return 'python3' }
|
|
1727
|
+
if (Get-Command python -ErrorAction SilentlyContinue) { return 'python' }
|
|
1728
|
+
return $null
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
function Read-FeatureDir {
|
|
1732
|
+
param([string]$RepoRoot)
|
|
1733
|
+
$fj = Join-Path $RepoRoot '.appweaver/feature.json'
|
|
1734
|
+
if (-not (Test-Path $fj)) { return '' }
|
|
1735
|
+
try { return (Get-Content $fj -Raw | ConvertFrom-Json).feature_directory } catch { return '' }
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
function Resolve-FeatureDir {
|
|
1739
|
+
param([string]$Arg, [string]$RepoRoot)
|
|
1740
|
+
$fd = $Arg
|
|
1741
|
+
if (-not $fd) { $fd = Read-FeatureDir -RepoRoot $RepoRoot }
|
|
1742
|
+
if (-not $fd) { return $null }
|
|
1743
|
+
if (-not [System.IO.Path]::IsPathRooted($fd)) { $fd = Join-Path $RepoRoot $fd }
|
|
1744
|
+
return $fd
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
function Resolve-NamedTemplate {
|
|
1748
|
+
param([string]$Name, [string]$RepoRoot)
|
|
1749
|
+
$base = Join-Path $RepoRoot '.appweaver/templates'
|
|
1750
|
+
$file = "$Name.md"
|
|
1751
|
+
$override = Join-Path $base "overrides/$file"
|
|
1752
|
+
if (Test-Path $override) { return $override }
|
|
1753
|
+
$presets = Join-Path $RepoRoot '.appweaver/presets'
|
|
1754
|
+
if (Test-Path $presets) {
|
|
1755
|
+
$hit = Get-ChildItem -Directory $presets -ErrorAction SilentlyContinue |
|
|
1756
|
+
ForEach-Object { Join-Path $_.FullName "templates/$file" } |
|
|
1757
|
+
Where-Object { Test-Path $_ } | Select-Object -First 1
|
|
1758
|
+
if ($hit) { return $hit }
|
|
1759
|
+
}
|
|
1760
|
+
$exts = Join-Path $RepoRoot '.appweaver/extensions'
|
|
1761
|
+
if (Test-Path $exts) {
|
|
1762
|
+
$hit = Get-ChildItem -Directory $exts -ErrorAction SilentlyContinue |
|
|
1763
|
+
Where-Object { -not $_.Name.StartsWith('.') } |
|
|
1764
|
+
ForEach-Object { Join-Path $_.FullName "templates/$file" } |
|
|
1765
|
+
Where-Object { Test-Path $_ } | Select-Object -First 1
|
|
1766
|
+
if ($hit) { return $hit }
|
|
1767
|
+
}
|
|
1768
|
+
$core = Join-Path $base $file
|
|
1769
|
+
if (Test-Path $core) { return $core }
|
|
1770
|
+
return $null
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
# The former path-tagged helpers (design-extract / -reconcile / -paths) are GONE, in
|
|
1774
|
+
# lockstep with the bash twin. They belonged to a retired four-document model in which
|
|
1775
|
+
# design.{domain,application,api,infra}.md carried the source inside ```lang path=…
|
|
1776
|
+
# fences. Nothing emits that fence any more, and their glob (`design.*.md`) could not
|
|
1777
|
+
# match a single `design.md` even if something did — so all three returned nothing,
|
|
1778
|
+
# silently, at exit 0. "Which files is this slice made of" is now an OKF query:
|
|
1779
|
+
# aw-xref concept-paths to read, concept-upsert --merge to write.
|
|
1780
|
+
function Invoke-DesignScaffold {
|
|
1781
|
+
param([string[]]$Rest)
|
|
1782
|
+
$fdArg = ''; $tplName = ''; $lane = 'service'
|
|
1783
|
+
# --lane service|ui: WHOSE design this is. It selects a coherent (template,
|
|
1784
|
+
# document) PAIR rather than two independent flags — service scaffolds
|
|
1785
|
+
# design-template into design.md, ui scaffolds ui-design-template into
|
|
1786
|
+
# design.ui.md — so no caller can write the service template under the UI lane's
|
|
1787
|
+
# filename. Default `service` leaves every existing caller byte-identical.
|
|
1788
|
+
# --template <name> overrides the TEMPLATE half only; the document name follows
|
|
1789
|
+
# --lane. In a brownfield repo onboard GENERATES a code-derived override for both
|
|
1790
|
+
# names, so the defaults already yield the as-built structure.
|
|
1791
|
+
for ($i = 0; $i -lt $Rest.Count; $i++) { switch ($Rest[$i]) { { $_ -in '--feature-dir','-FeatureDir' } { $fdArg = $Rest[++$i] } { $_ -in '--lane','-Lane' } { $lane = $Rest[++$i] } { $_ -in '--template','-Template' } { $tplName = $Rest[++$i] } } }
|
|
1792
|
+
switch ($lane) {
|
|
1793
|
+
'service' { $docName = 'design.md'; if (-not $tplName) { $tplName = 'design-template' } }
|
|
1794
|
+
'ui' { $docName = 'design.ui.md'; if (-not $tplName) { $tplName = 'ui-design-template' } }
|
|
1795
|
+
default { Write-Error ("design-scaffold: unknown --lane '" + $lane + "' (service|ui)"); exit 1 }
|
|
1796
|
+
}
|
|
1797
|
+
$repo = Get-RepoRoot
|
|
1798
|
+
$fd = Resolve-FeatureDir -Arg $fdArg -RepoRoot $repo
|
|
1799
|
+
if (-not $fd) { Write-Error 'design-scaffold: no feature set (run /appweaver select-feature first) and no --feature-dir'; exit 1 }
|
|
1800
|
+
New-Item -ItemType Directory -Force -Path $fd | Out-Null
|
|
1801
|
+
$target = Join-Path $fd $docName
|
|
1802
|
+
if (Test-Path $target) { $created = $false }
|
|
1803
|
+
else {
|
|
1804
|
+
$tpl = Resolve-NamedTemplate -Name $tplName -RepoRoot $repo
|
|
1805
|
+
if ($tpl -and (Test-Path $tpl)) { Copy-Item $tpl $target } else { New-Item -ItemType File -Force -Path $target | Out-Null }
|
|
1806
|
+
$created = $true
|
|
1807
|
+
}
|
|
1808
|
+
# override_active: is a PROJECT-level override installed for this template name?
|
|
1809
|
+
# Computed ALWAYS (a stable signal on bootstrap AND delta). onboard installs the
|
|
1810
|
+
# code-derived brownfield template as the `design-template` override, so
|
|
1811
|
+
# override_active=true means the forward design phase should author brownfield.
|
|
1812
|
+
$overrideActive = Test-Path (Join-Path $repo (".appweaver/templates/overrides/" + $tplName + ".md"))
|
|
1813
|
+
$templateSource = Resolve-NamedTemplate -Name $tplName -RepoRoot $repo
|
|
1814
|
+
$tsOut = if ($templateSource) { $templateSource } else { '' }
|
|
1815
|
+
Write-Json ([ordered]@{ feature_dir = $fd; design_doc = $target; lane = $lane; created = $created; override_active = $overrideActive; template = $tplName; template_source = $tsOut })
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
# Read one field out of the lane model. Twin of the bash inline python readers: the
|
|
1819
|
+
# shells never parse stacks.json themselves, so its schema has one reader.
|
|
1820
|
+
function Get-LaneField {
|
|
1821
|
+
param([string]$Field, [string]$Kind)
|
|
1822
|
+
$py = Get-Py
|
|
1823
|
+
if (-not $py) { return '' }
|
|
1824
|
+
$core = Join-Path $PSScriptRoot '..' 'python' 'aw-stack.py'
|
|
1825
|
+
if (-not (Test-Path $core)) { return '' }
|
|
1826
|
+
$argsList = @('--repo', (Get-RepoRoot), 'resolve')
|
|
1827
|
+
if ($Kind) { $argsList += @('--kind', $Kind) }
|
|
1828
|
+
try {
|
|
1829
|
+
$line = (& $py $core @argsList 2>$null | Select-Object -First 1)
|
|
1830
|
+
if (-not $line) { return '' }
|
|
1831
|
+
$v = ($line | ConvertFrom-Json).$Field
|
|
1832
|
+
if ($null -eq $v) { return '' }
|
|
1833
|
+
return [string]$v
|
|
1834
|
+
} catch { return '' }
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
# Every ecosystem the declared lanes belong to, deduped and empty when nothing is
|
|
1838
|
+
# declared. This keys the stack-partitioned corpora under foundations/<ecosystem>/ —
|
|
1839
|
+
# a Java repo has no business being offered a .NET bundle, and the lane model already
|
|
1840
|
+
# knows which it is. Twin of the bash _aw_ecosystems.
|
|
1841
|
+
function Get-Ecosystems {
|
|
1842
|
+
$out = @()
|
|
1843
|
+
foreach ($k in @('backend_ecosystem', 'frontend_ecosystem')) {
|
|
1844
|
+
$v = Get-LaneField -Field $k
|
|
1845
|
+
if ($v -and ($out -notcontains $v)) { $out += $v }
|
|
1846
|
+
}
|
|
1847
|
+
# NOT comma-wrapped. Every call site already writes @(Get-Ecosystems), and the
|
|
1848
|
+
# wrapper would make that a one-element array CONTAINING the array — which counts
|
|
1849
|
+
# as 1, contains no ecosystem name, and matches nothing.
|
|
1850
|
+
return $out
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
function Invoke-BuildCompile {
|
|
1854
|
+
param([string[]]$Rest)
|
|
1855
|
+
$repo = Get-RepoRoot
|
|
1856
|
+
|
|
1857
|
+
# A DECLARED lane model is the answer, and it is a better answer than the ladder
|
|
1858
|
+
# below in exactly the case that matters: a repo with two lanes. The sniffer takes
|
|
1859
|
+
# the first toolchain it finds and silently never builds the second, which reports
|
|
1860
|
+
# green for a frontend that was never compiled.
|
|
1861
|
+
if ((Get-LaneField -Field 'lane_count') -and (Get-LaneField -Field 'lane_count') -ne '0') {
|
|
1862
|
+
Invoke-AwStackCore -Op 'run-phase' -Rest @('--phase', 'build')
|
|
1863
|
+
return
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
# No declaration: the historic first-match-wins ladder, unchanged.
|
|
1867
|
+
$tool = 'none'; $ran = $false; $ok = $true; $cmd = ''; $log = ''
|
|
1868
|
+
$hasSln = @(Get-ChildItem -Path $repo -Filter *.sln -File -ErrorAction SilentlyContinue).Count -gt 0
|
|
1869
|
+
$hasCsproj = @(Get-ChildItem -Path (Join-Path $repo 'src') -Filter *.csproj -Recurse -ErrorAction SilentlyContinue).Count -gt 0
|
|
1870
|
+
if ($hasSln -or $hasCsproj) {
|
|
1871
|
+
$tool = 'dotnet'
|
|
1872
|
+
if (Get-Command dotnet -ErrorAction SilentlyContinue) {
|
|
1873
|
+
$cmd = 'dotnet build --nologo'; $ran = $true
|
|
1874
|
+
Push-Location $repo; try { $log = (dotnet build --nologo 2>&1 | Out-String); $ok = ($LASTEXITCODE -eq 0) } finally { Pop-Location }
|
|
1875
|
+
} else { $ok = $false }
|
|
1876
|
+
} elseif ((Test-Path (Join-Path $repo 'package.json')) -and (Get-Command npm -ErrorAction SilentlyContinue)) {
|
|
1877
|
+
$tool = 'npm'; $cmd = 'npm run build --if-present'; $ran = $true
|
|
1878
|
+
Push-Location $repo; try { $log = (npm run build --if-present 2>&1 | Out-String); $ok = ($LASTEXITCODE -eq 0) } finally { Pop-Location }
|
|
1879
|
+
}
|
|
1880
|
+
$tail = if ($log.Length -gt 1400) { $log.Substring($log.Length - 1400) } else { $log }
|
|
1881
|
+
Write-Json ([ordered]@{ ok = $ok; tool = $tool; ran = $ran; cmd = $cmd; log_tail = $tail })
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
function Invoke-TestRun {
|
|
1885
|
+
param([string[]]$Rest)
|
|
1886
|
+
# --scope repo (default): run the whole suite — the historic behaviour, every existing
|
|
1887
|
+
# caller is unaffected.
|
|
1888
|
+
# --scope slice : run ONLY the tests whose FullyQualifiedName contains the
|
|
1889
|
+
# feature's aggregate name (feature-dir basename) plus any --also referrer
|
|
1890
|
+
# names — the per-feature integrity scope. .NET only; npm/none ignore it
|
|
1891
|
+
# and run the full suite. (Kept byte-for-byte in step with the bash cmd_test_run.)
|
|
1892
|
+
$featureDir = ''; $scope = 'repo'; $also = ''
|
|
1893
|
+
for ($i = 0; $i -lt $Rest.Count; $i++) {
|
|
1894
|
+
switch ($Rest[$i]) {
|
|
1895
|
+
{ $_ -in '--feature-dir','-FeatureDir' } { $featureDir = $Rest[++$i] }
|
|
1896
|
+
{ $_ -in '--scope','-Scope' } { $scope = $Rest[++$i] }
|
|
1897
|
+
{ $_ -in '--also','-Also' } { $also = $Rest[++$i] }
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
$repo = Get-RepoRoot
|
|
1901
|
+
|
|
1902
|
+
# For scope=slice, build the token set = feature aggregate name + --also referrers, each
|
|
1903
|
+
# sanitised to an identifier (VSTest filter operators |&()!=~ are illegal) and de-duped.
|
|
1904
|
+
# A surviving token becomes a `FullyQualifiedName~<tok>` clause; substring match is
|
|
1905
|
+
# deliberate — it over-includes (safe for an integrity fence) rather than under-scoping.
|
|
1906
|
+
$slices = @()
|
|
1907
|
+
if ($scope -eq 'slice') {
|
|
1908
|
+
$names = @()
|
|
1909
|
+
if ($featureDir) { $names += (Split-Path -Leaf ($featureDir.TrimEnd('/','\'))) }
|
|
1910
|
+
if ($also) { $names += ($also -split ',') }
|
|
1911
|
+
foreach ($name in $names) {
|
|
1912
|
+
$tok = ($name -replace '[^A-Za-z0-9._-]','')
|
|
1913
|
+
if ($tok -notmatch '^[A-Za-z]') { continue } # must start with a letter
|
|
1914
|
+
if ($slices -notcontains $tok) { $slices += $tok } # de-dup
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
$tool = 'none'; $ran = $false; $ok = $true; $cmd = ''; $log = ''
|
|
1919
|
+
$matched = $false; $scoped = $false; $fenceGreen = $false; $filter = ''
|
|
1920
|
+
$hasSln = @(Get-ChildItem -Path $repo -Filter *.sln -File -ErrorAction SilentlyContinue).Count -gt 0
|
|
1921
|
+
$hasCsproj = @(Get-ChildItem -Path $repo -Filter *.csproj -Recurse -ErrorAction SilentlyContinue).Count -gt 0
|
|
1922
|
+
$dotnetPresent = ($hasSln -or $hasCsproj)
|
|
1923
|
+
|
|
1924
|
+
# A DECLARED lane model answers this the same way it answers build-compile, and for
|
|
1925
|
+
# the same reason: the ladder below recognises .NET and npm-at-the-root, and reports
|
|
1926
|
+
# `tool:"none" ran:false ok:true` for everything else. On a repo whose lane declares
|
|
1927
|
+
# `./mvnw test` that is a suite which never ran scoring the integrity fence GREEN.
|
|
1928
|
+
#
|
|
1929
|
+
# scope=slice keeps the .NET path whenever a .NET toolchain is present, because
|
|
1930
|
+
# VSTest's --filter is the only per-test selector any shipped profile has. Elsewhere a
|
|
1931
|
+
# slice run delegates and widens to the lane's whole suite, which the fence tolerates
|
|
1932
|
+
# by design, and reports scoped=false so fence_green comes from `ok`.
|
|
1933
|
+
# (Kept in step with the bash cmd_test_run.)
|
|
1934
|
+
$laneCount = Get-LaneField -Field 'lane_count'
|
|
1935
|
+
$useLanes = $false
|
|
1936
|
+
if ($laneCount -and $laneCount -ne '0') {
|
|
1937
|
+
if ($scope -ne 'slice' -or -not $dotnetPresent) { $useLanes = $true }
|
|
1938
|
+
}
|
|
1939
|
+
if ($useLanes) {
|
|
1940
|
+
$py = Get-Py
|
|
1941
|
+
$core = Join-Path $PSScriptRoot '..' 'python' 'aw-stack.py'
|
|
1942
|
+
$line = (& $py $core '--repo' $repo 'run-phase' '--phase' 'test' | Select-Object -First 1)
|
|
1943
|
+
$d = if ($line) { $line | ConvertFrom-Json } else { $null }
|
|
1944
|
+
$dLog = if ($d -and $d.log_tail) { [string]$d.log_tail } else { '' }
|
|
1945
|
+
$dOk = [bool]($d -and $d.ok)
|
|
1946
|
+
Write-Json ([ordered]@{
|
|
1947
|
+
ok = $dOk
|
|
1948
|
+
tool = $(if ($d -and $d.tool) { [string]$d.tool } else { 'lanes' })
|
|
1949
|
+
ran = [bool]($d -and $d.ran)
|
|
1950
|
+
scope = $scope
|
|
1951
|
+
slices = ($slices -join ' ')
|
|
1952
|
+
scoped = $false
|
|
1953
|
+
matched = ($dLog -match 'Total:\s*[1-9]')
|
|
1954
|
+
fence_green = $dOk
|
|
1955
|
+
cmd = $(if ($d -and $d.cmd) { [string]$d.cmd } else { '' })
|
|
1956
|
+
log_tail = $dLog
|
|
1957
|
+
lanes_run = @(if ($d) { $d.lanes_run } else { @() })
|
|
1958
|
+
lanes_skipped = @(if ($d) { $d.lanes_skipped } else { @() })
|
|
1959
|
+
})
|
|
1960
|
+
return
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
if ($dotnetPresent) {
|
|
1964
|
+
$tool = 'dotnet'
|
|
1965
|
+
if (Get-Command dotnet -ErrorAction SilentlyContinue) {
|
|
1966
|
+
if ($scope -eq 'slice') {
|
|
1967
|
+
if ($slices.Count -eq 0) {
|
|
1968
|
+
# scope=slice but nothing valid to filter on — fail loudly, never
|
|
1969
|
+
# silently fall back to a repo-wide run.
|
|
1970
|
+
$log = "scope=slice but no valid filter tokens from --feature-dir/--also`n"
|
|
1971
|
+
$ok = $false; $ran = $false; $cmd = '(no filter tokens)'
|
|
1972
|
+
} else {
|
|
1973
|
+
$filter = ($slices | ForEach-Object { "FullyQualifiedName~$_" }) -join '|'
|
|
1974
|
+
$scoped = $true; $cmd = "dotnet test --nologo --filter `"$filter`""; $ran = $true
|
|
1975
|
+
Push-Location $repo; try { $log = (dotnet test --nologo --filter $filter 2>&1 | Out-String); $ok = ($LASTEXITCODE -eq 0) } finally { Pop-Location }
|
|
1976
|
+
}
|
|
1977
|
+
} else {
|
|
1978
|
+
$cmd = 'dotnet test --nologo'; $ran = $true
|
|
1979
|
+
Push-Location $repo; try { $log = (dotnet test --nologo 2>&1 | Out-String); $ok = ($LASTEXITCODE -eq 0) } finally { Pop-Location }
|
|
1980
|
+
}
|
|
1981
|
+
} else { $ok = $false }
|
|
1982
|
+
} elseif ((Test-Path (Join-Path $repo 'package.json')) -and (Get-Command npm -ErrorAction SilentlyContinue)) {
|
|
1983
|
+
$tool = 'npm'; $cmd = 'npm test'; $ran = $true
|
|
1984
|
+
Push-Location $repo; try { $log = (npm test 2>&1 | Out-String); $ok = ($LASTEXITCODE -eq 0) } finally { Pop-Location }
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
# matched = at least one test was actually selected + run. Read from the LOG, never the
|
|
1988
|
+
# exit code: a .NET --filter that selects ZERO tests still EXITS 0, so trusting the exit
|
|
1989
|
+
# code would report an empty scoped run as green (the vacuous-pass trap).
|
|
1990
|
+
if ($log -match 'Total:\s*[1-9]') { $matched = $true }
|
|
1991
|
+
|
|
1992
|
+
# fence_green — a green that actually proves something ran.
|
|
1993
|
+
# scoped run → ok AND matched (guards the zero-match vacuous pass)
|
|
1994
|
+
# everything else (repo suite, npm, no test tool, the loud no-token fail) → ok.
|
|
1995
|
+
if ($scoped) { $fenceGreen = ($ok -and $matched) } else { $fenceGreen = $ok }
|
|
1996
|
+
|
|
1997
|
+
$tail = if ($log.Length -gt 1400) { $log.Substring($log.Length - 1400) } else { $log }
|
|
1998
|
+
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 })
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
function Invoke-RunMigrate {
|
|
2002
|
+
param([string[]]$Rest)
|
|
2003
|
+
$repo = Get-RepoRoot
|
|
2004
|
+
|
|
2005
|
+
# A DECLARED migration command wins outright. Schema migration is the phase stacks
|
|
2006
|
+
# answer most differently — Flyway, Liquibase, Alembic, Prisma, golang-migrate — and
|
|
2007
|
+
# no sniffer can pick between tools that happily coexist in one repo. When a lane
|
|
2008
|
+
# declares 'migrate' in .appweaver/stacks.json, that IS the answer and it runs in the
|
|
2009
|
+
# lane's own working directory like every other phase command.
|
|
2010
|
+
if ((Get-LaneField -Field 'backend_migrate') -or (Get-LaneField -Field 'frontend_migrate')) {
|
|
2011
|
+
$py = Get-Py
|
|
2012
|
+
$core = Join-Path $PSScriptRoot '..' 'python' 'aw-stack.py'
|
|
2013
|
+
$line = (& $py $core '--repo' $repo 'run-phase' '--phase' 'migrate' | Select-Object -First 1)
|
|
2014
|
+
$d = if ($line) { $line | ConvertFrom-Json } else { $null }
|
|
2015
|
+
Write-Json ([ordered]@{
|
|
2016
|
+
ok = [bool]($d -and $d.ok)
|
|
2017
|
+
tool = 'lanes'
|
|
2018
|
+
ran = [bool]($d -and $d.ran)
|
|
2019
|
+
cmd = $(if ($d) { [string]$d.cmd } else { '' })
|
|
2020
|
+
log_tail = $(if ($d) { [string]$d.log_tail } else { '' })
|
|
2021
|
+
lanes_run = @(if ($d) { $d.lanes_run } else { @() })
|
|
2022
|
+
})
|
|
2023
|
+
return
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
# WHERE to look for the migration project. The service lane's declared source path
|
|
2027
|
+
# when there is one, 'src' when there is not. Migrations belong to the backend lane
|
|
2028
|
+
# by definition, so a repo whose service code sits in 'Legacy/App/' or 'backend/'
|
|
2029
|
+
# gets searched where its code actually is instead of being told it has no .NET
|
|
2030
|
+
# project at all.
|
|
2031
|
+
$svcRoot = Get-LaneField -Field 'backend_root'
|
|
2032
|
+
if (-not $svcRoot) { $svcRoot = 'src' }
|
|
2033
|
+
$svcDir = Join-Path $repo ($svcRoot.TrimEnd('/'))
|
|
2034
|
+
|
|
2035
|
+
$tool = 'none'; $ran = $false; $ok = $true; $cmd = ''; $log = ''
|
|
2036
|
+
$hasSln = @(Get-ChildItem -Path $repo -Filter *.sln -File -ErrorAction SilentlyContinue).Count -gt 0
|
|
2037
|
+
$hasCsproj = @(Get-ChildItem -Path $svcDir -Filter *.csproj -Recurse -ErrorAction SilentlyContinue).Count -gt 0
|
|
2038
|
+
$efOk = $false
|
|
2039
|
+
if (($hasSln -or $hasCsproj) -and (Get-Command dotnet -ErrorAction SilentlyContinue)) {
|
|
2040
|
+
Push-Location $repo; try { dotnet ef --version *> $null; $efOk = ($LASTEXITCODE -eq 0) } catch { $efOk = $false } finally { Pop-Location }
|
|
2041
|
+
}
|
|
2042
|
+
if ($efOk) {
|
|
2043
|
+
# Prefer *Service.csproj (API host with ApplicationDbContext); fall back to first
|
|
2044
|
+
# non-AppHost/Web/ServiceDefaults/test project under the service lane's root.
|
|
2045
|
+
$project = Get-ChildItem -Path $svcDir -Filter '*.Service.csproj' -Recurse -ErrorAction SilentlyContinue |
|
|
2046
|
+
Select-Object -First 1 -ExpandProperty FullName
|
|
2047
|
+
if (-not $project) {
|
|
2048
|
+
$project = Get-ChildItem -Path $svcDir -Filter '*.csproj' -Recurse -ErrorAction SilentlyContinue |
|
|
2049
|
+
Where-Object { $_.FullName -notmatch 'AppHost|ServiceDefaults|\.Web\.|Tests' } |
|
|
2050
|
+
Select-Object -First 1 -ExpandProperty FullName
|
|
2051
|
+
}
|
|
2052
|
+
if ($project) {
|
|
2053
|
+
$tool = 'dotnet-ef'
|
|
2054
|
+
$cmd = "dotnet ef database update --project $project --context ApplicationDbContext"
|
|
2055
|
+
$ran = $true
|
|
2056
|
+
Push-Location $repo
|
|
2057
|
+
try {
|
|
2058
|
+
$log = (dotnet ef database update --project $project --context ApplicationDbContext 2>&1 | Out-String)
|
|
2059
|
+
$ok = ($LASTEXITCODE -eq 0)
|
|
2060
|
+
} finally { Pop-Location }
|
|
2061
|
+
} else {
|
|
2062
|
+
$tool = 'dotnet-ef'; $cmd = 'dotnet ef database update'; $ran = $true
|
|
2063
|
+
Push-Location $repo; try { $log = (dotnet ef database update 2>&1 | Out-String); $ok = ($LASTEXITCODE -eq 0) } finally { Pop-Location }
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
$tail = if ($log.Length -gt 1400) { $log.Substring($log.Length - 1400) } else { $log }
|
|
2067
|
+
Write-Json ([ordered]@{ ok = $ok; tool = $tool; ran = $ran; cmd = $cmd; log_tail = $tail })
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
function Invoke-DeployDispatch {
|
|
2071
|
+
param([string[]]$Rest)
|
|
2072
|
+
$repo = Get-RepoRoot; $cfg = Join-Path $repo '.appweaver/deploy.json'
|
|
2073
|
+
$configured = $false; $ran = $false; $ok = $true; $cmd = ''; $log = ''
|
|
2074
|
+
if (Test-Path $cfg) { try { $cmd = (Get-Content $cfg -Raw | ConvertFrom-Json).command } catch { $cmd = '' } }
|
|
2075
|
+
if ($cmd) {
|
|
2076
|
+
$configured = $true; $ran = $true
|
|
2077
|
+
Push-Location $repo; try { $log = (Invoke-Expression $cmd 2>&1 | Out-String); $ok = ($LASTEXITCODE -eq 0) } catch { $ok = $false; $log = $_.ToString() } finally { Pop-Location }
|
|
2078
|
+
}
|
|
2079
|
+
$tail = if ($log.Length -gt 1400) { $log.Substring($log.Length - 1400) } else { $log }
|
|
2080
|
+
Write-Json ([ordered]@{ ok = $ok; configured = $configured; ran = $ran; cmd = $cmd; log_tail = $tail })
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
function Invoke-CrSetup {
|
|
2084
|
+
param([string[]]$Rest)
|
|
2085
|
+
$crId = ''; $fdArg = ''; $title = ''; $force = ''
|
|
2086
|
+
for ($i = 0; $i -lt $Rest.Count; $i++) {
|
|
2087
|
+
switch ($Rest[$i]) {
|
|
2088
|
+
{ $_ -in '--cr-id','-CrId' } { $crId = $Rest[++$i] }
|
|
2089
|
+
{ $_ -in '--feature-dir','-FeatureDir' } { $fdArg = $Rest[++$i] }
|
|
2090
|
+
{ $_ -in '--title','-Title' } { $title = $Rest[++$i] }
|
|
2091
|
+
{ $_ -in '--force','-Force' } { $force = '1' }
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
if (-not $crId) { $crId = 'CR-' + (Get-Date -Format 'yyyyMMdd-HHmmss') }
|
|
2095
|
+
$repo = Get-RepoRoot; $fd = Resolve-FeatureDir -Arg $fdArg -RepoRoot $repo
|
|
2096
|
+
if (-not $fd) { Write-Error 'cr-setup: no feature set and no --feature-dir'; exit 1 }
|
|
2097
|
+
# Single-active-change guard: open the change FIRST so a second concurrent CR on the
|
|
2098
|
+
# same aggregate is refused BEFORE any changes/<cr>/ dir is scaffolded (no orphan
|
|
2099
|
+
# left behind). Re-opening the same cr_id is a no-op. --force overrides.
|
|
2100
|
+
$avOpen = Invoke-ActiveVersion -FeatureDir $fd -Op 'open' -CrId $crId -Force $force
|
|
2101
|
+
if ($LASTEXITCODE -and $LASTEXITCODE -ne 0) {
|
|
2102
|
+
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."
|
|
2103
|
+
$avOpen
|
|
2104
|
+
exit 1
|
|
2105
|
+
}
|
|
2106
|
+
$crDir = Join-Path $fd "changes/$crId"
|
|
2107
|
+
New-Item -ItemType Directory -Force -Path $crDir | Out-Null
|
|
2108
|
+
$dspec = Join-Path $crDir 'delta-spec.md'; $ddesign = Join-Path $crDir 'delta-design.md'; $created = $false
|
|
2109
|
+
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 }
|
|
2110
|
+
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 }
|
|
2111
|
+
Write-Json ([ordered]@{ cr_id = $crId; cr_dir = $crDir; delta_spec = $dspec; delta_design = $ddesign; title = $title; created = $created })
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
function Invoke-CrArchive {
|
|
2115
|
+
param([string[]]$Rest)
|
|
2116
|
+
$crId = ''; $fdArg = ''
|
|
2117
|
+
for ($i = 0; $i -lt $Rest.Count; $i++) {
|
|
2118
|
+
switch ($Rest[$i]) {
|
|
2119
|
+
{ $_ -in '--cr-id','-CrId' } { $crId = $Rest[++$i] }
|
|
2120
|
+
{ $_ -in '--feature-dir','-FeatureDir' } { $fdArg = $Rest[++$i] }
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
if (-not $crId) { Write-Error 'cr-archive: --cr-id required'; exit 1 }
|
|
2124
|
+
$repo = Get-RepoRoot; $fd = Resolve-FeatureDir -Arg $fdArg -RepoRoot $repo
|
|
2125
|
+
if (-not $fd) { Write-Error 'cr-archive: no feature set and no --feature-dir'; exit 1 }
|
|
2126
|
+
$crDir = Join-Path $fd "changes/$crId"; $archRoot = Join-Path $fd 'changes/archive'; $archived = ''
|
|
2127
|
+
if (Test-Path $crDir) {
|
|
2128
|
+
New-Item -ItemType Directory -Force -Path $archRoot | Out-Null
|
|
2129
|
+
$dest = Join-Path $archRoot $crId
|
|
2130
|
+
if (Test-Path $dest) { Remove-Item -Recurse -Force $dest }
|
|
2131
|
+
Move-Item $crDir $dest; $archived = $dest
|
|
2132
|
+
}
|
|
2133
|
+
# Close the change in active_version.json (single version source): bump + clear.
|
|
2134
|
+
$av = Invoke-ActiveVersion -FeatureDir $fd -Op 'close' | ConvertFrom-Json
|
|
2135
|
+
Write-Json ([ordered]@{ ok = $true; archived_dir = $archived; version = $av.version; previous = $av.previous; closed_change = $av.closed_change })
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
# Stamp a version into the MASTER documents' `**Version**:` headers — the
|
|
2139
|
+
# deterministic twin of cr-archive's active_version.json bump. close-delta calls
|
|
2140
|
+
# this with the version cr-archive just computed, so all THREE carriers of the
|
|
2141
|
+
# number (spec.md, design.md, active_version.json) move in ONE stage by ONE
|
|
2142
|
+
# mechanism. Before this, design.md's header was bumped by the MODEL at
|
|
2143
|
+
# finalize-delta — a stage early, and guessing the target since the number was
|
|
2144
|
+
# never passed into the merge prompt — while spec.md's header was bumped by
|
|
2145
|
+
# nobody at all and sat at v1.0 forever.
|
|
2146
|
+
# Idempotent: re-stamping the same version reports `unchanged`, writes nothing.
|
|
2147
|
+
# Only the value moves; any trailing inline comment on the header line is kept.
|
|
2148
|
+
function Invoke-VersionStamp {
|
|
2149
|
+
param([string[]]$Rest)
|
|
2150
|
+
$ver = ''; $fdArg = ''
|
|
2151
|
+
for ($i = 0; $i -lt $Rest.Count; $i++) {
|
|
2152
|
+
switch ($Rest[$i]) {
|
|
2153
|
+
{ $_ -in '--version','-Version' } { $ver = $Rest[++$i] }
|
|
2154
|
+
{ $_ -in '--feature-dir','-FeatureDir' } { $fdArg = $Rest[++$i] }
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
if (-not $ver) { Write-Error 'version-stamp: --version required'; exit 1 }
|
|
2158
|
+
$repo = Get-RepoRoot; $fd = Resolve-FeatureDir -Arg $fdArg -RepoRoot $repo
|
|
2159
|
+
if (-not $fd) { Write-Error 'version-stamp: no feature set and no --feature-dir'; exit 1 }
|
|
2160
|
+
$ver = $ver.Trim()
|
|
2161
|
+
[string[]]$stamped = @(); [string[]]$unchanged = @(); [string[]]$missing = @(); [string[]]$noHeader = @()
|
|
2162
|
+
# First `**Version**: <value>` line only — that is the header. The Change History
|
|
2163
|
+
# table's `| Version |` column head does not match this pattern.
|
|
2164
|
+
# design.ui.md joins the spine where the feature has a UI half: the masters move in
|
|
2165
|
+
# LOCKSTEP, so a document left behind at the old number is the one lie the version
|
|
2166
|
+
# spine cannot tolerate. A feature without one reports it `missing` and nothing fails.
|
|
2167
|
+
foreach ($name in @('spec.md', 'design.md', 'design.ui.md')) {
|
|
2168
|
+
$p = Join-Path $fd $name
|
|
2169
|
+
if (-not (Test-Path $p)) { $missing += $name; continue }
|
|
2170
|
+
$src = Get-Content -Raw -Encoding UTF8 $p
|
|
2171
|
+
$m = [regex]::Match($src, '(?m)^(\*\*Version\*\*:[ \t]*)(\S+)(.*)$')
|
|
2172
|
+
if (-not $m.Success) { $noHeader += $name; continue }
|
|
2173
|
+
if ($m.Groups[2].Value -eq $ver) { $unchanged += $name; continue }
|
|
2174
|
+
$out = $src.Substring(0, $m.Index) + $m.Groups[1].Value + $ver + $m.Groups[3].Value +
|
|
2175
|
+
$src.Substring($m.Index + $m.Length)
|
|
2176
|
+
[System.IO.File]::WriteAllText($p, $out)
|
|
2177
|
+
$stamped += $name
|
|
2178
|
+
}
|
|
2179
|
+
Write-Json ([ordered]@{ ok = $true; version = $ver; stamped = $stamped; unchanged = $unchanged;
|
|
2180
|
+
missing = $missing; no_header = $noHeader; count = $stamped.Count })
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
# ─────────────────────────────── main ──────────────────────────────────────
|
|
2184
|
+
if ($args.Count -lt 1) { Write-Output 'Usage: aw-spec-actions.ps1 <subcommand> [options]'; exit 0 }
|
|
2185
|
+
$sub = $args[0]
|
|
2186
|
+
$rest = @(); if ($args.Count -gt 1) { $rest = $args[1..($args.Count - 1)] }
|
|
2187
|
+
|
|
2188
|
+
switch ($sub) {
|
|
2189
|
+
'resolve-domain' { Invoke-ResolveDomain -Rest $rest }
|
|
2190
|
+
'resolve-tech' { Invoke-ResolveTech -Rest $rest }
|
|
2191
|
+
'scaffold' { Invoke-Scaffold -Rest $rest }
|
|
2192
|
+
'resolve-hooks' { Invoke-ResolveHooks -Rest $rest }
|
|
2193
|
+
'create-checklist' { Invoke-CreateChecklist -Rest $rest }
|
|
2194
|
+
'list-domains' { Invoke-ListDomains -Rest $rest }
|
|
2195
|
+
'set-domain' { Invoke-SetDomain -Rest $rest }
|
|
2196
|
+
'stack-list' { Invoke-StackList -Rest $rest }
|
|
2197
|
+
'stack-detect' { Invoke-StackDetect -Rest $rest }
|
|
2198
|
+
'lane-declare' { Invoke-LaneDeclare -Rest $rest }
|
|
2199
|
+
'lane-resolve' { Invoke-LaneResolve -Rest $rest }
|
|
2200
|
+
'lane-verify' { Invoke-LaneVerify -Rest $rest }
|
|
2201
|
+
'lane-probe' { Invoke-LaneProbe -Rest $rest }
|
|
2202
|
+
'list-features' { Invoke-ListFeatures -Rest $rest }
|
|
2203
|
+
'set-feature' { Invoke-SetFeature -Rest $rest }
|
|
2204
|
+
'version-read' { Invoke-VersionState 'read' $rest }
|
|
2205
|
+
'version-init' { Invoke-VersionState 'init' $rest }
|
|
2206
|
+
'feature-paths' { Invoke-FeaturePaths -Rest $rest }
|
|
2207
|
+
'design-scaffold' { Invoke-DesignScaffold -Rest $rest }
|
|
2208
|
+
'build-compile' { Invoke-BuildCompile -Rest $rest }
|
|
2209
|
+
'test-run' { Invoke-TestRun -Rest $rest }
|
|
2210
|
+
'run-migrate' { Invoke-RunMigrate -Rest $rest }
|
|
2211
|
+
'deploy-dispatch' { Invoke-DeployDispatch -Rest $rest }
|
|
2212
|
+
'cr-setup' { Invoke-CrSetup -Rest $rest }
|
|
2213
|
+
'cr-archive' { Invoke-CrArchive -Rest $rest }
|
|
2214
|
+
'version-stamp' { Invoke-VersionStamp -Rest $rest }
|
|
2215
|
+
'checklist-read' { Invoke-ChecklistRead -Rest $rest }
|
|
2216
|
+
'checklist-apply' { Invoke-ChecklistApply -Rest $rest }
|
|
2217
|
+
'detect-project' { Invoke-DetectProject -Rest $rest }
|
|
2218
|
+
'foundation-list' { Invoke-FoundationList -Rest $rest }
|
|
2219
|
+
'foundation-locate' { Invoke-FoundationLocate -Rest $rest }
|
|
2220
|
+
'foundation-unpack' { Invoke-FoundationUnpack -Rest $rest }
|
|
2221
|
+
'foundation-verify' { Invoke-FoundationVerify -Rest $rest }
|
|
2222
|
+
'foundation-stash-dbcontext' { Invoke-FoundationStashDbContext -Rest $rest }
|
|
2223
|
+
'list-components' { Invoke-ListComponents -Rest $rest }
|
|
2224
|
+
'component-catalog' { Invoke-ComponentCatalog -Rest $rest }
|
|
2225
|
+
'component-verify' { Invoke-ComponentVerify -Rest $rest }
|
|
2226
|
+
'find-host-project' { Invoke-FindHostProject -Rest $rest }
|
|
2227
|
+
'setup-constitution' { Invoke-SetupConstitution -Rest $rest }
|
|
2228
|
+
'compose-constitution' { Invoke-ComposeConstitution -Rest $rest }
|
|
2229
|
+
'constitution-verify' { Invoke-ConstitutionVerify -Rest $rest }
|
|
2230
|
+
'agent-context-update' { Invoke-AgentContextUpdate -Rest $rest }
|
|
2231
|
+
default { Write-Error "Unknown subcommand: $sub"; exit 1 }
|
|
2232
|
+
}
|