aw-spec-cli 0.1.26 → 0.1.28

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 (137) hide show
  1. package/assets/.agents/skills/appweaver/SKILL.md +7 -47
  2. package/assets/.appweaver/ORCH-DIALECT.md +13 -9
  3. package/assets/.appweaver/RULE-PARTITION.md +11 -12
  4. package/assets/.appweaver/STACK-PROFILES.md +260 -108
  5. package/assets/.appweaver/domain.json +1 -1
  6. package/assets/.appweaver/interpreter/run-skill.py +28 -7
  7. package/assets/.appweaver/orchestrator/AGENT-PROFILE.md +38 -10
  8. package/assets/.appweaver/orchestrator/LAUNCHER.md +88 -0
  9. package/assets/.appweaver/orchestrator/README.md +120 -53
  10. package/assets/.appweaver/orchestrator/adapters/claude_agent_sdk.py +1 -1
  11. package/assets/.appweaver/orchestrator/modules/delta-plan.orch.yaml +17 -7
  12. package/assets/.appweaver/orchestrator/modules/domain-select.orch.yaml +6 -4
  13. package/assets/.appweaver/orchestrator/modules/foundation-lane.orch.yaml +237 -0
  14. package/assets/.appweaver/orchestrator/modules/foundation-pack.orch.yaml +392 -0
  15. package/assets/.appweaver/orchestrator/modules/foundation-spec.orch.yaml +548 -0
  16. package/assets/.appweaver/orchestrator/modules/lane-gate.orch.yaml +4 -4
  17. package/assets/.appweaver/orchestrator/modules/tech-select.orch.yaml +3 -3
  18. package/assets/.appweaver/orchestrator/orchestrator.py +162 -63
  19. package/assets/.appweaver/orchestrator/rev-templates/delta-design-template.skeleton.md +5 -4
  20. package/assets/.appweaver/orchestrator/rev-templates/design-template.skeleton.md +3 -1
  21. package/assets/.appweaver/orchestrator/rev-templates/domain-constitution.skeleton.md +2 -2
  22. package/assets/.appweaver/orchestrator/rev-templates/domain-requirements.skeleton.md +2 -2
  23. package/assets/.appweaver/orchestrator/rev-templates/domain-rule-bindings.skeleton.md +21 -13
  24. package/assets/.appweaver/orchestrator/rev-templates/frontend-rule-file.skeleton.md +1 -1
  25. package/assets/.appweaver/orchestrator/rev-templates/tech-rule-bindings.skeleton.md +28 -31
  26. package/assets/.appweaver/orchestrator/rev-templates/technical-constitution.skeleton.md +2 -2
  27. package/assets/.appweaver/orchestrator/rev-templates/technical-rule-file.skeleton.md +1 -1
  28. package/assets/.appweaver/orchestrator/rev-templates/ui-constitution.skeleton.md +3 -3
  29. package/assets/.appweaver/orchestrator/rev-templates/ui-delta-design-template.skeleton.md +4 -1
  30. package/assets/.appweaver/orchestrator/rev-templates/ui-design-template.skeleton.md +6 -4
  31. package/assets/.appweaver/orchestrator/workflows/_smoke.orch.yaml +41 -3
  32. package/assets/.appweaver/orchestrator/workflows/aw-spec-add-component.orch.yaml +1 -1
  33. package/assets/.appweaver/orchestrator/workflows/aw-spec-adopt.orch.yaml +17 -16
  34. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan-delta.orch.yaml +7 -1
  35. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan.orch.yaml +7 -1
  36. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice-delta.orch.yaml +3 -3
  37. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice.orch.yaml +3 -3
  38. package/assets/.appweaver/orchestrator/workflows/aw-spec-build.orch.yaml +4 -4
  39. package/assets/.appweaver/orchestrator/workflows/aw-spec-characterize.orch.yaml +2 -2
  40. package/assets/.appweaver/orchestrator/workflows/aw-spec-constitution.orch.yaml +61 -12
  41. package/assets/.appweaver/orchestrator/workflows/aw-spec-deploy.orch.yaml +1 -1
  42. package/assets/.appweaver/orchestrator/workflows/aw-spec-foundation.orch.yaml +172 -293
  43. package/assets/.appweaver/orchestrator/workflows/aw-spec-implement.orch.yaml +253 -44
  44. package/assets/.appweaver/orchestrator/workflows/aw-spec-onboard.orch.yaml +72 -71
  45. package/assets/.appweaver/orchestrator/workflows/aw-spec-parity.orch.yaml +2 -2
  46. package/assets/.appweaver/orchestrator/workflows/aw-spec-plan.orch.yaml +38 -14
  47. package/assets/.appweaver/orchestrator/workflows/aw-spec-run.orch.yaml +4 -4
  48. package/assets/.appweaver/orchestrator/workflows/aw-spec-select-bounded-context.orch.yaml +26 -2
  49. package/assets/.appweaver/orchestrator/workflows/aw-spec-select-domain.orch.yaml +21 -15
  50. package/assets/.appweaver/orchestrator/workflows/aw-spec-select-stack.orch.yaml +14 -14
  51. package/assets/.appweaver/orchestrator/workflows/aw-spec-specify.orch.yaml +3 -3
  52. package/assets/.appweaver/orchestrator/workflows/aw-spec-ui-model.orch.yaml +3 -3
  53. package/assets/.appweaver/orchestrator/workflows/aw-spec-ui-prototype.orch.yaml +6 -6
  54. package/assets/.appweaver/scripts/bash/aw-spec-actions.sh +371 -131
  55. package/assets/.appweaver/scripts/powershell/aw-spec-actions.ps1 +293 -76
  56. package/assets/.appweaver/scripts/python/aw-foundation.py +885 -20
  57. package/assets/.appweaver/scripts/python/aw-inventory.py +28 -21
  58. package/assets/.appweaver/scripts/python/aw-plan-coverage.py +2 -2
  59. package/assets/.appweaver/scripts/python/aw-rev-constitution.py +6 -6
  60. package/assets/.appweaver/scripts/python/aw-stack.py +676 -99
  61. package/assets/.appweaver/stack-profiles/angular.json +2 -1
  62. package/assets/.appweaver/stack-profiles/blazor-wasm.json +2 -1
  63. package/assets/.appweaver/stack-profiles/dotnet-webapi.json +18 -3
  64. package/assets/.appweaver/stack-profiles/go-chi.json +6 -1
  65. package/assets/.appweaver/stack-profiles/java-spring.json +14 -5
  66. package/assets/.appweaver/stack-profiles/nextjs.json +2 -1
  67. package/assets/.appweaver/stack-profiles/node-express.json +8 -2
  68. package/assets/.appweaver/stack-profiles/python-fastapi.json +9 -2
  69. package/assets/.appweaver/stack-profiles/react-vite.json +2 -1
  70. package/assets/.appweaver/stack-profiles/sveltekit.json +2 -1
  71. package/assets/.appweaver/stack-profiles/vue-vite.json +2 -1
  72. package/assets/.appweaver/stacks.json +40 -1
  73. package/assets/.appweaver/templates/constitution-ui-template.md +2 -2
  74. package/assets/.appweaver/templates/delta-design-template.md +7 -5
  75. package/assets/.appweaver/templates/design-template.md +3 -0
  76. package/assets/.appweaver/templates/ui-delta-design-template.md +1 -1
  77. package/assets/.appweaver/templates/ui-design-template.md +6 -4
  78. package/assets/.claude/skills/appweaver/SKILL.md +7 -46
  79. package/assets/foundations/dotnet/ModularMonolith/ModularMonolith.Reference.rules.json +31 -0
  80. package/assets/foundations/dotnet/ModularMonolith/ModularMonolith.lane.json +6 -0
  81. package/assets/foundations/dotnet/VerticalSlice/VerticalSlice.Reference.rules.json +16 -0
  82. package/assets/foundations/dotnet/VerticalSlice/VerticalSlice.lane.json +6 -0
  83. package/assets/foundations/foundation.spec.template.txt +98 -0
  84. package/assets/foundations/java/Hexagonal/Hexagonal.lane.json +6 -0
  85. package/assets/foundations/java/Hexagonal/Hexagonal.pack.txt +8 -1
  86. package/assets/foundations/java/Hexagonal/unpack-foundation.sh +4 -5
  87. package/assets/foundations/node/NextAppRouter/NextAppRouter.spec.txt +68 -0
  88. package/assets/guardrails/README.md +31 -17
  89. package/assets/guardrails/{projects/dotnet-default-project → project-specific}/domain-rule-bindings.md +25 -13
  90. package/assets/guardrails/project-specific/tech-rule-bindings.md +296 -0
  91. package/assets/guardrails/technology/dotnet-webapi/PACKAGING.md +58 -4
  92. package/assets/guardrails/technology/dotnet-webapi/api-versioning.md +1 -1
  93. package/assets/guardrails/technology/dotnet-webapi/auditing.md +1 -1
  94. package/assets/guardrails/technology/dotnet-webapi/authorization.md +1 -1
  95. package/assets/guardrails/technology/dotnet-webapi/clean-architecture.md +1 -1
  96. package/assets/guardrails/technology/dotnet-webapi/coding-standards.md +1 -1
  97. package/assets/guardrails/technology/dotnet-webapi/cqrs.md +1 -1
  98. package/assets/guardrails/technology/dotnet-webapi/data-classification.md +1 -1
  99. package/assets/guardrails/technology/dotnet-webapi/dependency-policy.md +1 -1
  100. package/assets/guardrails/technology/dotnet-webapi/eventing.md +1 -1
  101. package/assets/guardrails/technology/dotnet-webapi/folder-structure.md +1 -1
  102. package/assets/guardrails/technology/dotnet-webapi/framework-api-map.md +1 -1
  103. package/assets/guardrails/technology/dotnet-webapi/modular-monolith.md +1 -1
  104. package/assets/guardrails/technology/dotnet-webapi/persistence.md +1 -1
  105. package/assets/guardrails/technology/dotnet-webapi/testing.md +1 -1
  106. package/assets/guardrails/technology/dotnet-webapi/vertical-slices.md +1 -1
  107. package/assets/guardrails/technology/dotnet-webapi/zero-trust.md +1 -1
  108. package/assets/guardrails/technology/nextjs/accessibility-implementation.md +1 -1
  109. package/assets/guardrails/technology/nextjs/api-client.md +1 -1
  110. package/assets/guardrails/technology/nextjs/api-integration.dotnet-webapi.md +1 -1
  111. package/assets/guardrails/technology/nextjs/api-integration.java-spring.md +1 -1
  112. package/assets/guardrails/technology/nextjs/auth-session.md +1 -1
  113. package/assets/guardrails/technology/nextjs/client-security.md +1 -1
  114. package/assets/guardrails/technology/nextjs/component-architecture.md +1 -1
  115. package/assets/guardrails/technology/nextjs/component-conventions.md +2 -2
  116. package/assets/guardrails/technology/nextjs/design-system.md +1 -1
  117. package/assets/guardrails/technology/nextjs/error-handling.md +1 -1
  118. package/assets/guardrails/technology/nextjs/forms-and-validation.md +1 -1
  119. package/assets/guardrails/technology/nextjs/frontend-testing.md +1 -1
  120. package/assets/guardrails/technology/nextjs/i18n.md +1 -1
  121. package/assets/guardrails/technology/nextjs/observability.md +1 -1
  122. package/assets/guardrails/technology/nextjs/performance-budgets.md +1 -1
  123. package/assets/guardrails/technology/nextjs/routing-navigation.md +1 -1
  124. package/assets/guardrails/technology/nextjs/state-management.md +1 -1
  125. package/assets/guardrails/technology/nextjs/ui-constitution.md +3 -3
  126. package/package.json +1 -1
  127. package/assets/.agents/skills/appweaver-demo/SKILL.md +0 -59
  128. package/assets/.appweaver/orchestrator/.smoke-out/smoke.md +0 -6
  129. package/assets/.appweaver/orchestrator/workflows/aw-spec-demo.orch.yaml +0 -100
  130. package/assets/.claude/skills/appweaver-demo/SKILL.md +0 -59
  131. package/assets/.kilocode/agent.json +0 -28
  132. package/assets/.kilocode/workflows/appweaver-demo.md +0 -59
  133. package/assets/.kilocode/workflows/appweaver.md +0 -55
  134. package/assets/guardrails/projects/dotnet-default-project/tech-rule-bindings.md +0 -271
  135. /package/assets/guardrails/{projects/dotnet-default-project → project-specific}/domain/domain-requirements.md +0 -0
  136. /package/assets/guardrails/{projects/dotnet-default-project → project-specific}/frontend/ui-requirements.md +0 -0
  137. /package/assets/guardrails/{projects/dotnet-default-project → project-specific}/service/technical-requirements.md +0 -0
