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.
Files changed (74) hide show
  1. package/assets/.appweaver/RULE-PARTITION.md +8 -9
  2. package/assets/.appweaver/STACK-PROFILES.md +74 -9
  3. package/assets/.appweaver/interpreter/run-skill.py +28 -7
  4. package/assets/.appweaver/orchestrator/modules/delta-plan.orch.yaml +17 -7
  5. package/assets/.appweaver/orchestrator/modules/domain-select.orch.yaml +1 -1
  6. package/assets/.appweaver/orchestrator/modules/foundation-pack.orch.yaml +44 -1
  7. package/assets/.appweaver/orchestrator/modules/foundation-spec.orch.yaml +172 -5
  8. package/assets/.appweaver/orchestrator/rev-templates/delta-design-template.skeleton.md +5 -4
  9. package/assets/.appweaver/orchestrator/rev-templates/design-template.skeleton.md +3 -1
  10. package/assets/.appweaver/orchestrator/rev-templates/domain-rule-bindings.skeleton.md +16 -8
  11. package/assets/.appweaver/orchestrator/rev-templates/tech-rule-bindings.skeleton.md +15 -18
  12. package/assets/.appweaver/orchestrator/rev-templates/ui-delta-design-template.skeleton.md +4 -1
  13. package/assets/.appweaver/orchestrator/rev-templates/ui-design-template.skeleton.md +6 -4
  14. package/assets/.appweaver/orchestrator/workflows/aw-spec-adopt.orch.yaml +7 -6
  15. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan-delta.orch.yaml +7 -1
  16. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan.orch.yaml +7 -1
  17. package/assets/.appweaver/orchestrator/workflows/aw-spec-deploy.orch.yaml +1 -1
  18. package/assets/.appweaver/orchestrator/workflows/aw-spec-implement.orch.yaml +224 -16
  19. package/assets/.appweaver/orchestrator/workflows/aw-spec-onboard.orch.yaml +20 -19
  20. package/assets/.appweaver/orchestrator/workflows/aw-spec-plan.orch.yaml +35 -11
  21. package/assets/.appweaver/orchestrator/workflows/aw-spec-select-stack.orch.yaml +1 -1
  22. package/assets/.appweaver/orchestrator/workflows/aw-spec-specify.orch.yaml +1 -1
  23. package/assets/.appweaver/scripts/bash/aw-spec-actions.sh +91 -7
  24. package/assets/.appweaver/scripts/powershell/aw-spec-actions.ps1 +70 -7
  25. package/assets/.appweaver/scripts/python/aw-foundation.py +88 -12
  26. package/assets/.appweaver/scripts/python/aw-stack.py +481 -10
  27. package/assets/.appweaver/stack-profiles/dotnet-webapi.json +2 -0
  28. package/assets/.appweaver/stack-profiles/java-spring.json +2 -0
  29. package/assets/.appweaver/stacks.json +5 -2
  30. package/assets/.appweaver/templates/delta-design-template.md +7 -5
  31. package/assets/.appweaver/templates/design-template.md +3 -0
  32. package/assets/.appweaver/templates/ui-delta-design-template.md +1 -1
  33. package/assets/.appweaver/templates/ui-design-template.md +6 -4
  34. package/assets/foundations/dotnet/ModularMonolith/ModularMonolith.lane.json +6 -0
  35. package/assets/foundations/dotnet/VerticalSlice/VerticalSlice.lane.json +6 -0
  36. package/assets/foundations/foundation.spec.template.txt +26 -1
  37. package/assets/foundations/java/Hexagonal/Hexagonal.lane.json +6 -0
  38. package/assets/foundations/java/Hexagonal/Hexagonal.pack.txt +7 -0
  39. package/assets/guardrails/project-specific/domain-rule-bindings.md +20 -8
  40. package/assets/guardrails/project-specific/tech-rule-bindings.md +25 -37
  41. package/assets/guardrails/technology/dotnet-webapi/api-versioning.md +1 -1
  42. package/assets/guardrails/technology/dotnet-webapi/auditing.md +1 -1
  43. package/assets/guardrails/technology/dotnet-webapi/authorization.md +1 -1
  44. package/assets/guardrails/technology/dotnet-webapi/clean-architecture.md +1 -1
  45. package/assets/guardrails/technology/dotnet-webapi/coding-standards.md +1 -1
  46. package/assets/guardrails/technology/dotnet-webapi/cqrs.md +1 -1
  47. package/assets/guardrails/technology/dotnet-webapi/data-classification.md +1 -1
  48. package/assets/guardrails/technology/dotnet-webapi/dependency-policy.md +1 -1
  49. package/assets/guardrails/technology/dotnet-webapi/eventing.md +1 -1
  50. package/assets/guardrails/technology/dotnet-webapi/folder-structure.md +1 -1
  51. package/assets/guardrails/technology/dotnet-webapi/framework-api-map.md +1 -1
  52. package/assets/guardrails/technology/dotnet-webapi/modular-monolith.md +1 -1
  53. package/assets/guardrails/technology/dotnet-webapi/persistence.md +1 -1
  54. package/assets/guardrails/technology/dotnet-webapi/testing.md +1 -1
  55. package/assets/guardrails/technology/dotnet-webapi/vertical-slices.md +1 -1
  56. package/assets/guardrails/technology/dotnet-webapi/zero-trust.md +1 -1
  57. package/assets/guardrails/technology/nextjs/accessibility-implementation.md +1 -1
  58. package/assets/guardrails/technology/nextjs/api-client.md +1 -1
  59. package/assets/guardrails/technology/nextjs/api-integration.dotnet-webapi.md +1 -1
  60. package/assets/guardrails/technology/nextjs/api-integration.java-spring.md +1 -1
  61. package/assets/guardrails/technology/nextjs/auth-session.md +1 -1
  62. package/assets/guardrails/technology/nextjs/client-security.md +1 -1
  63. package/assets/guardrails/technology/nextjs/component-architecture.md +1 -1
  64. package/assets/guardrails/technology/nextjs/component-conventions.md +1 -1
  65. package/assets/guardrails/technology/nextjs/design-system.md +1 -1
  66. package/assets/guardrails/technology/nextjs/error-handling.md +1 -1
  67. package/assets/guardrails/technology/nextjs/forms-and-validation.md +1 -1
  68. package/assets/guardrails/technology/nextjs/frontend-testing.md +1 -1
  69. package/assets/guardrails/technology/nextjs/i18n.md +1 -1
  70. package/assets/guardrails/technology/nextjs/observability.md +1 -1
  71. package/assets/guardrails/technology/nextjs/performance-budgets.md +1 -1
  72. package/assets/guardrails/technology/nextjs/routing-navigation.md +1 -1
  73. package/assets/guardrails/technology/nextjs/state-management.md +1 -1
  74. 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 "<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."
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 \"{{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."
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 is the SOLE WRITER of .appweaver/stacks.json and has exactly one
721
- # caller (aw-spec-select-stack). lane-resolve can never write — which is why
722
- # these are two subcommands rather than one with a mode flag.
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 is the SOLE WRITER of .appweaver/stacks.json, with two callers:
567
- # aw-spec-select-stack, where a person picks, and aw-spec-onboard, which declares
568
- # what it reverse-engineered (those lanes carry origin: reverse-engineered, and
569
- # onboard never overwrites a lane that is already declared). stack-detect proposes
570
- # candidates and writes nothing. lane-resolve can never write — which is why these
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
- -> {"ok": bool, "spec_path": ".appweaver/foundation/<id>.spec.md", "source": "...", ...}
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
- HEADER_RE = re.compile(r"^#\s*([a-z_]+)\s*:\s*(.*?)\s*$")
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
- if m and m.group(1) in HEADER_KEYS and m.group(1) not in head:
223
- head[m.group(1)] = m.group(2)
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
- def capture_spec(repo, lane, kind="", file="", text=""):
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
- return {"ok": False, "spec_path": "", "source": "", "chars": 0, "title": "",
400
- "reason": why}
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
- title = read_header_text(body).get("title", "")
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
- return {"ok": True, "spec_path": rel, "source": source, "chars": len(body),
449
- "title": title, "reason": ""}
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__":