aw-spec-cli 0.1.27 → 0.1.29
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/RULE-PARTITION.md +8 -9
- package/assets/.appweaver/STACK-PROFILES.md +74 -9
- package/assets/.appweaver/interpreter/run-skill.py +28 -7
- package/assets/.appweaver/orchestrator/modules/delta-plan.orch.yaml +17 -7
- package/assets/.appweaver/orchestrator/modules/domain-select.orch.yaml +1 -1
- package/assets/.appweaver/orchestrator/modules/foundation-pack.orch.yaml +44 -1
- package/assets/.appweaver/orchestrator/modules/foundation-spec.orch.yaml +172 -5
- package/assets/.appweaver/orchestrator/rev-templates/delta-design-template.skeleton.md +5 -4
- package/assets/.appweaver/orchestrator/rev-templates/design-template.skeleton.md +3 -1
- package/assets/.appweaver/orchestrator/rev-templates/domain-rule-bindings.skeleton.md +16 -8
- package/assets/.appweaver/orchestrator/rev-templates/tech-rule-bindings.skeleton.md +15 -18
- package/assets/.appweaver/orchestrator/rev-templates/ui-delta-design-template.skeleton.md +4 -1
- package/assets/.appweaver/orchestrator/rev-templates/ui-design-template.skeleton.md +6 -4
- package/assets/.appweaver/orchestrator/workflows/aw-spec-adopt.orch.yaml +7 -6
- package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan-delta.orch.yaml +7 -1
- package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan.orch.yaml +7 -1
- package/assets/.appweaver/orchestrator/workflows/aw-spec-deploy.orch.yaml +1 -1
- package/assets/.appweaver/orchestrator/workflows/aw-spec-implement.orch.yaml +224 -16
- package/assets/.appweaver/orchestrator/workflows/aw-spec-onboard.orch.yaml +20 -19
- package/assets/.appweaver/orchestrator/workflows/aw-spec-plan.orch.yaml +35 -11
- package/assets/.appweaver/orchestrator/workflows/aw-spec-select-stack.orch.yaml +1 -1
- package/assets/.appweaver/orchestrator/workflows/aw-spec-specify.orch.yaml +1 -1
- package/assets/.appweaver/scripts/bash/aw-spec-actions.sh +91 -7
- package/assets/.appweaver/scripts/powershell/aw-spec-actions.ps1 +70 -7
- package/assets/.appweaver/scripts/python/aw-foundation.py +88 -12
- package/assets/.appweaver/scripts/python/aw-stack.py +481 -10
- package/assets/.appweaver/stack-profiles/dotnet-webapi.json +2 -0
- package/assets/.appweaver/stack-profiles/java-spring.json +2 -0
- package/assets/.appweaver/stacks.json +5 -2
- package/assets/.appweaver/templates/delta-design-template.md +7 -5
- package/assets/.appweaver/templates/design-template.md +3 -0
- package/assets/.appweaver/templates/ui-delta-design-template.md +1 -1
- package/assets/.appweaver/templates/ui-design-template.md +6 -4
- package/assets/foundations/dotnet/ModularMonolith/ModularMonolith.lane.json +6 -0
- package/assets/foundations/dotnet/VerticalSlice/VerticalSlice.lane.json +6 -0
- package/assets/foundations/foundation.spec.template.txt +26 -1
- package/assets/foundations/java/Hexagonal/Hexagonal.lane.json +6 -0
- package/assets/foundations/java/Hexagonal/Hexagonal.pack.txt +7 -0
- package/assets/guardrails/project-specific/domain-rule-bindings.md +20 -8
- package/assets/guardrails/project-specific/tech-rule-bindings.md +25 -37
- package/assets/guardrails/technology/dotnet-webapi/api-versioning.md +1 -1
- package/assets/guardrails/technology/dotnet-webapi/auditing.md +1 -1
- package/assets/guardrails/technology/dotnet-webapi/authorization.md +1 -1
- package/assets/guardrails/technology/dotnet-webapi/clean-architecture.md +1 -1
- package/assets/guardrails/technology/dotnet-webapi/coding-standards.md +1 -1
- package/assets/guardrails/technology/dotnet-webapi/cqrs.md +1 -1
- package/assets/guardrails/technology/dotnet-webapi/data-classification.md +1 -1
- package/assets/guardrails/technology/dotnet-webapi/dependency-policy.md +1 -1
- package/assets/guardrails/technology/dotnet-webapi/eventing.md +1 -1
- package/assets/guardrails/technology/dotnet-webapi/folder-structure.md +1 -1
- package/assets/guardrails/technology/dotnet-webapi/framework-api-map.md +1 -1
- package/assets/guardrails/technology/dotnet-webapi/modular-monolith.md +1 -1
- package/assets/guardrails/technology/dotnet-webapi/persistence.md +1 -1
- package/assets/guardrails/technology/dotnet-webapi/testing.md +1 -1
- package/assets/guardrails/technology/dotnet-webapi/vertical-slices.md +1 -1
- package/assets/guardrails/technology/dotnet-webapi/zero-trust.md +1 -1
- package/assets/guardrails/technology/nextjs/accessibility-implementation.md +1 -1
- package/assets/guardrails/technology/nextjs/api-client.md +1 -1
- package/assets/guardrails/technology/nextjs/api-integration.dotnet-webapi.md +1 -1
- package/assets/guardrails/technology/nextjs/api-integration.java-spring.md +1 -1
- package/assets/guardrails/technology/nextjs/auth-session.md +1 -1
- package/assets/guardrails/technology/nextjs/client-security.md +1 -1
- package/assets/guardrails/technology/nextjs/component-architecture.md +1 -1
- package/assets/guardrails/technology/nextjs/component-conventions.md +1 -1
- package/assets/guardrails/technology/nextjs/design-system.md +1 -1
- package/assets/guardrails/technology/nextjs/error-handling.md +1 -1
- package/assets/guardrails/technology/nextjs/forms-and-validation.md +1 -1
- package/assets/guardrails/technology/nextjs/frontend-testing.md +1 -1
- package/assets/guardrails/technology/nextjs/i18n.md +1 -1
- package/assets/guardrails/technology/nextjs/observability.md +1 -1
- package/assets/guardrails/technology/nextjs/performance-budgets.md +1 -1
- package/assets/guardrails/technology/nextjs/routing-navigation.md +1 -1
- package/assets/guardrails/technology/nextjs/state-management.md +1 -1
- package/package.json +1 -1
|
@@ -316,5 +316,5 @@ outputs:
|
|
|
316
316
|
api_detected: "{{api_detected}}"
|
|
317
317
|
ui_detected: "{{ui_detected}}"
|
|
318
318
|
prototype: "{{prototype}}"
|
|
319
|
-
note: "Lanes persisted to .appweaver/stacks.json. Each lane carries a STACK and a LANE ROOT FOLDER, and both are load-bearing. The LANE ROOT FOLDER is ownership: implement's service lane writes its code only under the service lane's and its frontend lane only under the UI lane's, build/test/run act on it, and onboard reverse-engineers out of it. Each lane also owns a TEST PATH, taken from its stack profile — tests/ beside a .NET src/, the lane root folder itself for a stack that keeps its tests inside — and implement writes the lane's tests there. Overlapping paths of either kind are refused here rather than discovered later as two generators writing one file. The build/test/run commands are project-supplied strings the engine shells out to, which is how it stays stdlib-only and stack-ignorant. api_detected / ui_detected report what this repo's own files pointed at, whether or not it was chosen, as
|
|
319
|
+
note: "Lanes persisted to .appweaver/stacks.json. Each lane carries a STACK and a LANE ROOT FOLDER, and both are load-bearing. The LANE ROOT FOLDER is ownership: implement's service lane writes its code only under the service lane's and its frontend lane only under the UI lane's, build/test/run act on it, and onboard reverse-engineers out of it. Each lane also owns a TEST PATH, taken from its stack profile — tests/ beside a .NET src/, the lane root folder itself for a stack that keeps its tests inside — and implement writes the lane's tests there. Overlapping paths of either kind are refused here rather than discovered later as two generators writing one file. The build/test/run commands are project-supplied strings the engine shells out to, which is how it stays stdlib-only and stack-ignorant. api_detected / ui_detected report what this repo's own files pointed at, whether or not it was chosen, as '<stack> at <path>' — or 'nothing detected' on a greenfield repo with nothing to detect. A lane root folder that does not exist yet is NORMAL here and reported rather than refused: greenfield declares src/ before implement creates it. The on-disk check runs in the phases that read the subtree — implement, build, run and onboard all call lane-verify and STOP with the fix named. Declining a lane with (none) is a real declaration and removes any lane of that kind a previous run left behind; it is also what stops that lane's constitution half and rule files being authored. `prototype` is not a lane — it has a root and a run command so an operator can open it, and nothing else. Re-run this workflow any time to re-stack a lane, move a lane root folder, or drop a lane with (none); it is the only workflow that may write this file."
|
|
320
320
|
next: "GREENFIELD: /appweaver foundation → constitution → specify. The project rules live in guardrails/project-specific/, which .appweaver/domain.json names by default — fill in its rule files and its two maps; /appweaver select-domain switches to another project folder under guardrails/ when you keep more than one. On a project with no specs yet, constitution creates the default bounded context MainContext and its feature CoreFeature and makes both active; to use your own names, run /appweaver select-bounded-context <Name> and then /appweaver select-feature <Name> before specify. Once any bounded context exists, select-feature refuses to run until one is selected. foundation scaffolds every lane declared here in one run, backend then frontend — from a verified pack, from a spec the agent builds the project from, or skipped for a lane whose code already exists — and a spec build follows the technical rules the active project maps under `## /aw-spec-foundation-backend` and `## /aw-spec-foundation-frontend`. BROWNFIELD: /appweaver inventory — it scans the lane root folders declared here and proposes the bounded contexts and features as specs/proposed-inventory.md; edit that map, run /appweaver inventory apply, then /appweaver onboard <Context> once per bounded context, which reverse-engineers the governance out of the same lane root folders and activates it: onboard creates guardrails/brf-<domain>/ from the code and names it in .appweaver/domain.json. Run this workflow FIRST in either sequence: foundation scaffolds into the lane root folders declared here, constitution reads the declaration to decide which halves to author, and inventory and onboard read it to decide what to scan."
|
|
@@ -643,7 +643,7 @@ workflow:
|
|
|
643
643
|
action: verify_context
|
|
644
644
|
require_true: nn_compliant
|
|
645
645
|
on_false: stop
|
|
646
|
-
on_false_message: "The answer to
|
|
646
|
+
on_false_message: "The answer to '{{c.question}}' departs from NON-NEGOTIABLE principle {{nn_principle}}: {{nn_why}}. That principle is ratified in .appweaver/memory/ and this answer has no approver — it was settled in this run, not by a domain owner, and the constitution's Governance section holds that a deviation with no named approver is a defect rather than a decision. Re-run specify and answer in a way the principle allows; if the project genuinely intends to depart from it, amend the constitution through its own amendment process FIRST (which names the approver the departure needs) and re-run. Nothing was written to {{quality_target}} for this clarification."
|
|
647
647
|
|
|
648
648
|
- id: resolve_clar
|
|
649
649
|
action: agent_author
|
|
@@ -47,6 +47,12 @@
|
|
|
47
47
|
# Always exits 0 — the workflow's verify_context owns the stop)
|
|
48
48
|
# lane-probe [--kind <k>] [--hint <text>] -> {declared, declared_profile, hint_profile, hint_terms[], signals[], any, evidence}
|
|
49
49
|
# (the FILE signals for "does this lane exist"; resolve-tech carries the rule signal)
|
|
50
|
+
# lane-set (--id <lane>|--kind <k>) [--set KEY=VALUE]… [--unset KEY]… -> {ok, lane_id, set, removed, written}
|
|
51
|
+
# (KEY: contract_run, contract, client; an empty VALUE changes nothing)
|
|
52
|
+
# contract-export [--timeout <s>] -> {ok, ran, cmd, url, doc, doc_exists, operations, unnamed, already_running, reason, log_tail}
|
|
53
|
+
# (start the service lane with contract_run, save the document at contract, stop it)
|
|
54
|
+
# contract-client -> run-phase result for the frontend lane's `client` command
|
|
55
|
+
# contract-check --plan <p> [--doc <d>] -> {ok, cited, ids[], missing[], uncited, doc_exists, reason}
|
|
50
56
|
# list-features [--preselect <name>] [--bundle <dir>] -> {features[], names[], count, select_options[], sentinel} (specs/ ∪ features the OKF concepts serve)
|
|
51
57
|
# set-feature --feature <name> -> {feature, feature_dir, spec_file, existed, created, spec_exists}
|
|
52
58
|
# default-scope --context <name> --feature <name> -> {created, skipped_reason, context_dir, feature_dir}
|
|
@@ -70,8 +76,8 @@
|
|
|
70
76
|
# (one lane's pick list: its packs + specs, then [manual spec] and [skip])
|
|
71
77
|
# foundation-choice --selection <option> [same filters as foundation-list]
|
|
72
78
|
# -> {ok, route (pack|spec|manual|skip), name, file, reason}
|
|
73
|
-
# foundation-spec-capture --lane <id> [--kind <k>] (--file <path> | --input <path|text>)
|
|
74
|
-
# -> {ok, spec_path, source, chars, title, reason}
|
|
79
|
+
# foundation-spec-capture --lane <id> [--kind <k>] (--file <path> | --input <path|text>) [--app <A>]
|
|
80
|
+
# -> {ok, spec_path, source, chars, title, reason, contract_run, contract, client, settings_text}
|
|
75
81
|
# foundation-lane-files --source <p> [--test <p>] [--workdir <p>] [--profile <id>]
|
|
76
82
|
# -> {has_files, count, sample[], generated[], root_files[], folders[], workdir, summary}
|
|
77
83
|
# foundation-snapshot --out <repo-relative file> -> {ok, snapshot, entries}
|
|
@@ -85,6 +91,8 @@
|
|
|
85
91
|
# -> {ok, reason, rules_file, rules, merges[], merge_count, stale[], note}
|
|
86
92
|
# (what the pack's <pack name>.rules.json asks of this unpack; puts the files to merge aside)
|
|
87
93
|
# foundation-rules-done [--state <folder>] -> {ok, removed}
|
|
94
|
+
# foundation-pack-lane --name <F> --app <A> -> {found, file, contract_run, contract, client, reason}
|
|
95
|
+
# (the lane settings in the pack's <pack name>.lane.json, sentinel renamed)
|
|
88
96
|
# foundation-verify --out <d> --sentinel <s> -> {clean, residue[]}
|
|
89
97
|
# list-components -> {components[]} (foundations/dotnet/components/* with integration.md)
|
|
90
98
|
# find-host-project -> {found, program_cs, project_dir, csproj}
|
|
@@ -717,9 +725,10 @@ cmd_set_domain() {
|
|
|
717
725
|
# The lane model. All four delegate to the shared python core so the bash and
|
|
718
726
|
# powershell twins cannot drift: the JSON contract has one implementation.
|
|
719
727
|
#
|
|
720
|
-
# lane-declare
|
|
721
|
-
#
|
|
722
|
-
#
|
|
728
|
+
# lane-declare writes WHOLE lanes into .appweaver/stacks.json and has exactly one caller
|
|
729
|
+
# (aw-spec-select-stack); lane-set changes only the API-contract keys on a lane already
|
|
730
|
+
# there (foundation and implement call it). lane-resolve can never write — which is why
|
|
731
|
+
# these are separate subcommands rather than one with a mode flag.
|
|
723
732
|
_aw_stack_py() {
|
|
724
733
|
local repo_root; repo_root=$(get_repo_root)
|
|
725
734
|
local core="$SCRIPT_DIR/../python/aw-stack.py"
|
|
@@ -833,6 +842,60 @@ cmd_lane_resolve() {
|
|
|
833
842
|
_aw_stack_py resolve "${fwd[@]}"
|
|
834
843
|
}
|
|
835
844
|
|
|
845
|
+
# Change the API-contract keys (contract_run, contract, client) on a lane already
|
|
846
|
+
# declared, leaving the rest of it alone. `--set KEY=` with an empty value changes
|
|
847
|
+
# nothing; `--unset KEY` removes one. Callers: foundation (a pack's lane settings) and
|
|
848
|
+
# implement (the client command a developer supplies).
|
|
849
|
+
cmd_lane_set() {
|
|
850
|
+
local -a fwd=()
|
|
851
|
+
while [ $# -gt 0 ]; do
|
|
852
|
+
case "$1" in
|
|
853
|
+
--id|--kind|--set|--unset) fwd+=("$1" "$2"); shift 2 ;;
|
|
854
|
+
--help|-h) echo "lane-set (--id <lane>|--kind backend|frontend) [--set KEY=VALUE]… [--unset KEY]… (KEY: contract_run, contract, client)"; return 0 ;;
|
|
855
|
+
*) echo "lane-set: unknown arg '$1'" >&2; return 1 ;;
|
|
856
|
+
esac
|
|
857
|
+
done
|
|
858
|
+
_aw_stack_py set "${fwd[@]}"
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
# ─────────────── contract-export / contract-client / contract-check ───────────────
|
|
862
|
+
# The API contract between the lanes: export the service lane's API document from the
|
|
863
|
+
# running service, generate the frontend lane's client from it, and check the operation
|
|
864
|
+
# ids a frontend plan cites against it. All three delegate to aw-stack.py.
|
|
865
|
+
cmd_contract_export() {
|
|
866
|
+
local -a fwd=()
|
|
867
|
+
while [ $# -gt 0 ]; do
|
|
868
|
+
case "$1" in
|
|
869
|
+
--timeout) fwd+=("$1" "$2"); shift 2 ;;
|
|
870
|
+
--help|-h) echo "contract-export [--timeout <seconds>] -> {ok, ran, cmd, url, doc, doc_exists, operations, unnamed, already_running, reason, log_tail}"; return 0 ;;
|
|
871
|
+
*) echo "contract-export: unknown arg '$1'" >&2; return 1 ;;
|
|
872
|
+
esac
|
|
873
|
+
done
|
|
874
|
+
_aw_stack_py contract-export "${fwd[@]}"
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
cmd_contract_client() {
|
|
878
|
+
while [ $# -gt 0 ]; do
|
|
879
|
+
case "$1" in
|
|
880
|
+
--help|-h) echo "contract-client -> run-phase --phase client for the frontend lane"; return 0 ;;
|
|
881
|
+
*) echo "contract-client: unknown arg '$1'" >&2; return 1 ;;
|
|
882
|
+
esac
|
|
883
|
+
done
|
|
884
|
+
_aw_stack_py run-phase --phase client --kind frontend
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
cmd_contract_check() {
|
|
888
|
+
local -a fwd=()
|
|
889
|
+
while [ $# -gt 0 ]; do
|
|
890
|
+
case "$1" in
|
|
891
|
+
--plan|--doc) fwd+=("$1" "$2"); shift 2 ;;
|
|
892
|
+
--help|-h) echo "contract-check --plan <frontend plan> [--doc <api document>] -> {ok, cited, ids, missing, uncited, doc_exists, reason}"; return 0 ;;
|
|
893
|
+
*) echo "contract-check: unknown arg '$1'" >&2; return 1 ;;
|
|
894
|
+
esac
|
|
895
|
+
done
|
|
896
|
+
_aw_stack_py contract-check "${fwd[@]}"
|
|
897
|
+
}
|
|
898
|
+
|
|
836
899
|
# The gate: is the declaration USABLE for the phase about to run. --require names a
|
|
837
900
|
# lane kind the phase cannot run without and is repeatable; every DECLARED lane is
|
|
838
901
|
# checked for a sound lane root folder whether or not it was required, because a stack
|
|
@@ -2131,12 +2194,14 @@ cmd_foundation_choice() {
|
|
|
2131
2194
|
# Copy the spec a lane is about to be built from to .appweaver/foundation/<lane>.spec.md
|
|
2132
2195
|
# — from a bundle's *.spec.txt (--file), or from what the operator gave (--input: a path
|
|
2133
2196
|
# when it names a file, the spec text otherwise). Always exits 0; `ok` carries the verdict.
|
|
2197
|
+
# Also returns the lane settings the spec's header gives (contract_run, contract, client),
|
|
2198
|
+
# with `<App>` replaced by --app.
|
|
2134
2199
|
cmd_foundation_spec_capture() {
|
|
2135
2200
|
local -a fwd=()
|
|
2136
2201
|
while [ $# -gt 0 ]; do
|
|
2137
2202
|
case "$1" in
|
|
2138
|
-
--lane|--kind|--file|--input) fwd+=("$1" "$2"); shift 2 ;;
|
|
2139
|
-
--help|-h) echo "foundation-spec-capture --lane <id> [--kind <k>] (--file <path> | --input <path|text>) -> {ok, spec_path, source, chars, title, reason}"; return 0 ;;
|
|
2203
|
+
--lane|--kind|--file|--input|--app) fwd+=("$1" "$2"); shift 2 ;;
|
|
2204
|
+
--help|-h) echo "foundation-spec-capture --lane <id> [--kind <k>] (--file <path> | --input <path|text>) [--app <App>] -> {ok, spec_path, source, chars, title, reason, contract_run, contract, client, settings_text}"; return 0 ;;
|
|
2140
2205
|
*) echo "foundation-spec-capture: unknown arg '$1'" >&2; return 1 ;;
|
|
2141
2206
|
esac
|
|
2142
2207
|
done
|
|
@@ -2367,6 +2432,20 @@ cmd_foundation_rules_done() {
|
|
|
2367
2432
|
_aw_foundation_py rules-done "${fwd[@]}"
|
|
2368
2433
|
}
|
|
2369
2434
|
|
|
2435
|
+
# A pack's lane settings (<pack name>.lane.json beside it), sentinel renamed — what
|
|
2436
|
+
# foundation records on the lane with lane-set once the pack is unpacked.
|
|
2437
|
+
cmd_foundation_pack_lane() {
|
|
2438
|
+
local -a fwd=()
|
|
2439
|
+
while [ $# -gt 0 ]; do
|
|
2440
|
+
case "$1" in
|
|
2441
|
+
--name|--app) fwd+=("$1" "$2"); shift 2 ;;
|
|
2442
|
+
--help|-h) echo "foundation-pack-lane --name <F> --app <A> -> {found, file, contract_run, contract, client, reason}"; return 0 ;;
|
|
2443
|
+
*) echo "foundation-pack-lane: unknown arg '$1'" >&2; return 1 ;;
|
|
2444
|
+
esac
|
|
2445
|
+
done
|
|
2446
|
+
_aw_foundation_py pack-lane "${fwd[@]}"
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2370
2449
|
# ─────────────────────────── foundation-verify ─────────────────────────────
|
|
2371
2450
|
# Scan the output tree for leftover sentinel tokens (should be none).
|
|
2372
2451
|
cmd_foundation_verify() {
|
|
@@ -5200,6 +5279,10 @@ case "$sub" in
|
|
|
5200
5279
|
lane-resolve) cmd_lane_resolve "$@" ;;
|
|
5201
5280
|
lane-verify) cmd_lane_verify "$@" ;;
|
|
5202
5281
|
lane-probe) cmd_lane_probe "$@" ;;
|
|
5282
|
+
lane-set) cmd_lane_set "$@" ;;
|
|
5283
|
+
contract-export) cmd_contract_export "$@" ;;
|
|
5284
|
+
contract-client) cmd_contract_client "$@" ;;
|
|
5285
|
+
contract-check) cmd_contract_check "$@" ;;
|
|
5203
5286
|
list-contexts) cmd_list_contexts "$@" ;;
|
|
5204
5287
|
set-context) cmd_set_context "$@" ;;
|
|
5205
5288
|
list-features) cmd_list_features "$@" ;;
|
|
@@ -5255,6 +5338,7 @@ case "$sub" in
|
|
|
5255
5338
|
foundation-verify) cmd_foundation_verify "$@" ;;
|
|
5256
5339
|
foundation-rules) cmd_foundation_rules "$@" ;;
|
|
5257
5340
|
foundation-rules-done) cmd_foundation_rules_done "$@" ;;
|
|
5341
|
+
foundation-pack-lane) cmd_foundation_pack_lane "$@" ;;
|
|
5258
5342
|
list-components) cmd_list_components "$@" ;;
|
|
5259
5343
|
component-catalog) cmd_component_catalog "$@" ;;
|
|
5260
5344
|
component-verify) cmd_component_verify "$@" ;;
|
|
@@ -563,12 +563,11 @@ function Invoke-SetDomain {
|
|
|
563
563
|
# SAME python core, so the JSON contract has one implementation and the pair cannot
|
|
564
564
|
# drift.
|
|
565
565
|
#
|
|
566
|
-
# lane-declare
|
|
567
|
-
# aw-spec-select-stack, where a person picks
|
|
568
|
-
#
|
|
569
|
-
#
|
|
570
|
-
#
|
|
571
|
-
# are separate subcommands rather than one with a mode flag.
|
|
566
|
+
# lane-declare writes WHOLE lanes into .appweaver/stacks.json and has exactly one caller:
|
|
567
|
+
# aw-spec-select-stack, where a person picks. lane-set changes only the API-contract keys
|
|
568
|
+
# on a lane already there (foundation and implement call it).
|
|
569
|
+
# stack-detect proposes candidates and writes nothing. lane-resolve can never write —
|
|
570
|
+
# which is why these are separate subcommands rather than one with a mode flag.
|
|
572
571
|
function Invoke-AwStackCore {
|
|
573
572
|
param([string]$Op, [string[]]$Rest)
|
|
574
573
|
$py = Get-Py
|
|
@@ -641,6 +640,52 @@ function Invoke-LaneResolve {
|
|
|
641
640
|
Invoke-AwStackCore -Op 'resolve' -Rest $fwd
|
|
642
641
|
}
|
|
643
642
|
|
|
643
|
+
# Change the API-contract keys (contract_run, contract, client) on a lane already
|
|
644
|
+
# declared. Twin of bash cmd_lane_set.
|
|
645
|
+
function Invoke-LaneSet {
|
|
646
|
+
param([string[]]$Rest)
|
|
647
|
+
$fwd = @()
|
|
648
|
+
for ($i = 0; $i -lt $Rest.Count; $i++) {
|
|
649
|
+
switch ($Rest[$i]) {
|
|
650
|
+
{ $_ -in '--id','--kind','--set','--unset' } { $fwd += $Rest[$i]; $fwd += $Rest[++$i] }
|
|
651
|
+
default { Write-Error "lane-set: unknown arg '$($Rest[$i])'"; exit 1 }
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
Invoke-AwStackCore -Op 'set' -Rest $fwd
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
# ─────────────── contract-export / contract-client / contract-check ───────────────
|
|
658
|
+
# Twins of the bash cmd_contract_export / cmd_contract_client / cmd_contract_check.
|
|
659
|
+
function Invoke-ContractExport {
|
|
660
|
+
param([string[]]$Rest)
|
|
661
|
+
$fwd = @()
|
|
662
|
+
for ($i = 0; $i -lt $Rest.Count; $i++) {
|
|
663
|
+
switch ($Rest[$i]) {
|
|
664
|
+
'--timeout' { $fwd += $Rest[$i]; $fwd += $Rest[++$i] }
|
|
665
|
+
default { Write-Error "contract-export: unknown arg '$($Rest[$i])'"; exit 1 }
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
Invoke-AwStackCore -Op 'contract-export' -Rest $fwd
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function Invoke-ContractClient {
|
|
672
|
+
param([string[]]$Rest)
|
|
673
|
+
if ($Rest.Count -gt 0) { Write-Error "contract-client: unknown arg '$($Rest[0])'"; exit 1 }
|
|
674
|
+
Invoke-AwStackCore -Op 'run-phase' -Rest @('--phase', 'client', '--kind', 'frontend')
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function Invoke-ContractCheck {
|
|
678
|
+
param([string[]]$Rest)
|
|
679
|
+
$fwd = @()
|
|
680
|
+
for ($i = 0; $i -lt $Rest.Count; $i++) {
|
|
681
|
+
switch ($Rest[$i]) {
|
|
682
|
+
{ $_ -in '--plan','--doc' } { $fwd += $Rest[$i]; $fwd += $Rest[++$i] }
|
|
683
|
+
default { Write-Error "contract-check: unknown arg '$($Rest[$i])'"; exit 1 }
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
Invoke-AwStackCore -Op 'contract-check' -Rest $fwd
|
|
687
|
+
}
|
|
688
|
+
|
|
644
689
|
# The gate: is the declaration USABLE for the phase about to run. Twin of bash
|
|
645
690
|
# cmd_lane_verify. --require names a lane kind the phase cannot run without;
|
|
646
691
|
# --allow-missing-root is the greenfield state, where a lane root folder is declared
|
|
@@ -1639,7 +1684,7 @@ function Invoke-FoundationSpecCapture {
|
|
|
1639
1684
|
$fwd = @()
|
|
1640
1685
|
for ($i = 0; $i -lt $Rest.Count; $i++) {
|
|
1641
1686
|
switch ($Rest[$i]) {
|
|
1642
|
-
{ $_ -in '--lane','--kind','--file','--input' } { $fwd += $Rest[$i]; $fwd += $Rest[++$i] }
|
|
1687
|
+
{ $_ -in '--lane','--kind','--file','--input','--app' } { $fwd += $Rest[$i]; $fwd += $Rest[++$i] }
|
|
1643
1688
|
default { Write-Error "foundation-spec-capture: unknown arg '$($Rest[$i])'"; exit 1 }
|
|
1644
1689
|
}
|
|
1645
1690
|
}
|
|
@@ -1808,6 +1853,19 @@ function Invoke-FoundationRulesDone {
|
|
|
1808
1853
|
Invoke-AwFoundationCore -Op 'rules-done' -Rest $fwd
|
|
1809
1854
|
}
|
|
1810
1855
|
|
|
1856
|
+
# A pack's lane settings (<pack name>.lane.json). Twin of bash cmd_foundation_pack_lane.
|
|
1857
|
+
function Invoke-FoundationPackLane {
|
|
1858
|
+
param([string[]]$Rest)
|
|
1859
|
+
$fwd = @()
|
|
1860
|
+
for ($i = 0; $i -lt $Rest.Count; $i++) {
|
|
1861
|
+
switch ($Rest[$i]) {
|
|
1862
|
+
{ $_ -in '--name','--app' } { $fwd += $Rest[$i]; $fwd += $Rest[++$i] }
|
|
1863
|
+
default { Write-Error "foundation-pack-lane: unknown arg '$($Rest[$i])'"; exit 1 }
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
Invoke-AwFoundationCore -Op 'pack-lane' -Rest $fwd
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1811
1869
|
# ─────────────────────────── foundation-verify ─────────────────────────────
|
|
1812
1870
|
function Invoke-FoundationVerify {
|
|
1813
1871
|
param([string[]]$Rest)
|
|
@@ -4598,6 +4656,10 @@ switch ($sub) {
|
|
|
4598
4656
|
'lane-resolve' { Invoke-LaneResolve -Rest $rest }
|
|
4599
4657
|
'lane-verify' { Invoke-LaneVerify -Rest $rest }
|
|
4600
4658
|
'lane-probe' { Invoke-LaneProbe -Rest $rest }
|
|
4659
|
+
'lane-set' { Invoke-LaneSet -Rest $rest }
|
|
4660
|
+
'contract-export' { Invoke-ContractExport -Rest $rest }
|
|
4661
|
+
'contract-client' { Invoke-ContractClient -Rest $rest }
|
|
4662
|
+
'contract-check' { Invoke-ContractCheck -Rest $rest }
|
|
4601
4663
|
'list-contexts' { Invoke-ListContexts -Rest $rest }
|
|
4602
4664
|
'set-context' { Invoke-SetContext -Rest $rest }
|
|
4603
4665
|
'list-features' { Invoke-ListFeatures -Rest $rest }
|
|
@@ -4652,6 +4714,7 @@ switch ($sub) {
|
|
|
4652
4714
|
'foundation-verify' { Invoke-FoundationVerify -Rest $rest }
|
|
4653
4715
|
'foundation-rules' { Invoke-FoundationRules -Rest $rest }
|
|
4654
4716
|
'foundation-rules-done' { Invoke-FoundationRulesDone -Rest $rest }
|
|
4717
|
+
'foundation-pack-lane' { Invoke-FoundationPackLane -Rest $rest }
|
|
4655
4718
|
'list-components' { Invoke-ListComponents -Rest $rest }
|
|
4656
4719
|
'component-catalog' { Invoke-ComponentCatalog -Rest $rest }
|
|
4657
4720
|
'component-verify' { Invoke-ComponentVerify -Rest $rest }
|
|
@@ -64,7 +64,9 @@ Usage:
|
|
|
64
64
|
aw-foundation.py choice --selection <option|name|n> [same filters as list]
|
|
65
65
|
-> {"ok": bool, "route": "pack|spec|manual|skip", "name": "...", "file": "...", ...}
|
|
66
66
|
aw-foundation.py spec-capture --lane <id> [--kind <k>] (--file <path> | --input <path|text>)
|
|
67
|
-
|
|
67
|
+
[--app <App>]
|
|
68
|
+
-> {"ok": bool, "spec_path": ".appweaver/foundation/<id>.spec.md", "source": "...",
|
|
69
|
+
"contract_run": "...", "contract": "...", "client": "...", "settings_text": "...", ...}
|
|
68
70
|
aw-foundation.py lane-files --source <path> [--test <path>] [--workdir <path>] [--profile <id>]
|
|
69
71
|
-> {"has_files": bool, "count": n, "sample": [...], "generated": [...],
|
|
70
72
|
"root_files": [...], "folders": [...], "workdir": "...", "summary": "...",
|
|
@@ -80,6 +82,12 @@ Usage:
|
|
|
80
82
|
"merges": [{"target", "existing", "instruction", "rule"}], "merge_count": n,
|
|
81
83
|
"stale": [...], "note": "..."}
|
|
82
84
|
aw-foundation.py rules-done [--state <repo-relative folder>] -> {"ok": true, "removed": bool}
|
|
85
|
+
aw-foundation.py pack-lane --name <Foundation> --app <App>
|
|
86
|
+
-> {"found": bool, "file": "...", "contract_run": "...", "contract": "...",
|
|
87
|
+
"client": "...", "reason": "..."}
|
|
88
|
+
|
|
89
|
+
A pack may also carry LANE SETTINGS, `<pack name>.lane.json` beside it: values the lane
|
|
90
|
+
records in stacks.json once the pack is unpacked (see pack_lane).
|
|
83
91
|
"""
|
|
84
92
|
|
|
85
93
|
import argparse
|
|
@@ -196,8 +204,10 @@ def redirect_tests(repo, out, test_path, force=False):
|
|
|
196
204
|
LABEL_SEP = " — "
|
|
197
205
|
MANUAL = "[manual spec]"
|
|
198
206
|
SKIP = "[skip]"
|
|
199
|
-
HEADER_KEYS = ("kind", "profiles", "title", "foundation", "sentinel", "files"
|
|
200
|
-
|
|
207
|
+
HEADER_KEYS = ("kind", "profiles", "title", "foundation", "sentinel", "files",
|
|
208
|
+
"contract_run", "contract", "client")
|
|
209
|
+
# A key may be written with hyphens (`contract-run`); it is read as the underscored name.
|
|
210
|
+
HEADER_RE = re.compile(r"^#\s*([a-z][a-z_-]*)\s*:\s*(.*?)\s*$")
|
|
201
211
|
|
|
202
212
|
|
|
203
213
|
HEADER_LINES = 40
|
|
@@ -219,8 +229,9 @@ def read_header_text(text):
|
|
|
219
229
|
if not s.startswith("#"):
|
|
220
230
|
break
|
|
221
231
|
m = HEADER_RE.match(s)
|
|
222
|
-
|
|
223
|
-
|
|
232
|
+
key = m.group(1).replace("-", "_") if m else ""
|
|
233
|
+
if key in HEADER_KEYS and key not in head:
|
|
234
|
+
head[key] = m.group(2)
|
|
224
235
|
return head
|
|
225
236
|
|
|
226
237
|
|
|
@@ -386,7 +397,11 @@ def resolve_choice(repo, selection, kind="", profile="", ecosystems=None):
|
|
|
386
397
|
SPEC_LIMIT = 200_000
|
|
387
398
|
|
|
388
399
|
|
|
389
|
-
|
|
400
|
+
SETTING_LABELS = (("contract_run", "start command"), ("contract", "document URL"),
|
|
401
|
+
("client", "client command"))
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def capture_spec(repo, lane, kind="", file="", text="", app=""):
|
|
390
405
|
"""Copy the spec a lane is being built from to .appweaver/foundation/<lane>.spec.md.
|
|
391
406
|
|
|
392
407
|
The copy is the record of what the project was built from: a bundle spec can change
|
|
@@ -394,10 +409,17 @@ def capture_spec(repo, lane, kind="", file="", text=""):
|
|
|
394
409
|
it names a file, and as the spec itself otherwise — except a single word that is
|
|
395
410
|
shaped like a path (a separator, or a file extension), which is a path that was
|
|
396
411
|
mistyped: capturing it as a fifteen-character spec would build a project from it.
|
|
412
|
+
|
|
413
|
+
Also returns the lane settings the spec's header gives — `# contract-run:`,
|
|
414
|
+
`# contract:` and `# client:`, with `<App>` replaced by `app` — which foundation
|
|
415
|
+
records on the lane once the project is built. They are read here, from the spec as
|
|
416
|
+
given, because the copy opens with a comment and has no header left to read.
|
|
397
417
|
"""
|
|
398
418
|
def fail(why):
|
|
399
|
-
|
|
400
|
-
|
|
419
|
+
out = {"ok": False, "spec_path": "", "source": "", "chars": 0, "title": "",
|
|
420
|
+
"reason": why, "settings_text": "none", "placeholder": "{{contract}}"}
|
|
421
|
+
out.update({k: "" for k in LANE_KEYS})
|
|
422
|
+
return out
|
|
401
423
|
|
|
402
424
|
source, body = "", ""
|
|
403
425
|
cand = (file or "").strip()
|
|
@@ -427,7 +449,8 @@ def capture_spec(repo, lane, kind="", file="", text=""):
|
|
|
427
449
|
if not body.strip():
|
|
428
450
|
return fail("no spec was given — enter a file path or the spec text")
|
|
429
451
|
|
|
430
|
-
|
|
452
|
+
head = read_header_text(body)
|
|
453
|
+
title = head.get("title", "")
|
|
431
454
|
if not title:
|
|
432
455
|
for line in body.splitlines():
|
|
433
456
|
s = line.strip()
|
|
@@ -445,8 +468,17 @@ def capture_spec(repo, lane, kind="", file="", text=""):
|
|
|
445
468
|
" source: %s · captured %s -->\n\n"
|
|
446
469
|
% ((kind + " lane") if kind else "lane", lane_slug, source, stamp))
|
|
447
470
|
fh.write(body if body.endswith("\n") else body + "\n")
|
|
448
|
-
|
|
449
|
-
|
|
471
|
+
settings = {k: head.get(k, "").replace("<App>", app) if app else head.get(k, "")
|
|
472
|
+
for k in LANE_KEYS}
|
|
473
|
+
text_parts = ["%s `%s`" % (label, settings[k]) for k, label in SETTING_LABELS if settings[k]]
|
|
474
|
+
out = {"ok": True, "spec_path": rel, "source": source, "chars": len(body),
|
|
475
|
+
"title": title, "reason": "",
|
|
476
|
+
# One line for a prompt, and the placeholder a client command names the API
|
|
477
|
+
# document by, spelled out — a workflow cannot write it literally.
|
|
478
|
+
"settings_text": "; ".join(text_parts) or "none",
|
|
479
|
+
"placeholder": "{{contract}}"}
|
|
480
|
+
out.update(settings)
|
|
481
|
+
return out
|
|
450
482
|
|
|
451
483
|
|
|
452
484
|
# ───────────────────────────── what a lane already holds ─────────────────────────────
|
|
@@ -854,6 +886,44 @@ def pack_rules(repo, name, app, out, reference=False, state=MERGE_STATE):
|
|
|
854
886
|
note="".join("; " + p for p in parts))
|
|
855
887
|
|
|
856
888
|
|
|
889
|
+
LANE_FORMAT = "aw-spec-pack-lane/v1"
|
|
890
|
+
LANE_KEYS = ("contract_run", "contract", "client")
|
|
891
|
+
|
|
892
|
+
|
|
893
|
+
def pack_lane(repo, name, app):
|
|
894
|
+
"""The lane settings a pack carries in `<pack name>.lane.json` beside it, with the
|
|
895
|
+
sentinel renamed as in the pack's own files.
|
|
896
|
+
|
|
897
|
+
These are what only the pack knows about the project it scaffolds — chiefly how to
|
|
898
|
+
start its service on its own to export the API document, which for a solution with
|
|
899
|
+
an orchestration host is not the lane's `run` command. A pack with no such file has
|
|
900
|
+
nothing to add, and the lane keeps its stack profile's values.
|
|
901
|
+
|
|
902
|
+
{"format": "aw-spec-pack-lane/v1",
|
|
903
|
+
"contract_run": "dotnet run --project src/<Sentinel>.Service -- --urls http://localhost:5000",
|
|
904
|
+
"contract": "http://localhost:5000/swagger/v1/swagger.json"}
|
|
905
|
+
"""
|
|
906
|
+
pack = _pack_path(repo, name)
|
|
907
|
+
result = {"found": False, "file": "", "reason": ""}
|
|
908
|
+
result.update({k: "" for k in LANE_KEYS})
|
|
909
|
+
if not pack:
|
|
910
|
+
return dict(result, reason="the %s pack was not found" % name)
|
|
911
|
+
rel = pack[:-len(".pack.txt")] + ".lane.json"
|
|
912
|
+
path = os.path.join(repo, rel)
|
|
913
|
+
if not os.path.isfile(path):
|
|
914
|
+
return dict(result, reason="the %s pack carries no lane settings (%s)" % (name, rel))
|
|
915
|
+
try:
|
|
916
|
+
with open(path, encoding="utf-8") as fh:
|
|
917
|
+
data = json.load(fh)
|
|
918
|
+
except (ValueError, OSError) as e:
|
|
919
|
+
return dict(result, file=rel, reason="%s cannot be read (%s)" % (rel, e))
|
|
920
|
+
if not isinstance(data, dict) or data.get("format", LANE_FORMAT) != LANE_FORMAT:
|
|
921
|
+
return dict(result, file=rel, reason='%s is not an "%s" file' % (rel, LANE_FORMAT))
|
|
922
|
+
forms = _rename_forms(read_header(os.path.join(repo, pack)).get("sentinel", ""), app)
|
|
923
|
+
values = {k: _renamed(str(data.get(k) or "").strip(), forms) for k in LANE_KEYS}
|
|
924
|
+
return dict(result, found=any(values.values()), file=rel, **values)
|
|
925
|
+
|
|
926
|
+
|
|
857
927
|
def rules_done(repo, state=MERGE_STATE):
|
|
858
928
|
"""Remove the files `rules` put aside, once the merges have read them."""
|
|
859
929
|
stash = _state_dir(state)
|
|
@@ -894,6 +964,7 @@ def main():
|
|
|
894
964
|
sc.add_argument("--kind", default="")
|
|
895
965
|
sc.add_argument("--file", default="")
|
|
896
966
|
sc.add_argument("--input", default="")
|
|
967
|
+
sc.add_argument("--app", default="", help="the app name `<App>` in the header stands for")
|
|
897
968
|
lf = sub.add_parser("lane-files")
|
|
898
969
|
lf.add_argument("--source", default="", help="the lane root folder")
|
|
899
970
|
lf.add_argument("--test", default="", help="the lane's test path")
|
|
@@ -913,6 +984,9 @@ def main():
|
|
|
913
984
|
ru.add_argument("--state", default=MERGE_STATE, help="where existing files are put aside")
|
|
914
985
|
rd = sub.add_parser("rules-done")
|
|
915
986
|
rd.add_argument("--state", default=MERGE_STATE)
|
|
987
|
+
pl = sub.add_parser("pack-lane")
|
|
988
|
+
pl.add_argument("--name", required=True, help="the foundation")
|
|
989
|
+
pl.add_argument("--app", required=True, help="the app name the sentinel is renamed to")
|
|
916
990
|
args = ap.parse_args()
|
|
917
991
|
|
|
918
992
|
if args.cmd == "redirect-tests":
|
|
@@ -923,7 +997,7 @@ def main():
|
|
|
923
997
|
elif args.cmd == "choice":
|
|
924
998
|
emit(resolve_choice(args.repo, args.selection, args.kind, args.profile, args.ecosystem))
|
|
925
999
|
elif args.cmd == "spec-capture":
|
|
926
|
-
emit(capture_spec(args.repo, args.lane, args.kind, args.file, args.input))
|
|
1000
|
+
emit(capture_spec(args.repo, args.lane, args.kind, args.file, args.input, args.app))
|
|
927
1001
|
elif args.cmd == "lane-files":
|
|
928
1002
|
emit(lane_files(args.repo, args.source, args.test, args.workdir, args.profile))
|
|
929
1003
|
elif args.cmd == "snapshot":
|
|
@@ -934,6 +1008,8 @@ def main():
|
|
|
934
1008
|
emit(pack_rules(args.repo, args.name, args.app, args.out, args.reference, args.state))
|
|
935
1009
|
elif args.cmd == "rules-done":
|
|
936
1010
|
emit(rules_done(args.repo, args.state))
|
|
1011
|
+
elif args.cmd == "pack-lane":
|
|
1012
|
+
emit(pack_lane(args.repo, args.name, args.app))
|
|
937
1013
|
|
|
938
1014
|
|
|
939
1015
|
if __name__ == "__main__":
|