@@ -2,7 +2,7 @@
2
2
  #
3
3
  # aw-spec-actions.sh — common action dispatcher for the aw-spec orchestrator.
4
4
  #
5
- # ONE shared script for every integration (.claude / .agents / .kilocode). The
5
+ # ONE shared script for every integration (.claude / .agents / .kilo). The
6
6
  # orchestrator workflows reach these subcommands through `execute_script` steps.
7
7
  # Each subcommand performs deterministic, governance-safe work and prints a single
8
8
  # JSON object on stdout. NONE of these actions author spec/code content — content
@@ -47,8 +47,16 @@
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}
58
+ # default-scope --context <name> --feature <name> -> {created, skipped_reason, context_dir, feature_dir}
59
+ # (creates and activates both only on a project with no specs yet)
52
60
  # version-read [--feature-dir <path>] -> {feature, version, active_change, exists, bootstrap_pending}
53
61
  # version-init [--feature-dir <path>] [--bootstrap] -> {…, created, bootstrap_pending} (create active_version.json v1.0; --bootstrap marks a FIRST draft in progress)
54
62
  # version-bootstrap-done [--feature-dir <path>] -> {…, cleared} (the ONLY clearer of the --bootstrap marker; call it when the first draft is finished)
@@ -63,11 +71,28 @@
63
71
  # -> {ok, total, names[], names_csv, elements[], gaps[], gap_count, gaps_text}
64
72
  # (which plan elements the lane's code names and the model reported done)
65
73
  # detect-project -> {scaffolded, markers[]}
