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,2626 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# aw-spec-actions.sh — common action dispatcher for the aw-spec orchestrator.
|
|
4
|
+
#
|
|
5
|
+
# ONE shared script for every integration (.claude / .agents / .kilocode). The
|
|
6
|
+
# orchestrator workflows reach these subcommands through `execute_script` steps.
|
|
7
|
+
# Each subcommand performs deterministic, governance-safe work and prints a single
|
|
8
|
+
# JSON object on stdout. NONE of these actions author spec/code content — content
|
|
9
|
+
# generation is the LLM's job, in the workflow's agent_* steps.
|
|
10
|
+
#
|
|
11
|
+
# Usage:
|
|
12
|
+
# aw-spec-actions.sh <subcommand> [options]
|
|
13
|
+
#
|
|
14
|
+
# Subcommands:
|
|
15
|
+
# resolve-domain --command <name> -> {domain, rulemap_exists, candidates[]}
|
|
16
|
+
# resolve-tech --command <name> -> {domain, techrulemap_exists, candidates[]}
|
|
17
|
+
# scaffold --feature-dir <path> [--allow-existing]
|
|
18
|
+
# -> {feature_dir, spec_file, template, created, spec_created}
|
|
19
|
+
# resolve-hooks --phase <before_specify|after_specify>
|
|
20
|
+
# -> {phase, hooks[]}
|
|
21
|
+
# create-checklist --feature-dir <path> [--feature-name <name>]
|
|
22
|
+
# -> {checklist_file, created}
|
|
23
|
+
# list-domains -> {domains[]}
|
|
24
|
+
# set-domain --domain <name> -> {domain, map_path, map_exists, written}
|
|
25
|
+
# stack-list [--kind backend|frontend|all] [--preselect <text>] [--include-none] [--suggest]
|
|
26
|
+
# -> {profiles[], ids[], select_options[], count, preselect,
|
|
27
|
+
# detected_profile, detected_root, root_options[], none_sentinel}
|
|
28
|
+
# (--suggest also detects, so the menu opens on what the repo IS)
|
|
29
|
+
# stack-detect [--kind <k>] [--exclude-root <p>]… -> {matches[], backend, frontend, count}
|
|
30
|
+
# (matches the repo's OWN files against the profiles; writes nothing)
|
|
31
|
+
# lane-declare --profile <id|label|none|detected> [--kind <k>] [--id <lane>] [--source-path|--root <p>]
|
|
32
|
+
# [--stack <label>] [--origin declared|reverse-engineered] [--require-existing-root]
|
|
33
|
+
# [--build <c>] [--test <c>] [--run <c>] [--prototype-root <p>] [--prototype-run <c>] [--no-prototype]
|
|
34
|
+
# -> {declared, lane, lanes[], lane_count, source_path, root_exists, prototype, written}
|
|
35
|
+
# (SOLE WRITER of .appweaver/stacks.json; callers = select-stack, onboard)
|
|
36
|
+
# lane-resolve [--kind <k>] [--id <lane>] -> {declared, lanes[], found, lane, root, source_path, build, test, run,
|
|
37
|
+
# backend_*/frontend_* (declared, root, source_path, root_exists, build, test, run), prototype}
|
|
38
|
+
# (absence is a valid state: no stacks.json -> declared:false, exit 0)
|
|
39
|
+
# lane-verify [--require backend|frontend]… [--allow-missing-root] [--command <phase>]
|
|
40
|
+
# -> {ok, declared, issues[], issue_count, message, backend_*/frontend_*}
|
|
41
|
+
# (the GATE: is the declaration usable for the phase about to run.
|
|
42
|
+
# Always exits 0 — the workflow's verify_context owns the stop)
|
|
43
|
+
# lane-probe [--kind <k>] [--hint <text>] -> {declared, declared_profile, hint_profile, hint_terms[], signals[], any, evidence}
|
|
44
|
+
# (the FILE signals for "does this lane exist"; resolve-tech carries the rule signal)
|
|
45
|
+
# list-features [--preselect <name>] [--bundle <dir>] -> {features[], names[], count, select_options[], sentinel} (specs/ ∪ OKF aggregates)
|
|
46
|
+
# set-feature --feature <name> -> {feature, feature_dir, spec_file, existed, created, spec_exists}
|
|
47
|
+
# version-read [--feature-dir <path>] -> {feature, version, active_change, exists}
|
|
48
|
+
# version-init [--feature-dir <path>] -> {feature, version, active_change, created} (create active_version.json v1.0)
|
|
49
|
+
# feature-paths -> {feature, feature_dir, spec_file, design_file, spec_exists, design_exists}
|
|
50
|
+
# checklist-read --file <path> -> {exists, total, checked, items[]}
|
|
51
|
+
# checklist-apply --file <path> --results <json>-> {exists, before_checked, after_checked, total, newly_passing[], regressions[]}
|
|
52
|
+
# detect-project -> {scaffolded, markers[]}
|
|
53
|
+
# foundation-list [--args '<F> <A> [out]'] -> {foundations[], count, preselect_name, preselect_app, out_dir}
|
|
54
|
+
# foundation-locate --name <F> --app <A> [--out <d>]
|
|
55
|
+
# -> {ok, reason, name, app, out_dir, pack_file, unpacker, runner, sentinel, file_count, available[]}
|
|
56
|
+
# foundation-unpack --name <F> --app <A> --out <d> [--dry-run] [--force <yes|no>] [--reference]
|
|
57
|
+
# -> {ok, mode, written, has_collisions, stdout}
|
|
58
|
+
# foundation-verify --out <d> --sentinel <s> -> {clean, residue[]}
|
|
59
|
+
# list-components -> {components[]} (foundations/dotnet/components/* with integration.md)
|
|
60
|
+
# find-host-project -> {found, program_cs, project_dir, csproj}
|
|
61
|
+
# setup-constitution [--ui] -> {constitution_technical_file, constitution_domain_file, constitution_ui_file, technical_created, domain_created, ui_created}
|
|
62
|
+
# compose-constitution -> {composed_file, ok, domain_included, ui_included, numbering_ok, numbering_checked, issues, technical_last, domain_first}
|
|
63
|
+
# constitution-verify --file <path> [--template <name>]
|
|
64
|
+
# -> {ok, exists, issues, placeholders, placeholder_check, version_body, version_report, version_match, bad_dates}
|
|
65
|
+
# agent-context-update [--plan <path>] -> {ok, ran, message}
|
|
66
|
+
#
|
|
67
|
+
# design-scaffold --feature-dir <path> [--template <name>]
|
|
68
|
+
# -> {feature_dir, design_doc, created, override_active, template, template_source}
|
|
69
|
+
# (design-extract / design-reconcile / design-paths are RETIRED — the path-tagged
|
|
70
|
+
# four-document model is gone. "Which files is this slice made of" is now an OKF
|
|
71
|
+
# query: aw-xref concept-paths / concept-upsert --merge.)
|
|
72
|
+
# build-compile [--feature-dir <path>] -> {ok, tool, ran, cmd, log_tail}
|
|
73
|
+
# test-run [--feature-dir <path>] -> {ok, tool, ran, cmd, log_tail}
|
|
74
|
+
# run-migrate [--feature-dir <path>] -> {ok, tool, ran, cmd, log_tail}
|
|
75
|
+
# deploy-dispatch -> {ok, configured, ran, cmd, log_tail}
|
|
76
|
+
# cr-setup --cr-id <id> [--feature-dir <path>] [--title <t>]
|
|
77
|
+
# -> {cr_dir, delta_spec, delta_design, created}
|
|
78
|
+
# cr-archive --cr-id <id> [--feature-dir <path>]
|
|
79
|
+
# -> {ok, archived_dir, version, previous}
|
|
80
|
+
#
|
|
81
|
+
# All subcommands accept --help.
|
|
82
|
+
|
|
83
|
+
set -e
|
|
84
|
+
|
|
85
|
+
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
86
|
+
# shellcheck source=common.sh
|
|
87
|
+
source "$SCRIPT_DIR/common.sh"
|
|
88
|
+
|
|
89
|
+
# ─────────────────────────── small JSON helpers ────────────────────────────
|
|
90
|
+
# Emit a JSON array of strings from remaining args.
|
|
91
|
+
json_str_array() {
|
|
92
|
+
local out="[" first=1 s
|
|
93
|
+
for s in "$@"; do
|
|
94
|
+
[ $first -eq 1 ] && first=0 || out+=","
|
|
95
|
+
out+="\"$(json_escape "$s")\""
|
|
96
|
+
done
|
|
97
|
+
out+="]"
|
|
98
|
+
printf '%s' "$out"
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
# ─────────────────────────────── resolve-domain ────────────────────────────
|
|
102
|
+
cmd_resolve_domain() {
|
|
103
|
+
local command_name="aw-spec-specify"
|
|
104
|
+
while [ $# -gt 0 ]; do
|
|
105
|
+
case "$1" in
|
|
106
|
+
--command) command_name="$2"; shift 2 ;;
|
|
107
|
+
--help|-h) echo "resolve-domain --command <name>"; return 0 ;;
|
|
108
|
+
*) echo "resolve-domain: unknown arg '$1'" >&2; return 1 ;;
|
|
109
|
+
esac
|
|
110
|
+
done
|
|
111
|
+
|
|
112
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
113
|
+
local domain_json="$repo_root/.appweaver/domain.json"
|
|
114
|
+
local domain=""
|
|
115
|
+
if [ -f "$domain_json" ]; then
|
|
116
|
+
if command -v jq >/dev/null 2>&1; then
|
|
117
|
+
domain=$(jq -r '.domain // ""' "$domain_json" 2>/dev/null || echo "")
|
|
118
|
+
else
|
|
119
|
+
domain=$( { grep -E '"domain"[[:space:]]*:' "$domain_json" 2>/dev/null || true; } \
|
|
120
|
+
| head -n1 | sed -E 's/^[^:]*:[[:space:]]*"([^"]*)".*$/\1/' )
|
|
121
|
+
fi
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
local rulemap="$repo_root/guardrails/projects/$domain/domain-rule-bindings.md"
|
|
125
|
+
local rulemap_exists=false
|
|
126
|
+
[ -n "$domain" ] && [ -f "$rulemap" ] && rulemap_exists=true
|
|
127
|
+
|
|
128
|
+
# Collect bullet paths under "## /<command>" in the domain's domain-rule-bindings.md.
|
|
129
|
+
local -a candidates=()
|
|
130
|
+
if [ "$rulemap_exists" = true ]; then
|
|
131
|
+
# awk: capture "- path" bullets between the target "## <heading>" and the next "## "
|
|
132
|
+
while IFS= read -r line; do
|
|
133
|
+
[ -n "$line" ] && candidates+=("$line")
|
|
134
|
+
done < <(awk -v h="## /$command_name" '
|
|
135
|
+
$0==h {grab=1; next}
|
|
136
|
+
/^## / {grab=0}
|
|
137
|
+
grab && /^[[:space:]]*-[[:space:]]+/ {
|
|
138
|
+
sub(/^[[:space:]]*-[[:space:]]+/,""); sub(/[[:space:]]+$/,""); print
|
|
139
|
+
}
|
|
140
|
+
' "$rulemap")
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
printf '{"domain":"%s","rulemap_exists":%s,"candidates":%s}\n' \
|
|
144
|
+
"$(json_escape "$domain")" "$rulemap_exists" "$(json_str_array "${candidates[@]}")"
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
# ──────────────────────────────── resolve-tech ─────────────────────────────
|
|
148
|
+
# Technical rule-file mapping — parallel to resolve-domain, but reads
|
|
149
|
+
# guardrails/projects/<project>/tech-rule-bindings.md. The TECHNICAL rule files live in the
|
|
150
|
+
# shared per-stack corpora at guardrails/technology/<stack>/ (rules) and
|
|
151
|
+
# guardrails/technology/<stack>/examples/ (the worked sample for a rule file), and are
|
|
152
|
+
# configured per-command in this map, so they can be tuned per project WITHOUT editing the
|
|
153
|
+
# workflows and a second project can map the same corpus. The technical CONSTITUTION
|
|
154
|
+
# (constitution.technical.md) is NOT mapped — it is still referred to directly by
|
|
155
|
+
# each consumer, as-is.
|
|
156
|
+
#
|
|
157
|
+
# A bullet may name its stack with a PLACEHOLDER — {{stack}} / {{backend_stack}} /
|
|
158
|
+
# {{frontend_stack}} / {{backend_ecosystem}} / {{frontend_ecosystem}} — resolved from
|
|
159
|
+
# the lanes in .appweaver/stacks.json. That is what lets one map serve a Spring repo and
|
|
160
|
+
# a .NET repo without an edit. A bullet that instead spells a stack directory out is kept
|
|
161
|
+
# and USED (it may be a deliberate cross-stack reference) but reported through
|
|
162
|
+
# stack_mismatch when it is not a stack this repo declares, because a map pinned to the
|
|
163
|
+
# wrong corpus is how a Java project ends up being handed C# rules.
|
|
164
|
+
cmd_resolve_tech() {
|
|
165
|
+
local command_name="aw-spec-design"
|
|
166
|
+
while [ $# -gt 0 ]; do
|
|
167
|
+
case "$1" in
|
|
168
|
+
--command) command_name="$2"; shift 2 ;;
|
|
169
|
+
--help|-h) echo "resolve-tech --command <name>"; return 0 ;;
|
|
170
|
+
*) echo "resolve-tech: unknown arg '$1'" >&2; return 1 ;;
|
|
171
|
+
esac
|
|
172
|
+
done
|
|
173
|
+
|
|
174
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
175
|
+
local domain_json="$repo_root/.appweaver/domain.json"
|
|
176
|
+
local domain=""
|
|
177
|
+
if [ -f "$domain_json" ]; then
|
|
178
|
+
if command -v jq >/dev/null 2>&1; then
|
|
179
|
+
domain=$(jq -r '.domain // ""' "$domain_json" 2>/dev/null || echo "")
|
|
180
|
+
else
|
|
181
|
+
domain=$( { grep -E '"domain"[[:space:]]*:' "$domain_json" 2>/dev/null || true; } \
|
|
182
|
+
| head -n1 | sed -E 's/^[^:]*:[[:space:]]*"([^"]*)".*$/\1/' )
|
|
183
|
+
fi
|
|
184
|
+
fi
|
|
185
|
+
|
|
186
|
+
# The declared stacks, so one map can be written for any of them.
|
|
187
|
+
local be_prof fe_prof be_eco fe_eco
|
|
188
|
+
be_prof=$(_aw_lane_field backend profile); fe_prof=$(_aw_lane_field frontend profile)
|
|
189
|
+
be_eco=$(_aw_lane_field backend ecosystem); fe_eco=$(_aw_lane_field frontend ecosystem)
|
|
190
|
+
|
|
191
|
+
AW_REPO="$repo_root" AW_DOMAIN="$domain" AW_COMMAND="$command_name" \
|
|
192
|
+
AW_BE_PROFILE="$be_prof" AW_FE_PROFILE="$fe_prof" \
|
|
193
|
+
AW_BE_ECO="$be_eco" AW_FE_ECO="$fe_eco" python3 - <<'RESOLVE_TECH_PY'
|
|
194
|
+
import json, os, re
|
|
195
|
+
|
|
196
|
+
repo = os.environ["AW_REPO"]
|
|
197
|
+
domain = os.environ.get("AW_DOMAIN", "")
|
|
198
|
+
command = os.environ.get("AW_COMMAND", "")
|
|
199
|
+
|
|
200
|
+
techmap = os.path.join(repo, "guardrails", "projects", domain, "tech-rule-bindings.md")
|
|
201
|
+
exists = bool(domain) and os.path.isfile(techmap)
|
|
202
|
+
|
|
203
|
+
# WHAT A PLACEHOLDER IS FOR. A technical rule corpus is stack-partitioned —
|
|
204
|
+
# guardrails/technology/<stack>/ — so a bullet that spells the stack out is pinned to
|
|
205
|
+
# it, and the same map read from a Spring repo loads .NET rules into the standards the
|
|
206
|
+
# generator obeys. The placeholders let ONE map serve every stack a project declares:
|
|
207
|
+
# they resolve out of .appweaver/stacks.json, already the single answer to "what is
|
|
208
|
+
# this repo built in". Bullets without one are untouched, so every existing map reads
|
|
209
|
+
# exactly as it did.
|
|
210
|
+
subs = {
|
|
211
|
+
"backend_stack": os.environ.get("AW_BE_PROFILE", ""),
|
|
212
|
+
"frontend_stack": os.environ.get("AW_FE_PROFILE", ""),
|
|
213
|
+
"backend_ecosystem": os.environ.get("AW_BE_ECO", ""),
|
|
214
|
+
"frontend_ecosystem": os.environ.get("AW_FE_ECO", ""),
|
|
215
|
+
}
|
|
216
|
+
subs["stack"] = subs["backend_stack"] # the unqualified form means the service lane
|
|
217
|
+
declared = set(v for v in subs.values() if v)
|
|
218
|
+
|
|
219
|
+
HOLE = re.compile(r"\{\{\s*([a-z_]+)\s*\}\}")
|
|
220
|
+
|
|
221
|
+
def expand(line):
|
|
222
|
+
"""(resolved path, reason) — reason is '' when the line resolved."""
|
|
223
|
+
for h in HOLE.findall(line):
|
|
224
|
+
if h not in subs:
|
|
225
|
+
return "", "unknown placeholder {{%s}}" % h
|
|
226
|
+
if not subs[h]:
|
|
227
|
+
# A frontend placeholder in a backend-only repo is not an error: the lane
|
|
228
|
+
# genuinely does not exist, so its rules genuinely do not apply.
|
|
229
|
+
return "", "no lane declared for {{%s}}" % h
|
|
230
|
+
return HOLE.sub(lambda m: subs[m.group(1)], line), ""
|
|
231
|
+
|
|
232
|
+
bullets = []
|
|
233
|
+
if exists:
|
|
234
|
+
text = open(techmap, encoding="utf-8").read().splitlines()
|
|
235
|
+
for heading in ("## /" + command, "## /" + command.replace("decl-", "")):
|
|
236
|
+
found, grab = [], False
|
|
237
|
+
for line in text:
|
|
238
|
+
if line.strip() == heading:
|
|
239
|
+
grab = True; continue
|
|
240
|
+
if line.startswith("## "):
|
|
241
|
+
grab = False; continue
|
|
242
|
+
if grab:
|
|
243
|
+
m = re.match(r"^\s*-\s+(.*?)\s*$", line)
|
|
244
|
+
if m:
|
|
245
|
+
found.append(m.group(1))
|
|
246
|
+
if found:
|
|
247
|
+
bullets = found
|
|
248
|
+
break
|
|
249
|
+
|
|
250
|
+
candidates, missing, skipped, mismatched = [], [], [], []
|
|
251
|
+
for b in bullets:
|
|
252
|
+
path, why = expand(b)
|
|
253
|
+
if why:
|
|
254
|
+
skipped.append({"bullet": b, "why": why}); continue
|
|
255
|
+
(candidates if os.path.isfile(os.path.join(repo, path)) else missing).append(path)
|
|
256
|
+
# A LITERAL stack directory that is not one this repo declares. Reported, never
|
|
257
|
+
# dropped: the file exists and the project chose it, so it may be a deliberate
|
|
258
|
+
# cross-stack reference — but it is also exactly how a Java repo ends up being
|
|
259
|
+
# handed C# rules, and that must not be invisible.
|
|
260
|
+
m = re.match(r"^guardrails/technology/([^/]+)/", path)
|
|
261
|
+
if m and declared and m.group(1) not in declared and "{{" not in b:
|
|
262
|
+
if m.group(1) not in mismatched:
|
|
263
|
+
mismatched.append(m.group(1))
|
|
264
|
+
|
|
265
|
+
print(json.dumps({
|
|
266
|
+
"domain": domain,
|
|
267
|
+
"techrulemap_exists": exists,
|
|
268
|
+
"candidates": candidates,
|
|
269
|
+
"missing": missing,
|
|
270
|
+
"skipped": skipped,
|
|
271
|
+
"declared_stacks": sorted(declared),
|
|
272
|
+
"stack_mismatch": bool(mismatched),
|
|
273
|
+
"mismatched_stacks": mismatched,
|
|
274
|
+
}))
|
|
275
|
+
RESOLVE_TECH_PY
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
# ─────────────────────────────── derive-names ──────────────────────────────
|
|
279
|
+
# Deterministic naming — NO LLM. Aggregate/Feature token wins; else kebab
|
|
280
|
+
# short-name (stop-word filtered) + General aggregate.
|
|
281
|
+
_pascal() { # kebab/space -> PascalCase
|
|
282
|
+
local in="$1" out="" w
|
|
283
|
+
in="${in//-/ }"
|
|
284
|
+
for w in $in; do
|
|
285
|
+
out+="$(tr '[:lower:]' '[:upper:]' <<< "${w:0:1}")${w:1}"
|
|
286
|
+
done
|
|
287
|
+
printf '%s' "$out"
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
_short_name() {
|
|
291
|
+
local description="$1"
|
|
292
|
+
local stop='^(i|a|an|the|to|for|of|in|on|at|by|with|from|is|are|was|were|be|been|being|have|has|had|do|does|did|will|would|should|could|can|may|might|must|shall|this|that|these|those|my|your|our|their|want|need|add|get|set)$'
|
|
293
|
+
local clean; clean=$(echo "$description" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/ /g')
|
|
294
|
+
local -a words=()
|
|
295
|
+
local word
|
|
296
|
+
for word in $clean; do
|
|
297
|
+
[ -z "$word" ] && continue
|
|
298
|
+
if ! echo "$word" | grep -qiE "$stop"; then
|
|
299
|
+
[ ${#word} -ge 3 ] && words+=("$word")
|
|
300
|
+
fi
|
|
301
|
+
done
|
|
302
|
+
local max=3 result="" count=0
|
|
303
|
+
[ ${#words[@]} -eq 4 ] && max=4
|
|
304
|
+
for word in "${words[@]}"; do
|
|
305
|
+
[ $count -ge $max ] && break
|
|
306
|
+
[ -n "$result" ] && result+="-"
|
|
307
|
+
result+="$word"; count=$((count+1))
|
|
308
|
+
done
|
|
309
|
+
[ -z "$result" ] && result="feature"
|
|
310
|
+
printf '%s' "$result"
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
# ───────────────────────────────── scaffold ────────────────────────────────
|
|
315
|
+
cmd_scaffold() {
|
|
316
|
+
local feature_dir_arg="" allow_existing=false
|
|
317
|
+
while [ $# -gt 0 ]; do
|
|
318
|
+
case "$1" in
|
|
319
|
+
--feature-dir) feature_dir_arg="$2"; shift 2 ;;
|
|
320
|
+
--allow-existing) allow_existing=true; shift ;;
|
|
321
|
+
--help|-h) echo "scaffold --feature-dir <path> [--allow-existing]"; return 0 ;;
|
|
322
|
+
*) echo "scaffold: unknown arg '$1'" >&2; return 1 ;;
|
|
323
|
+
esac
|
|
324
|
+
done
|
|
325
|
+
[ -n "$feature_dir_arg" ] || { echo "scaffold: --feature-dir is required" >&2; return 1; }
|
|
326
|
+
|
|
327
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
328
|
+
local feature_dir="$feature_dir_arg"
|
|
329
|
+
[[ "$feature_dir" != /* ]] && feature_dir="$repo_root/$feature_dir"
|
|
330
|
+
local spec_file="$feature_dir/spec.md"
|
|
331
|
+
|
|
332
|
+
local dir_existed=false; [ -d "$feature_dir" ] && dir_existed=true
|
|
333
|
+
if [ "$dir_existed" = true ] && [ "$allow_existing" != true ]; then
|
|
334
|
+
echo "scaffold: '$feature_dir' already exists (use --allow-existing)" >&2; return 1
|
|
335
|
+
fi
|
|
336
|
+
mkdir -p "$feature_dir"
|
|
337
|
+
local created=true; [ "$dir_existed" = true ] && created=false
|
|
338
|
+
|
|
339
|
+
local template="" spec_created=false
|
|
340
|
+
if [ ! -f "$spec_file" ]; then
|
|
341
|
+
template=$(resolve_template "spec-template" "$repo_root") || true
|
|
342
|
+
if [ -n "$template" ] && [ -f "$template" ]; then cp "$template" "$spec_file"; else touch "$spec_file"; fi
|
|
343
|
+
spec_created=true
|
|
344
|
+
fi
|
|
345
|
+
_persist_feature_json "$repo_root" "$feature_dir"
|
|
346
|
+
|
|
347
|
+
printf '{"feature_dir":"%s","spec_file":"%s","template":"%s","created":%s,"spec_created":%s}\n' \
|
|
348
|
+
"$(json_escape "$feature_dir")" "$(json_escape "$spec_file")" \
|
|
349
|
+
"$(json_escape "$template")" "$created" "$spec_created"
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
# ─────────────────────────────── resolve-hooks ─────────────────────────────
|
|
353
|
+
# Parse .appweaver/extensions.yml (no PyYAML dependency) and emit the executable
|
|
354
|
+
# hooks for a phase. Governance rules applied here (the agent only dispatches):
|
|
355
|
+
# - drop hooks with enabled: false (absent enabled => enabled)
|
|
356
|
+
# - drop hooks with a non-empty condition (leave to HookExecutor)
|
|
357
|
+
# - normalize command dots to hyphens for the slash form
|
|
358
|
+
cmd_resolve_hooks() {
|
|
359
|
+
local -a phases=()
|
|
360
|
+
while [ $# -gt 0 ]; do
|
|
361
|
+
case "$1" in
|
|
362
|
+
--phase) phases+=("$2"); shift 2 ;;
|
|
363
|
+
--help|-h) echo "resolve-hooks --phase <name> [--phase <name> ...] (multiple phases are merged + deduped by command)"; return 0 ;;
|
|
364
|
+
*) echo "resolve-hooks: unknown arg '$1'" >&2; return 1 ;;
|
|
365
|
+
esac
|
|
366
|
+
done
|
|
367
|
+
[ ${#phases[@]} -gt 0 ] || { echo "resolve-hooks: at least one --phase is required" >&2; return 1; }
|
|
368
|
+
|
|
369
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
370
|
+
local ext="$repo_root/.appweaver/extensions.yml"
|
|
371
|
+
local phases_csv; phases_csv=$(IFS=,; echo "${phases[*]}")
|
|
372
|
+
if [ ! -f "$ext" ]; then
|
|
373
|
+
printf '{"phases":%s,"hooks":[]}\n' "$(json_str_array "${phases[@]}")"; return 0
|
|
374
|
+
fi
|
|
375
|
+
|
|
376
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
377
|
+
AW_EXT="$ext" AW_PHASES="$phases_csv" python3 - <<'PY'
|
|
378
|
+
import json, os
|
|
379
|
+
path = os.environ["AW_EXT"]
|
|
380
|
+
phases = [p for p in os.environ["AW_PHASES"].split(",") if p]
|
|
381
|
+
lines = open(path, encoding="utf-8").read().splitlines()
|
|
382
|
+
|
|
383
|
+
def indent(s): return len(s) - len(s.lstrip(" "))
|
|
384
|
+
|
|
385
|
+
def parse_phase(phase):
|
|
386
|
+
# locate "hooks:" then "<phase>:" then its list items (targeted parser for the
|
|
387
|
+
# known extensions.yml shape; avoids a YAML dependency). List items ("- ") may
|
|
388
|
+
# sit at the SAME indent as their parent key.
|
|
389
|
+
i, n = 0, len(lines); hooks = []
|
|
390
|
+
while i < n:
|
|
391
|
+
if lines[i].rstrip() == "hooks:":
|
|
392
|
+
base = indent(lines[i]); i += 1
|
|
393
|
+
while i < n and (lines[i].strip() == "" or indent(lines[i]) > base):
|
|
394
|
+
st0 = lines[i].strip()
|
|
395
|
+
if st0.rstrip(":") == phase and st0.endswith(":") and not st0.startswith("- "):
|
|
396
|
+
ph_ind = indent(lines[i]); i += 1; cur = None
|
|
397
|
+
while i < n:
|
|
398
|
+
l = lines[i]
|
|
399
|
+
if l.strip() == "": i += 1; continue
|
|
400
|
+
ind = indent(l); st = l.strip()
|
|
401
|
+
if ind < ph_ind: break
|
|
402
|
+
if ind == ph_ind and st.endswith(":") and not st.startswith("- "): break
|
|
403
|
+
if st.startswith("- "):
|
|
404
|
+
if cur is not None: hooks.append(cur)
|
|
405
|
+
cur = {}; st = st[2:]
|
|
406
|
+
if cur is not None and ":" in st and not st.startswith("- "):
|
|
407
|
+
k, _, v = st.partition(":"); cur[k.strip()] = v.strip()
|
|
408
|
+
i += 1
|
|
409
|
+
if cur is not None: hooks.append(cur)
|
|
410
|
+
break
|
|
411
|
+
else: i += 1
|
|
412
|
+
break
|
|
413
|
+
i += 1
|
|
414
|
+
return hooks
|
|
415
|
+
|
|
416
|
+
def truthy_false(v): return str(v).lower() == "false"
|
|
417
|
+
def empty_cond(v): return v is None or str(v).strip().lower() in ("", "null", "~")
|
|
418
|
+
|
|
419
|
+
out, seen = [], set()
|
|
420
|
+
for phase in phases:
|
|
421
|
+
for h in parse_phase(phase):
|
|
422
|
+
if truthy_false(h.get("enabled", "true")): continue
|
|
423
|
+
if not empty_cond(h.get("condition")): continue
|
|
424
|
+
cmd = h.get("command", "")
|
|
425
|
+
if cmd in seen: continue # dedupe by command across phases
|
|
426
|
+
seen.add(cmd)
|
|
427
|
+
out.append({
|
|
428
|
+
"extension": h.get("extension", ""),
|
|
429
|
+
"command": cmd,
|
|
430
|
+
"slash": "/" + cmd.replace(".", "-") if cmd else "",
|
|
431
|
+
"optional": str(h.get("optional", "false")).lower() == "true",
|
|
432
|
+
"description": h.get("description", ""),
|
|
433
|
+
"prompt": h.get("prompt", ""),
|
|
434
|
+
})
|
|
435
|
+
print(json.dumps({"phases": phases, "hooks": out}))
|
|
436
|
+
PY
|
|
437
|
+
else
|
|
438
|
+
echo "resolve-hooks: python3 unavailable; skipping hook resolution" >&2
|
|
439
|
+
printf '{"phases":%s,"hooks":[]}\n' "$(json_str_array "${phases[@]}")"
|
|
440
|
+
fi
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
# ─────────────────────────────── create-checklist ──────────────────────────
|
|
444
|
+
cmd_create_checklist() {
|
|
445
|
+
local feature_dir_arg="" feature_name="" kind="master" change_dir_arg=""
|
|
446
|
+
while [ $# -gt 0 ]; do
|
|
447
|
+
case "$1" in
|
|
448
|
+
--feature-dir) feature_dir_arg="$2"; shift 2 ;;
|
|
449
|
+
--feature-name) feature_name="$2"; shift 2 ;;
|
|
450
|
+
--kind) kind="$2"; shift 2 ;;
|
|
451
|
+
--change-dir) change_dir_arg="$2"; shift 2 ;;
|
|
452
|
+
--help|-h) echo "create-checklist --feature-dir <path> [--feature-name <name>] [--kind master|delta] [--change-dir <path>]"; return 0 ;;
|
|
453
|
+
*) echo "create-checklist: unknown arg '$1'" >&2; return 1 ;;
|
|
454
|
+
esac
|
|
455
|
+
done
|
|
456
|
+
[ -n "$feature_dir_arg" ] || { echo "create-checklist: --feature-dir is required" >&2; return 1; }
|
|
457
|
+
case "$kind" in
|
|
458
|
+
master|delta) ;;
|
|
459
|
+
*) echo "create-checklist: --kind must be 'master' or 'delta' (got '$kind')" >&2; return 1 ;;
|
|
460
|
+
esac
|
|
461
|
+
|
|
462
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
463
|
+
local feature_dir="$feature_dir_arg"
|
|
464
|
+
[[ "$feature_dir" != /* ]] && feature_dir="$repo_root/$feature_dir"
|
|
465
|
+
[ -n "$feature_name" ] || feature_name="$(basename "$feature_dir")"
|
|
466
|
+
|
|
467
|
+
# WHERE the checklist lives and WHAT it grades are decided together here, and both
|
|
468
|
+
# are reported back, so a caller binds the pair from ONE call and can never point a
|
|
469
|
+
# checklist at the wrong document. master -> the feature's spec.md; delta -> the
|
|
470
|
+
# change's delta-spec.md (checklist sits beside it, inside changes/<cr-id>/).
|
|
471
|
+
local checklist_dir target_file cr_id=""
|
|
472
|
+
if [ "$kind" = delta ]; then
|
|
473
|
+
[ -n "$change_dir_arg" ] || { echo "create-checklist: --change-dir is required with --kind delta" >&2; return 1; }
|
|
474
|
+
local change_dir="$change_dir_arg"
|
|
475
|
+
[[ "$change_dir" != /* ]] && change_dir="$repo_root/$change_dir"
|
|
476
|
+
cr_id="$(basename "$change_dir")"
|
|
477
|
+
checklist_dir="$change_dir/checklists"
|
|
478
|
+
target_file="$change_dir/delta-spec.md"
|
|
479
|
+
else
|
|
480
|
+
checklist_dir="$feature_dir/checklists"
|
|
481
|
+
target_file="$feature_dir/spec.md"
|
|
482
|
+
fi
|
|
483
|
+
local checklist_file="$checklist_dir/requirements.md"
|
|
484
|
+
mkdir -p "$checklist_dir"
|
|
485
|
+
|
|
486
|
+
local created=true; [ -f "$checklist_file" ] && created=false
|
|
487
|
+
local today; today="$(date +%Y-%m-%d)"
|
|
488
|
+
|
|
489
|
+
if [ "$kind" = delta ]; then
|
|
490
|
+
cat > "$checklist_file" <<EOF
|
|
491
|
+
# Delta-Spec Quality Checklist: $feature_name — $cr_id
|
|
492
|
+
|
|
493
|
+
**Purpose**: Validate the CHANGE's delta-spec before proceeding to design
|
|
494
|
+
**Created**: $today
|
|
495
|
+
**Change**: [delta-spec.md](../delta-spec.md)
|
|
496
|
+
|
|
497
|
+
## Content Quality
|
|
498
|
+
|
|
499
|
+
- [ ] No implementation mechanics (languages, frameworks, code/adapter design; interop obligations named as contracts are fine)
|
|
500
|
+
- [ ] Focused on user value and business needs
|
|
501
|
+
- [ ] Written for non-technical stakeholders
|
|
502
|
+
- [ ] Change summary states what is changing and why
|
|
503
|
+
|
|
504
|
+
## Delta Completeness
|
|
505
|
+
|
|
506
|
+
- [ ] Every item sits under ADDED, MODIFIED or REMOVED (an empty section states None)
|
|
507
|
+
- [ ] MODIFIED items state both sides (was -> now)
|
|
508
|
+
- [ ] New behaviour is a user story (US-n) with priority, Given/When/Then acceptance, and an independent test
|
|
509
|
+
- [ ] New or changed system requirements keep EARS form
|
|
510
|
+
- [ ] Requirements are testable and unambiguous
|
|
511
|
+
- [ ] No [NEEDS CLARIFICATION] markers remain
|
|
512
|
+
|
|
513
|
+
## Domain Lift (Constraints / Classification / Contracts)
|
|
514
|
+
|
|
515
|
+
- [ ] Constraint / NFR changes are captured as CN- items, or the section states None
|
|
516
|
+
- [ ] Data sensitivity or exposure changes are captured as DC- items, or the section states None
|
|
517
|
+
- [ ] External interoperability changes are captured as IC- items at contract level, or the section states None
|
|
518
|
+
|
|
519
|
+
## Experience Lift (UI / UX standards)
|
|
520
|
+
|
|
521
|
+
- [ ] Experience-standard changes are captured as UX- items, or the section states None
|
|
522
|
+
- [ ] Where UX- deltas exist, each states a project-wide standard checkable on any surface — no screen, page, layout, navigation or component
|
|
523
|
+
- [ ] Where UX- deltas exist, a change to THIS aggregate's own interaction behaviour is an FR- delta, not a UX- one
|
|
524
|
+
- [ ] Where UX- deltas exist, a bound the service must meet stays a CN-, not a UX-
|
|
525
|
+
|
|
526
|
+
## Merge Readiness
|
|
527
|
+
|
|
528
|
+
- [ ] Header is complete: aggregate, change type, base version
|
|
529
|
+
- [ ] Impact lists the affected ids (FR / BR / CN / DC / IC / UX / US / SC)
|
|
530
|
+
- [ ] Regression risk names the existing behaviour that must still hold
|
|
531
|
+
- [ ] Scoped to THIS change — the unchanged master is neither restated here nor edited
|
|
532
|
+
|
|
533
|
+
## Notes
|
|
534
|
+
|
|
535
|
+
- This checklist grades the DELTA DOCUMENT only. Accuracy against the master spec
|
|
536
|
+
(id continuity, conflicts with existing requirements) is checked downstream by
|
|
537
|
+
/appweaver analyze-delta-design.
|
|
538
|
+
- Items marked incomplete require delta-spec updates before /appweaver design
|
|
539
|
+
EOF
|
|
540
|
+
else
|
|
541
|
+
cat > "$checklist_file" <<EOF
|
|
542
|
+
# Specification Quality Checklist: $feature_name
|
|
543
|
+
|
|
544
|
+
**Purpose**: Validate specification completeness and quality before proceeding to design
|
|
545
|
+
**Created**: $today
|
|
546
|
+
**Feature**: [spec.md](../spec.md)
|
|
547
|
+
|
|
548
|
+
## Content Quality
|
|
549
|
+
|
|
550
|
+
- [ ] No implementation mechanics (languages, frameworks, code/adapter design; interop obligations named as contracts are fine)
|
|
551
|
+
- [ ] Focused on user value and business needs
|
|
552
|
+
- [ ] Written for non-technical stakeholders
|
|
553
|
+
- [ ] All mandatory sections completed
|
|
554
|
+
|
|
555
|
+
## Requirement Completeness
|
|
556
|
+
|
|
557
|
+
- [ ] No [NEEDS CLARIFICATION] markers remain
|
|
558
|
+
- [ ] Requirements are testable and unambiguous
|
|
559
|
+
- [ ] Success criteria are measurable
|
|
560
|
+
- [ ] Success criteria are technology-agnostic (no implementation details)
|
|
561
|
+
- [ ] All acceptance scenarios are defined
|
|
562
|
+
- [ ] Edge cases are identified
|
|
563
|
+
- [ ] Scope is clearly bounded
|
|
564
|
+
- [ ] Dependencies and assumptions identified
|
|
565
|
+
|
|
566
|
+
## Domain Lift (Constraints / Classification / Contracts)
|
|
567
|
+
|
|
568
|
+
- [ ] Domain-derived constraints are captured in Constraints & NFRs (CN-), or the section states None
|
|
569
|
+
- [ ] Data sensitivity and exposure rules are captured in Data Classification (DC-), or the section states None
|
|
570
|
+
- [ ] External interoperability obligations are captured as Integration Contracts (IC-) at contract level, or the section states None
|
|
571
|
+
|
|
572
|
+
## Experience Lift (UI / UX standards)
|
|
573
|
+
|
|
574
|
+
- [ ] Experience Scope states the actors, channels and device / browser / assistive-technology floors, or states None
|
|
575
|
+
- [ ] Project-wide experience standards are captured as UX- items, or the section states None
|
|
576
|
+
- [ ] Where UX- items exist, each is checkable on any surface — no screen, page, layout, navigation or component appears
|
|
577
|
+
- [ ] Where UX- items exist, none of them is a per-feature interaction behaviour — those stay FR-
|
|
578
|
+
- [ ] Where UX- items exist, a bound the SERVICE must meet is a CN-, cross-referenced where the obligation binds both lanes
|
|
579
|
+
|
|
580
|
+
## Feature Readiness
|
|
581
|
+
|
|
582
|
+
- [ ] All functional requirements have clear acceptance criteria
|
|
583
|
+
- [ ] User scenarios cover primary flows
|
|
584
|
+
- [ ] Feature meets measurable outcomes defined in Success Criteria
|
|
585
|
+
- [ ] No implementation details leak into specification
|
|
586
|
+
|
|
587
|
+
## Notes
|
|
588
|
+
|
|
589
|
+
- Items marked incomplete require spec updates before /appweaver design
|
|
590
|
+
EOF
|
|
591
|
+
fi
|
|
592
|
+
|
|
593
|
+
printf '{"checklist_file":"%s","kind":"%s","target":"%s","created":%s}\n' \
|
|
594
|
+
"$(json_escape "$checklist_file")" "$(json_escape "$kind")" \
|
|
595
|
+
"$(json_escape "$target_file")" "$created"
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
# ─────────────────────────────── list-domains ──────────────────────────────
|
|
599
|
+
# The selectable projects: immediate subdirs of guardrails/projects/ that carry a rule
|
|
600
|
+
# map. The map is what makes a directory a project, exactly as a *.pack.txt is what makes
|
|
601
|
+
# a foundations/ directory a foundation — without that test any stray sibling would be
|
|
602
|
+
# offered as a governance target. EITHER map qualifies, because onboard writes the domain
|
|
603
|
+
# map several steps before the technical one and a half-built project must stay visible.
|
|
604
|
+
cmd_list_domains() {
|
|
605
|
+
while [ $# -gt 0 ]; do
|
|
606
|
+
case "$1" in
|
|
607
|
+
--help|-h) echo "list-domains"; return 0 ;;
|
|
608
|
+
*) echo "list-domains: unknown arg '$1'" >&2; return 1 ;;
|
|
609
|
+
esac
|
|
610
|
+
done
|
|
611
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
612
|
+
local base="$repo_root/guardrails/projects"
|
|
613
|
+
local -a domains=()
|
|
614
|
+
if [ -d "$base" ]; then
|
|
615
|
+
local d
|
|
616
|
+
for d in "$base"/*/; do
|
|
617
|
+
[ -d "$d" ] || continue
|
|
618
|
+
[ -f "$d/tech-rule-bindings.md" ] || [ -f "$d/domain-rule-bindings.md" ] || continue
|
|
619
|
+
domains+=("$(basename "$d")")
|
|
620
|
+
done
|
|
621
|
+
fi
|
|
622
|
+
printf '{"domains":%s}\n' "$(json_str_array "${domains[@]}")"
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
# ─────────────────────────────── set-domain ────────────────────────────────
|
|
626
|
+
cmd_set_domain() {
|
|
627
|
+
local domain=""
|
|
628
|
+
while [ $# -gt 0 ]; do
|
|
629
|
+
case "$1" in
|
|
630
|
+
--domain) domain="$2"; shift 2 ;;
|
|
631
|
+
--help|-h) echo "set-domain --domain <name>"; return 0 ;;
|
|
632
|
+
*) echo "set-domain: unknown arg '$1'" >&2; return 1 ;;
|
|
633
|
+
esac
|
|
634
|
+
done
|
|
635
|
+
[ -n "$domain" ] || { echo "set-domain: --domain is required" >&2; return 1; }
|
|
636
|
+
|
|
637
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
638
|
+
local map_rel="guardrails/projects/$domain/domain-rule-bindings.md"
|
|
639
|
+
local map_exists=false
|
|
640
|
+
[ -f "$repo_root/$map_rel" ] && map_exists=true
|
|
641
|
+
|
|
642
|
+
mkdir -p "$repo_root/.appweaver"
|
|
643
|
+
local dj="$repo_root/.appweaver/domain.json"
|
|
644
|
+
if command -v jq >/dev/null 2>&1; then
|
|
645
|
+
jq -cn --arg d "$domain" '{domain:$d}' > "$dj"
|
|
646
|
+
else
|
|
647
|
+
printf '{"domain":"%s"}\n' "$(json_escape "$domain")" > "$dj"
|
|
648
|
+
fi
|
|
649
|
+
|
|
650
|
+
printf '{"domain":"%s","map_path":"%s","map_exists":%s,"written":true}\n' \
|
|
651
|
+
"$(json_escape "$domain")" "$(json_escape "$map_rel")" "$map_exists"
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
# ────────── stack-list / lane-declare / lane-resolve / lane-probe ──────────
|
|
655
|
+
# The lane model. All four delegate to the shared python core so the bash and
|
|
656
|
+
# powershell twins cannot drift: the JSON contract has one implementation.
|
|
657
|
+
#
|
|
658
|
+
# lane-declare is the SOLE WRITER of .appweaver/stacks.json and has exactly one
|
|
659
|
+
# caller (aw-spec-select-stack). lane-resolve can never write — which is why
|
|
660
|
+
# these are two subcommands rather than one with a mode flag.
|
|
661
|
+
_aw_stack_py() {
|
|
662
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
663
|
+
local core="$SCRIPT_DIR/../python/aw-stack.py"
|
|
664
|
+
if [ ! -f "$core" ]; then
|
|
665
|
+
echo "aw-stack: core not found at $core" >&2; return 1
|
|
666
|
+
fi
|
|
667
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
668
|
+
echo "aw-stack: python3 is required for the lane model" >&2; return 1
|
|
669
|
+
fi
|
|
670
|
+
python3 "$core" --repo "$repo_root" "$@"
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
# ONE flattened field of a lane kind, empty when none is declared. The shells never
|
|
674
|
+
# parse stacks.json themselves — aw-stack is its one reader — so every command that needs
|
|
675
|
+
# to know something about a lane asks through here instead of assuming `src/` or `dotnet`.
|
|
676
|
+
# Field is the kind_block suffix: root, profile, ecosystem, language, title, build, test,
|
|
677
|
+
# run, migrate.
|
|
678
|
+
_aw_lane_field() {
|
|
679
|
+
local kind="$1" field="$2"
|
|
680
|
+
_aw_stack_py resolve --kind "$kind" 2>/dev/null | AW_KEY="${kind}_${field}" python3 -c '
|
|
681
|
+
import json, os, sys
|
|
682
|
+
try:
|
|
683
|
+
print(json.loads(sys.stdin.readline()).get(os.environ["AW_KEY"]) or "")
|
|
684
|
+
except Exception:
|
|
685
|
+
print("")
|
|
686
|
+
' 2>/dev/null || echo ""
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
# The declared SOURCE PATH of a lane kind — the field asked for most often.
|
|
690
|
+
_aw_lane_root() { _aw_lane_field "$1" root; }
|
|
691
|
+
|
|
692
|
+
# Every ecosystem the declared lanes belong to, one per line, deduped and empty when
|
|
693
|
+
# nothing is declared. This is what keys the stack-partitioned corpora under
|
|
694
|
+
# foundations/<ecosystem>/ — a Java repo has no business being offered a .NET bundle,
|
|
695
|
+
# and the lane model already knows which it is.
|
|
696
|
+
_aw_ecosystems() {
|
|
697
|
+
_aw_stack_py resolve 2>/dev/null | python3 -c '
|
|
698
|
+
import json, sys
|
|
699
|
+
try:
|
|
700
|
+
d = json.loads(sys.stdin.readline())
|
|
701
|
+
except Exception:
|
|
702
|
+
raise SystemExit
|
|
703
|
+
seen = []
|
|
704
|
+
for k in ("backend_ecosystem", "frontend_ecosystem"):
|
|
705
|
+
v = d.get(k) or ""
|
|
706
|
+
if v and v not in seen:
|
|
707
|
+
seen.append(v)
|
|
708
|
+
print("\n".join(seen))
|
|
709
|
+
' 2>/dev/null || true
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
cmd_stack_list() {
|
|
713
|
+
local -a fwd=()
|
|
714
|
+
while [ $# -gt 0 ]; do
|
|
715
|
+
case "$1" in
|
|
716
|
+
--kind) fwd+=("--kind" "$2"); shift 2 ;;
|
|
717
|
+
--preselect) fwd+=("--preselect" "$2"); shift 2 ;;
|
|
718
|
+
--include-none) fwd+=("--include-none"); shift ;;
|
|
719
|
+
--suggest) fwd+=("--suggest"); shift ;;
|
|
720
|
+
--help|-h) echo "stack-list [--kind backend|frontend|all] [--preselect <text>] [--include-none] [--suggest]"; return 0 ;;
|
|
721
|
+
*) echo "stack-list: unknown arg '$1'" >&2; return 1 ;;
|
|
722
|
+
esac
|
|
723
|
+
done
|
|
724
|
+
_aw_stack_py list "${fwd[@]}"
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
cmd_stack_detect() {
|
|
728
|
+
local -a fwd=()
|
|
729
|
+
while [ $# -gt 0 ]; do
|
|
730
|
+
case "$1" in
|
|
731
|
+
--kind) fwd+=("--kind" "$2"); shift 2 ;;
|
|
732
|
+
--exclude-root) fwd+=("--exclude-root" "$2"); shift 2 ;;
|
|
733
|
+
--help|-h) echo "stack-detect [--kind backend|frontend|all] [--exclude-root <path>]…"; return 0 ;;
|
|
734
|
+
*) echo "stack-detect: unknown arg '$1'" >&2; return 1 ;;
|
|
735
|
+
esac
|
|
736
|
+
done
|
|
737
|
+
_aw_stack_py detect "${fwd[@]}"
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
cmd_lane_declare() {
|
|
741
|
+
local -a fwd=()
|
|
742
|
+
while [ $# -gt 0 ]; do
|
|
743
|
+
case "$1" in
|
|
744
|
+
# --source-path is the same field as --root; both spellings reach the core.
|
|
745
|
+
--profile|--kind|--id|--root|--source-path|--stack|--origin|--build|--test|--run|--prototype-root|--prototype-run)
|
|
746
|
+
fwd+=("$1" "$2"); shift 2 ;;
|
|
747
|
+
--no-prototype) fwd+=("--no-prototype"); shift ;;
|
|
748
|
+
--require-existing-root) fwd+=("--require-existing-root"); shift ;;
|
|
749
|
+
--help|-h)
|
|
750
|
+
echo "lane-declare --profile <id|label|none|detected> [--kind backend|frontend] [--id <lane>] [--source-path|--root <path>] [--stack <label>] [--origin declared|reverse-engineered] [--build <cmd>] [--test <cmd>] [--run <cmd>] [--prototype-root <path>] [--prototype-run <cmd>] [--no-prototype] [--require-existing-root]"
|
|
751
|
+
return 0 ;;
|
|
752
|
+
*) echo "lane-declare: unknown arg '$1'" >&2; return 1 ;;
|
|
753
|
+
esac
|
|
754
|
+
done
|
|
755
|
+
_aw_stack_py declare "${fwd[@]}"
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
cmd_lane_resolve() {
|
|
759
|
+
local -a fwd=()
|
|
760
|
+
while [ $# -gt 0 ]; do
|
|
761
|
+
case "$1" in
|
|
762
|
+
--kind|--id) fwd+=("$1" "$2"); shift 2 ;;
|
|
763
|
+
--help|-h) echo "lane-resolve [--kind backend|frontend] [--id <lane>]"; return 0 ;;
|
|
764
|
+
*) echo "lane-resolve: unknown arg '$1'" >&2; return 1 ;;
|
|
765
|
+
esac
|
|
766
|
+
done
|
|
767
|
+
_aw_stack_py resolve "${fwd[@]}"
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
# The gate: is the declaration USABLE for the phase about to run. --require names a
|
|
771
|
+
# lane kind the phase cannot run without and is repeatable; every DECLARED lane is
|
|
772
|
+
# checked for a sound source path whether or not it was required, because a stack
|
|
773
|
+
# named against a directory that is not there is a mistake in every phase.
|
|
774
|
+
# --allow-missing-root is the greenfield state, where select-stack declares a source
|
|
775
|
+
# path before implement creates it.
|
|
776
|
+
cmd_lane_verify() {
|
|
777
|
+
local -a fwd=()
|
|
778
|
+
while [ $# -gt 0 ]; do
|
|
779
|
+
case "$1" in
|
|
780
|
+
--require) fwd+=("--require" "$2"); shift 2 ;;
|
|
781
|
+
# --flag=value: --command carries a phase name that starts with '/'.
|
|
782
|
+
--command) fwd+=("--command=$2"); shift 2 ;;
|
|
783
|
+
--allow-missing-root) fwd+=("--allow-missing-root"); shift ;;
|
|
784
|
+
--help|-h) echo "lane-verify [--require backend|frontend]… [--allow-missing-root] [--command <phase>]"; return 0 ;;
|
|
785
|
+
*) echo "lane-verify: unknown arg '$1'" >&2; return 1 ;;
|
|
786
|
+
esac
|
|
787
|
+
done
|
|
788
|
+
_aw_stack_py verify "${fwd[@]}"
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
# Evidence that a lane of this kind exists, from the FILE signals only: a lane in
|
|
792
|
+
# stacks.json, and a stack name appearing in free prose. The authoritative signal
|
|
793
|
+
# — whether the active project's technical map carries `## /aw-spec-<command>` for the
|
|
794
|
+
# lane's phases — is resolve-tech's, and callers OR the two together.
|
|
795
|
+
cmd_lane_probe() {
|
|
796
|
+
local -a fwd=()
|
|
797
|
+
while [ $# -gt 0 ]; do
|
|
798
|
+
case "$1" in
|
|
799
|
+
# --flag=value, not two tokens: --hint carries raw user prose, and a value
|
|
800
|
+
# that starts with '-' reads as another flag when passed separately.
|
|
801
|
+
--kind|--hint) fwd+=("$1=$2"); shift 2 ;;
|
|
802
|
+
--help|-h) echo "lane-probe [--kind backend|frontend] [--hint <text>]"; return 0 ;;
|
|
803
|
+
*) echo "lane-probe: unknown arg '$1'" >&2; return 1 ;;
|
|
804
|
+
esac
|
|
805
|
+
done
|
|
806
|
+
_aw_stack_py probe "${fwd[@]}"
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
# ─────────────────────────────── list-features ─────────────────────────────
|
|
810
|
+
# Enumerate the aggregates a user can pick as the active feature. Two sources,
|
|
811
|
+
# MERGED (deduped by feature name, case-insensitive):
|
|
812
|
+
# 1. specs/ sub-folders — features already scaffolded/adopted (greenfield or
|
|
813
|
+
# brownfield-post-adopt); annotated with spec + design docs + version.
|
|
814
|
+
# 2. the OKF bundle's Aggregate concepts — the BROWNFIELD FEATURE MAP onboard
|
|
815
|
+
# reverse-engineers (the discovered aggregates NOT yet under specs/). This is
|
|
816
|
+
# why select-feature can offer real code features in a brownfield repo before
|
|
817
|
+
# anything is adopted, instead of an empty specs/ list.
|
|
818
|
+
# On greenfield (no bundle) source 2 is empty → identical to the old specs-only
|
|
819
|
+
# behaviour. `select_options` = merged names + (an injected --preselect that is not
|
|
820
|
+
# yet a feature) + a "create new" sentinel, so select-feature can offer add-new and
|
|
821
|
+
# honour a direct-argument fast path with ONE list.
|
|
822
|
+
cmd_list_features() {
|
|
823
|
+
local preselect="" bundle=""
|
|
824
|
+
while [ $# -gt 0 ]; do
|
|
825
|
+
case "$1" in
|
|
826
|
+
--preselect) preselect="$2"; shift 2 ;;
|
|
827
|
+
--bundle) bundle="$2"; shift 2 ;;
|
|
828
|
+
--help|-h) echo "list-features [--preselect <name>] [--bundle <dir>] -> {features[], names[], count, select_options[], sentinel}"; return 0 ;;
|
|
829
|
+
*) echo "list-features: unknown arg '$1'" >&2; return 1 ;;
|
|
830
|
+
esac
|
|
831
|
+
done
|
|
832
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
833
|
+
[ -n "$bundle" ] || bundle="knowledge"
|
|
834
|
+
case "$bundle" in /*) ;; *) bundle="$repo_root/$bundle" ;; esac
|
|
835
|
+
# Best-effort OKF aggregate list from the shared python core (single source of
|
|
836
|
+
# truth for OKF parsing; the ps1 twin calls the SAME script). Empty on greenfield.
|
|
837
|
+
local okf_json="{}" invpy="$SCRIPT_DIR/../python/aw-inventory.py"
|
|
838
|
+
if [ -d "$bundle" ] && [ -f "$invpy" ] && command -v python3 >/dev/null 2>&1; then
|
|
839
|
+
okf_json=$(python3 "$invpy" list-aggregates --bundle "$bundle" 2>/dev/null) || okf_json="{}"
|
|
840
|
+
fi
|
|
841
|
+
AW_SPECS="$repo_root/specs" AW_PRESELECT="$preselect" AW_OKF="$okf_json" python3 - <<'PY'
|
|
842
|
+
import json, os, glob
|
|
843
|
+
base = os.environ["AW_SPECS"]
|
|
844
|
+
pre = os.environ.get("AW_PRESELECT", "").strip()
|
|
845
|
+
try:
|
|
846
|
+
okf = json.loads(os.environ.get("AW_OKF", "{}") or "{}")
|
|
847
|
+
except Exception:
|
|
848
|
+
okf = {}
|
|
849
|
+
sentinel = "+ Create a new feature"
|
|
850
|
+
feats, names = [], []
|
|
851
|
+
seen = set() # case-insensitive dedup key across both sources
|
|
852
|
+
if os.path.isdir(base):
|
|
853
|
+
for name in sorted(os.listdir(base)):
|
|
854
|
+
d = os.path.join(base, name)
|
|
855
|
+
if not os.path.isdir(d):
|
|
856
|
+
continue
|
|
857
|
+
ver, active_change = "", ""
|
|
858
|
+
avf = os.path.join(d, "active_version.json")
|
|
859
|
+
if os.path.isfile(avf):
|
|
860
|
+
try:
|
|
861
|
+
av = json.load(open(avf, encoding="utf-8")) or {}
|
|
862
|
+
ver = av.get("version", ""); active_change = av.get("active_change", "")
|
|
863
|
+
except Exception: pass
|
|
864
|
+
names.append(name); seen.add(name.lower())
|
|
865
|
+
feats.append({
|
|
866
|
+
"name": name,
|
|
867
|
+
"dir": "specs/" + name,
|
|
868
|
+
"source": "spec",
|
|
869
|
+
"has_spec": os.path.isfile(os.path.join(d, "spec.md")),
|
|
870
|
+
# `design*.md`, NOT `design.*.md`: the second dot made this miss the single
|
|
871
|
+
# `design.md` the framework actually produces (it only matched the retired
|
|
872
|
+
# design.domain.md / design.technical.md family). Informational count only.
|
|
873
|
+
"design_docs": len(glob.glob(os.path.join(d, "design*.md"))),
|
|
874
|
+
"version": ver,
|
|
875
|
+
"active_change": active_change,
|
|
876
|
+
})
|
|
877
|
+
# Merge OKF-discovered aggregates NOT already scaffolded under specs/ (a specs/
|
|
878
|
+
# folder is the adopted/created form of the same aggregate — it wins, no dup).
|
|
879
|
+
for agg in (okf.get("aggregates") or []):
|
|
880
|
+
nm = (agg.get("name") or "").strip()
|
|
881
|
+
if not nm or nm.lower() in seen:
|
|
882
|
+
continue
|
|
883
|
+
names.append(nm); seen.add(nm.lower())
|
|
884
|
+
feats.append({
|
|
885
|
+
"name": nm,
|
|
886
|
+
"dir": "specs/" + nm, # candidate slice (not yet created)
|
|
887
|
+
"source": "okf",
|
|
888
|
+
"has_spec": False,
|
|
889
|
+
"adopted": False,
|
|
890
|
+
"concept_path": agg.get("concept_path", ""),
|
|
891
|
+
"design_docs": 0,
|
|
892
|
+
"version": "",
|
|
893
|
+
"active_change": "",
|
|
894
|
+
})
|
|
895
|
+
opts = list(names)
|
|
896
|
+
# Inject a not-yet-existing --preselect so `preselect_from: USER_INPUT` can
|
|
897
|
+
# create-or-select it without an extra round trip (the direct-arg fast path).
|
|
898
|
+
if pre and pre.lower() not in seen:
|
|
899
|
+
opts.append(pre)
|
|
900
|
+
opts.append(sentinel)
|
|
901
|
+
print(json.dumps({"features": feats, "names": names, "count": len(names),
|
|
902
|
+
"select_options": opts, "sentinel": sentinel}))
|
|
903
|
+
PY
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
# ─────────────────────────────── set-feature ───────────────────────────────
|
|
907
|
+
# Create-or-select an aggregate under specs/ and persist it as the active feature
|
|
908
|
+
# (.appweaver/feature.json). feature = aggregate: the name is normalized to a
|
|
909
|
+
# PascalCase identifier (e.g. "order management" -> OrderManagement). Idempotent:
|
|
910
|
+
# selecting an existing feature just re-points feature.json; a new one is created.
|
|
911
|
+
cmd_set_feature() {
|
|
912
|
+
local raw=""
|
|
913
|
+
while [ $# -gt 0 ]; do
|
|
914
|
+
case "$1" in
|
|
915
|
+
--feature) raw="$2"; shift 2 ;;
|
|
916
|
+
--help|-h) echo "set-feature --feature <name> (create-or-select the aggregate under specs/)"; return 0 ;;
|
|
917
|
+
*) echo "set-feature: unknown arg '$1'" >&2; return 1 ;;
|
|
918
|
+
esac
|
|
919
|
+
done
|
|
920
|
+
[ -n "$raw" ] || { echo "set-feature: --feature is required" >&2; return 1; }
|
|
921
|
+
|
|
922
|
+
# Normalize to a PascalCase aggregate identifier (feature = aggregate).
|
|
923
|
+
local cleaned; cleaned=$(echo "$raw" | sed -E 's/[^A-Za-z0-9]+/ /g; s/^ +| +$//g')
|
|
924
|
+
local feature; feature=$(_pascal "$cleaned")
|
|
925
|
+
if ! [[ "$feature" =~ ^[A-Za-z][A-Za-z0-9]*$ ]]; then
|
|
926
|
+
echo "set-feature: '$raw' is not a valid feature name (need a letter-led alphanumeric aggregate, e.g. Order)" >&2
|
|
927
|
+
return 1
|
|
928
|
+
fi
|
|
929
|
+
|
|
930
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
931
|
+
local feature_dir_rel="specs/$feature"
|
|
932
|
+
local feature_dir="$repo_root/$feature_dir_rel"
|
|
933
|
+
local existed=false; [ -d "$feature_dir" ] && existed=true
|
|
934
|
+
mkdir -p "$feature_dir"
|
|
935
|
+
_persist_feature_json "$repo_root" "$feature_dir_rel"
|
|
936
|
+
|
|
937
|
+
local created=true; [ "$existed" = true ] && created=false
|
|
938
|
+
local spec_exists=false; [ -f "$feature_dir/spec.md" ] && spec_exists=true
|
|
939
|
+
printf '{"feature":"%s","feature_dir":"%s","spec_file":"%s","existed":%s,"created":%s,"spec_exists":%s}\n' \
|
|
940
|
+
"$(json_escape "$feature")" "$(json_escape "$feature_dir_rel")" \
|
|
941
|
+
"$(json_escape "$feature_dir_rel/spec.md")" "$existed" "$created" "$spec_exists"
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
# ═══════════════════════════ active_version.json ════════════════════════════
|
|
945
|
+
# PER-FEATURE state at <feature_dir>/active_version.json:
|
|
946
|
+
# {feature, version, active_change, updated}
|
|
947
|
+
# Distinct from the GLOBAL .appweaver/feature.json (which only SELECTS the active
|
|
948
|
+
# feature). This is the single source of truth for a feature's version number and
|
|
949
|
+
# its open change — the CR that spans cr-setup (opens) -> cr-archive (closes).
|
|
950
|
+
# "Every applied change bumps" => the close op bumps minor ONLY when a change is
|
|
951
|
+
# open (the initial bootstrap build stays v1.0; it is the baseline, not a change).
|
|
952
|
+
# The open/close OPS are reached solely through cr-setup / cr-archive; the former
|
|
953
|
+
# standalone `version-open` / `version-close` subcommands were unused and removed.
|
|
954
|
+
_resolve_feature_dir() { # $1 = --feature-dir arg (may be empty), $2 = repo_root; prints ABS dir or fails
|
|
955
|
+
local arg="$1" repo_root="$2" fd="$1"
|
|
956
|
+
[ -n "$fd" ] || fd=$(read_feature_json_feature_directory "$repo_root")
|
|
957
|
+
[ -n "$fd" ] || return 1
|
|
958
|
+
[[ "$fd" != /* ]] && fd="$repo_root/$fd"
|
|
959
|
+
printf '%s' "$fd"
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
# Shared reader/mutator: op in {read,init,open,close}. Prints one JSON object.
|
|
963
|
+
# op=open enforces the single-active-change guard: opening a DIFFERENT change while
|
|
964
|
+
# one is already open fails (exit 3, ok:false) unless force ("1") is passed — this
|
|
965
|
+
# stops a second concurrent CR on the same aggregate from silently overwriting the
|
|
966
|
+
# first. (Concurrency is ACROSS features; changes to one feature are serialized.)
|
|
967
|
+
_active_version() { # $1 = abs feature_dir, $2 = op, $3 = cr_id (open only), $4 = force ("1" overrides open-guard)
|
|
968
|
+
AW_FD="$1" AW_OP="$2" AW_CR="${3:-}" AW_FORCE="${4:-}" python3 - <<'PY'
|
|
969
|
+
import json, os, re, datetime, sys
|
|
970
|
+
fd = os.environ["AW_FD"]; op = os.environ["AW_OP"]; cr = os.environ.get("AW_CR", "")
|
|
971
|
+
force = os.environ.get("AW_FORCE", "")
|
|
972
|
+
path = os.path.join(fd, "active_version.json")
|
|
973
|
+
feature = os.path.basename(fd.rstrip("/"))
|
|
974
|
+
def now(): return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
975
|
+
def load():
|
|
976
|
+
if os.path.isfile(path):
|
|
977
|
+
try: return json.load(open(path, encoding="utf-8")), True
|
|
978
|
+
except Exception: pass
|
|
979
|
+
return {"feature": feature, "version": "v1.0", "active_change": "", "updated": now()}, False
|
|
980
|
+
def save(d):
|
|
981
|
+
d["updated"] = now()
|
|
982
|
+
open(path, "w", encoding="utf-8").write(json.dumps(d, indent=2) + "\n")
|
|
983
|
+
def bump(v):
|
|
984
|
+
m = re.match(r'v?(\d+)(?:\.(\d+))?', (v or "v1.0").strip())
|
|
985
|
+
maj = int(m.group(1)) if m else 1
|
|
986
|
+
mnr = int(m.group(2)) if (m and m.group(2)) else 0
|
|
987
|
+
return "v%d.%d" % (maj, mnr + 1)
|
|
988
|
+
|
|
989
|
+
d, existed = load()
|
|
990
|
+
d.setdefault("feature", feature); d.setdefault("version", "v1.0"); d.setdefault("active_change", "")
|
|
991
|
+
out = {"feature": d["feature"], "version": d["version"], "active_change": d.get("active_change", ""),
|
|
992
|
+
"exists": existed, "path": "active_version.json", "ok": True}
|
|
993
|
+
if op == "read":
|
|
994
|
+
pass
|
|
995
|
+
elif op == "init":
|
|
996
|
+
if not existed: save(d)
|
|
997
|
+
out["created"] = (not existed)
|
|
998
|
+
elif op == "open":
|
|
999
|
+
cur = d.get("active_change", "")
|
|
1000
|
+
if cur and cur != cr and force != "1": # a DIFFERENT change is already open -> refuse
|
|
1001
|
+
out["ok"] = False
|
|
1002
|
+
out["error"] = ("change '%s' is already open on this feature; finish it "
|
|
1003
|
+
"(finalize-delta -> analyze-slice -> close-delta) before opening "
|
|
1004
|
+
"'%s', or pass --force" % (cur, cr))
|
|
1005
|
+
out["conflict"] = cur; out["requested"] = cr; out["has_change"] = True
|
|
1006
|
+
print(json.dumps(out)); sys.exit(3)
|
|
1007
|
+
d["active_change"] = cr; save(d)
|
|
1008
|
+
out["active_change"] = cr; out["opened"] = cr
|
|
1009
|
+
if cur and cur == cr: out["reopened"] = True # idempotent re-open of the same CR
|
|
1010
|
+
elif op == "close":
|
|
1011
|
+
prev = d["version"]; closed = d.get("active_change", "")
|
|
1012
|
+
if closed: # a real change was open -> materialized -> bump
|
|
1013
|
+
d["version"] = bump(prev)
|
|
1014
|
+
d["active_change"] = ""; save(d)
|
|
1015
|
+
out["version"] = d["version"]; out["active_change"] = ""
|
|
1016
|
+
out["previous"] = prev; out["closed_change"] = closed
|
|
1017
|
+
out["has_change"] = bool(out.get("active_change", "")) # clean branch signal for design
|
|
1018
|
+
# What a close from THIS state would produce. One bump() authority, read by BOTH
|
|
1019
|
+
# finalize-delta (to write the right number into the Change History rows it appends)
|
|
1020
|
+
# and close-delta (which actually performs the bump and stamps the two headers), so
|
|
1021
|
+
# the two stages can never disagree about the target. Equals `version` when no change
|
|
1022
|
+
# is open, mirroring close's "only bump if a change was really open" guard.
|
|
1023
|
+
out["next_version"] = bump(out["version"]) if out.get("active_change", "") else out["version"]
|
|
1024
|
+
print(json.dumps(out))
|
|
1025
|
+
PY
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
cmd_version_read() { _version_cmd "read" "$@"; }
|
|
1029
|
+
cmd_version_init() { _version_cmd "init" "$@"; }
|
|
1030
|
+
_version_cmd() { # $1 = op (read|init), then --feature-dir arg
|
|
1031
|
+
local op="$1"; shift
|
|
1032
|
+
local fd_arg=""
|
|
1033
|
+
while [ $# -gt 0 ]; do
|
|
1034
|
+
case "$1" in
|
|
1035
|
+
--feature-dir) fd_arg="$2"; shift 2 ;;
|
|
1036
|
+
--help|-h) echo "version-$op [--feature-dir <path>]"; return 0 ;;
|
|
1037
|
+
*) echo "version-$op: unknown arg '$1'" >&2; return 1 ;;
|
|
1038
|
+
esac
|
|
1039
|
+
done
|
|
1040
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
1041
|
+
local fd; fd=$(_resolve_feature_dir "$fd_arg" "$repo_root") || { echo "version-$op: no feature set and no --feature-dir" >&2; return 1; }
|
|
1042
|
+
_active_version "$fd" "$op"
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
|
|
1046
|
+
|
|
1047
|
+
# ─────────────────────────────── feature-paths ─────────────────────────────
|
|
1048
|
+
cmd_feature_paths() {
|
|
1049
|
+
while [ $# -gt 0 ]; do
|
|
1050
|
+
case "$1" in
|
|
1051
|
+
--help|-h) echo "feature-paths"; return 0 ;;
|
|
1052
|
+
*) echo "feature-paths: unknown arg '$1'" >&2; return 1 ;;
|
|
1053
|
+
esac
|
|
1054
|
+
done
|
|
1055
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
1056
|
+
local fd; fd=$(read_feature_json_feature_directory "$repo_root")
|
|
1057
|
+
if [ -z "$fd" ]; then
|
|
1058
|
+
echo "feature-paths: no feature set (run /appweaver select-feature first)" >&2
|
|
1059
|
+
return 1
|
|
1060
|
+
fi
|
|
1061
|
+
[[ "$fd" != /* ]] && fd="$repo_root/$fd"
|
|
1062
|
+
local feature; feature=$(basename "$fd") # the aggregate name (feature = specs/<Feature>)
|
|
1063
|
+
# The master documents of a feature: spec.md (WHAT/WHY) + design.md (the service
|
|
1064
|
+
# lane's HOW) + design.ui.md (the frontend lane's HOW, where the feature has a UI
|
|
1065
|
+
# half). plan.md / tasks.md belonged to the retired /aw-spec-plan + /aw-spec-tasks
|
|
1066
|
+
# phases and had zero consumers.
|
|
1067
|
+
#
|
|
1068
|
+
# ui_design_exists IS THE UI-HALF SIGNAL for the whole CR loop. Every delta stage
|
|
1069
|
+
# asks the same question — is there a UI master to keep in sync? — and the master's
|
|
1070
|
+
# own presence answers it without re-deriving the lane declaration and the slice's
|
|
1071
|
+
# UI file count at each stage. It is also self-correcting: whatever authors
|
|
1072
|
+
# design.ui.md (adopt today) enrolls the feature in the UI half of the delta loop
|
|
1073
|
+
# by the act of writing the file.
|
|
1074
|
+
local spec="$fd/spec.md" design="$fd/design.md" ui_design="$fd/design.ui.md"
|
|
1075
|
+
local spec_exists=false; [ -f "$spec" ] && spec_exists=true
|
|
1076
|
+
local design_exists=false; [ -f "$design" ] && design_exists=true
|
|
1077
|
+
local ui_design_exists=false; [ -f "$ui_design" ] && ui_design_exists=true
|
|
1078
|
+
printf '{"feature":"%s","feature_dir":"%s","spec_file":"%s","design_file":"%s","ui_design_file":"%s","spec_exists":%s,"design_exists":%s,"ui_design_exists":%s}\n' \
|
|
1079
|
+
"$(json_escape "$feature")" "$(json_escape "$fd")" "$(json_escape "$spec")" "$(json_escape "$design")" \
|
|
1080
|
+
"$(json_escape "$ui_design")" "$spec_exists" "$design_exists" "$ui_design_exists"
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
# ────────────────────────── checklist-read / -apply ────────────────────────
|
|
1084
|
+
# Parse/toggle GitHub task-list checkboxes outside code fences. Python3 does the
|
|
1085
|
+
# parsing (robust, fence-aware); toggling changes ONLY the [ ]/[x] marker.
|
|
1086
|
+
cmd_checklist_read() {
|
|
1087
|
+
local file=""
|
|
1088
|
+
while [ $# -gt 0 ]; do
|
|
1089
|
+
case "$1" in
|
|
1090
|
+
--file) file="$2"; shift 2 ;;
|
|
1091
|
+
--help|-h) echo "checklist-read --file <path>"; return 0 ;;
|
|
1092
|
+
*) echo "checklist-read: unknown arg '$1'" >&2; return 1 ;;
|
|
1093
|
+
esac
|
|
1094
|
+
done
|
|
1095
|
+
[ -n "$file" ] || { echo "checklist-read: --file required" >&2; return 1; }
|
|
1096
|
+
AW_FILE="$file" python3 - <<'PY'
|
|
1097
|
+
import json, os
|
|
1098
|
+
f = os.environ["AW_FILE"]
|
|
1099
|
+
if not os.path.exists(f):
|
|
1100
|
+
print(json.dumps({"exists": False, "total": 0, "checked": 0, "items": []})); raise SystemExit
|
|
1101
|
+
items, n, fence = [], 0, False
|
|
1102
|
+
for line in open(f, encoding="utf-8").read().splitlines():
|
|
1103
|
+
s = line.strip()
|
|
1104
|
+
if s.startswith("```"): fence = not fence; continue
|
|
1105
|
+
if fence: continue
|
|
1106
|
+
st = line.lstrip()
|
|
1107
|
+
if st[:3] in ("- [",) and len(st) >= 5 and st[3] in " xX" and st[4] == "]":
|
|
1108
|
+
n += 1
|
|
1109
|
+
items.append({"n": n, "checked": st[3] in "xX", "text": st[5:].strip()})
|
|
1110
|
+
print(json.dumps({"exists": True, "total": len(items),
|
|
1111
|
+
"checked": sum(1 for i in items if i["checked"]), "items": items}))
|
|
1112
|
+
PY
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
cmd_checklist_apply() {
|
|
1116
|
+
local file="" results="{}"
|
|
1117
|
+
while [ $# -gt 0 ]; do
|
|
1118
|
+
case "$1" in
|
|
1119
|
+
--file) file="$2"; shift 2 ;;
|
|
1120
|
+
--results) results="$2"; shift 2 ;;
|
|
1121
|
+
--help|-h) echo "checklist-apply --file <path> --results <json>"; return 0 ;;
|
|
1122
|
+
*) echo "checklist-apply: unknown arg '$1'" >&2; return 1 ;;
|
|
1123
|
+
esac
|
|
1124
|
+
done
|
|
1125
|
+
[ -n "$file" ] || { echo "checklist-apply: --file required" >&2; return 1; }
|
|
1126
|
+
AW_FILE="$file" AW_RESULTS="$results" python3 - <<'PY'
|
|
1127
|
+
import json, os, re
|
|
1128
|
+
f = os.environ["AW_FILE"]
|
|
1129
|
+
if not os.path.exists(f):
|
|
1130
|
+
print(json.dumps({"exists": False, "before_checked": 0, "after_checked": 0,
|
|
1131
|
+
"total": 0, "newly_passing": [], "regressions": []})); raise SystemExit
|
|
1132
|
+
try:
|
|
1133
|
+
results = {int(k): bool(v) for k, v in json.loads(os.environ["AW_RESULTS"] or "{}").items()}
|
|
1134
|
+
except Exception:
|
|
1135
|
+
results = {}
|
|
1136
|
+
lines = open(f, encoding="utf-8").read().splitlines(keepends=True)
|
|
1137
|
+
n, fence = 0, False
|
|
1138
|
+
before = after = 0
|
|
1139
|
+
newly, regress = [], []
|
|
1140
|
+
out = []
|
|
1141
|
+
for line in lines:
|
|
1142
|
+
s = line.strip()
|
|
1143
|
+
if s.startswith("```"):
|
|
1144
|
+
fence = not fence; out.append(line); continue
|
|
1145
|
+
m = re.match(r'^(\s*- \[)([ xX])(\].*)$', line.rstrip("\n")) if not fence else None
|
|
1146
|
+
if m:
|
|
1147
|
+
n += 1
|
|
1148
|
+
cur = m.group(2) in "xX"
|
|
1149
|
+
before += 1 if cur else 0
|
|
1150
|
+
desired = results.get(n, cur)
|
|
1151
|
+
if desired != cur:
|
|
1152
|
+
if desired: newly.append(n)
|
|
1153
|
+
else: regress.append(n)
|
|
1154
|
+
marker = "x" if desired else " "
|
|
1155
|
+
nl = "\n" if line.endswith("\n") else ""
|
|
1156
|
+
out.append(f"{m.group(1)}{marker}{m.group(3)}{nl}")
|
|
1157
|
+
after += 1 if desired else 0
|
|
1158
|
+
else:
|
|
1159
|
+
out.append(line)
|
|
1160
|
+
open(f, "w", encoding="utf-8").write("".join(out))
|
|
1161
|
+
print(json.dumps({"exists": True, "before_checked": before, "after_checked": after,
|
|
1162
|
+
"total": n, "newly_passing": newly, "regressions": regress}))
|
|
1163
|
+
PY
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
|
|
1167
|
+
|
|
1168
|
+
# ─────────────────────────────── detect-project ────────────────────────────
|
|
1169
|
+
# Project Structure Gate: is there a scaffolded app (not just framework files)?
|
|
1170
|
+
#
|
|
1171
|
+
# EVERY ecosystem's manifest is searched to the same depth. A project is where its
|
|
1172
|
+
# manifest is, not where the repository root is: a Maven service copied into
|
|
1173
|
+
# backend/orderservice/ is as scaffolded as a .sln at the top. Searching *.sln
|
|
1174
|
+
# recursively while testing pom.xml and package.json only at the root reported
|
|
1175
|
+
# every non-.NET layout that nests its project as no-project-at-all, which reads
|
|
1176
|
+
# at the gate as "run /appweaver foundation first" — advice a stack with no
|
|
1177
|
+
# foundation bundle cannot take.
|
|
1178
|
+
#
|
|
1179
|
+
# --root scopes the search to one lane's subtree: the directory the calling phase
|
|
1180
|
+
# is about to write into, so the answer is about that lane's project rather than
|
|
1181
|
+
# about any project anywhere. It falls back to the whole repository when the
|
|
1182
|
+
# subtree holds no manifest of its own, which is the ordinary .NET layout — a
|
|
1183
|
+
# lane rooted at src/ under a solution file that sits beside it.
|
|
1184
|
+
_project_markers() {
|
|
1185
|
+
local repo_root="$1" start="$2"
|
|
1186
|
+
find "$start" \
|
|
1187
|
+
\( -path "$repo_root/.appweaver" -o -path "$repo_root/guardrails" \
|
|
1188
|
+
-o -path "$repo_root/foundations" -o -path "$repo_root/specs" \
|
|
1189
|
+
-o -path "$repo_root/.git" -o -name node_modules \) -prune -o \
|
|
1190
|
+
\( -name "*.sln" -o -name "*.csproj" -o -name "package.json" \
|
|
1191
|
+
-o -name "pom.xml" -o -name "build.gradle" -o -name "build.gradle.kts" \
|
|
1192
|
+
-o -name "go.mod" -o -name "Cargo.toml" -o -name "pyproject.toml" \) -print 2>/dev/null \
|
|
1193
|
+
| sed "s#^$repo_root/##" | LC_ALL=C sort | head -20
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
cmd_detect_project() {
|
|
1197
|
+
local scope_root=""
|
|
1198
|
+
while [ $# -gt 0 ]; do
|
|
1199
|
+
case "$1" in
|
|
1200
|
+
--root|--source-path) scope_root="$2"; shift 2 ;;
|
|
1201
|
+
--help|-h) echo "detect-project [--root <dir>] -> {scaffolded, markers[], scope}"; return 0 ;;
|
|
1202
|
+
*) echo "detect-project: unknown arg '$1'" >&2; return 1 ;;
|
|
1203
|
+
esac
|
|
1204
|
+
done
|
|
1205
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
1206
|
+
local -a markers=()
|
|
1207
|
+
local scope="repo" start="$repo_root" rel
|
|
1208
|
+
# A lane's source path arrives as the value declare stored, so it may carry a
|
|
1209
|
+
# trailing slash; an option label ("path — why") never reaches a script argument.
|
|
1210
|
+
rel="${scope_root#./}"; rel="${rel%/}"
|
|
1211
|
+
if [ -n "$rel" ] && [ -d "$repo_root/$rel" ]; then
|
|
1212
|
+
start="$repo_root/$rel"; scope="lane"
|
|
1213
|
+
fi
|
|
1214
|
+
while IFS= read -r m; do [ -n "$m" ] && markers+=("$m"); done < <(_project_markers "$repo_root" "$start")
|
|
1215
|
+
if [ ${#markers[@]} -eq 0 ] && [ "$scope" = "lane" ]; then
|
|
1216
|
+
scope="repo"
|
|
1217
|
+
while IFS= read -r m; do [ -n "$m" ] && markers+=("$m"); done < <(_project_markers "$repo_root" "$repo_root")
|
|
1218
|
+
fi
|
|
1219
|
+
local scaffolded=false
|
|
1220
|
+
[ ${#markers[@]} -gt 0 ] && scaffolded=true
|
|
1221
|
+
printf '{"scaffolded":%s,"markers":%s,"scope":"%s"}\n' "$scaffolded" "$(json_str_array "${markers[@]}")" "$scope"
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
|
|
1225
|
+
# ───────────────────────── foundation resolution ───────────────────────────
|
|
1226
|
+
# Foundations live at foundations/<stack>/<Name>/, grouped by the technology stack
|
|
1227
|
+
# they scaffold. Names are unique across stacks, so callers pass a bare <Name> and
|
|
1228
|
+
# the lookup searches every stack.
|
|
1229
|
+
|
|
1230
|
+
# Echo "<stack>/<Name>" for each foundation directory, one per line. A stack also
|
|
1231
|
+
# holds siblings that are not foundations (components/), so carrying a *.pack.txt is
|
|
1232
|
+
# what makes a directory one — without that test they surface as bundle-less
|
|
1233
|
+
# foundations in every lookup.
|
|
1234
|
+
_foundation_dirs() {
|
|
1235
|
+
local repo_root="$1" s d
|
|
1236
|
+
[ -d "$repo_root/foundations" ] || return 0
|
|
1237
|
+
for s in "$repo_root/foundations"/*/; do
|
|
1238
|
+
[ -d "$s" ] || continue
|
|
1239
|
+
for d in "$s"*/; do
|
|
1240
|
+
[ -d "$d" ] || continue
|
|
1241
|
+
ls "$d"*.pack.txt >/dev/null 2>&1 && printf '%s/%s\n' "$(basename "$s")" "$(basename "$d")"
|
|
1242
|
+
done
|
|
1243
|
+
done
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
# Echo the stack a foundation NAME belongs to; empty when the name is unknown.
|
|
1247
|
+
_foundation_stack() {
|
|
1248
|
+
local repo_root="$1" name="$2" rel
|
|
1249
|
+
while IFS= read -r rel; do
|
|
1250
|
+
[ "${rel##*/}" = "$name" ] && { printf '%s' "${rel%%/*}"; return 0; }
|
|
1251
|
+
done < <(_foundation_dirs "$repo_root")
|
|
1252
|
+
return 0
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
# ──────────────────────────── foundation-list ──────────────────────────────
|
|
1256
|
+
# Enumerate available foundations (foundations/<stack>/<Name>/ that carry a real
|
|
1257
|
+
# <Name>.pack.txt) so the workflow can let the user PICK one — deterministic, no LLM. An optional
|
|
1258
|
+
# --args "<Foundation> <App> [out]" is parsed as a direct-argument fast path:
|
|
1259
|
+
# preselect_name is echoed back only when it names a real foundation; preselect_app
|
|
1260
|
+
# / out_dir carry any 2nd/3rd tokens through (empty/"." otherwise).
|
|
1261
|
+
cmd_foundation_list() {
|
|
1262
|
+
local raw=""
|
|
1263
|
+
while [ $# -gt 0 ]; do
|
|
1264
|
+
case "$1" in
|
|
1265
|
+
--args) raw="$2"; shift 2 ;;
|
|
1266
|
+
--help|-h) echo "foundation-list [--args '<Foundation> <App> [out]'] -> {foundations[], count, preselect_name, preselect_app, out_dir}"; return 0 ;;
|
|
1267
|
+
*) echo "foundation-list: unknown arg '$1'" >&2; return 1 ;;
|
|
1268
|
+
esac
|
|
1269
|
+
done
|
|
1270
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
1271
|
+
|
|
1272
|
+
# WHICH foundations this repo may be offered. A foundation bundle is a whole
|
|
1273
|
+
# compiling project in one ecosystem's languages and build system, so unpacking
|
|
1274
|
+
# a .NET one into a Spring repo does not produce a scaffold — it produces a
|
|
1275
|
+
# second, foreign project beside the real one. The declared lanes already say
|
|
1276
|
+
# which ecosystems this repo is, so filter to those and let the gate say why
|
|
1277
|
+
# when that leaves nothing. Nothing declared (a bare greenfield repo, before
|
|
1278
|
+
# select-stack) filters nothing: there is no answer yet to filter on.
|
|
1279
|
+
local -a lane_ecos=()
|
|
1280
|
+
local e
|
|
1281
|
+
while IFS= read -r e; do [ -n "$e" ] && lane_ecos+=("$e"); done < <(_aw_ecosystems)
|
|
1282
|
+
|
|
1283
|
+
local -a foundations=() all=() eco_of=()
|
|
1284
|
+
local rel name eco keep
|
|
1285
|
+
while IFS= read -r rel; do
|
|
1286
|
+
name="${rel##*/}"; eco="${rel%%/*}"
|
|
1287
|
+
[ -f "$repo_root/foundations/$rel/$name.pack.txt" ] || continue # the primary pack (excludes Reference-only dirs)
|
|
1288
|
+
all+=("$name"); eco_of+=("$eco")
|
|
1289
|
+
keep=true
|
|
1290
|
+
if [ ${#lane_ecos[@]} -gt 0 ]; then
|
|
1291
|
+
keep=false
|
|
1292
|
+
for e in "${lane_ecos[@]}"; do [ "$e" = "$eco" ] && keep=true; done
|
|
1293
|
+
fi
|
|
1294
|
+
[ "$keep" = true ] && foundations+=("$name")
|
|
1295
|
+
done < <(_foundation_dirs "$repo_root")
|
|
1296
|
+
# Optional direct-argument fast path: "<FoundationName> <AppName> [OutputDir]".
|
|
1297
|
+
local pre_name="" pre_app="" out_dir="."
|
|
1298
|
+
if [ -n "$raw" ]; then
|
|
1299
|
+
# shellcheck disable=SC2086
|
|
1300
|
+
set -- $raw
|
|
1301
|
+
[ -n "${1:-}" ] && pre_name="$1"; [ -n "${2:-}" ] && pre_app="$2"; [ -n "${3:-}" ] && out_dir="$3"
|
|
1302
|
+
fi
|
|
1303
|
+
# Only preselect a name that actually names an available foundation.
|
|
1304
|
+
local match="" f
|
|
1305
|
+
for f in "${foundations[@]}"; do [ "$f" = "$pre_name" ] && match="$pre_name"; done
|
|
1306
|
+
printf '{"foundations":%s,"count":%s,"all_foundations":%s,"all_count":%s,"ecosystems":%s,"lane_ecosystems":%s,"filtered":%s,"preselect_name":"%s","preselect_app":"%s","out_dir":"%s"}\n' \
|
|
1307
|
+
"$(json_str_array "${foundations[@]}")" "${#foundations[@]}" \
|
|
1308
|
+
"$(json_str_array "${all[@]}")" "${#all[@]}" \
|
|
1309
|
+
"$(json_str_array "${eco_of[@]}")" "$(json_str_array "${lane_ecos[@]}")" \
|
|
1310
|
+
"$([ ${#lane_ecos[@]} -gt 0 ] && echo true || echo false)" \
|
|
1311
|
+
"$(json_escape "$match")" "$(json_escape "$pre_app")" "$(json_escape "$out_dir")"
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
# ─────────────────────────── foundation-locate ─────────────────────────────
|
|
1315
|
+
# Parse/validate args, locate the pack + unpacker, read the pack header (bounded).
|
|
1316
|
+
cmd_foundation_locate() {
|
|
1317
|
+
local name="" app="" out="." raw=""
|
|
1318
|
+
while [ $# -gt 0 ]; do
|
|
1319
|
+
case "$1" in
|
|
1320
|
+
--name) name="$2"; shift 2 ;;
|
|
1321
|
+
--app) app="$2"; shift 2 ;;
|
|
1322
|
+
--out) out="$2"; shift 2 ;;
|
|
1323
|
+
--args) raw="$2"; shift 2 ;;
|
|
1324
|
+
--help|-h) echo "foundation-locate (--args '<F> <A> [out]' | --name <F> --app <A> [--out <dir>])"; return 0 ;;
|
|
1325
|
+
*) echo "foundation-locate: unknown arg '$1'" >&2; return 1 ;;
|
|
1326
|
+
esac
|
|
1327
|
+
done
|
|
1328
|
+
# positional parse: "<FoundationName> <AppName> [OutputDir]"
|
|
1329
|
+
if [ -n "$raw" ]; then
|
|
1330
|
+
# shellcheck disable=SC2086
|
|
1331
|
+
set -- $raw
|
|
1332
|
+
[ -n "$1" ] && name="$1"; [ -n "$2" ] && app="$2"; [ -n "$3" ] && out="$3"
|
|
1333
|
+
fi
|
|
1334
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
1335
|
+
# available = every foundation dir (_foundation_dirs already requires a *.pack.txt)
|
|
1336
|
+
local -a available=()
|
|
1337
|
+
local rel
|
|
1338
|
+
while IFS= read -r rel; do
|
|
1339
|
+
available+=("${rel##*/}")
|
|
1340
|
+
done < <(_foundation_dirs "$repo_root")
|
|
1341
|
+
_fnd_fail() { printf '{"ok":false,"reason":"%s","available":%s}\n' "$(json_escape "$1")" "$(json_str_array "${available[@]}")"; }
|
|
1342
|
+
if [ -z "$name" ] || [ -z "$app" ]; then
|
|
1343
|
+
_fnd_fail "Usage: /aw-spec-foundation <FoundationName> <AppName> [OutputDirectory]"; return 0
|
|
1344
|
+
fi
|
|
1345
|
+
local stack; stack=$(_foundation_stack "$repo_root" "$name")
|
|
1346
|
+
if [ -z "$stack" ]; then _fnd_fail "Foundation '$name' not found (expected foundations/<stack>/$name/)"; return 0; fi
|
|
1347
|
+
local rel="foundations/$stack/$name"
|
|
1348
|
+
local fdir="$repo_root/$rel"
|
|
1349
|
+
local pack="$fdir/$name.pack.txt"
|
|
1350
|
+
if [ ! -f "$pack" ]; then _fnd_fail "Foundation '$name' has no bundle yet (expected $pack). Build it via the pack scripts in $rel/."; return 0; fi
|
|
1351
|
+
# runner + unpacker
|
|
1352
|
+
local runner unpacker
|
|
1353
|
+
if command -v pwsh >/dev/null 2>&1 && [ -f "$fdir/unpack-foundation.ps1" ]; then
|
|
1354
|
+
runner="pwsh"; unpacker="$rel/unpack-foundation.ps1"
|
|
1355
|
+
elif [ -f "$fdir/unpack-foundation.sh" ]; then
|
|
1356
|
+
runner="sh"; unpacker="$rel/unpack-foundation.sh"
|
|
1357
|
+
else
|
|
1358
|
+
emit_fail "No unpacker found in $rel/ (unpack-foundation.ps1 or .sh)"; return 0
|
|
1359
|
+
fi
|
|
1360
|
+
# bounded header read (never load the pack body)
|
|
1361
|
+
local header sentinel file_count
|
|
1362
|
+
header=$(grep -m 30 '^#' "$pack" 2>/dev/null || true)
|
|
1363
|
+
sentinel=$(printf '%s\n' "$header" | grep -iE 'sentinel' | head -1 | sed -E 's/.*[sS]entinel[^A-Za-z0-9_.]*//; s/[^A-Za-z0-9_.].*$//')
|
|
1364
|
+
file_count=$(printf '%s\n' "$header" | grep -iE 'file' | grep -oE '[0-9]+' | head -1)
|
|
1365
|
+
[ -n "$file_count" ] || file_count="0"
|
|
1366
|
+
|
|
1367
|
+
printf '{"ok":true,"reason":"","name":"%s","stack":"%s","app":"%s","out_dir":"%s","pack_file":"%s","unpacker":"%s","runner":"%s","sentinel":"%s","file_count":%s,"available":%s}\n' \
|
|
1368
|
+
"$(json_escape "$name")" "$(json_escape "$stack")" "$(json_escape "$app")" "$(json_escape "$out")" "$(json_escape "$rel/$name.pack.txt")" \
|
|
1369
|
+
"$(json_escape "$unpacker")" "$runner" "$(json_escape "$sentinel")" "$file_count" "$(json_str_array "${available[@]}")"
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
# ─────────────────────────── foundation-unpack ─────────────────────────────
|
|
1373
|
+
# Run the OS-selected unpacker for the foundation (or reference) pack.
|
|
1374
|
+
cmd_foundation_unpack() {
|
|
1375
|
+
local name="" app="" out="." dry=false force="no" reference=false
|
|
1376
|
+
while [ $# -gt 0 ]; do
|
|
1377
|
+
case "$1" in
|
|
1378
|
+
--name) name="$2"; shift 2 ;;
|
|
1379
|
+
--app) app="$2"; shift 2 ;;
|
|
1380
|
+
--out) out="$2"; shift 2 ;;
|
|
1381
|
+
--dry-run) dry=true; shift ;;
|
|
1382
|
+
--force) force="$2"; shift 2 ;;
|
|
1383
|
+
--reference) reference=true; shift ;;
|
|
1384
|
+
--help|-h) echo "foundation-unpack --name <F> --app <A> --out <d> [--dry-run] [--force <yes|no>] [--reference]"; return 0 ;;
|
|
1385
|
+
*) echo "foundation-unpack: unknown arg '$1'" >&2; return 1 ;;
|
|
1386
|
+
esac
|
|
1387
|
+
done
|
|
1388
|
+
[ -n "$name" ] && [ -n "$app" ] || { echo "foundation-unpack: --name and --app required" >&2; return 1; }
|
|
1389
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
1390
|
+
local stack; stack=$(_foundation_stack "$repo_root" "$name")
|
|
1391
|
+
local fdir="$repo_root/foundations/$stack/$name"
|
|
1392
|
+
local packbase="$name" script_stem="unpack-foundation"
|
|
1393
|
+
if [ "$reference" = true ]; then packbase="$name.Reference"; script_stem="unpack-reference-foundation"; fi
|
|
1394
|
+
local pack="$fdir/$packbase.pack.txt"
|
|
1395
|
+
if [ ! -f "$pack" ]; then printf '{"ok":false,"mode":"%s","written":0,"has_collisions":false,"stdout":"pack not found: %s"}\n' "$([ "$dry" = true ] && echo dry || echo real)" "$(json_escape "$packbase.pack.txt")"; return 0; fi
|
|
1396
|
+
|
|
1397
|
+
# pick runner
|
|
1398
|
+
local -a cmd=()
|
|
1399
|
+
if command -v pwsh >/dev/null 2>&1 && [ -f "$fdir/$script_stem.ps1" ]; then
|
|
1400
|
+
cmd=(pwsh -NoProfile -File "$fdir/$script_stem.ps1" -PackFile "$pack" -TargetAppName "$app" -OutDir "$out")
|
|
1401
|
+
[ "$dry" = true ] && cmd+=(-DryRun)
|
|
1402
|
+
[ "$force" = "yes" ] && cmd+=(-Force)
|
|
1403
|
+
[ "$reference" = true ] && [ "$force" != "yes" ] && cmd+=(-Force) # reference always forces
|
|
1404
|
+
elif [ -f "$fdir/$script_stem.sh" ]; then
|
|
1405
|
+
cmd=(bash "$fdir/$script_stem.sh" --pack-file "$pack" --target-app-name "$app" --out-dir "$out")
|
|
1406
|
+
[ "$dry" = true ] && cmd+=(--dry-run)
|
|
1407
|
+
{ [ "$force" = "yes" ] || [ "$reference" = true ]; } && cmd+=(--force)
|
|
1408
|
+
else
|
|
1409
|
+
printf '{"ok":false,"mode":"real","written":0,"has_collisions":false,"stdout":"no unpacker script for %s"}\n' "$(json_escape "$name")"; return 0
|
|
1410
|
+
fi
|
|
1411
|
+
|
|
1412
|
+
local outp rc
|
|
1413
|
+
rc=0
|
|
1414
|
+
outp=$("${cmd[@]}" 2>&1) || rc=$? # set -e safe: a failing unpacker (dry-run collisions) must still yield JSON
|
|
1415
|
+
# unpackers say "Unpacked N file(s) to …" / "DryRun: N file(s) would be written …"
|
|
1416
|
+
local written; written=$(printf '%s' "$outp" | grep -oiE '[0-9]+ +file' | grep -oE '[0-9]+' | head -1)
|
|
1417
|
+
[ -n "$written" ] || written=0
|
|
1418
|
+
local has_coll=false
|
|
1419
|
+
if [ "$dry" = true ] && [ $rc -ne 0 ]; then has_coll=true; fi
|
|
1420
|
+
printf '{"ok":%s,"mode":"%s","written":%s,"has_collisions":%s,"stdout":"%s"}\n' \
|
|
1421
|
+
"$([ $rc -eq 0 ] && echo true || echo false)" "$([ "$dry" = true ] && echo dry || echo real)" \
|
|
1422
|
+
"$written" "$has_coll" "$(json_escape "$(printf '%s' "$outp" | tail -c 800)")"
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
# ─────────────────────── foundation-stash-dbcontext ────────────────────────
|
|
1426
|
+
# Copy the current ApplicationDbContext.cs to a .foundation.bak sibling before a
|
|
1427
|
+
# reference unpack overwrites it (so the two can be merged afterward).
|
|
1428
|
+
cmd_foundation_stash_dbcontext() {
|
|
1429
|
+
local out="."
|
|
1430
|
+
while [ $# -gt 0 ]; do
|
|
1431
|
+
case "$1" in
|
|
1432
|
+
--out) out="$2"; shift 2 ;;
|
|
1433
|
+
--help|-h) echo "foundation-stash-dbcontext --out <dir>"; return 0 ;;
|
|
1434
|
+
*) echo "foundation-stash-dbcontext: unknown arg '$1'" >&2; return 1 ;;
|
|
1435
|
+
esac
|
|
1436
|
+
done
|
|
1437
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
1438
|
+
[[ "$out" != /* ]] && out="$repo_root/$out"
|
|
1439
|
+
local ctx; ctx=$(find "$out/src" -name "ApplicationDbContext.cs" 2>/dev/null | head -1)
|
|
1440
|
+
if [ -z "$ctx" ]; then printf '{"found":false,"dbcontext":"","backup":""}\n'; return 0; fi
|
|
1441
|
+
local bak="$ctx.foundation.bak"
|
|
1442
|
+
cp "$ctx" "$bak"
|
|
1443
|
+
printf '{"found":true,"dbcontext":"%s","backup":"%s"}\n' "$(json_escape "$ctx")" "$(json_escape "$bak")"
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
# ─────────────────────────── foundation-verify ─────────────────────────────
|
|
1447
|
+
# Scan the output tree for leftover sentinel tokens (should be none).
|
|
1448
|
+
cmd_foundation_verify() {
|
|
1449
|
+
local out="." sentinel=""
|
|
1450
|
+
while [ $# -gt 0 ]; do
|
|
1451
|
+
case "$1" in
|
|
1452
|
+
--out) out="$2"; shift 2 ;;
|
|
1453
|
+
--sentinel) sentinel="$2"; shift 2 ;;
|
|
1454
|
+
--help|-h) echo "foundation-verify --out <dir> --sentinel <s>"; return 0 ;;
|
|
1455
|
+
*) echo "foundation-verify: unknown arg '$1'" >&2; return 1 ;;
|
|
1456
|
+
esac
|
|
1457
|
+
done
|
|
1458
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
1459
|
+
[[ "$out" != /* ]] && out="$repo_root/$out"
|
|
1460
|
+
local -a residue=()
|
|
1461
|
+
if [ -n "$sentinel" ] && [ -d "$out" ]; then
|
|
1462
|
+
# scan the generated tree only — never the bundle/framework dirs (the
|
|
1463
|
+
# .pack.txt legitimately contains the sentinel).
|
|
1464
|
+
while IFS= read -r m; do [ -n "$m" ] && residue+=("$m"); done < <(
|
|
1465
|
+
grep -rIl --exclude-dir=.git --exclude-dir=guardrails --exclude-dir=foundations --exclude-dir=.appweaver \
|
|
1466
|
+
--exclude-dir=specs --exclude-dir=node_modules -- "$sentinel" "$out" 2>/dev/null \
|
|
1467
|
+
| sed "s#^$repo_root/##" | head -50 )
|
|
1468
|
+
fi
|
|
1469
|
+
printf '{"clean":%s,"residue":%s}\n' "$([ ${#residue[@]} -eq 0 ] && echo true || echo false)" "$(json_str_array "${residue[@]}")"
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
# ─────────────────────────────── setup-constitution ────────────────────────
|
|
1473
|
+
# Ensure the split constitution files exist (copy from templates if missing):
|
|
1474
|
+
# .appweaver/memory/constitution.technical.md (from constitution-technical-template)
|
|
1475
|
+
# .appweaver/memory/constitution.domain.md (from constitution-domain-template)
|
|
1476
|
+
# .appweaver/memory/constitution.ui.md (from constitution-ui-template) — ONLY with --ui
|
|
1477
|
+
# The composed .appweaver/memory/constitution.md is produced by `compose-constitution`
|
|
1478
|
+
# after the halves are authored. Each half is authored + verified independently.
|
|
1479
|
+
#
|
|
1480
|
+
# The UI half is opt-in because it is the only one whose ABSENCE is meaningful: a
|
|
1481
|
+
# backend-only project has no frontend lane and must look exactly as it did before
|
|
1482
|
+
# the lane model existed. The other two are always created and left as unauthored
|
|
1483
|
+
# templates when unused, which compose-constitution then skips.
|
|
1484
|
+
cmd_setup_constitution() {
|
|
1485
|
+
# --ui / --no-technical follow the LANES the project declared. A half is a lane's
|
|
1486
|
+
# standing governance, so a lane the project does not have gets no half — and the
|
|
1487
|
+
# file is not created either, because an empty template left in memory/ is
|
|
1488
|
+
# indistinguishable from a half somebody forgot to author.
|
|
1489
|
+
local want_ui=false want_tech=true
|
|
1490
|
+
while [ $# -gt 0 ]; do
|
|
1491
|
+
case "$1" in
|
|
1492
|
+
--ui) want_ui=true; shift ;;
|
|
1493
|
+
--no-technical) want_tech=false; shift ;;
|
|
1494
|
+
# An EMPTY argument is a flag slot the caller left unfilled. The engine
|
|
1495
|
+
# builds a step's `args:` list before it knows which branch it is in, so a
|
|
1496
|
+
# workflow that varies one flag has to choose between duplicating the whole
|
|
1497
|
+
# step and passing "" — and duplicating it is how this call came to recreate
|
|
1498
|
+
# the technical half that the earlier call had just been told to skip.
|
|
1499
|
+
"") shift ;;
|
|
1500
|
+
--help|-h) echo "setup-constitution [--ui] [--no-technical]"; return 0 ;;
|
|
1501
|
+
*) echo "setup-constitution: unknown arg '$1'" >&2; return 1 ;;
|
|
1502
|
+
esac
|
|
1503
|
+
done
|
|
1504
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
1505
|
+
mkdir -p "$repo_root/.appweaver/memory"
|
|
1506
|
+
local tech="$repo_root/.appweaver/memory/constitution.technical.md"
|
|
1507
|
+
local dom="$repo_root/.appweaver/memory/constitution.domain.md"
|
|
1508
|
+
local ui="$repo_root/.appweaver/memory/constitution.ui.md"
|
|
1509
|
+
local tech_created=false dom_created=false ui_created=false ui_path=""
|
|
1510
|
+
if [ "$want_tech" != true ]; then tech=""; fi
|
|
1511
|
+
if [ -n "$tech" ] && [ ! -f "$tech" ]; then
|
|
1512
|
+
local tpl; tpl=$(resolve_template "constitution-technical-template" "$repo_root") || true
|
|
1513
|
+
if [ -n "$tpl" ] && [ -f "$tpl" ]; then cp "$tpl" "$tech"; else touch "$tech"; fi
|
|
1514
|
+
tech_created=true
|
|
1515
|
+
fi
|
|
1516
|
+
if [ ! -f "$dom" ]; then
|
|
1517
|
+
local tpl; tpl=$(resolve_template "constitution-domain-template" "$repo_root") || true
|
|
1518
|
+
if [ -n "$tpl" ] && [ -f "$tpl" ]; then cp "$tpl" "$dom"; else touch "$dom"; fi
|
|
1519
|
+
dom_created=true
|
|
1520
|
+
fi
|
|
1521
|
+
if [ "$want_ui" = true ]; then
|
|
1522
|
+
ui_path="$ui"
|
|
1523
|
+
if [ ! -f "$ui" ]; then
|
|
1524
|
+
local tpl; tpl=$(resolve_template "constitution-ui-template" "$repo_root") || true
|
|
1525
|
+
if [ -n "$tpl" ] && [ -f "$tpl" ]; then cp "$tpl" "$ui"; else touch "$ui"; fi
|
|
1526
|
+
ui_created=true
|
|
1527
|
+
fi
|
|
1528
|
+
fi
|
|
1529
|
+
printf '{"constitution_technical_file":"%s","constitution_domain_file":"%s","constitution_ui_file":"%s","technical_created":%s,"domain_created":%s,"ui_created":%s}\n' \
|
|
1530
|
+
"$(json_escape "$tech")" "$(json_escape "$dom")" "$(json_escape "$ui_path")" \
|
|
1531
|
+
"$tech_created" "$dom_created" "$ui_created"
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
# ─────────────────────────────── compose-constitution ──────────────────────
|
|
1535
|
+
# Deterministically concatenate the authored halves into the composed
|
|
1536
|
+
# .appweaver/memory/constitution.md (human/compat view; NOT loaded by the pipeline
|
|
1537
|
+
# phases — they load the halves directly). No model. If a half is absent or still
|
|
1538
|
+
# an unauthored template, it is skipped; a technical-only project's composed file
|
|
1539
|
+
# mirrors the technical half exactly.
|
|
1540
|
+
#
|
|
1541
|
+
# Order is technical → domain → ui. The domain half's principles CONTINUE the
|
|
1542
|
+
# technical base's roman numerals from wherever that base ended, so those two stay
|
|
1543
|
+
# adjacent; the UI half runs a parallel UX-* series and appends without interrupting
|
|
1544
|
+
# that run.
|
|
1545
|
+
#
|
|
1546
|
+
# NUMBERING CHECK. The boundary between the two roman-numeral halves is derived per
|
|
1547
|
+
# project — the technical half runs to whatever length its mapped technology base
|
|
1548
|
+
# defines — so a base of eighteen principles and a domain half that assumed it could
|
|
1549
|
+
# start at XVI would silently produce two Principle XVIs. Composition is the one place
|
|
1550
|
+
# both halves are read together, so it is where that is caught. A collision sets
|
|
1551
|
+
# numbering_ok=false; the file is still written, because a composed view that shows the
|
|
1552
|
+
# clash is more useful for fixing it than no file at all.
|
|
1553
|
+
#
|
|
1554
|
+
# UNFILLED-PLACEHOLDER TEST. "Still a raw template" is decided by intersecting the
|
|
1555
|
+
# half's [TOKEN]s with the TOKENS ITS OWN KIT TEMPLATE DEFINES — never by the shape of
|
|
1556
|
+
# the bracket. A constitution is free to write [HIPAA], [ISO_13485] or a [GDPR](url)
|
|
1557
|
+
# link; those are project content and match no template token, so they can no longer
|
|
1558
|
+
# make a whole ratified half vanish from the composed file.
|
|
1559
|
+
cmd_compose_constitution() {
|
|
1560
|
+
while [ $# -gt 0 ]; do
|
|
1561
|
+
case "$1" in
|
|
1562
|
+
--help|-h) echo "compose-constitution"; return 0 ;;
|
|
1563
|
+
*) echo "compose-constitution: unknown arg '$1'" >&2; return 1 ;;
|
|
1564
|
+
esac
|
|
1565
|
+
done
|
|
1566
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
1567
|
+
local tpl_tech tpl_dom tpl_ui
|
|
1568
|
+
tpl_tech=$(resolve_template "constitution-technical-template" "$repo_root") || true
|
|
1569
|
+
tpl_dom=$(resolve_template "constitution-domain-template" "$repo_root") || true
|
|
1570
|
+
tpl_ui=$(resolve_template "constitution-ui-template" "$repo_root") || true
|
|
1571
|
+
AW_MEM="$repo_root/.appweaver/memory" \
|
|
1572
|
+
AW_TPL_TECH="$tpl_tech" AW_TPL_DOM="$tpl_dom" AW_TPL_UI="$tpl_ui" python3 - <<'PY'
|
|
1573
|
+
import json, os, re
|
|
1574
|
+
|
|
1575
|
+
mem = os.environ["AW_MEM"]
|
|
1576
|
+
tech = os.path.join(mem, "constitution.technical.md")
|
|
1577
|
+
dom = os.path.join(mem, "constitution.domain.md")
|
|
1578
|
+
ui = os.path.join(mem, "constitution.ui.md")
|
|
1579
|
+
out = os.path.join(mem, "constitution.md")
|
|
1580
|
+
|
|
1581
|
+
include_tech = os.path.exists(tech)
|
|
1582
|
+
|
|
1583
|
+
# A project that declared no SERVICE lane authors no technical half, and that is a
|
|
1584
|
+
# complete constitution rather than a broken one — a UI-only project's governance is
|
|
1585
|
+
# the UI half plus the domain addenda. What is not composable is a project with
|
|
1586
|
+
# nothing at all, which means the halves were never authored.
|
|
1587
|
+
if not (include_tech or os.path.exists(dom) or os.path.exists(ui)):
|
|
1588
|
+
print(json.dumps({"composed_file": out, "ok": False, "domain_included": False,
|
|
1589
|
+
"ui_included": False, "technical_included": False,
|
|
1590
|
+
"numbering_ok": True, "numbering_checked": False,
|
|
1591
|
+
"issues": ["no constitution half found — run /appweaver constitution"]}))
|
|
1592
|
+
raise SystemExit
|
|
1593
|
+
|
|
1594
|
+
TOKEN = re.compile(r"\[([A-Z][A-Z0-9_]*)\]")
|
|
1595
|
+
|
|
1596
|
+
def read(p):
|
|
1597
|
+
return open(p, encoding="utf-8").read()
|
|
1598
|
+
|
|
1599
|
+
def strip_comments(s):
|
|
1600
|
+
return re.sub(r"<!--.*?-->", "", s, flags=re.S)
|
|
1601
|
+
|
|
1602
|
+
def token_family(tokens):
|
|
1603
|
+
# The template's numbered slots are a series the author is told to extend, so
|
|
1604
|
+
# PRINCIPLE_6_NAME is as much an unfilled slot as PRINCIPLE_1_NAME even though the
|
|
1605
|
+
# template only shows five. Digit runs generalise; nothing else does, which is what
|
|
1606
|
+
# keeps a bracketed regulation code out of the family.
|
|
1607
|
+
return {re.compile(r"\A" + re.sub(r"\d+", r"\\d+", re.escape(t)) + r"\Z")
|
|
1608
|
+
for t in tokens if re.search(r"\d", t)}
|
|
1609
|
+
|
|
1610
|
+
def template_tokens(env):
|
|
1611
|
+
# The kit template's own token vocabulary. None = template unresolvable.
|
|
1612
|
+
p = os.environ.get(env, "")
|
|
1613
|
+
if not p or not os.path.exists(p):
|
|
1614
|
+
return None
|
|
1615
|
+
return set(TOKEN.findall(read(p)))
|
|
1616
|
+
|
|
1617
|
+
def unfilled(found, tokens):
|
|
1618
|
+
fam = token_family(tokens)
|
|
1619
|
+
return sorted({t for t in found
|
|
1620
|
+
if t in tokens or any(rx.match(t) for rx in fam)})
|
|
1621
|
+
|
|
1622
|
+
def authored(p, env):
|
|
1623
|
+
# An unused half stays a raw template. Comments are stripped first so a Sync
|
|
1624
|
+
# Impact Report that NAMES the placeholders it filled ("Removed: [PROJECT_NAME]")
|
|
1625
|
+
# cannot be mistaken for the placeholders themselves.
|
|
1626
|
+
if not (os.path.exists(p) and os.path.getsize(p) > 0):
|
|
1627
|
+
return False
|
|
1628
|
+
tokens = template_tokens(env)
|
|
1629
|
+
if tokens is None:
|
|
1630
|
+
return True # no template to compare against: never drop a half on a guess
|
|
1631
|
+
return not unfilled(set(TOKEN.findall(strip_comments(read(p)))), tokens)
|
|
1632
|
+
|
|
1633
|
+
include_domain = authored(dom, "AW_TPL_DOM")
|
|
1634
|
+
include_ui = authored(ui, "AW_TPL_UI")
|
|
1635
|
+
|
|
1636
|
+
with open(out, "w", encoding="utf-8") as fh:
|
|
1637
|
+
fh.write("<!-- COMPOSED — generated by `aw-spec-actions compose-constitution`. Do not edit here.\n")
|
|
1638
|
+
fh.write(" Source of truth: the constitution halves — technical + domain + ui (edit those).\n")
|
|
1639
|
+
fh.write(" Pipeline phases load the halves directly; this composed view is for humans/compat. -->\n\n")
|
|
1640
|
+
if include_tech:
|
|
1641
|
+
fh.write(read(tech))
|
|
1642
|
+
if include_domain:
|
|
1643
|
+
fh.write("\n\n---\n\n"); fh.write(read(dom))
|
|
1644
|
+
if include_ui:
|
|
1645
|
+
fh.write("\n\n---\n\n"); fh.write(read(ui))
|
|
1646
|
+
|
|
1647
|
+
# ── principle numbering ────────────────────────────────────────────────────
|
|
1648
|
+
VALUES = [(1000,"M"),(900,"CM"),(500,"D"),(400,"CD"),(100,"C"),(90,"XC"),
|
|
1649
|
+
(50,"L"),(40,"XL"),(10,"X"),(9,"IX"),(5,"V"),(4,"IV"),(1,"I")]
|
|
1650
|
+
|
|
1651
|
+
def to_int(s):
|
|
1652
|
+
total, i = 0, 0
|
|
1653
|
+
for val, sym in VALUES:
|
|
1654
|
+
while s[i:i+len(sym)] == sym:
|
|
1655
|
+
total += val; i += len(sym)
|
|
1656
|
+
return total if i == len(s) else None
|
|
1657
|
+
|
|
1658
|
+
def to_roman(n):
|
|
1659
|
+
out_ = ""
|
|
1660
|
+
for val, sym in VALUES:
|
|
1661
|
+
while n >= val:
|
|
1662
|
+
out_ += sym; n -= val
|
|
1663
|
+
return out_
|
|
1664
|
+
|
|
1665
|
+
# A principle heading opens with its numeral: "### VII — Name" or "### VII. Name".
|
|
1666
|
+
# UX-* is a PARALLEL series belonging to the frontend lane and is deliberately not
|
|
1667
|
+
# part of this run, so it is skipped rather than checked against it.
|
|
1668
|
+
HEAD = re.compile(r"^#{2,4}\s+(UX-)?([IVXLCDM]+)\s*[—–\-.:)]", re.M)
|
|
1669
|
+
|
|
1670
|
+
def principles(path):
|
|
1671
|
+
found = []
|
|
1672
|
+
if not os.path.exists(path):
|
|
1673
|
+
return found
|
|
1674
|
+
body = re.sub(r"<!--.*?-->", "", read(path), flags=re.S)
|
|
1675
|
+
for prefix, roman in HEAD.findall(body):
|
|
1676
|
+
if prefix: # UX- series — parallel, not this sequence
|
|
1677
|
+
continue
|
|
1678
|
+
n = to_int(roman)
|
|
1679
|
+
# Round-trip + a sane ceiling keep ordinary words made of roman letters
|
|
1680
|
+
# (and stray four-digit numerals) out of the sequence.
|
|
1681
|
+
if n and n <= 100 and to_roman(n) == roman:
|
|
1682
|
+
found.append((n, roman))
|
|
1683
|
+
return found
|
|
1684
|
+
|
|
1685
|
+
tech_p = principles(tech)
|
|
1686
|
+
dom_p = principles(dom) if include_domain else []
|
|
1687
|
+
|
|
1688
|
+
issues, numbering_ok = [], True
|
|
1689
|
+
|
|
1690
|
+
# The cross-half check can only run on halves whose principles it can actually read.
|
|
1691
|
+
# A half numbered some other way (1., 2. / P-01) parses to nothing, and silence there
|
|
1692
|
+
# would look identical to a clean result — so say so instead of reporting all-clear.
|
|
1693
|
+
NUMBERING_FORM = ("principles must be headed '### I — Title' or '### I. Title' "
|
|
1694
|
+
"(the UI half runs its own UX-I, UX-II, … series)")
|
|
1695
|
+
numbering_checked = True
|
|
1696
|
+
if not include_tech:
|
|
1697
|
+
# No service lane, so no technical half and no boundary to check: the domain
|
|
1698
|
+
# half opens the roman-numeral sequence at I instead of continuing one. Not an
|
|
1699
|
+
# issue, and not a check that silently passed — it is a check with no subject.
|
|
1700
|
+
numbering_checked = False
|
|
1701
|
+
elif not tech_p:
|
|
1702
|
+
numbering_ok = False
|
|
1703
|
+
numbering_checked = False
|
|
1704
|
+
issues.append("no roman-numeral principle headings found in the technical half, so "
|
|
1705
|
+
"numbering could not be verified — " + NUMBERING_FORM)
|
|
1706
|
+
if include_domain and not dom_p:
|
|
1707
|
+
numbering_ok = False
|
|
1708
|
+
numbering_checked = False
|
|
1709
|
+
issues.append("no roman-numeral principle headings found in the domain half, so "
|
|
1710
|
+
"numbering could not be verified — " + NUMBERING_FORM)
|
|
1711
|
+
|
|
1712
|
+
def dupes_within(items, label):
|
|
1713
|
+
global numbering_ok
|
|
1714
|
+
seen, dup = set(), []
|
|
1715
|
+
for n, roman in items:
|
|
1716
|
+
if n in seen and roman not in dup:
|
|
1717
|
+
dup.append(roman)
|
|
1718
|
+
seen.add(n)
|
|
1719
|
+
if dup:
|
|
1720
|
+
numbering_ok = False
|
|
1721
|
+
issues.append("%s half reuses principle number(s): %s" % (label, ", ".join(dup)))
|
|
1722
|
+
|
|
1723
|
+
dupes_within(tech_p, "technical")
|
|
1724
|
+
dupes_within(dom_p, "domain")
|
|
1725
|
+
|
|
1726
|
+
if tech_p and dom_p:
|
|
1727
|
+
overlap = sorted({n for n, _ in tech_p} & {n for n, _ in dom_p})
|
|
1728
|
+
if overlap:
|
|
1729
|
+
numbering_ok = False
|
|
1730
|
+
issues.append(
|
|
1731
|
+
"principle number(s) %s appear in BOTH the technical and domain halves — "
|
|
1732
|
+
"the domain half must resume from the number after the technical half's "
|
|
1733
|
+
"last principle (%s), not from a fixed value"
|
|
1734
|
+
% (", ".join(to_roman(n) for n in overlap), to_roman(max(n for n, _ in tech_p))))
|
|
1735
|
+
else:
|
|
1736
|
+
last_tech = max(n for n, _ in tech_p)
|
|
1737
|
+
first_dom = min(n for n, _ in dom_p)
|
|
1738
|
+
if first_dom != last_tech + 1:
|
|
1739
|
+
numbering_ok = False
|
|
1740
|
+
issues.append(
|
|
1741
|
+
"domain half starts at %s but the technical half ends at %s — numbering "
|
|
1742
|
+
"must run unbroken across the two halves (expected %s)"
|
|
1743
|
+
% (to_roman(first_dom), to_roman(last_tech), to_roman(last_tech + 1)))
|
|
1744
|
+
|
|
1745
|
+
print(json.dumps({"composed_file": out, "ok": numbering_ok,
|
|
1746
|
+
"technical_included": include_tech,
|
|
1747
|
+
"domain_included": include_domain, "ui_included": include_ui,
|
|
1748
|
+
"numbering_ok": numbering_ok, "numbering_checked": numbering_checked,
|
|
1749
|
+
"issues": issues,
|
|
1750
|
+
"technical_last": (to_roman(max(n for n, _ in tech_p)) if tech_p else None),
|
|
1751
|
+
"domain_first": (to_roman(min(n for n, _ in dom_p)) if dom_p else None)}))
|
|
1752
|
+
PY
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
# ─────────────────────────── constitution-verify ───────────────────────────
|
|
1756
|
+
# Deterministic lint of a generated constitution (NO LLM): unfilled template
|
|
1757
|
+
# placeholders, body-vs-Sync-Impact-Report version agreement, and non-ISO dates.
|
|
1758
|
+
# Consumed by the constitution skill's bounded self-correction loop. ok=false ⇒ the
|
|
1759
|
+
# loop asks the model to patch.
|
|
1760
|
+
#
|
|
1761
|
+
# A placeholder is a token THE KIT TEMPLATE ACTUALLY DEFINES, resolved from the half's
|
|
1762
|
+
# own template (override/preset/extension/core, same order as setup-constitution) and
|
|
1763
|
+
# intersected with the file. Bracket SHAPE is not evidence: [HIPAA], [ISO_13485] and
|
|
1764
|
+
# [GDPR](url) are ordinary constitution content in a regulated domain and must never be
|
|
1765
|
+
# reported as unfilled slots. --template overrides the name derived from the filename.
|
|
1766
|
+
cmd_constitution_verify() {
|
|
1767
|
+
local file="" template=""
|
|
1768
|
+
while [ $# -gt 0 ]; do
|
|
1769
|
+
case "$1" in
|
|
1770
|
+
--file) file="$2"; shift 2 ;;
|
|
1771
|
+
--template) template="$2"; shift 2 ;;
|
|
1772
|
+
--help|-h) echo "constitution-verify --file <path> [--template <name>]"; return 0 ;;
|
|
1773
|
+
*) echo "constitution-verify: unknown arg '$1'" >&2; return 1 ;;
|
|
1774
|
+
esac
|
|
1775
|
+
done
|
|
1776
|
+
[ -n "$file" ] || { echo "constitution-verify: --file required" >&2; return 1; }
|
|
1777
|
+
if [ -z "$template" ]; then
|
|
1778
|
+
case "$(basename "$file")" in
|
|
1779
|
+
constitution.technical.md) template="constitution-technical-template" ;;
|
|
1780
|
+
constitution.domain.md) template="constitution-domain-template" ;;
|
|
1781
|
+
constitution.ui.md) template="constitution-ui-template" ;;
|
|
1782
|
+
esac
|
|
1783
|
+
fi
|
|
1784
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
1785
|
+
local tpl_path=""
|
|
1786
|
+
[ -n "$template" ] && { tpl_path=$(resolve_template "$template" "$repo_root") || true; }
|
|
1787
|
+
AW_FILE="$file" AW_TPL="$tpl_path" python3 - <<'PY'
|
|
1788
|
+
import json, os, re
|
|
1789
|
+
f = os.environ["AW_FILE"]
|
|
1790
|
+
if not os.path.exists(f):
|
|
1791
|
+
print(json.dumps({"ok": False, "exists": False,
|
|
1792
|
+
"issues": ["constitution file not found"], "placeholders": [],
|
|
1793
|
+
"version_body": None, "version_report": None,
|
|
1794
|
+
"version_match": None, "bad_dates": []})); raise SystemExit
|
|
1795
|
+
text = open(f, encoding="utf-8").read()
|
|
1796
|
+
# Strip HTML comments (template guidance + the Sync Impact Report) so they do not
|
|
1797
|
+
# trip the body scan; keep the first block as the report for the version check.
|
|
1798
|
+
m = re.search(r"<!--.*?-->", text, re.S)
|
|
1799
|
+
report = m.group(0) if m else ""
|
|
1800
|
+
body = re.sub(r"<!--.*?-->", "", text, flags=re.S)
|
|
1801
|
+
# 1. Unfilled slots = the file's [TOKEN]s ∩ the template's own token vocabulary.
|
|
1802
|
+
TOKEN = re.compile(r"\[([A-Z][A-Z0-9_]*)\]")
|
|
1803
|
+
tpl = os.environ.get("AW_TPL", "")
|
|
1804
|
+
template_missing = not (tpl and os.path.exists(tpl))
|
|
1805
|
+
if template_missing:
|
|
1806
|
+
placeholders = []
|
|
1807
|
+
else:
|
|
1808
|
+
known = set(TOKEN.findall(open(tpl, encoding="utf-8").read()))
|
|
1809
|
+
# Numbered slots generalise — the author is told to extend the series, so an
|
|
1810
|
+
# unfilled PRINCIPLE_6_NAME counts even though the template only shows five.
|
|
1811
|
+
family = {re.compile(r"\A" + re.sub(r"\d+", r"\\d+", re.escape(t)) + r"\Z")
|
|
1812
|
+
for t in known if re.search(r"\d", t)}
|
|
1813
|
+
placeholders = sorted({t for t in TOKEN.findall(body)
|
|
1814
|
+
if t in known or any(rx.match(t) for rx in family)})
|
|
1815
|
+
# 2. Version: the document's own labelled version line vs the report's "new" version.
|
|
1816
|
+
# Anchored to lines that OPEN with the label, so an "OpenAPI version 3.1.0" inside a
|
|
1817
|
+
# principle cannot be mistaken for the constitution's version. The last such line
|
|
1818
|
+
# wins because the templates carry the version in a footer.
|
|
1819
|
+
VERSION_LINE = re.compile(r"^[\s>*_#-]*version\b", re.I)
|
|
1820
|
+
def labeled(s, last_on_line=False):
|
|
1821
|
+
vals = []
|
|
1822
|
+
for line in s.splitlines():
|
|
1823
|
+
if VERSION_LINE.match(line):
|
|
1824
|
+
sem = re.findall(r"\d+\.\d+\.\d+", line)
|
|
1825
|
+
if sem:
|
|
1826
|
+
vals.append(sem[-1] if last_on_line else sem[0])
|
|
1827
|
+
return vals[-1] if vals else None
|
|
1828
|
+
version_body = labeled(body)
|
|
1829
|
+
arrow = re.search(r"(\d+\.\d+\.\d+)\s*(?:->|→|=>)\s*(\d+\.\d+\.\d+)", report)
|
|
1830
|
+
# On the report line the LAST semver is the new version — which also survives an
|
|
1831
|
+
# "old → new" whose arrow was mangled by an encoding round-trip.
|
|
1832
|
+
version_report = arrow.group(2) if arrow else labeled(report, last_on_line=True)
|
|
1833
|
+
version_match = (version_body == version_report) if (version_body and version_report) else None
|
|
1834
|
+
# 3. Obviously non-ISO dates on ratification/amended/date lines (slash forms,
|
|
1835
|
+
# DD-MM-YYYY, US textual). ISO YYYY-MM-DD never matches these patterns.
|
|
1836
|
+
bad = []
|
|
1837
|
+
for line in body.splitlines():
|
|
1838
|
+
if re.search(r"(?i)ratif|amend|\bdate\b", line):
|
|
1839
|
+
bad += re.findall(r"\b\d{1,2}/\d{1,2}/\d{2,4}\b", line)
|
|
1840
|
+
bad += re.findall(r"\b\d{1,2}-\d{1,2}-\d{4}\b", line)
|
|
1841
|
+
bad += re.findall(r"\b(?:January|February|March|April|May|June|July|August|"
|
|
1842
|
+
r"September|October|November|December)\s+\d{1,2},?\s+\d{4}\b", line)
|
|
1843
|
+
bad = sorted(set(bad))
|
|
1844
|
+
issues = []
|
|
1845
|
+
if placeholders:
|
|
1846
|
+
issues.append("%d unfilled placeholder(s): %s" % (len(placeholders), ", ".join(placeholders)))
|
|
1847
|
+
if template_missing:
|
|
1848
|
+
# Reported, not failed: a missing kit template is an install problem, and blocking
|
|
1849
|
+
# the authoring loop over it would help nobody.
|
|
1850
|
+
issues.append("placeholder check skipped — no kit template resolved for this half")
|
|
1851
|
+
if version_match is False:
|
|
1852
|
+
issues.append("version mismatch: body %s vs Sync Impact Report %s" % (version_body, version_report))
|
|
1853
|
+
if bad:
|
|
1854
|
+
issues.append("non-ISO date(s) (use YYYY-MM-DD): %s" % ", ".join(bad))
|
|
1855
|
+
ok = (not placeholders) and (version_match is not False) and (not bad)
|
|
1856
|
+
print(json.dumps({"ok": ok, "exists": True, "issues": issues,
|
|
1857
|
+
"placeholders": placeholders, "placeholder_check": not template_missing,
|
|
1858
|
+
"version_body": version_body,
|
|
1859
|
+
"version_report": version_report, "version_match": version_match,
|
|
1860
|
+
"bad_dates": bad}))
|
|
1861
|
+
PY
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
# ────────────────────────── agent-context-update ───────────────────────────
|
|
1865
|
+
# Run the agent-context extension's updater (refreshes the managed AW-Spec block).
|
|
1866
|
+
cmd_agent_context_update() {
|
|
1867
|
+
local plan=""
|
|
1868
|
+
while [ $# -gt 0 ]; do
|
|
1869
|
+
case "$1" in
|
|
1870
|
+
--plan) plan="$2"; shift 2 ;;
|
|
1871
|
+
--help|-h) echo "agent-context-update [--plan <path>]"; return 0 ;;
|
|
1872
|
+
*) echo "agent-context-update: unknown arg '$1'" >&2; return 1 ;;
|
|
1873
|
+
esac
|
|
1874
|
+
done
|
|
1875
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
1876
|
+
local script="$repo_root/.appweaver/extensions/agent-context/scripts/bash/update-agent-context.sh"
|
|
1877
|
+
if [ ! -f "$script" ]; then
|
|
1878
|
+
printf '{"ok":true,"ran":false,"message":"agent-context extension not installed; nothing to do"}\n'; return 0
|
|
1879
|
+
fi
|
|
1880
|
+
local out rc
|
|
1881
|
+
if [ -n "$plan" ]; then out=$(bash "$script" "$plan" 2>&1); else out=$(bash "$script" 2>&1); fi
|
|
1882
|
+
rc=$?
|
|
1883
|
+
printf '{"ok":%s,"ran":true,"message":"%s"}\n' \
|
|
1884
|
+
"$([ $rc -eq 0 ] && echo true || echo false)" "$(json_escape "$(printf '%s' "$out" | tail -c 400)")"
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
|
|
1888
|
+
|
|
1889
|
+
# ─────────────────────────────── list-components ───────────────────────────
|
|
1890
|
+
# Immediate subdirs of foundations/<ecosystem>/components/ that contain an integration.md
|
|
1891
|
+
# runbook, for the ecosystem the BACKEND lane declares. Components are wired into a host
|
|
1892
|
+
# project by editing its build files and its startup registration, so a component only
|
|
1893
|
+
# exists for the ecosystem whose build files and startup it knows how to edit — the
|
|
1894
|
+
# catalogue is per-ecosystem for the same reason the foundations are.
|
|
1895
|
+
cmd_list_components() {
|
|
1896
|
+
while [ $# -gt 0 ]; do
|
|
1897
|
+
case "$1" in
|
|
1898
|
+
--help|-h) echo "list-components -> {components[], ecosystem, dir}"; return 0 ;;
|
|
1899
|
+
*) echo "list-components: unknown arg '$1'" >&2; return 1 ;;
|
|
1900
|
+
esac
|
|
1901
|
+
done
|
|
1902
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
1903
|
+
local eco; eco=$(_aw_lane_field backend ecosystem)
|
|
1904
|
+
# No backend lane yet: `dotnet` is the only ecosystem that ships components, so it is
|
|
1905
|
+
# what an undeclared repo would have been shown before the lane model and stays the
|
|
1906
|
+
# answer until select-stack says otherwise.
|
|
1907
|
+
[ -z "$eco" ] && eco="dotnet"
|
|
1908
|
+
local base="$repo_root/foundations/$eco/components"
|
|
1909
|
+
local -a comps=()
|
|
1910
|
+
if [ -d "$base" ]; then
|
|
1911
|
+
local d
|
|
1912
|
+
for d in "$base"/*/; do
|
|
1913
|
+
[ -d "$d" ] || continue
|
|
1914
|
+
[ -f "$d/integration.md" ] && comps+=("$(basename "$d")")
|
|
1915
|
+
done
|
|
1916
|
+
fi
|
|
1917
|
+
printf '{"components":%s,"ecosystem":"%s","dir":"foundations/%s/components"}\n' \
|
|
1918
|
+
"$(json_str_array "${comps[@]}")" "$(json_escape "$eco")" "$(json_escape "$eco")"
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
# ─────────────────────────────── find-host-project ─────────────────────────
|
|
1922
|
+
# Locate the .NET host/startup project by finding Program.cs (never assume a name).
|
|
1923
|
+
#
|
|
1924
|
+
# DELIBERATELY .NET-ONLY, and it reports which stack it looked at so a caller can say
|
|
1925
|
+
# so. `Program.cs` is not a cross-stack concept: a Spring lane's entry point is an
|
|
1926
|
+
# @SpringBootApplication class, an Express lane's is whatever `main` in package.json
|
|
1927
|
+
# names, and neither has the .csproj this returns. Callers that need a LAUNCH COMMAND
|
|
1928
|
+
# ask the lane instead (its declared `run`); this exists for the two callers that need
|
|
1929
|
+
# the .NET host DIRECTORY — add-component, which edits its registration, and run, which
|
|
1930
|
+
# names it for information. found:false on a non-.NET lane is the correct answer, and
|
|
1931
|
+
# `ecosystem` is what lets the message say "this is a java lane" instead of "no project".
|
|
1932
|
+
cmd_find_host_project() {
|
|
1933
|
+
while [ $# -gt 0 ]; do
|
|
1934
|
+
case "$1" in
|
|
1935
|
+
--help|-h) echo "find-host-project"; return 0 ;;
|
|
1936
|
+
*) echo "find-host-project: unknown arg '$1'" >&2; return 1 ;;
|
|
1937
|
+
esac
|
|
1938
|
+
done
|
|
1939
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
1940
|
+
local prog
|
|
1941
|
+
# WHERE the host lives. The service lane's declared source path when there is one,
|
|
1942
|
+
# `src/` when there is not — the same question run-migrate asks, and the same answer.
|
|
1943
|
+
# A repo whose service sits in `backend/` or `Legacy/App/` is searched where its code
|
|
1944
|
+
# actually is instead of being reported as having no host project at all.
|
|
1945
|
+
local svc_root; svc_root=$(_aw_lane_root backend)
|
|
1946
|
+
[ -z "$svc_root" ] && svc_root="src"
|
|
1947
|
+
local eco; eco=$(_aw_lane_field backend ecosystem)
|
|
1948
|
+
prog=$(find "$repo_root/${svc_root%/}" -name "Program.cs" 2>/dev/null | head -1)
|
|
1949
|
+
[ -z "$prog" ] && prog=$(find "$repo_root" \( -path "$repo_root/.appweaver" -o -path "$repo_root/guardrails" -o -path "$repo_root/foundations" -o -path "$repo_root/specs" -o -path "$repo_root/.git" -o -name obj -o -name bin \) -prune -o -name "Program.cs" -print 2>/dev/null | head -1)
|
|
1950
|
+
if [ -z "$prog" ]; then
|
|
1951
|
+
printf '{"found":false,"program_cs":"","project_dir":"","csproj":"","ecosystem":"%s","searched":"%s"}\n' \
|
|
1952
|
+
"$(json_escape "$eco")" "$(json_escape "${svc_root%/}")"
|
|
1953
|
+
return 0
|
|
1954
|
+
fi
|
|
1955
|
+
local pdir; pdir=$(dirname "$prog")
|
|
1956
|
+
local csproj; csproj=$(find "$pdir" -maxdepth 1 -name "*.csproj" 2>/dev/null | head -1)
|
|
1957
|
+
printf '{"found":true,"program_cs":"%s","project_dir":"%s","csproj":"%s","ecosystem":"%s","searched":"%s"}\n' \
|
|
1958
|
+
"$(json_escape "$prog")" "$(json_escape "$pdir")" "$(json_escape "$csproj")" \
|
|
1959
|
+
"$(json_escape "$eco")" "$(json_escape "${svc_root%/}")"
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
# ─────────────────────────────── component-catalog ──────────────────────────
|
|
1963
|
+
# Deterministic parse of a component's integration.md "Implementation catalog
|
|
1964
|
+
# (selection list)" markdown table (NO LLM). Replaces the extract_catalog llm_query.
|
|
1965
|
+
# Returns {integrable, impl_keys[], implementations[{key,desc}]}.
|
|
1966
|
+
#
|
|
1967
|
+
# Delegates to the shared python core so the bash and powershell twins cannot drift:
|
|
1968
|
+
# the parse and the JSON contract have one implementation.
|
|
1969
|
+
cmd_component_catalog() {
|
|
1970
|
+
local file=""
|
|
1971
|
+
while [ $# -gt 0 ]; do
|
|
1972
|
+
case "$1" in
|
|
1973
|
+
--file) file="$2"; shift 2 ;;
|
|
1974
|
+
--help|-h) echo "component-catalog --file <integration.md>"; return 0 ;;
|
|
1975
|
+
*) echo "component-catalog: unknown arg '$1'" >&2; return 1 ;;
|
|
1976
|
+
esac
|
|
1977
|
+
done
|
|
1978
|
+
[ -n "$file" ] || { echo "component-catalog: --file required" >&2; return 1; }
|
|
1979
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
1980
|
+
local core="$SCRIPT_DIR/../python/aw-component-catalog.py"
|
|
1981
|
+
if [ ! -f "$core" ]; then
|
|
1982
|
+
echo "component-catalog: core not found at $core" >&2; return 1
|
|
1983
|
+
fi
|
|
1984
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
1985
|
+
echo "component-catalog: python3 is required to parse the catalog" >&2; return 1
|
|
1986
|
+
fi
|
|
1987
|
+
python3 "$core" --repo "$repo_root" --file "$file"
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
# ─────────────────────────────── component-verify ───────────────────────────
|
|
1991
|
+
# Post-wiring verification (NO LLM decision): (1) `dotnet build` the host project
|
|
1992
|
+
# (graceful skip if the SDK is absent); (2) R0.2a dual-registration guard — when the
|
|
1993
|
+
# host has BOTH AddInfrastructure and AddAspireInfrastructure, assert `--marker`
|
|
1994
|
+
# appears in both. Returns {ok, ran, build_ok, errors[], dual_registration_ok,
|
|
1995
|
+
# fix_target, fix_reason, can_fix} for a bounded fix loop.
|
|
1996
|
+
cmd_component_verify() {
|
|
1997
|
+
local host_dir="" host_csproj="" marker=""
|
|
1998
|
+
while [ $# -gt 0 ]; do
|
|
1999
|
+
case "$1" in
|
|
2000
|
+
--host-dir) host_dir="$2"; shift 2 ;;
|
|
2001
|
+
--host-csproj) host_csproj="$2"; shift 2 ;;
|
|
2002
|
+
--marker) marker="$2"; shift 2 ;;
|
|
2003
|
+
--help|-h) echo "component-verify --host-dir <dir> [--host-csproj <path>] [--marker <token>]"; return 0 ;;
|
|
2004
|
+
*) echo "component-verify: unknown arg '$1'" >&2; return 1 ;;
|
|
2005
|
+
esac
|
|
2006
|
+
done
|
|
2007
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
2008
|
+
AW_REPO="$repo_root" AW_HOST_DIR="$host_dir" AW_HOST_CSPROJ="$host_csproj" AW_MARKER="$marker" python3 - <<'PY'
|
|
2009
|
+
import json, os, re, shutil, subprocess
|
|
2010
|
+
repo = os.environ.get("AW_REPO", "")
|
|
2011
|
+
hostd = os.environ.get("AW_HOST_DIR", "")
|
|
2012
|
+
csproj = os.environ.get("AW_HOST_CSPROJ", "")
|
|
2013
|
+
marker = os.environ.get("AW_MARKER", "").strip()
|
|
2014
|
+
def relp(p):
|
|
2015
|
+
try: return os.path.relpath(p, repo).replace("\\", "/") if (repo and p) else p
|
|
2016
|
+
except Exception: return p
|
|
2017
|
+
# --- (1) build ---
|
|
2018
|
+
ran, build_ok, errors, primary = False, True, [], ""
|
|
2019
|
+
if shutil.which("dotnet"):
|
|
2020
|
+
target = csproj if csproj else hostd
|
|
2021
|
+
if target and os.path.exists(target):
|
|
2022
|
+
ran = True
|
|
2023
|
+
proc = subprocess.run(["dotnet", "build", target, "-nologo"],
|
|
2024
|
+
capture_output=True, text=True)
|
|
2025
|
+
seen = set()
|
|
2026
|
+
for line in (proc.stdout + "\n" + proc.stderr).splitlines():
|
|
2027
|
+
if re.search(r':\s*error\s', line):
|
|
2028
|
+
t = line.strip()
|
|
2029
|
+
if t not in seen:
|
|
2030
|
+
seen.add(t); errors.append(t)
|
|
2031
|
+
errors = errors[:20]
|
|
2032
|
+
build_ok = (proc.returncode == 0)
|
|
2033
|
+
if not build_ok and errors:
|
|
2034
|
+
m = re.match(r'^(.*?)\(\d+,\d+\)', errors[0])
|
|
2035
|
+
if m: primary = relp(m.group(1).strip())
|
|
2036
|
+
if not build_ok and not primary and csproj:
|
|
2037
|
+
primary = relp(csproj)
|
|
2038
|
+
# --- (2) R0.2a dual-registration guard ---
|
|
2039
|
+
def defining_files(method):
|
|
2040
|
+
root = hostd if (hostd and os.path.isdir(hostd)) else repo
|
|
2041
|
+
hits = []
|
|
2042
|
+
if not root: return hits
|
|
2043
|
+
pat = re.compile(r'\b' + re.escape(method) + r'\s*\(')
|
|
2044
|
+
for dp, dns, fns in os.walk(root):
|
|
2045
|
+
dns[:] = [d for d in dns if d not in ("bin", "obj", ".git")]
|
|
2046
|
+
for fn in fns:
|
|
2047
|
+
if fn.endswith(".cs"):
|
|
2048
|
+
fp = os.path.join(dp, fn)
|
|
2049
|
+
try: txt = open(fp, encoding="utf-8", errors="ignore").read()
|
|
2050
|
+
except Exception: continue
|
|
2051
|
+
if pat.search(txt): hits.append((fp, txt))
|
|
2052
|
+
return hits
|
|
2053
|
+
infra, aspire = defining_files("AddInfrastructure"), defining_files("AddAspireInfrastructure")
|
|
2054
|
+
dual_app = bool(infra) and bool(aspire)
|
|
2055
|
+
dual_ok, dual_note, dual_fix = True, "", ""
|
|
2056
|
+
def pick_fix(hits):
|
|
2057
|
+
non_prog = [fp for fp, _ in hits if os.path.basename(fp) != "Program.cs"]
|
|
2058
|
+
if non_prog: return relp(non_prog[0])
|
|
2059
|
+
return relp(hits[0][0]) if hits else ""
|
|
2060
|
+
if not dual_app:
|
|
2061
|
+
dual_note = "single registration path (R0.2) — dual-registration check N/A"
|
|
2062
|
+
elif not marker:
|
|
2063
|
+
dual_note = "both AddInfrastructure and AddAspireInfrastructure exist; no marker provided to verify the component is in both"
|
|
2064
|
+
else:
|
|
2065
|
+
in_infra = any(marker in txt for _, txt in infra)
|
|
2066
|
+
in_aspire = any(marker in txt for _, txt in aspire)
|
|
2067
|
+
dual_ok = in_infra and in_aspire
|
|
2068
|
+
if not dual_ok:
|
|
2069
|
+
missing = "AddInfrastructure" if not in_infra else "AddAspireInfrastructure"
|
|
2070
|
+
dual_fix = pick_fix(infra if not in_infra else aspire)
|
|
2071
|
+
dual_note = "marker '%s' missing from %s (R0.2a: register in BOTH paths)" % (marker, missing)
|
|
2072
|
+
else:
|
|
2073
|
+
dual_note = "marker '%s' present in both registration paths" % marker
|
|
2074
|
+
ok = build_ok and dual_ok
|
|
2075
|
+
fix_target, fix_reason = "", ""
|
|
2076
|
+
if not build_ok:
|
|
2077
|
+
fix_target = primary or (relp(csproj) if csproj else "")
|
|
2078
|
+
fix_reason = "build failed: " + " | ".join(errors[:5])
|
|
2079
|
+
elif not dual_ok:
|
|
2080
|
+
fix_target, fix_reason = dual_fix, dual_note
|
|
2081
|
+
print(json.dumps({"ok": ok, "ran": ran, "build_ok": build_ok, "errors": errors,
|
|
2082
|
+
"primary_error_file": primary, "dual_applicable": dual_app,
|
|
2083
|
+
"dual_registration_ok": dual_ok, "dual_note": dual_note,
|
|
2084
|
+
"dual_fix_file": dual_fix, "fix_target": fix_target,
|
|
2085
|
+
"fix_reason": fix_reason, "can_fix": bool(fix_target)}))
|
|
2086
|
+
PY
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
# ─────────────────────────────────── main ──────────────────────────────────
|
|
2090
|
+
sub="${1:-}"; shift || true
|
|
2091
|
+
# ──────────────────────────────── design-scaffold ──────────────────────────
|
|
2092
|
+
# The spec-anchored Design phase: ONE high-level design doc per feature
|
|
2093
|
+
# (design.md) — a tech-agnostic HLD that `implement` GENERATES code from (not a
|
|
2094
|
+
# deterministic extract). See .appweaver/DESIGN-FORMAT.md + design-template.md.
|
|
2095
|
+
#
|
|
2096
|
+
# The former path-tagged helpers (design-extract / -reconcile / -paths) are GONE.
|
|
2097
|
+
# They belonged to a retired four-document model in which design.{domain,application,
|
|
2098
|
+
# api,infra}.md carried the source inside ```lang path=… fences. Nothing emits that
|
|
2099
|
+
# fence any more, and their glob (`design.*.md`) could not match a single `design.md`
|
|
2100
|
+
# even if something did — so all three had been returning nothing, silently, at exit 0.
|
|
2101
|
+
# The question they answered ("which files is this slice made of") is now answered by
|
|
2102
|
+
# the OKF concept: `aw-xref concept-paths` to read, `concept-upsert --merge` to write.
|
|
2103
|
+
|
|
2104
|
+
# Copy the single HLD design template into the feature dir as design.md (idempotent).
|
|
2105
|
+
cmd_design_scaffold() {
|
|
2106
|
+
local feature_dir_arg="" template_arg="" lane_arg="service"
|
|
2107
|
+
while [ $# -gt 0 ]; do
|
|
2108
|
+
case "$1" in
|
|
2109
|
+
--feature-dir) feature_dir_arg="$2"; shift 2 ;;
|
|
2110
|
+
# --lane service|ui: WHOSE design this is. It selects a coherent
|
|
2111
|
+
# (template, document) PAIR rather than two independent flags — service
|
|
2112
|
+
# scaffolds design-template into design.md, ui scaffolds
|
|
2113
|
+
# ui-design-template into design.ui.md — so no caller can write the
|
|
2114
|
+
# service template under the UI lane's filename. Default `service`
|
|
2115
|
+
# leaves every existing caller byte-identical.
|
|
2116
|
+
--lane) lane_arg="$2"; shift 2 ;;
|
|
2117
|
+
# --template <name>: override the TEMPLATE half of that pair (the document
|
|
2118
|
+
# name still follows --lane). In a brownfield repo /appweaver onboard
|
|
2119
|
+
# GENERATES a code-derived override for both names (resolve_template
|
|
2120
|
+
# Priority-1), so the defaults already yield the as-built structure for
|
|
2121
|
+
# forward `design` and adopt alike, and this is rarely needed.
|
|
2122
|
+
--template) template_arg="$2"; shift 2 ;;
|
|
2123
|
+
--help|-h) echo "design-scaffold --feature-dir <path> [--lane service|ui] [--template <name>]"; return 0 ;;
|
|
2124
|
+
*) echo "design-scaffold: unknown arg '$1'" >&2; return 1 ;;
|
|
2125
|
+
esac
|
|
2126
|
+
done
|
|
2127
|
+
local doc_name
|
|
2128
|
+
case "$lane_arg" in
|
|
2129
|
+
service) doc_name="design.md"; [ -n "$template_arg" ] || template_arg="design-template" ;;
|
|
2130
|
+
ui) doc_name="design.ui.md"; [ -n "$template_arg" ] || template_arg="ui-design-template" ;;
|
|
2131
|
+
*) echo "design-scaffold: unknown --lane '$lane_arg' (service|ui)" >&2; return 1 ;;
|
|
2132
|
+
esac
|
|
2133
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
2134
|
+
local fd="$feature_dir_arg"
|
|
2135
|
+
[ -n "$fd" ] || fd=$(read_feature_json_feature_directory "$repo_root")
|
|
2136
|
+
[ -n "$fd" ] || { echo "design-scaffold: no feature set (run /appweaver select-feature first) and no --feature-dir" >&2; return 1; }
|
|
2137
|
+
[[ "$fd" != /* ]] && fd="$repo_root/$fd"
|
|
2138
|
+
mkdir -p "$fd"
|
|
2139
|
+
|
|
2140
|
+
local target="$fd/$doc_name" created="false" tpl
|
|
2141
|
+
if [ -f "$target" ]; then
|
|
2142
|
+
created="false"
|
|
2143
|
+
else
|
|
2144
|
+
tpl=$(resolve_template "$template_arg" "$repo_root") || true
|
|
2145
|
+
if [ -n "$tpl" ] && [ -f "$tpl" ]; then cp "$tpl" "$target"; else touch "$target"; fi
|
|
2146
|
+
created="true"
|
|
2147
|
+
fi
|
|
2148
|
+
# override_active: is a PROJECT-level override installed for this template name?
|
|
2149
|
+
# Computed ALWAYS (independent of the idempotent scaffold skip above) so it is a
|
|
2150
|
+
# stable signal on both the bootstrap AND delta paths. In the brownfield-dev flow,
|
|
2151
|
+
# /appweaver onboard GENERATES the code-derived `design-template` override — so
|
|
2152
|
+
# override_active=true means the forward `design` phase should author with the
|
|
2153
|
+
# (code-derived) as-built structure, not the greenfield four-layer layout.
|
|
2154
|
+
local override_active="false"
|
|
2155
|
+
[ -f "$repo_root/.appweaver/templates/overrides/${template_arg}.md" ] && override_active="true"
|
|
2156
|
+
local template_source; template_source=$(resolve_template "$template_arg" "$repo_root") || true
|
|
2157
|
+
printf '{"feature_dir":"%s","design_doc":"%s","lane":"%s","created":%s,"override_active":%s,"template":"%s","template_source":"%s"}\n' \
|
|
2158
|
+
"$(json_escape "$fd")" "$(json_escape "$target")" "$(json_escape "$lane_arg")" "$created" "$override_active" \
|
|
2159
|
+
"$(json_escape "$template_arg")" "$(json_escape "${template_source:-}")"
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
# ─────────────────────── build / test / migrate / deploy ────────────────────
|
|
2163
|
+
# Detect the project's toolchain, run the phase command, and report structured
|
|
2164
|
+
# status. Tolerant: an unrecognized stack is a no-op pass (wire a hook instead).
|
|
2165
|
+
# The FIX is an LLM step in the workflow; these only run + report.
|
|
2166
|
+
|
|
2167
|
+
# Is a lane declared in .appweaver/stacks.json? The question every shell-out step asks
|
|
2168
|
+
# before it starts sniffing. Prints "true"/"" so a caller can test it directly.
|
|
2169
|
+
_aw_lanes_declared() {
|
|
2170
|
+
_aw_stack_py resolve 2>/dev/null | python3 -c '
|
|
2171
|
+
import json, sys
|
|
2172
|
+
try:
|
|
2173
|
+
print("true" if json.loads(sys.stdin.readline()).get("lanes") else "")
|
|
2174
|
+
except Exception:
|
|
2175
|
+
print("")
|
|
2176
|
+
' 2>/dev/null || echo ""
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
cmd_build_compile() {
|
|
2180
|
+
while [ $# -gt 0 ]; do
|
|
2181
|
+
case "$1" in
|
|
2182
|
+
--feature-dir) shift 2 ;;
|
|
2183
|
+
--help|-h) echo "build-compile [--feature-dir <path>]"; return 0 ;;
|
|
2184
|
+
*) echo "build-compile: unknown arg '$1'" >&2; return 1 ;;
|
|
2185
|
+
esac
|
|
2186
|
+
done
|
|
2187
|
+
|
|
2188
|
+
# A DECLARED lane model is the answer, and it is a better answer than the
|
|
2189
|
+
# ladder below in exactly the case that matters: a repo with two lanes. The
|
|
2190
|
+
# sniffer takes the first toolchain it finds and silently never builds the
|
|
2191
|
+
# second, which reports green for a frontend that was never compiled.
|
|
2192
|
+
if [ -n "$(_aw_lanes_declared)" ]; then
|
|
2193
|
+
_aw_stack_py run-phase --phase build
|
|
2194
|
+
return $?
|
|
2195
|
+
fi
|
|
2196
|
+
|
|
2197
|
+
# No declaration: the historic first-match-wins ladder, unchanged. A project
|
|
2198
|
+
# that never ran select-stack behaves exactly as it did before the lane model.
|
|
2199
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
2200
|
+
local tool="none" ran=false ok=true cmd="" log; log=$(mktemp)
|
|
2201
|
+
if ls "$repo_root"/*.sln >/dev/null 2>&1 || find "$repo_root/src" -maxdepth 4 -name "*.csproj" 2>/dev/null | grep -q .; then
|
|
2202
|
+
tool="dotnet"
|
|
2203
|
+
if command -v dotnet >/dev/null 2>&1; then
|
|
2204
|
+
cmd="dotnet build --nologo"; ran=true
|
|
2205
|
+
if ( cd "$repo_root" && dotnet build --nologo ) >"$log" 2>&1; then ok=true; else ok=false; fi
|
|
2206
|
+
else ok=false; fi
|
|
2207
|
+
elif [ -f "$repo_root/package.json" ] && command -v npm >/dev/null 2>&1; then
|
|
2208
|
+
tool="npm"; cmd="npm run build --if-present"; ran=true
|
|
2209
|
+
if ( cd "$repo_root" && npm run build --if-present ) >"$log" 2>&1; then ok=true; else ok=false; fi
|
|
2210
|
+
else
|
|
2211
|
+
tool="none"; ran=false; ok=true
|
|
2212
|
+
fi
|
|
2213
|
+
local t; t=$(tail -c 1400 "$log" 2>/dev/null || true); rm -f "$log"
|
|
2214
|
+
printf '{"ok":%s,"tool":"%s","ran":%s,"cmd":"%s","log_tail":"%s"}\n' \
|
|
2215
|
+
"$ok" "$(json_escape "$tool")" "$ran" "$(json_escape "$cmd")" "$(json_escape "$t")"
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2218
|
+
cmd_test_run() {
|
|
2219
|
+
# --scope repo (default): run the whole suite — the historic behaviour, every
|
|
2220
|
+
# existing caller is unaffected.
|
|
2221
|
+
# --scope slice : run ONLY the tests whose FullyQualifiedName contains the
|
|
2222
|
+
# feature's aggregate name (feature-dir basename) plus any --also
|
|
2223
|
+
# referrer names — the per-feature integrity scope. .NET only; npm/none
|
|
2224
|
+
# ignore it and run the full suite.
|
|
2225
|
+
local feature_dir="" scope="repo" also=""
|
|
2226
|
+
while [ $# -gt 0 ]; do
|
|
2227
|
+
case "$1" in
|
|
2228
|
+
--feature-dir) feature_dir="$2"; shift 2 ;;
|
|
2229
|
+
--scope) scope="$2"; shift 2 ;;
|
|
2230
|
+
--also) also="$2"; shift 2 ;;
|
|
2231
|
+
--help|-h) echo "test-run [--feature-dir <path>] [--scope slice|repo] [--also <csv>]"; return 0 ;;
|
|
2232
|
+
*) echo "test-run: unknown arg '$1'" >&2; return 1 ;;
|
|
2233
|
+
esac
|
|
2234
|
+
done
|
|
2235
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
2236
|
+
|
|
2237
|
+
# For scope=slice, build the token set = feature aggregate name + --also referrers,
|
|
2238
|
+
# each sanitised to an identifier (VSTest filter operators |&()!=~ are illegal) and
|
|
2239
|
+
# de-duplicated. A token that survives sanitising becomes a `FullyQualifiedName~<tok>`
|
|
2240
|
+
# clause; substring match is deliberate — it over-includes (safe for an integrity
|
|
2241
|
+
# fence) rather than risking an under-scoped miss.
|
|
2242
|
+
local slices="" name tok also_sp
|
|
2243
|
+
if [ "$scope" = "slice" ]; then
|
|
2244
|
+
also_sp=$(printf '%s' "$also" | tr ',' ' ')
|
|
2245
|
+
for name in "$(basename "${feature_dir%/}")" $also_sp; do
|
|
2246
|
+
tok=$(printf '%s' "$name" | tr -cd 'A-Za-z0-9._-')
|
|
2247
|
+
case "$tok" in [A-Za-z]*) ;; *) continue ;; esac # must start with a letter
|
|
2248
|
+
case " $slices " in *" $tok "*) continue ;; esac # de-dup
|
|
2249
|
+
slices="${slices:+$slices }$tok"
|
|
2250
|
+
done
|
|
2251
|
+
fi
|
|
2252
|
+
|
|
2253
|
+
local tool="none" ran=false ok=true cmd="" log; log=$(mktemp)
|
|
2254
|
+
local matched=false scoped=false fence_green=false filter="" t2
|
|
2255
|
+
|
|
2256
|
+
local dotnet_present=false
|
|
2257
|
+
if ls "$repo_root"/*.sln >/dev/null 2>&1 || find "$repo_root" -maxdepth 5 -name "*.csproj" 2>/dev/null | grep -q .; then
|
|
2258
|
+
dotnet_present=true
|
|
2259
|
+
fi
|
|
2260
|
+
|
|
2261
|
+
# A DECLARED lane model answers this the same way it answers build-compile, and for
|
|
2262
|
+
# the same reason: the ladder below recognises .NET and npm-at-the-root, and reports
|
|
2263
|
+
# `tool:"none" ran:false ok:true` for everything else. On a repo whose lane declares
|
|
2264
|
+
# `./mvnw test` that is a suite which never ran scoring the integrity fence GREEN —
|
|
2265
|
+
# the vacuous pass this command guards against everywhere else.
|
|
2266
|
+
#
|
|
2267
|
+
# scope=slice keeps the .NET path whenever a .NET toolchain is present, because
|
|
2268
|
+
# VSTest's --filter is the only per-test selector any shipped profile has. Elsewhere a
|
|
2269
|
+
# slice run delegates and widens to the lane's whole suite, which the fence tolerates
|
|
2270
|
+
# by design — it over-includes rather than risking an under-scoped miss — and reports
|
|
2271
|
+
# scoped=false so fence_green comes from `ok` rather than from the zero-match guard.
|
|
2272
|
+
local use_lanes=false
|
|
2273
|
+
if [ -n "$(_aw_lanes_declared)" ]; then
|
|
2274
|
+
if [ "$scope" != "slice" ] || [ "$dotnet_present" = false ]; then use_lanes=true; fi
|
|
2275
|
+
fi
|
|
2276
|
+
if [ "$use_lanes" = true ]; then
|
|
2277
|
+
rm -f "$log"
|
|
2278
|
+
_aw_stack_py run-phase --phase test | AW_SCOPE="$scope" AW_SLICES="$slices" python3 -c '
|
|
2279
|
+
import json, os, re, sys
|
|
2280
|
+
d = json.loads(sys.stdin.readline() or "{}")
|
|
2281
|
+
log, ok = d.get("log_tail", ""), bool(d.get("ok"))
|
|
2282
|
+
print(json.dumps({
|
|
2283
|
+
"ok": ok, "tool": d.get("tool", "lanes"), "ran": bool(d.get("ran")),
|
|
2284
|
+
"scope": os.environ.get("AW_SCOPE", "repo"), "slices": os.environ.get("AW_SLICES", ""),
|
|
2285
|
+
"scoped": False, "matched": bool(re.search(r"Total:\s*[1-9]", log)),
|
|
2286
|
+
"fence_green": ok, "cmd": d.get("cmd", ""), "log_tail": log,
|
|
2287
|
+
"lanes_run": d.get("lanes_run", []), "lanes_skipped": d.get("lanes_skipped", []),
|
|
2288
|
+
}))
|
|
2289
|
+
'
|
|
2290
|
+
return 0
|
|
2291
|
+
fi
|
|
2292
|
+
|
|
2293
|
+
if [ "$dotnet_present" = true ]; then
|
|
2294
|
+
tool="dotnet"
|
|
2295
|
+
if command -v dotnet >/dev/null 2>&1; then
|
|
2296
|
+
if [ "$scope" = "slice" ]; then
|
|
2297
|
+
if [ -z "$slices" ]; then
|
|
2298
|
+
# scope=slice but nothing valid to filter on — fail loudly, never
|
|
2299
|
+
# silently fall back to a repo-wide run.
|
|
2300
|
+
printf 'scope=slice but no valid filter tokens from --feature-dir/--also\n' >"$log"
|
|
2301
|
+
ok=false; ran=false; cmd="(no filter tokens)"
|
|
2302
|
+
else
|
|
2303
|
+
for t2 in $slices; do filter="${filter:+$filter|}FullyQualifiedName~$t2"; done
|
|
2304
|
+
scoped=true; cmd="dotnet test --nologo --filter \"$filter\""; ran=true
|
|
2305
|
+
if ( cd "$repo_root" && dotnet test --nologo --filter "$filter" ) >"$log" 2>&1; then ok=true; else ok=false; fi
|
|
2306
|
+
fi
|
|
2307
|
+
else
|
|
2308
|
+
cmd="dotnet test --nologo"; ran=true
|
|
2309
|
+
if ( cd "$repo_root" && dotnet test --nologo ) >"$log" 2>&1; then ok=true; else ok=false; fi
|
|
2310
|
+
fi
|
|
2311
|
+
else ok=false; fi
|
|
2312
|
+
elif [ -f "$repo_root/package.json" ] && command -v npm >/dev/null 2>&1; then
|
|
2313
|
+
tool="npm"; cmd="npm test"; ran=true
|
|
2314
|
+
if ( cd "$repo_root" && npm test ) >"$log" 2>&1; then ok=true; else ok=false; fi
|
|
2315
|
+
else
|
|
2316
|
+
tool="none"; ran=false; ok=true
|
|
2317
|
+
fi
|
|
2318
|
+
|
|
2319
|
+
# matched = at least one test was actually selected + run. Read from the LOG, never
|
|
2320
|
+
# the exit code: a .NET --filter that selects ZERO tests still EXITS 0, so trusting
|
|
2321
|
+
# the exit code would report an empty scoped run as green (the vacuous-pass trap).
|
|
2322
|
+
if grep -Eq 'Total:[[:space:]]*[1-9]' "$log" 2>/dev/null; then matched=true; fi
|
|
2323
|
+
|
|
2324
|
+
# fence_green — a green that actually proves something ran.
|
|
2325
|
+
# scoped run → ok AND matched (guards the zero-match vacuous pass)
|
|
2326
|
+
# everything else (repo suite, npm, no test tool, the loud no-token fail) → ok,
|
|
2327
|
+
# preserving the historic pass-through for a project with no/zero tests.
|
|
2328
|
+
if [ "$scoped" = true ]; then
|
|
2329
|
+
if [ "$ok" = true ] && [ "$matched" = true ]; then fence_green=true; fi
|
|
2330
|
+
else
|
|
2331
|
+
fence_green=$ok
|
|
2332
|
+
fi
|
|
2333
|
+
|
|
2334
|
+
local t; t=$(tail -c 1400 "$log" 2>/dev/null || true); rm -f "$log"
|
|
2335
|
+
printf '{"ok":%s,"tool":"%s","ran":%s,"scope":"%s","slices":"%s","scoped":%s,"matched":%s,"fence_green":%s,"cmd":"%s","log_tail":"%s"}\n' \
|
|
2336
|
+
"$ok" "$(json_escape "$tool")" "$ran" "$(json_escape "$scope")" "$(json_escape "$slices")" \
|
|
2337
|
+
"$scoped" "$matched" "$fence_green" "$(json_escape "$cmd")" "$(json_escape "$t")"
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
cmd_run_migrate() {
|
|
2341
|
+
while [ $# -gt 0 ]; do
|
|
2342
|
+
case "$1" in
|
|
2343
|
+
--feature-dir) shift 2 ;;
|
|
2344
|
+
--help|-h) echo "run-migrate [--feature-dir <path>]"; return 0 ;;
|
|
2345
|
+
*) echo "run-migrate: unknown arg '$1'" >&2; return 1 ;;
|
|
2346
|
+
esac
|
|
2347
|
+
done
|
|
2348
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
2349
|
+
|
|
2350
|
+
# A DECLARED migration command wins outright. Schema migration is the phase stacks
|
|
2351
|
+
# answer most differently — Flyway, Liquibase, Alembic, Prisma, golang-migrate — and
|
|
2352
|
+
# no sniffer can pick between tools that happily coexist in one repo. When a lane
|
|
2353
|
+
# declares `migrate` in .appweaver/stacks.json, that IS the answer and it runs in the
|
|
2354
|
+
# lane's own working directory like every other phase command.
|
|
2355
|
+
if [ -n "$(_aw_lane_field backend migrate)$(_aw_lane_field frontend migrate)" ]; then
|
|
2356
|
+
_aw_stack_py run-phase --phase migrate | python3 -c '
|
|
2357
|
+
import json, sys
|
|
2358
|
+
d = json.loads(sys.stdin.readline() or "{}")
|
|
2359
|
+
print(json.dumps({"ok": bool(d.get("ok")), "tool": "lanes", "ran": bool(d.get("ran")),
|
|
2360
|
+
"cmd": d.get("cmd", ""), "log_tail": d.get("log_tail", ""),
|
|
2361
|
+
"lanes_run": d.get("lanes_run", [])}))
|
|
2362
|
+
'
|
|
2363
|
+
return 0
|
|
2364
|
+
fi
|
|
2365
|
+
|
|
2366
|
+
# WHERE to look for the migration project. The service lane's declared source
|
|
2367
|
+
# path when there is one, `src/` when there is not. Migrations belong to the
|
|
2368
|
+
# backend lane by definition, so a repo whose service code sits in `Legacy/App/`
|
|
2369
|
+
# or `backend/` gets searched where its code actually is instead of being told
|
|
2370
|
+
# it has no .NET project at all.
|
|
2371
|
+
local svc_root; svc_root=$(_aw_lane_root backend)
|
|
2372
|
+
[ -z "$svc_root" ] && svc_root="src"
|
|
2373
|
+
local svc_dir="$repo_root/${svc_root%/}"
|
|
2374
|
+
|
|
2375
|
+
local tool="none" ran=false ok=true cmd="" log; log=$(mktemp)
|
|
2376
|
+
local has_dotnet=false
|
|
2377
|
+
if ls "$repo_root"/*.sln >/dev/null 2>&1 || find "$svc_dir" -maxdepth 4 -name "*.csproj" 2>/dev/null | grep -q .; then has_dotnet=true; fi
|
|
2378
|
+
if [ "$has_dotnet" = true ] && command -v dotnet >/dev/null 2>&1 && ( cd "$repo_root" && dotnet ef --version >/dev/null 2>&1 ); then
|
|
2379
|
+
# Prefer *Service.csproj (API host with ApplicationDbContext); fall back to first
|
|
2380
|
+
# non-AppHost/Web/ServiceDefaults/test project under the service lane's root.
|
|
2381
|
+
local project=""
|
|
2382
|
+
project=$(find "$svc_dir" -maxdepth 3 -name '*.Service.csproj' 2>/dev/null | head -1)
|
|
2383
|
+
if [ -z "$project" ]; then
|
|
2384
|
+
project=$(find "$svc_dir" -maxdepth 3 -name '*.csproj' 2>/dev/null \
|
|
2385
|
+
| grep -vE 'AppHost|ServiceDefaults|\.Web\.|Tests' | head -1)
|
|
2386
|
+
fi
|
|
2387
|
+
if [ -n "$project" ]; then
|
|
2388
|
+
# Multi-context solutions need an explicit context (ApplicationDbContext is the app model).
|
|
2389
|
+
tool="dotnet-ef"
|
|
2390
|
+
cmd="dotnet ef database update --project $project --context ApplicationDbContext"
|
|
2391
|
+
ran=true
|
|
2392
|
+
if ( cd "$repo_root" && dotnet ef database update --project "$project" --context ApplicationDbContext ) >"$log" 2>&1; then
|
|
2393
|
+
ok=true
|
|
2394
|
+
else
|
|
2395
|
+
ok=false
|
|
2396
|
+
fi
|
|
2397
|
+
else
|
|
2398
|
+
tool="dotnet-ef"; cmd="dotnet ef database update"; ran=true
|
|
2399
|
+
if ( cd "$repo_root" && dotnet ef database update ) >"$log" 2>&1; then ok=true; else ok=false; fi
|
|
2400
|
+
fi
|
|
2401
|
+
else
|
|
2402
|
+
# No lane declares `migrate` and there is no dotnet-ef to fall back on, so there
|
|
2403
|
+
# is genuinely nothing to run. ok stays true: a repo with no migrations is a
|
|
2404
|
+
# normal state, not a failure — but `ran:false` says so rather than implying a
|
|
2405
|
+
# migration succeeded.
|
|
2406
|
+
tool="none"; ran=false; ok=true
|
|
2407
|
+
fi
|
|
2408
|
+
local t; t=$(tail -c 1400 "$log" 2>/dev/null || true); rm -f "$log"
|
|
2409
|
+
printf '{"ok":%s,"tool":"%s","ran":%s,"cmd":"%s","log_tail":"%s"}\n' \
|
|
2410
|
+
"$ok" "$(json_escape "$tool")" "$ran" "$(json_escape "$cmd")" "$(json_escape "$t")"
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
# Deploy is intentionally config-driven: reads .appweaver/deploy.json {"command": "..."}.
|
|
2414
|
+
cmd_deploy_dispatch() {
|
|
2415
|
+
while [ $# -gt 0 ]; do
|
|
2416
|
+
case "$1" in
|
|
2417
|
+
--help|-h) echo "deploy-dispatch"; return 0 ;;
|
|
2418
|
+
*) echo "deploy-dispatch: unknown arg '$1'" >&2; return 1 ;;
|
|
2419
|
+
esac
|
|
2420
|
+
done
|
|
2421
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
2422
|
+
local cfg="$repo_root/.appweaver/deploy.json"
|
|
2423
|
+
local configured=false ran=false ok=true cmd="" log; log=$(mktemp)
|
|
2424
|
+
if [ -f "$cfg" ] && command -v python3 >/dev/null 2>&1; then
|
|
2425
|
+
cmd=$(AW_CFG="$cfg" python3 -c "import json,os;print(json.load(open(os.environ['AW_CFG'])).get('command',''))" 2>/dev/null || true)
|
|
2426
|
+
fi
|
|
2427
|
+
if [ -n "$cmd" ]; then
|
|
2428
|
+
configured=true; ran=true
|
|
2429
|
+
if ( cd "$repo_root" && bash -c "$cmd" ) >"$log" 2>&1; then ok=true; else ok=false; fi
|
|
2430
|
+
fi
|
|
2431
|
+
local t; t=$(tail -c 1400 "$log" 2>/dev/null || true); rm -f "$log"
|
|
2432
|
+
printf '{"ok":%s,"configured":%s,"ran":%s,"cmd":"%s","log_tail":"%s"}\n' \
|
|
2433
|
+
"$ok" "$configured" "$ran" "$(json_escape "$cmd")" "$(json_escape "$t")"
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
# ─────────────────────────────── cr-setup / cr-archive ──────────────────────
|
|
2437
|
+
# Change-request scaffolding: a delta lives under <feature>/changes/<cr-id>/,
|
|
2438
|
+
# archives to <feature>/changes/archive/<cr-id>/, and bumps the version on close.
|
|
2439
|
+
# Single-active-change guard: cr-setup refuses to open a 2nd concurrent change on
|
|
2440
|
+
# the same aggregate (design must close the open one first) unless --force.
|
|
2441
|
+
cmd_cr_setup() {
|
|
2442
|
+
local cr_id="" feature_dir_arg="" title="" force=""
|
|
2443
|
+
while [ $# -gt 0 ]; do
|
|
2444
|
+
case "$1" in
|
|
2445
|
+
--cr-id) cr_id="$2"; shift 2 ;;
|
|
2446
|
+
--feature-dir) feature_dir_arg="$2"; shift 2 ;;
|
|
2447
|
+
--title) title="$2"; shift 2 ;;
|
|
2448
|
+
--force) force=1; shift ;;
|
|
2449
|
+
--help|-h) echo "cr-setup --cr-id <id> [--feature-dir <path>] [--title <t>] [--force]"; return 0 ;;
|
|
2450
|
+
*) echo "cr-setup: unknown arg '$1'" >&2; return 1 ;;
|
|
2451
|
+
esac
|
|
2452
|
+
done
|
|
2453
|
+
[ -n "$cr_id" ] || cr_id="CR-$(date +%Y%m%d-%H%M%S)"
|
|
2454
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
2455
|
+
local fd="$feature_dir_arg"
|
|
2456
|
+
[ -n "$fd" ] || fd=$(read_feature_json_feature_directory "$repo_root")
|
|
2457
|
+
[ -n "$fd" ] || { echo "cr-setup: no feature set and no --feature-dir" >&2; return 1; }
|
|
2458
|
+
[[ "$fd" != /* ]] && fd="$repo_root/$fd"
|
|
2459
|
+
# Single-active-change guard: open the change FIRST so a second concurrent CR on
|
|
2460
|
+
# the same aggregate is refused BEFORE any changes/<cr>/ dir is scaffolded (no
|
|
2461
|
+
# orphan left behind). Re-opening the same cr_id is a no-op. --force overrides.
|
|
2462
|
+
local av_open
|
|
2463
|
+
if ! av_open=$(_active_version "$fd" open "$cr_id" "$force"); then
|
|
2464
|
+
echo "cr-setup: refusing to open '$cr_id' — a change is already open on this feature; finish it (finalize-delta -> analyze-slice -> close-delta) first, or pass --force." >&2
|
|
2465
|
+
printf '%s\n' "$av_open"
|
|
2466
|
+
return 1
|
|
2467
|
+
fi
|
|
2468
|
+
local cr_dir="$fd/changes/$cr_id"
|
|
2469
|
+
mkdir -p "$cr_dir"
|
|
2470
|
+
local dspec="$cr_dir/delta-spec.md" ddesign="$cr_dir/delta-design.md" tpl created=false
|
|
2471
|
+
if [ ! -f "$dspec" ]; then
|
|
2472
|
+
tpl=$(resolve_template "delta-spec-template" "$repo_root") || true
|
|
2473
|
+
if [ -n "$tpl" ] && [ -f "$tpl" ]; then cp "$tpl" "$dspec"; else touch "$dspec"; fi
|
|
2474
|
+
created=true
|
|
2475
|
+
fi
|
|
2476
|
+
if [ ! -f "$ddesign" ]; then
|
|
2477
|
+
tpl=$(resolve_template "delta-design-template" "$repo_root") || true
|
|
2478
|
+
if [ -n "$tpl" ] && [ -f "$tpl" ]; then cp "$tpl" "$ddesign"; else touch "$ddesign"; fi
|
|
2479
|
+
created=true
|
|
2480
|
+
fi
|
|
2481
|
+
printf '{"cr_id":"%s","cr_dir":"%s","delta_spec":"%s","delta_design":"%s","title":"%s","created":%s}\n' \
|
|
2482
|
+
"$(json_escape "$cr_id")" "$(json_escape "$cr_dir")" "$(json_escape "$dspec")" \
|
|
2483
|
+
"$(json_escape "$ddesign")" "$(json_escape "$title")" "$created"
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
cmd_cr_archive() {
|
|
2487
|
+
local cr_id="" feature_dir_arg=""
|
|
2488
|
+
while [ $# -gt 0 ]; do
|
|
2489
|
+
case "$1" in
|
|
2490
|
+
--cr-id) cr_id="$2"; shift 2 ;;
|
|
2491
|
+
--feature-dir) feature_dir_arg="$2"; shift 2 ;;
|
|
2492
|
+
--help|-h) echo "cr-archive --cr-id <id> [--feature-dir <path>]"; return 0 ;;
|
|
2493
|
+
*) echo "cr-archive: unknown arg '$1'" >&2; return 1 ;;
|
|
2494
|
+
esac
|
|
2495
|
+
done
|
|
2496
|
+
[ -n "$cr_id" ] || { echo "cr-archive: --cr-id required" >&2; return 1; }
|
|
2497
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
2498
|
+
local fd="$feature_dir_arg"
|
|
2499
|
+
[ -n "$fd" ] || fd=$(read_feature_json_feature_directory "$repo_root")
|
|
2500
|
+
[ -n "$fd" ] || { echo "cr-archive: no feature set and no --feature-dir" >&2; return 1; }
|
|
2501
|
+
[[ "$fd" != /* ]] && fd="$repo_root/$fd"
|
|
2502
|
+
local cr_dir="$fd/changes/$cr_id" archive_root="$fd/changes/archive" archived=""
|
|
2503
|
+
if [ -d "$cr_dir" ]; then
|
|
2504
|
+
mkdir -p "$archive_root"
|
|
2505
|
+
rm -rf "$archive_root/$cr_id"
|
|
2506
|
+
mv "$cr_dir" "$archive_root/$cr_id"
|
|
2507
|
+
archived="$archive_root/$cr_id"
|
|
2508
|
+
fi
|
|
2509
|
+
# Close the change in active_version.json (single version source): bump minor +
|
|
2510
|
+
# clear active_change. (Supersedes the old bare <feature>/VERSION file.)
|
|
2511
|
+
local av; av=$(_active_version "$fd" close)
|
|
2512
|
+
AW_AV="$av" AW_ARCHIVED="$archived" python3 - <<'PY'
|
|
2513
|
+
import json, os
|
|
2514
|
+
try: av = json.loads(os.environ.get("AW_AV") or "{}")
|
|
2515
|
+
except Exception: av = {}
|
|
2516
|
+
print(json.dumps({"ok": True, "archived_dir": os.environ.get("AW_ARCHIVED", ""),
|
|
2517
|
+
"version": av.get("version", "v1.1"), "previous": av.get("previous", "v1.0"),
|
|
2518
|
+
"closed_change": av.get("closed_change", "")}))
|
|
2519
|
+
PY
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
# Stamp a version into the MASTER documents' `**Version**:` headers — the
|
|
2523
|
+
# deterministic twin of cr-archive's active_version.json bump. close-delta calls
|
|
2524
|
+
# this with the version cr-archive just computed, so all THREE carriers of the
|
|
2525
|
+
# number (spec.md, design.md, active_version.json) move in ONE stage by ONE
|
|
2526
|
+
# mechanism. Before this, design.md's header was bumped by the MODEL at
|
|
2527
|
+
# finalize-delta — a stage early, and guessing the target since the number was
|
|
2528
|
+
# never passed into the merge prompt — while spec.md's header was bumped by
|
|
2529
|
+
# nobody at all and sat at v1.0 forever.
|
|
2530
|
+
# Idempotent: re-stamping the same version reports `unchanged`, writes nothing.
|
|
2531
|
+
# Only the value moves; any trailing inline comment on the header line is kept.
|
|
2532
|
+
cmd_version_stamp() {
|
|
2533
|
+
local version="" feature_dir_arg=""
|
|
2534
|
+
while [ $# -gt 0 ]; do
|
|
2535
|
+
case "$1" in
|
|
2536
|
+
--version) version="$2"; shift 2 ;;
|
|
2537
|
+
--feature-dir) feature_dir_arg="$2"; shift 2 ;;
|
|
2538
|
+
--help|-h) echo "version-stamp --version <vX.Y> [--feature-dir <path>]"; return 0 ;;
|
|
2539
|
+
*) echo "version-stamp: unknown arg '$1'" >&2; return 1 ;;
|
|
2540
|
+
esac
|
|
2541
|
+
done
|
|
2542
|
+
[ -n "$version" ] || { echo "version-stamp: --version required" >&2; return 1; }
|
|
2543
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
2544
|
+
local fd="$feature_dir_arg"
|
|
2545
|
+
[ -n "$fd" ] || fd=$(read_feature_json_feature_directory "$repo_root")
|
|
2546
|
+
[ -n "$fd" ] || { echo "version-stamp: no feature set and no --feature-dir" >&2; return 1; }
|
|
2547
|
+
[[ "$fd" != /* ]] && fd="$repo_root/$fd"
|
|
2548
|
+
AW_FD="$fd" AW_VER="$version" python3 - <<'PY'
|
|
2549
|
+
import json, os, re
|
|
2550
|
+
fd = os.environ["AW_FD"]; ver = os.environ["AW_VER"].strip()
|
|
2551
|
+
stamped, unchanged, missing, no_header = [], [], [], []
|
|
2552
|
+
# First `**Version**: <value>` line only — that is the header. The Change History
|
|
2553
|
+
# table's `| Version |` column head does not match this pattern.
|
|
2554
|
+
pat = re.compile(r'^(\*\*Version\*\*:[ \t]*)(\S+)(.*)$', re.M)
|
|
2555
|
+
# design.ui.md joins the spine where the feature has a UI half: the masters move in
|
|
2556
|
+
# LOCKSTEP, so a document left behind at the old number is the one lie the version
|
|
2557
|
+
# spine cannot tolerate. A feature without one reports it `missing` and nothing fails.
|
|
2558
|
+
for name in ("spec.md", "design.md", "design.ui.md"):
|
|
2559
|
+
p = os.path.join(fd, name)
|
|
2560
|
+
if not os.path.isfile(p):
|
|
2561
|
+
missing.append(name); continue
|
|
2562
|
+
src = open(p, encoding="utf-8").read()
|
|
2563
|
+
m = pat.search(src)
|
|
2564
|
+
if not m:
|
|
2565
|
+
no_header.append(name); continue
|
|
2566
|
+
if m.group(2) == ver:
|
|
2567
|
+
unchanged.append(name); continue
|
|
2568
|
+
open(p, "w", encoding="utf-8").write(
|
|
2569
|
+
src[:m.start()] + m.group(1) + ver + m.group(3) + src[m.end():])
|
|
2570
|
+
stamped.append(name)
|
|
2571
|
+
print(json.dumps({"ok": True, "version": ver, "stamped": stamped,
|
|
2572
|
+
"unchanged": unchanged, "missing": missing,
|
|
2573
|
+
"no_header": no_header, "count": len(stamped)}))
|
|
2574
|
+
PY
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
case "$sub" in
|
|
2578
|
+
resolve-domain) cmd_resolve_domain "$@" ;;
|
|
2579
|
+
resolve-tech) cmd_resolve_tech "$@" ;;
|
|
2580
|
+
scaffold) cmd_scaffold "$@" ;;
|
|
2581
|
+
resolve-hooks) cmd_resolve_hooks "$@" ;;
|
|
2582
|
+
create-checklist) cmd_create_checklist "$@" ;;
|
|
2583
|
+
list-domains) cmd_list_domains "$@" ;;
|
|
2584
|
+
set-domain) cmd_set_domain "$@" ;;
|
|
2585
|
+
stack-list) cmd_stack_list "$@" ;;
|
|
2586
|
+
stack-detect) cmd_stack_detect "$@" ;;
|
|
2587
|
+
lane-declare) cmd_lane_declare "$@" ;;
|
|
2588
|
+
lane-resolve) cmd_lane_resolve "$@" ;;
|
|
2589
|
+
lane-verify) cmd_lane_verify "$@" ;;
|
|
2590
|
+
lane-probe) cmd_lane_probe "$@" ;;
|
|
2591
|
+
list-features) cmd_list_features "$@" ;;
|
|
2592
|
+
set-feature) cmd_set_feature "$@" ;;
|
|
2593
|
+
version-read) cmd_version_read "$@" ;;
|
|
2594
|
+
version-init) cmd_version_init "$@" ;;
|
|
2595
|
+
feature-paths) cmd_feature_paths "$@" ;;
|
|
2596
|
+
design-scaffold) cmd_design_scaffold "$@" ;;
|
|
2597
|
+
build-compile) cmd_build_compile "$@" ;;
|
|
2598
|
+
test-run) cmd_test_run "$@" ;;
|
|
2599
|
+
run-migrate) cmd_run_migrate "$@" ;;
|
|
2600
|
+
deploy-dispatch) cmd_deploy_dispatch "$@" ;;
|
|
2601
|
+
cr-setup) cmd_cr_setup "$@" ;;
|
|
2602
|
+
cr-archive) cmd_cr_archive "$@" ;;
|
|
2603
|
+
version-stamp) cmd_version_stamp "$@" ;;
|
|
2604
|
+
checklist-read) cmd_checklist_read "$@" ;;
|
|
2605
|
+
checklist-apply) cmd_checklist_apply "$@" ;;
|
|
2606
|
+
detect-project) cmd_detect_project "$@" ;;
|
|
2607
|
+
foundation-list) cmd_foundation_list "$@" ;;
|
|
2608
|
+
foundation-locate) cmd_foundation_locate "$@" ;;
|
|
2609
|
+
foundation-unpack) cmd_foundation_unpack "$@" ;;
|
|
2610
|
+
foundation-verify) cmd_foundation_verify "$@" ;;
|
|
2611
|
+
foundation-stash-dbcontext) cmd_foundation_stash_dbcontext "$@" ;;
|
|
2612
|
+
list-components) cmd_list_components "$@" ;;
|
|
2613
|
+
component-catalog) cmd_component_catalog "$@" ;;
|
|
2614
|
+
component-verify) cmd_component_verify "$@" ;;
|
|
2615
|
+
find-host-project) cmd_find_host_project "$@" ;;
|
|
2616
|
+
setup-constitution) cmd_setup_constitution "$@" ;;
|
|
2617
|
+
compose-constitution) cmd_compose_constitution "$@" ;;
|
|
2618
|
+
constitution-verify) cmd_constitution_verify "$@" ;;
|
|
2619
|
+
agent-context-update) cmd_agent_context_update "$@" ;;
|
|
2620
|
+
""|--help|-h)
|
|
2621
|
+
sed -n '2,37p' "$0" | sed 's/^#\{0,1\} \{0,1\}//' ;;
|
|
2622
|
+
*)
|
|
2623
|
+
echo "Unknown subcommand: $sub" >&2
|
|
2624
|
+
echo "Run '$0 --help' for usage." >&2
|
|
2625
|
+
exit 1 ;;
|
|
2626
|
+
esac
|