66
- # foundation-list [--args '<F> <A> [out]'] -> {foundations[], count, preselect_name, preselect_app, out_dir}
74
+ # foundation-list [--kind <k>] [--profile <id>] [--ecosystem <e>] [--lane-id <id>] [--args '<F> <A>']
75
+ # -> {options[], entries[], count, foundations[], specs[], preselect_option, preselect_app, rejected_out}
76
+ # (one lane's pick list: its packs + specs, then [manual spec] and [skip])
77
+ # foundation-choice --selection <option> [same filters as foundation-list]
78
+ # -> {ok, route (pack|spec|manual|skip), name, file, 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}
81
+ # foundation-lane-files --source <p> [--test <p>] [--workdir <p>] [--profile <id>]
82
+ # -> {has_files, count, sample[], generated[], root_files[], folders[], workdir, summary}
83
+ # foundation-snapshot --out <repo-relative file> -> {ok, snapshot, entries}
84
+ # foundation-outside --snapshot <file> --lane <lane root folder> [--undo]
85
+ # -> {clean, created[], changed[], deleted[], removed[], summary}
67
86
  # foundation-locate --name <F> --app <A> [--out <d>]
68
- # -> {ok, reason, name, app, out_dir, pack_file, unpacker, runner, sentinel, file_count, available[]}
69
- # foundation-unpack --name <F> --app <A> --out <d> [--dry-run] [--force <yes|no>] [--reference]
70
- # -> {ok, mode, written, has_collisions, stdout}
87
+ # -> {ok, reason, name, app, out_dir, pack_file, unpacker, runner, sentinel, file_count, has_reference, available[]}
88
+ # foundation-unpack --name <F> --app <A> --out <d> [--test-path <d>] [--dry-run] [--force <yes|no>] [--reference]
89
+ # -> {ok, mode, written, has_collisions, tests_moved, tests_message, stdout}
90
+ # foundation-rules --name <F> --app <A> --out <lane root folder> [--reference] [--state <folder>]
91
+ # -> {ok, reason, rules_file, rules, merges[], merge_count, stale[], note}
92
+ # (what the pack's <pack name>.rules.json asks of this unpack; puts the files to merge aside)
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)
71
96
  # foundation-verify --out <d> --sentinel <s> -> {clean, residue[]}
72
97
  # list-components -> {components[]} (foundations/dotnet/components/* with integration.md)
73
98
  # find-host-project -> {found, program_cs, project_dir, csproj}
@@ -86,7 +111,9 @@
86
111
  # (design-extract / design-reconcile / design-paths are RETIRED — the path-tagged
87
112
  # four-document model is gone. "Which files is this slice made of" is now an OKF
88
113
  # query: aw-xref concept-paths / concept-upsert --merge.)
89
- # build-compile [--feature-dir <path>] -> {ok, tool, ran, cmd, log_tail}
114
+ # build-compile [--feature-dir <path>] [--kind backend|frontend]
115
+ # -> {ok, tool, ran, cmd, log_tail}
116
+ # (--kind builds that one declared lane; ignored when no lane is declared)
90
117
  # test-run [--feature-dir <path>] -> {ok, tool, ran, cmd, log_tail,
91
118
  # failing_excerpt, failing_lanes[],
92
119
  # no_setup, setup_reason, setup_missing[]}
@@ -163,7 +190,7 @@ cmd_resolve_domain() {
163
190
  fi
164
191
  fi
165
192
 
166
- local rulemap="$repo_root/guardrails/projects/$domain/domain-rule-bindings.md"
193
+ local rulemap="$repo_root/guardrails/$domain/domain-rule-bindings.md"
167
194
  local rulemap_exists=false
168
195
  [ -n "$domain" ] && [ -f "$rulemap" ] && rulemap_exists=true
169
196
 
@@ -188,7 +215,7 @@ cmd_resolve_domain() {
188
215
 
189
216
  # ──────────────────────────────── resolve-tech ─────────────────────────────
190
217
  # Technical rule-file mapping — parallel to resolve-domain, but reads
191
- # guardrails/projects/<project>/tech-rule-bindings.md. The TECHNICAL rule files live in the
218
+ # guardrails/<project>/tech-rule-bindings.md. The TECHNICAL rule files live in the
192
219
  # shared per-stack corpora at guardrails/technology/<stack>/ (rules) and
193
220
  # guardrails/technology/<stack>/examples/ (the worked sample for a rule file), and are
194
221
  # configured per-command in this map, so they can be tuned per project WITHOUT editing the
@@ -239,7 +266,7 @@ repo = os.environ["AW_REPO"]
239
266
  domain = os.environ.get("AW_DOMAIN", "")
240
267
  command = os.environ.get("AW_COMMAND", "")
241
268
 
242
- techmap = os.path.join(repo, "guardrails", "projects", domain, "tech-rule-bindings.md")
269
+ techmap = os.path.join(repo, "guardrails", domain, "tech-rule-bindings.md")
243
270
  exists = bool(domain) and os.path.isfile(techmap)
244
271
 
245
272
  # WHAT A PLACEHOLDER IS FOR. A technical rule corpus is stack-partitioned —
@@ -638,11 +665,12 @@ EOF
638
665
  }
639
666
 
640
667
  # ─────────────────────────────── list-domains ──────────────────────────────
641
- # The selectable projects: immediate subdirs of guardrails/projects/ that carry a rule
642
- # map. The map is what makes a directory a project, exactly as a *.pack.txt is what makes
643
- # a foundations/ directory a foundation — without that test any stray sibling would be
644
- # offered as a governance target. EITHER map qualifies, because onboard writes the domain
645
- # map several steps before the technical one and a half-built project must stay visible.
668
+ # The projects: immediate subdirs of guardrails/ that carry a rule map. The map is what
669
+ # makes a directory a project, exactly as a *.pack.txt is what makes a foundations/
670
+ # directory a foundation — the shared corpora technology/ and domain/ sit beside the
671
+ # projects and carry no map, so they are never listed. EITHER map qualifies, because
672
+ # onboard writes the domain map several steps before the technical one and a half-built
673
+ # project must stay visible.
646
674
  cmd_list_domains() {
647
675
  while [ $# -gt 0 ]; do
648
676
  case "$1" in
@@ -651,7 +679,7 @@ cmd_list_domains() {
651
679
  esac
652
680
  done
653
681
  local repo_root; repo_root=$(get_repo_root)
654
- local base="$repo_root/guardrails/projects"
682
+ local base="$repo_root/guardrails"
655
683
  local -a domains=()
656
684
  if [ -d "$base" ]; then
657
685
  local d
@@ -677,7 +705,7 @@ cmd_set_domain() {
677
705
  [ -n "$domain" ] || { echo "set-domain: --domain is required" >&2; return 1; }
678
706
 
679
707
  local repo_root; repo_root=$(get_repo_root)
680
- local map_rel="guardrails/projects/$domain/domain-rule-bindings.md"
708
+ local map_rel="guardrails/$domain/domain-rule-bindings.md"
681
709
  local map_exists=false
682
710
  [ -f "$repo_root/$map_rel" ] && map_exists=true
683
711
 
@@ -697,9 +725,10 @@ cmd_set_domain() {
697
725
  # The lane model. All four delegate to the shared python core so the bash and
698
726
  # powershell twins cannot drift: the JSON contract has one implementation.
699
727
  #
700
- # lane-declare is the SOLE WRITER of .appweaver/stacks.json and has exactly one
701
- # caller (aw-spec-select-stack). lane-resolve can never write — which is why
702
- # 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.
703
732
  _aw_stack_py() {
704
733
  local repo_root; repo_root=$(get_repo_root)
705
734
  local core="$SCRIPT_DIR/../python/aw-stack.py"
@@ -728,7 +757,7 @@ except Exception:
728
757
  ' 2>/dev/null || echo ""
729
758
  }
730
759
 
731
- # The declared SOURCE PATH of a lane kind — the field asked for most often.
760
+ # The declared LANE ROOT FOLDER of a lane kind — the field asked for most often.
732
761
  _aw_lane_root() { _aw_lane_field "$1" root; }
733
762
 
734
763
  # Every ecosystem the declared lanes belong to, one per line, deduped and empty when
@@ -813,12 +842,66 @@ cmd_lane_resolve() {
813
842
  _aw_stack_py resolve "${fwd[@]}"
814
843
  }
815
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
+
816
899
  # The gate: is the declaration USABLE for the phase about to run. --require names a
817
900
  # lane kind the phase cannot run without and is repeatable; every DECLARED lane is
818
- # checked for a sound source path whether or not it was required, because a stack
901
+ # checked for a sound lane root folder whether or not it was required, because a stack
819
902
  # named against a directory that is not there is a mistake in every phase.
820
- # --allow-missing-root is the greenfield state, where select-stack declares a source
821
- # path before implement creates it.
903
+ # --allow-missing-root is the greenfield state, where select-stack declares a lane root
904
+ # folder before implement creates it.
822
905
  cmd_lane_verify() {
823
906
  local -a fwd=()
824
907
  while [ $# -gt 0 ]; do
@@ -949,6 +1032,13 @@ PY
949
1032
  # file; a new one is created. The context directory is created but domain-model.md is
950
1033
  # NOT — that document is authored by specify, which is the only writer of the model,
951
1034
  # and an empty placeholder would read as "the model exists" to every later gate.
1035
+ #
1036
+ # The active feature belongs to the context it was created in. When the selected
1037
+ # context does not contain it, .appweaver/feature.json is cleared, so the next
1038
+ # feature-scoped command stops with "No active feature" instead of writing into the
1039
+ # context the developer just left (and adopt with no argument does not recreate that
1040
+ # feature's name in the new context). A flat specs/<Feature> is outside every context
1041
+ # and is cleared the same way.
952
1042
  cmd_set_context() {
953
1043
  local raw=""
954
1044
  while [ $# -gt 0 ]; do
@@ -991,11 +1081,20 @@ cmd_set_context() {
991
1081
  "$(json_escape "$context")" "$(json_escape "$context_dir_rel")" > "$cj"
992
1082
  fi
993
1083
 
1084
+ local active_fd; active_fd=$(read_feature_json_feature_directory "$repo_root")
1085
+ active_fd="${active_fd#"$repo_root/"}"; active_fd="${active_fd#./}"; active_fd="${active_fd%/}"
1086
+ local feature_cleared=false cleared_feature=""
1087
+ if [ -n "$active_fd" ] && [[ "$active_fd" != "$context_dir_rel/"* ]]; then
1088
+ _persist_feature_json "$repo_root" ""
1089
+ feature_cleared=true; cleared_feature="$active_fd"
1090
+ fi
1091
+
994
1092
  local created=true; [ "$existed" = true ] && created=false
995
1093
  local dm_exists=false; [ -f "$context_dir/domain-model.md" ] && dm_exists=true
996
- printf '{"context":"%s","context_dir":"%s","domain_model":"%s","existed":%s,"created":%s,"domain_model_exists":%s,"written":true}\n' \
1094
+ printf '{"context":"%s","context_dir":"%s","domain_model":"%s","existed":%s,"created":%s,"domain_model_exists":%s,"feature_cleared":%s,"cleared_feature":"%s","written":true}\n' \
997
1095
  "$(json_escape "$context")" "$(json_escape "$context_dir_rel")" \
998
- "$(json_escape "$context_dir_rel/domain-model.md")" "$existed" "$created" "$dm_exists"
1096
+ "$(json_escape "$context_dir_rel/domain-model.md")" "$existed" "$created" "$dm_exists" \
1097
+ "$feature_cleared" "$(json_escape "$cleared_feature")"
999
1098
  }
1000
1099
 
1001
1100
  # ══════════════════════ domain model — context-level helpers ════════════════
@@ -1460,6 +1559,47 @@ PY
1460
1559
  "$(json_escape "$context")"
1461
1560
  }
1462
1561
 
1562
+ # ─────────────────────────────── default-scope ─────────────────────────────
1563
+ # The bounded context and feature a new project starts in, so a simple project goes
1564
+ # from /appweaver constitution straight to specify. Acts only on a project with no specs
1565
+ # yet: no directory under specs/ (no context, no feature) and no active feature. Every
1566
+ # other state is reported as skipped and left untouched, so a re-run of constitution
1567
+ # never moves the active feature and an onboarded brownfield tree is never added to.
1568
+ # Creation goes through set-context and set-feature, so the folders and both pointer
1569
+ # files are written exactly as select-bounded-context and select-feature write them.
1570
+ # context_dir / feature_dir report the active pair after the call, created or not.
1571
+ cmd_default_scope() {
1572
+ local ctx="" feat=""
1573
+ while [ $# -gt 0 ]; do
1574
+ case "$1" in
1575
+ --context) ctx="$2"; shift 2 ;;
1576
+ --feature) feat="$2"; shift 2 ;;
1577
+ --help|-h) echo "default-scope --context <name> --feature <name> (create and activate both on a project with no specs yet)"; return 0 ;;
1578
+ *) echo "default-scope: unknown arg '$1'" >&2; return 1 ;;
1579
+ esac
1580
+ done
1581
+ { [ -n "$ctx" ] && [ -n "$feat" ]; } || { echo "default-scope: --context and --feature are required" >&2; return 1; }
1582
+
1583
+ local repo_root; repo_root=$(get_repo_root)
1584
+ local reason=""
1585
+ if [ -d "$repo_root/specs" ] && [ -n "$(find "$repo_root/specs" -mindepth 1 -maxdepth 1 -type d -print -quit 2>/dev/null)" ]; then
1586
+ reason="specs/ already holds a bounded context or a feature"
1587
+ elif [ -n "$(read_feature_json_feature_directory "$repo_root")" ]; then
1588
+ reason="a feature is already active in .appweaver/feature.json"
1589
+ fi
1590
+
1591
+ local created=false
1592
+ if [ -z "$reason" ]; then
1593
+ cmd_set_context --context "$ctx" >/dev/null || return 1
1594
+ cmd_set_feature --feature "$feat" >/dev/null || return 1
1595
+ created=true
1596
+ fi
1597
+ printf '{"created":%s,"skipped_reason":"%s","context_dir":"%s","feature_dir":"%s"}\n' \
1598
+ "$created" "$(json_escape "$reason")" \
1599
+ "$(json_escape "$(read_context_json_context_directory "$repo_root")")" \
1600
+ "$(json_escape "$(read_feature_json_feature_directory "$repo_root")")"
1601
+ }
1602
+
1463
1603
  # ═══════════════════════════ active_version.json ════════════════════════════
1464
1604
  # PER-FEATURE state at <feature_dir>/active_version.json:
1465
1605
  # {feature, version, active_change, updated}
@@ -1938,7 +2078,7 @@ cmd_detect_project() {
1938
2078
  local repo_root; repo_root=$(get_repo_root)
1939
2079
  local -a markers=()
1940
2080
  local scope="repo" start="$repo_root" rel
1941
- # A lane's source path arrives as the value declare stored, so it may carry a
2081
+ # A lane root folder arrives as the value declare stored, so it may carry a
1942
2082
  # trailing slash; an option label ("path — why") never reaches a script argument.
1943
2083
  rel="${scope_root#./}"; rel="${rel%/}"
1944
2084
  if [ -n "$rel" ] && [ -d "$repo_root/$rel" ]; then
@@ -1971,10 +2111,12 @@ cmd_detect_project() {
1971
2111
  # they scaffold. Names are unique across stacks, so callers pass a bare <Name> and
1972
2112
  # the lookup searches every stack.
1973
2113
 
1974
- # Echo "<stack>/<Name>" for each foundation directory, one per line. A stack also
1975
- # holds siblings that are not foundations (components/), so carrying a *.pack.txt is
1976
- # what makes a directory one — without that test they surface as bundle-less
1977
- # foundations in every lookup.
2114
+ # Echo "<stack>/<Name>" for each PACK foundation directory, one per line — the scan
2115
+ # foundation-locate and foundation-unpack resolve a pack name through. A stack also holds
2116
+ # siblings that are not foundations (components/), so carrying a *.pack.txt is what makes
2117
+ # a directory one here — without that test they surface as bundle-less foundations in
2118
+ # every lookup. A directory carrying only *.spec.txt is listed by foundation-list
2119
+ # (aw-foundation.py) and never unpacked, so it is deliberately absent from this scan.
1978
2120
  _foundation_dirs() {
1979
2121
  local repo_root="$1" s d
1980
2122
  [ -d "$repo_root/foundations" ] || return 0
@@ -1997,98 +2139,147 @@ _foundation_stack() {
1997
2139
  }
1998
2140
 
1999
2141
  # ──────────────────────────── foundation-list ──────────────────────────────
2000
- # Enumerate available foundations (foundations/<stack>/<Name>/ that carry a real
2001
- # <Name>.pack.txt) so the workflow can let the user PICK one — deterministic, no LLM. An optional
2002
- # --args "<Foundation> <App>" is parsed as a direct-argument fast path:
2003
- # preselect_name is echoed back only when it names a real foundation; preselect_app
2004
- # carries any 2nd token through (empty otherwise).
2142
+ # What ONE lane can be scaffolded from, as a pick list — deterministic, no LLM. Every
2143
+ # pack (<Name>.pack.txt) and every spec (*.spec.txt) under foundations/<ecosystem>/<Name>/
2144
+ # that fits the lane, then "[manual spec]" and "[skip]". The scan, the header filters
2145
+ # (# kind / # profiles) and the option labels live in aw-foundation.py so the two shells
2146
+ # answer from one implementation.
2147
+ #
2148
+ # WHICH entries a lane may be offered. A pack is a whole compiling project in one
2149
+ # ecosystem's build system, and an ecosystem is shared by stacks that cannot use each
2150
+ # other's scaffold, so three filters apply: --ecosystem (the lane's), --kind (backend |
2151
+ # frontend; an entry with no `# kind:` is backend) and --profile (only when the entry
2152
+ # names profiles). Without --ecosystem the filter falls back to every declared lane's
2153
+ # ecosystem, and to nothing at all on a bare repo; an EMPTY --ecosystem filters to none.
2005
2154
  #
2006
- # A THIRD token is no longer an output directory. A scaffold lands in the source path of
2007
- # the lane it is scaffolding, which is the only thing that keeps generated code inside a
2008
- # declared lane, so there is no caller-chosen destination left to accept. It comes back as
2009
- # `rejected_out` for the workflow to refuse by name rather than be silently ignored —
2010
- # a path argument that is quietly dropped is how someone believes they scaffolded a
2011
- # sandbox and finds they overwrote their service.
2155
+ # --args "<Foundation> <App>" is the direct-argument fast path: preselect_option is the
2156
+ # option for that name when this lane offers it; preselect_app carries the 2nd token. A
2157
+ # THIRD token comes back as `rejected_out` — a scaffold lands in its lane root folder,
2158
+ # so there is no caller-chosen destination to accept, and the workflow refuses it by name.
2012
2159
  cmd_foundation_list() {
2013
- local raw="" eco_filter=""
2160
+ local -a fwd=()
2161
+ local eco_given=false
2014
2162
  while [ $# -gt 0 ]; do
2015
2163
  case "$1" in
2016
- --args) raw="$2"; shift 2 ;;
2017
- --ecosystem) eco_filter="$2"; shift 2 ;;
2018
- --help|-h) echo "foundation-list [--args '<Foundation> <App>'] [--ecosystem <eco>] -> {foundations[], count, preselect_name, preselect_app, rejected_out}"; return 0 ;;
2164
+ --args|--kind|--profile|--lane-id) fwd+=("$1" "$2"); shift 2 ;;
2165
+ --ecosystem) fwd+=(--ecosystem "$2"); eco_given=true; shift 2 ;;
2166
+ --help|-h) echo "foundation-list [--kind backend|frontend] [--profile <id>] [--ecosystem <eco>] [--lane-id <id>] [--args '<Foundation> <App>'] -> {options[], entries[], count, foundations[], specs[], all_foundations[], preselect_option, preselect_name, preselect_app, rejected_out}"; return 0 ;;
2019
2167
  *) echo "foundation-list: unknown arg '$1'" >&2; return 1 ;;
2020
2168
  esac
2021
2169
  done
2022
- local repo_root; repo_root=$(get_repo_root)
2023
-
2024
- # WHICH foundations this repo may be offered. A foundation bundle is a whole
2025
- # compiling project in one ecosystem's languages and build system, so unpacking
2026
- # a .NET one into a Spring repo does not produce a scaffold — it produces a
2027
- # second, foreign project beside the real one.
2028
- #
2029
- # --ecosystem narrows to the ONE lane being scaffolded, which is stricter than the
2030
- # union of every declared lane: a repo with a .NET service and a Node UI must not be
2031
- # offered its .NET bundle while the UI lane is the target. Without it the filter falls
2032
- # back to every declared lane's ecosystem, and to nothing at all on a bare repo.
2033
- local -a lane_ecos=()
2034
- local e
2035
- if [ -n "$eco_filter" ]; then
2036
- lane_ecos=("$eco_filter")
2037
- else
2038
- while IFS= read -r e; do [ -n "$e" ] && lane_ecos+=("$e"); done < <(_aw_ecosystems)
2170
+ if [ "$eco_given" = false ]; then
2171
+ local e
2172
+ while IFS= read -r e; do [ -n "$e" ] && fwd+=(--ecosystem "$e"); done < <(_aw_ecosystems)
2039
2173
  fi
2174
+ _aw_foundation_py list "${fwd[@]}"
2175
+ }
2040
2176
 
2041
- local -a foundations=() all=() eco_of=()
2042
- local rel name eco keep
2043
- while IFS= read -r rel; do
2044
- name="${rel##*/}"; eco="${rel%%/*}"
2045
- [ -f "$repo_root/foundations/$rel/$name.pack.txt" ] || continue # the primary pack (excludes Reference-only dirs)
2046
- all+=("$name"); eco_of+=("$eco")
2047
- keep=true
2048
- if [ ${#lane_ecos[@]} -gt 0 ]; then
2049
- keep=false
2050
- for e in "${lane_ecos[@]}"; do [ "$e" = "$eco" ] && keep=true; done
2051
- fi
2052
- [ "$keep" = true ] && foundations+=("$name")
2053
- done < <(_foundation_dirs "$repo_root")
2054
- # Optional direct-argument fast path: "<FoundationName> <AppName>".
2055
- local pre_name="" pre_app="" rejected_out=""
2056
- if [ -n "$raw" ]; then
2057
- # shellcheck disable=SC2086
2058
- set -- $raw
2059
- [ -n "${1:-}" ] && pre_name="$1"; [ -n "${2:-}" ] && pre_app="$2"; [ -n "${3:-}" ] && rejected_out="$3"
2177
+ # ─────────────────────────── foundation-choice ─────────────────────────────
2178
+ # Resolve the option picked from foundation-list back to its entry: route (pack | spec |
2179
+ # manual | skip), name and file. Takes the same filters as the list, so a numbered answer
2180
+ # counts the same rows the operator saw.
2181
+ cmd_foundation_choice() {
2182
+ local -a fwd=()
2183
+ while [ $# -gt 0 ]; do
2184
+ case "$1" in
2185
+ --selection|--kind|--profile|--ecosystem) fwd+=("$1" "$2"); shift 2 ;;
2186
+ --help|-h) echo "foundation-choice --selection <option> [--kind <k>] [--profile <id>] [--ecosystem <eco>] -> {ok, route, name, file, ecosystem, title, reason}"; return 0 ;;
2187
+ *) echo "foundation-choice: unknown arg '$1'" >&2; return 1 ;;
2188
+ esac
2189
+ done
2190
+ _aw_foundation_py choice "${fwd[@]}"
2191
+ }
2192
+
2193
+ # ────────────────────────── foundation-spec-capture ────────────────────────
2194
+ # Copy the spec a lane is about to be built from to .appweaver/foundation/<lane>.spec.md
2195
+ # — from a bundle's *.spec.txt (--file), or from what the operator gave (--input: a path
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.
2199
+ cmd_foundation_spec_capture() {
2200
+ local -a fwd=()
2201
+ while [ $# -gt 0 ]; do
2202
+ case "$1" in
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 ;;
2205
+ *) echo "foundation-spec-capture: unknown arg '$1'" >&2; return 1 ;;
2206
+ esac
2207
+ done
2208
+ _aw_foundation_py spec-capture "${fwd[@]}"
2209
+ }
2210
+
2211
+ # ─────────────────────────── foundation-lane-files ─────────────────────────
2212
+ # What a lane already holds: its lane root folder, its test path when that is another
2213
+ # subtree, and its build files where its commands run when that is outside the lane (the
2214
+ # profile's `detect` entries with no `/`). Build output and dependency directories are
2215
+ # named, not walked. Asked before anything is scaffolded into the lane.
2216
+ cmd_foundation_lane_files() {
2217
+ local -a fwd=()
2218
+ while [ $# -gt 0 ]; do
2219
+ case "$1" in
2220
+ --source|--test|--workdir|--profile) fwd+=("$1" "$2"); shift 2 ;;
2221
+ --help|-h) echo "foundation-lane-files --source <p> [--test <p>] [--workdir <p>] [--profile <id>] -> {has_files, count, sample[], generated[], root_files[], folders[], workdir, summary}"; return 0 ;;
2222
+ *) echo "foundation-lane-files: unknown arg '$1'" >&2; return 1 ;;
2223
+ esac
2224
+ done
2225
+ _aw_foundation_py lane-files "${fwd[@]}"
2226
+ }
2227
+
2228
+ # ─────────────────────── foundation-snapshot / -outside ────────────────────
2229
+ # A spec build is told to write only inside its lane root folder; these check that it did.
2230
+ # `snapshot` records every file in the repo before the build; `outside` lists what was
2231
+ # created, changed or deleted anywhere else afterwards and, with --undo, removes what was
2232
+ # created. Build output and dependency folders are one entry each, not walked.
2233
+ cmd_foundation_snapshot() {
2234
+ local -a fwd=()
2235
+ while [ $# -gt 0 ]; do
2236
+ case "$1" in
2237
+ --out) fwd+=("$1" "$2"); shift 2 ;;
2238
+ --help|-h) echo "foundation-snapshot --out <repo-relative file> -> {ok, snapshot, entries}"; return 0 ;;
2239
+ *) echo "foundation-snapshot: unknown arg '$1'" >&2; return 1 ;;
2240
+ esac
2241
+ done
2242
+ _aw_foundation_py snapshot "${fwd[@]}"
2243
+ }
2244
+
2245
+ cmd_foundation_outside() {
2246
+ local -a fwd=()
2247
+ while [ $# -gt 0 ]; do
2248
+ case "$1" in
2249
+ --snapshot|--lane) fwd+=("$1" "$2"); shift 2 ;;
2250
+ --undo) fwd+=("$1"); shift ;;
2251
+ --help|-h) echo "foundation-outside --snapshot <file> --lane <lane root folder> [--undo] -> {clean, created[], changed[], deleted[], removed[], summary}"; return 0 ;;
2252
+ *) echo "foundation-outside: unknown arg '$1'" >&2; return 1 ;;
2253
+ esac
2254
+ done
2255
+ _aw_foundation_py outside "${fwd[@]}"
2256
+ }
2257
+
2258
+ _aw_foundation_py() {
2259
+ local repo_root; repo_root=$(get_repo_root)
2260
+ local core="$SCRIPT_DIR/../python/aw-foundation.py"
2261
+ if [ ! -f "$core" ]; then
2262
+ echo "aw-foundation: core not found at $core" >&2; return 1
2263
+ fi
2264
+ if ! command -v python3 >/dev/null 2>&1; then
2265
+ echo "aw-foundation: python3 is required to list foundations" >&2; return 1
2060
2266
  fi
2061
- # Only preselect a name that actually names an available foundation.
2062
- local match="" f
2063
- for f in "${foundations[@]}"; do [ "$f" = "$pre_name" ] && match="$pre_name"; done
2064
- printf '{"foundations":%s,"count":%s,"all_foundations":%s,"all_count":%s,"ecosystems":%s,"lane_ecosystems":%s,"filtered":%s,"preselect_name":"%s","preselect_app":"%s","rejected_out":"%s"}\n' \
2065
- "$(json_str_array "${foundations[@]}")" "${#foundations[@]}" \
2066
- "$(json_str_array "${all[@]}")" "${#all[@]}" \
2067
- "$(json_str_array "${eco_of[@]}")" "$(json_str_array "${lane_ecos[@]}")" \
2068
- "$([ ${#lane_ecos[@]} -gt 0 ] && echo true || echo false)" \
2069
- "$(json_escape "$match")" "$(json_escape "$pre_app")" "$(json_escape "$rejected_out")"
2267
+ python3 "$core" --repo "$repo_root" "$@"
2070
2268
  }
2071
2269
 
2072
2270
  # ─────────────────────────── foundation-locate ─────────────────────────────
2073
2271
  # Parse/validate args, locate the pack + unpacker, read the pack header (bounded).
2074
2272
  cmd_foundation_locate() {
2075
- local name="" app="" out="." raw=""
2273
+ local name="" app="" out="."
2076
2274
  while [ $# -gt 0 ]; do
2077
2275
  case "$1" in
2078
2276
  --name) name="$2"; shift 2 ;;
2079
2277
  --app) app="$2"; shift 2 ;;
2080
2278
  --out) out="$2"; shift 2 ;;
2081
- --args) raw="$2"; shift 2 ;;
2082
- --help|-h) echo "foundation-locate (--args '<F> <A> [out]' | --name <F> --app <A> [--out <dir>])"; return 0 ;;
2279
+ --help|-h) echo "foundation-locate --name <F> --app <A> [--out <dir>]"; return 0 ;;
2083
2280
  *) echo "foundation-locate: unknown arg '$1'" >&2; return 1 ;;
2084
2281
  esac
2085
2282
  done
2086
- # positional parse: "<FoundationName> <AppName> [OutputDir]"
2087
- if [ -n "$raw" ]; then
2088
- # shellcheck disable=SC2086
2089
- set -- $raw
2090
- [ -n "$1" ] && name="$1"; [ -n "$2" ] && app="$2"; [ -n "$3" ] && out="$3"
2091
- fi
2092
2283
  local repo_root; repo_root=$(get_repo_root)
2093
2284
  # available = every foundation dir (_foundation_dirs already requires a *.pack.txt)
2094
2285
  local -a available=()
@@ -2098,7 +2289,7 @@ cmd_foundation_locate() {
2098
2289
  done < <(_foundation_dirs "$repo_root")
2099
2290
  _fnd_fail() { printf '{"ok":false,"reason":"%s","available":%s}\n' "$(json_escape "$1")" "$(json_str_array "${available[@]}")"; }
2100
2291
  if [ -z "$name" ] || [ -z "$app" ]; then
2101
- _fnd_fail "Usage: /aw-spec-foundation <FoundationName> <AppName> [OutputDirectory]"; return 0
2292
+ _fnd_fail "foundation-locate needs --name <FoundationName> and --app <AppName>"; return 0
2102
2293
  fi
2103
2294
  local stack; stack=$(_foundation_stack "$repo_root" "$name")
2104
2295
  if [ -z "$stack" ]; then _fnd_fail "Foundation '$name' not found (expected foundations/<stack>/$name/)"; return 0; fi
@@ -2113,7 +2304,7 @@ cmd_foundation_locate() {
2113
2304
  elif [ -f "$fdir/unpack-foundation.sh" ]; then
2114
2305
  runner="sh"; unpacker="$rel/unpack-foundation.sh"
2115
2306
  else
2116
- emit_fail "No unpacker found in $rel/ (unpack-foundation.ps1 or .sh)"; return 0
2307
+ _fnd_fail "No unpacker found in $rel/ (unpack-foundation.ps1 or .sh)"; return 0
2117
2308
  fi
2118
2309
  # bounded header read (never load the pack body)
2119
2310
  local header sentinel file_count
@@ -2122,17 +2313,21 @@ cmd_foundation_locate() {
2122
2313
  file_count=$(printf '%s\n' "$header" | grep -iE 'file' | grep -oE '[0-9]+' | head -1)
2123
2314
  [ -n "$file_count" ] || file_count="0"
2124
2315
 
2125
- 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' \
2316
+ # A reference sample is offered only when its pack is there to unpack.
2317
+ local has_reference=false
2318
+ [ -f "$fdir/$name.Reference.pack.txt" ] && has_reference=true
2319
+
2320
+ 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,"has_reference":%s,"available":%s}\n' \
2126
2321
  "$(json_escape "$name")" "$(json_escape "$stack")" "$(json_escape "$app")" "$(json_escape "$out")" "$(json_escape "$rel/$name.pack.txt")" \
2127
- "$(json_escape "$unpacker")" "$runner" "$(json_escape "$sentinel")" "$file_count" "$(json_str_array "${available[@]}")"
2322
+ "$(json_escape "$unpacker")" "$runner" "$(json_escape "$sentinel")" "$file_count" "$has_reference" "$(json_str_array "${available[@]}")"
2128
2323
  }
2129
2324
 
2130
2325
  # ─────────────────────────── foundation-unpack ─────────────────────────────
2131
2326
  # Run the OS-selected unpacker for the foundation (or reference) pack.
2132
2327
  #
2133
- # --out is the LANE'S SOURCE PATH, so every file a pack carries lands inside the subtree
2328
+ # --out is the LANE ROOT FOLDER, so every file a pack carries lands inside the subtree
2134
2329
  # its lane owns. --test-path is the lane's other subtree: a pack that carries its own test
2135
- # project puts it under the source path like everything else, and aw-foundation.py lifts it
2330
+ # project puts it under the lane root folder like everything else, and aw-foundation.py lifts it
2136
2331
  # onto the path the lane actually keeps tests in. See that module for when it is a no-op.
2137
2332
  cmd_foundation_unpack() {
2138
2333
  local name="" app="" out="." dry=false force="no" reference=false test_path=""
@@ -2206,27 +2401,49 @@ cmd_foundation_unpack() {
2206
2401
  "$(json_escape "$(printf '%s' "$outp" | tail -c 800)")"
2207
2402
  }
2208
2403
 
2209
- # ─────────────────────── foundation-stash-dbcontext ────────────────────────
2210
- # Copy the current ApplicationDbContext.cs to a .foundation.bak sibling before a
2211
- # reference unpack overwrites it (so the two can be merged afterward).
2212
- cmd_foundation_stash_dbcontext() {
2213
- local out="."
2404
+ # ───────────────────── foundation-rules / -rules-done ──────────────────────
2405
+ # A pack's rule file (<pack name>.rules.json beside it) names the files and folders that
2406
+ # are merged with, or replaced over, what the lane already holds; every other pack file is
2407
+ # written as packed. `rules` runs before an unpack: it lists the merges and puts the files
2408
+ # they merge with aside under --state; `rules-done` removes them once merged. Both
2409
+ # delegate to aw-foundation.py.
2410
+ cmd_foundation_rules() {
2411
+ local -a fwd=()
2214
2412
  while [ $# -gt 0 ]; do
2215
2413
  case "$1" in
2216
- --out) out="$2"; shift 2 ;;
2217
- --help|-h) echo "foundation-stash-dbcontext --out <dir>"; return 0 ;;
2218
- *) echo "foundation-stash-dbcontext: unknown arg '$1'" >&2; return 1 ;;
2414
+ --name|--app|--out|--state) fwd+=("$1" "$2"); shift 2 ;;
2415
+ --reference) fwd+=("$1"); shift ;;
2416
+ --help|-h) echo "foundation-rules --name <F> --app <A> --out <lane root folder> [--reference] [--state <folder>] -> {ok, reason, rules_file, rules, merges[], merge_count, stale[], note}"; return 0 ;;
2417
+ *) echo "foundation-rules: unknown arg '$1'" >&2; return 1 ;;
2219
2418
  esac
2220
2419
  done
2221
- local repo_root; repo_root=$(get_repo_root)
2222
- [[ "$out" != /* ]] && out="$repo_root/$out"
2223
- # Searched from the unpack root itself, which IS the lane's source path — a `src/`
2224
- # segment underneath it is the old repo-root layout and is not there any more.
2225
- local ctx; ctx=$(find "$out" -name "ApplicationDbContext.cs" 2>/dev/null | head -1)
2226
- if [ -z "$ctx" ]; then printf '{"found":false,"dbcontext":"","backup":""}\n'; return 0; fi
2227
- local bak="$ctx.foundation.bak"
2228
- cp "$ctx" "$bak"
2229
- printf '{"found":true,"dbcontext":"%s","backup":"%s"}\n' "$(json_escape "$ctx")" "$(json_escape "$bak")"
2420
+ _aw_foundation_py rules "${fwd[@]}"
2421
+ }
2422
+
2423
+ cmd_foundation_rules_done() {
2424
+ local -a fwd=()
2425
+ while [ $# -gt 0 ]; do
2426
+ case "$1" in
2427
+ --state) fwd+=("$1" "$2"); shift 2 ;;
2428
+ --help|-h) echo "foundation-rules-done [--state <folder>] -> {ok, removed}"; return 0 ;;
2429
+ *) echo "foundation-rules-done: unknown arg '$1'" >&2; return 1 ;;
2430
+ esac
2431
+ done
2432
+ _aw_foundation_py rules-done "${fwd[@]}"
2433
+ }
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[@]}"
2230
2447
  }
2231
2448
 
2232
2449
  # ─────────────────────────── foundation-verify ─────────────────────────────
@@ -2246,9 +2463,11 @@ cmd_foundation_verify() {
2246
2463
  local -a residue=()
2247
2464
  if [ -n "$sentinel" ] && [ -d "$out" ]; then
2248
2465
  # scan the generated tree only — never the bundle/framework dirs (the
2249
- # .pack.txt legitimately contains the sentinel).
2466
+ # .pack.txt legitimately contains the sentinel). -F: the sentinel is literal
2467
+ # text, and as a pattern its dots match any character ("AppWeaver Foo" would
2468
+ # count as AppWeaver.Foo) — the PowerShell twin uses -SimpleMatch for the same.
2250
2469
  while IFS= read -r m; do [ -n "$m" ] && residue+=("$m"); done < <(
2251
- grep -rIl --exclude-dir=.git --exclude-dir=guardrails --exclude-dir=foundations --exclude-dir=.appweaver \
2470
+ grep -rIlF --exclude-dir=.git --exclude-dir=guardrails --exclude-dir=foundations --exclude-dir=.appweaver \
2252
2471
  --exclude-dir=specs --exclude-dir=node_modules -- "$sentinel" "$out" 2>/dev/null \
2253
2472
  | sed "s#^$repo_root/##" | head -50 )
2254
2473
  fi
@@ -2743,7 +2962,7 @@ cmd_find_host_project() {
2743
2962
  done
2744
2963
  local repo_root; repo_root=$(get_repo_root)
2745
2964
  local prog
2746
- # WHERE the host lives. The service lane's declared source path when there is one,
2965
+ # WHERE the host lives. The declared lane root folder of the service lane when there is one,
2747
2966
  # `src/` when there is not — the same question run-migrate asks, and the same answer.
2748
2967
  # A repo whose service sits in `backend/` or `Legacy/App/` is searched where its code
2749
2968
  # actually is instead of being reported as having no host project at all.
@@ -3061,10 +3280,15 @@ except Exception:
3061
3280
  }
3062
3281
 
3063
3282
  cmd_build_compile() {
3283
+ # --kind builds ONE declared lane — what `foundation` uses to check the lane it just
3284
+ # created without compiling a lane it did not touch. Ignored on the undeclared ladder
3285
+ # below, which has no lanes to choose between.
3286
+ local kind=""
3064
3287
  while [ $# -gt 0 ]; do
3065
3288
  case "$1" in
3066
3289
  --feature-dir) shift 2 ;;
3067
- --help|-h) echo "build-compile [--feature-dir <path>]"; return 0 ;;
3290
+ --kind) kind="$2"; shift 2 ;;
3291
+ --help|-h) echo "build-compile [--feature-dir <path>] [--kind backend|frontend]"; return 0 ;;
3068
3292
  *) echo "build-compile: unknown arg '$1'" >&2; return 1 ;;
3069
3293
  esac
3070
3294
  done
@@ -3074,7 +3298,11 @@ cmd_build_compile() {
3074
3298
  # sniffer takes the first toolchain it finds and silently never builds the
3075
3299
  # second, which reports green for a frontend that was never compiled.
3076
3300
  if [ -n "$(_aw_lanes_declared)" ]; then
3077
- _aw_stack_py run-phase --phase build
3301
+ if [ -n "$kind" ]; then
3302
+ _aw_stack_py run-phase --phase build --kind "$kind"
3303
+ else
3304
+ _aw_stack_py run-phase --phase build
3305
+ fi
3078
3306
  return $?
3079
3307
  fi
3080
3308
 
@@ -3283,8 +3511,8 @@ print(json.dumps({"ok": bool(d.get("ok")), "tool": "lanes", "ran": bool(d.get("r
3283
3511
  return 0
3284
3512
  fi
3285
3513
 
3286
- # WHERE to look for the migration project. The service lane's declared source
3287
- # path when there is one, `src/` when there is not. Migrations belong to the
3514
+ # WHERE to look for the migration project. The declared lane root folder of the
3515
+ # service lane when there is one, `src/` when there is not. Migrations belong to the
3288
3516
  # backend lane by definition, so a repo whose service code sits in `Legacy/App/`
3289
3517
  # or `backend/` gets searched where its code actually is instead of being told
3290
3518
  # it has no .NET project at all.
@@ -3297,7 +3525,7 @@ print(json.dumps({"ok": bool(d.get("ok")), "tool": "lanes", "ran": bool(d.get("r
3297
3525
  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
3298
3526
  if [ "$has_dotnet" = true ] && command -v dotnet >/dev/null 2>&1 && ( cd "$repo_root" && dotnet ef --version >/dev/null 2>&1 ); then
3299
3527
  # Prefer *Service.csproj (API host with ApplicationDbContext); fall back to first
3300
- # non-AppHost/Web/ServiceDefaults/test project under the service lane's root.
3528
+ # non-AppHost/Web/ServiceDefaults/test project under the service lane root folder.
3301
3529
  local project=""
3302
3530
  project=$(find "$svc_dir" -maxdepth 3 -name '*.Service.csproj' 2>/dev/null | head -1)
3303
3531
  if [ -z "$project" ]; then
@@ -5051,10 +5279,15 @@ case "$sub" in
5051
5279
  lane-resolve) cmd_lane_resolve "$@" ;;
5052
5280
  lane-verify) cmd_lane_verify "$@" ;;
5053
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 "$@" ;;
5054
5286
  list-contexts) cmd_list_contexts "$@" ;;
5055
5287
  set-context) cmd_set_context "$@" ;;
5056
5288
  list-features) cmd_list_features "$@" ;;
5057
5289
  set-feature) cmd_set_feature "$@" ;;
5290
+ default-scope) cmd_default_scope "$@" ;;
5058
5291
  version-read) cmd_version_read "$@" ;;
5059
5292
  version-init) cmd_version_init "$@" ;;
5060
5293
  version-bootstrap-done) cmd_version_bootstrap_done "$@" ;;
@@ -5095,10 +5328,17 @@ case "$sub" in
5095
5328
  checklist-apply) cmd_checklist_apply "$@" ;;
5096
5329
  detect-project) cmd_detect_project "$@" ;;
5097
5330
  foundation-list) cmd_foundation_list "$@" ;;
5331
+ foundation-choice) cmd_foundation_choice "$@" ;;
5332
+ foundation-spec-capture) cmd_foundation_spec_capture "$@" ;;
5333
+ foundation-lane-files) cmd_foundation_lane_files "$@" ;;
5334
+ foundation-snapshot) cmd_foundation_snapshot "$@" ;;
5335
+ foundation-outside) cmd_foundation_outside "$@" ;;
5098
5336
  foundation-locate) cmd_foundation_locate "$@" ;;
5099
5337
  foundation-unpack) cmd_foundation_unpack "$@" ;;
5100
5338
  foundation-verify) cmd_foundation_verify "$@" ;;
5101
- foundation-stash-dbcontext) cmd_foundation_stash_dbcontext "$@" ;;
5339
+ foundation-rules) cmd_foundation_rules "$@" ;;
5340
+ foundation-rules-done) cmd_foundation_rules_done "$@" ;;
5341
+ foundation-pack-lane) cmd_foundation_pack_lane "$@" ;;
5102
5342
  list-components) cmd_list_components "$@" ;;
5103
5343
  component-catalog) cmd_component_catalog "$@" ;;
5104
5344
  component-verify) cmd_component_verify "$@" ;;