aw-spec-cli 0.1.20 → 0.1.22

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 (62) hide show
  1. package/assets/.appweaver/ORCH-DIALECT.md +8 -3
  2. package/assets/.appweaver/STACK-PROFILES.md +87 -8
  3. package/assets/.appweaver/orchestrator/modules/delta-plan.orch.yaml +30 -0
  4. package/assets/.appweaver/orchestrator/modules/delta-spec.orch.yaml +22 -1
  5. package/assets/.appweaver/orchestrator/orchestrator.py +65 -3
  6. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan-delta.orch.yaml +6 -6
  7. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan.orch.yaml +144 -52
  8. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice-delta.orch.yaml +78 -10
  9. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice.orch.yaml +283 -25
  10. package/assets/.appweaver/orchestrator/workflows/aw-spec-constitution.orch.yaml +4 -4
  11. package/assets/.appweaver/orchestrator/workflows/aw-spec-finalize-delta.orch.yaml +202 -40
  12. package/assets/.appweaver/orchestrator/workflows/aw-spec-foundation.orch.yaml +140 -36
  13. package/assets/.appweaver/orchestrator/workflows/aw-spec-implement.orch.yaml +557 -57
  14. package/assets/.appweaver/orchestrator/workflows/aw-spec-onboard.orch.yaml +3 -3
  15. package/assets/.appweaver/orchestrator/workflows/aw-spec-plan.orch.yaml +39 -0
  16. package/assets/.appweaver/orchestrator/workflows/aw-spec-recall-delta.orch.yaml +28 -7
  17. package/assets/.appweaver/orchestrator/workflows/aw-spec-select-stack.orch.yaml +47 -23
  18. package/assets/.appweaver/orchestrator/workflows/aw-spec-specify.orch.yaml +101 -1
  19. package/assets/.appweaver/orchestrator/workflows/aw-spec-ui-model.orch.yaml +141 -0
  20. package/assets/.appweaver/orchestrator/workflows/aw-spec-ui-prototype.orch.yaml +386 -0
  21. package/assets/.appweaver/orchestrator/workflows/aw-spec-workflow-model.orch.yaml +135 -0
  22. package/assets/.appweaver/scripts/bash/aw-spec-actions.sh +772 -216
  23. package/assets/.appweaver/scripts/bash/aw-xref.sh +4 -1
  24. package/assets/.appweaver/scripts/powershell/aw-spec-actions.ps1 +708 -209
  25. package/assets/.appweaver/scripts/powershell/aw-xref.ps1 +4 -1
  26. package/assets/.appweaver/scripts/python/aw-crstate.py +319 -0
  27. package/assets/.appweaver/scripts/python/aw-foundation.py +151 -0
  28. package/assets/.appweaver/scripts/python/aw-gaps.py +119 -0
  29. package/assets/.appweaver/scripts/python/aw-payload-parity.py +19 -0
  30. package/assets/.appweaver/scripts/python/aw-plan-coverage.py +503 -0
  31. package/assets/.appweaver/scripts/python/aw-plangate.py +216 -0
  32. package/assets/.appweaver/scripts/python/aw-principles.py +108 -0
  33. package/assets/.appweaver/scripts/python/aw-stack.py +257 -28
  34. package/assets/.appweaver/scripts/python/aw-testlog.py +148 -0
  35. package/assets/.appweaver/scripts/python/aw-xref.py +141 -4
  36. package/assets/.appweaver/stack-profiles/angular.json +26 -0
  37. package/assets/.appweaver/stack-profiles/blazor-wasm.json +38 -0
  38. package/assets/.appweaver/stack-profiles/dotnet-webapi.json +39 -0
  39. package/assets/.appweaver/stack-profiles/java-spring.json +20 -0
  40. package/assets/.appweaver/stack-profiles/nextjs.json +26 -0
  41. package/assets/.appweaver/stack-profiles/node-express.json +27 -0
  42. package/assets/.appweaver/stack-profiles/python-fastapi.json +16 -0
  43. package/assets/.appweaver/stack-profiles/react-vite.json +26 -0
  44. package/assets/.appweaver/stack-profiles/sveltekit.json +26 -0
  45. package/assets/.appweaver/stack-profiles/vue-vite.json +26 -0
  46. package/assets/.appweaver/templates/constitution-domain-template.md +17 -2
  47. package/assets/.appweaver/templates/ui-model-template.md +67 -0
  48. package/assets/.appweaver/templates/workflow-model-template.md +62 -0
  49. package/assets/guardrails/projects/dotnet-default-project/domain-rule-bindings.md +10 -6
  50. package/assets/guardrails/projects/dotnet-default-project/tech-rule-bindings.md +4 -0
  51. package/assets/guardrails/technology/dotnet-webapi/auditing.md +3 -1
  52. package/assets/guardrails/technology/dotnet-webapi/coding-standards.md +5 -3
  53. package/assets/guardrails/technology/dotnet-webapi/cqrs.md +5 -1
  54. package/assets/guardrails/technology/dotnet-webapi/domain-driven-design.md +13 -5
  55. package/assets/guardrails/technology/dotnet-webapi/examples/cqrs.md +2 -0
  56. package/assets/guardrails/technology/dotnet-webapi/examples/domain-driven-design.md +6 -2
  57. package/assets/guardrails/technology/dotnet-webapi/framework-api-map.md +114 -0
  58. package/assets/guardrails/technology/dotnet-webapi/technology-constitution.md +3 -0
  59. package/assets/guardrails/technology/dotnet-webapi/testing.md +10 -0
  60. package/assets/guardrails/technology/nextjs/ui-constitution.md +8 -1
  61. package/package.json +1 -1
  62. package/assets/.appweaver/orchestrator/.state/smoke.state.json +0 -32
@@ -137,7 +137,7 @@
137
137
 
138
138
  name: aw-spec-onboard
139
139
  description: Onboard an existing codebase for brownfield DEVELOPMENT — a deterministic whole-repo scan produces a high-level OKF feature map (folders/concepts) PLUS the project-level governance scaffold (split constitution + guardrails/projects/brf-<domain>/ with its DOMAIN rule files at domain/ + domain-rule-bindings + code-derived TECHNICAL rule files at service/ + tech-rule-bindings + activated domain), all provisional-from-scan and refined per slice by adopt. Where the scan finds a frontend lane in the CODE, it also authors that lane's half — constitution.ui.md (UX-*) — and its code-derived rule files at brf-<domain>/frontend/, mapped under `## /aw-spec-ui-constitution`; a backend-only repo gets none of it. Every rule tier is reverse-engineered from the CODE alone (never a greenfield rule file), evidenced-only, and WIRED per-workflow so the whole forward pipeline AND adopt govern against THIS codebase — never a greenfield fallback. It also reverse-engineers the repo's own technical structure into a project-specific design + delta-design template pair, installed as the project template overrides, and — where the lane exists — the frontend lane's own pair from the UI source. Every authoring step is shaped by a NEUTRAL rev-templates skeleton; no greenfield constitution, rule file or document template is ever placed in context. Accepts an optional concerns list to look for (onboard decides which are actually present). Reads any declared lane, never writes one. No migration content; never modifies source.
140
- version: 2.0.0
140
+ version: 2.1.0
141
141
  agent_agnostic: true
142
142
  dialect: aw-spec.orchestrator/v1
143
143
 
@@ -1270,8 +1270,8 @@ workflow:
1270
1270
  numeral is whatever the code supported — never a value carried over from another
1271
1271
  constitution. If it mandates no modelling methodology, say so plainly rather than
1272
1272
  supplying one.
1273
- - Author ONLY the domain layer: domain principles, domain folder additions,
1274
- regulatory/security posture, domain governance — from the project + charter. NUMBER
1273
+ - Author ONLY the domain layer: project identity, domain principles, domain folder
1274
+ additions, regulatory/security posture, domain governance — from the project + charter. NUMBER
1275
1275
  them by CONTINUING the technical half's roman numerals: READ
1276
1276
  constitution.technical.md (in context), find its LAST numeral N, start the first
1277
1277
  domain principle at N+1 (do NOT assume it ends at XV). TITLE each by DOMAIN
@@ -154,6 +154,10 @@ vars:
154
154
  # answer; master_exists is what makes the menu appear at all.
155
155
  master_exists: "false" # a DECLARED lane's master plan was already on disk
156
156
  plan_target: "main" # main | delta — the answer to the one question this phase asks
157
+ # The OKF edges this run publishes from the master service plan (main path only).
158
+ record_master_edges: "false"
159
+ xref_edge_count: "0"
160
+ xref_stubbed: "[]" # used siblings that had no concept yet and were stubbed
157
161
  plan_choice: "" # the raw selection (a label): the MAIN row, a change, or the NEW row
158
162
  main_label: "" # cr-list --include-main's exact label, to test the selection against
159
163
  new_label: "" # cr-list --include-new's
@@ -1069,6 +1073,41 @@ workflow:
1069
1073
  action: include
1070
1074
  module: .appweaver/orchestrator/modules/delta-plan.orch.yaml
1071
1075
 
1076
+ # 6b. CROSS-SLICE EDGES, from the document that states them. `aw-xref record` reads the
1077
+ # MASTER service plan — §1 References-by-id and the External integrations table — and
1078
+ # writes a Cross-References row into every sibling aggregate this one uses. Its input
1079
+ # is the plan and nothing else, yet it used to run only at implement: until a feature
1080
+ # was BUILT it contributed no edges, so `xref list` answered "0 downstream users" for
1081
+ # an aggregate a planned feature already depended on, and `xref tree` sorted the two
1082
+ # into the same wave. The information was in this document the whole time.
1083
+ #
1084
+ # MAIN PATH ONLY. On the delta path the master plan still holds the PRE-change
1085
+ # dependencies, so recording here would publish stale references; finalize-delta
1086
+ # records them after the change merges, which is the same reason implement skips it.
1087
+ # Reconcile-on-write, so re-recording at implement is idempotent and a dependency
1088
+ # dropped by a later re-derivation self-heals. Best-effort: never fails the authoring.
1089
+ - id: record_edges_clear
1090
+ action: set
1091
+ vars: { record_master_edges: "false" }
1092
+ - id: record_edges_main
1093
+ condition: "{{plan_target}} == main"
1094
+ action: set
1095
+ vars: { record_master_edges: "true" }
1096
+ - id: record_edges_no_lane
1097
+ condition: "{{be_mode}} == skip"
1098
+ action: set
1099
+ vars: { record_master_edges: "false" }
1100
+ - id: xref_record
1101
+ condition: "{{record_master_edges}} == true"
1102
+ action: execute_script
1103
+ on_error: continue
1104
+ script: { ref: xref }
1105
+ subcommand: record
1106
+ args: ["--feature-dir", "{{feature_dir}}", "--design", "{{backend_plan}}", "--origin", "auto"]
1107
+ outputs:
1108
+ - { name: xref_edge_count, from: "$.edge_count" }
1109
+ - { name: xref_stubbed, from: "$.stubbed" }
1110
+
1072
1111
  # ═══════════ HANDOFF — authoring only; the gates are SEPARATE stages ═══════
1073
1112
  # • MAIN (bootstrap or re-derivation) → /appweaver analyze-plan (spec↔plan
1074
1113
  # conformance gate) → implement. No change is opened either way.
@@ -95,10 +95,14 @@ vars:
95
95
  has_dependants: "false"
96
96
  dependant_ids: ""
97
97
  dependant_count: "0"
98
- # The two refusal flags, both DOWN by default and raised only by the good state — see
98
+ wave_problem: "" # which changes could not be ordered, and why that ends the check
99
+ unordered_ids: ""
100
+ unordered_count: "0"
101
+ # The three refusal flags, all DOWN by default and raised only by the good state — see
99
102
  # the gates below for why fail-closed is the right default here.
100
103
  not_merged: "false"
101
104
  no_dependants: "false"
105
+ wave_ordering_ok: "false"
102
106
  recall_confirmed: "false"
103
107
  recalled: "false"
104
108
  archived_dir: ""
@@ -170,7 +174,7 @@ workflow:
170
174
  - { name: delta_model, from: "$.delta_model" }
171
175
  - { name: wave, from: "$.wave" }
172
176
 
173
- # 4. THE TWO REFUSALS, both answered from disk in one call — see the header for why each
177
+ # 4. THE THREE REFUSALS, all answered from disk in one call — see the header for why each
174
178
  # one is a stop rather than a warning.
175
179
  - id: recall_check
176
180
  action: execute_script
@@ -178,11 +182,16 @@ workflow:
178
182
  subcommand: cr-recall-check
179
183
  args: ["--cr-id", "{{cr_choice}}", "--feature-dir", "{{feature_dir}}"]
180
184
  outputs:
181
- - { name: merged, from: "$.merged" }
182
- - { name: merge_evidence, from: "$.merge_evidence" }
183
- - { name: has_dependants, from: "$.has_dependants" }
184
- - { name: dependant_ids, from: "$.dependant_ids" }
185
- - { name: dependant_count, from: "$.dependant_count" }
185
+ - { name: merged, from: "$.merged" }
186
+ - { name: merge_evidence, from: "$.merge_evidence" }
187
+ - { name: has_dependants, from: "$.has_dependants" }
188
+ - { name: dependant_ids, from: "$.dependant_ids" }
189
+ - { name: dependant_count, from: "$.dependant_count" }
190
+ - { name: wave, from: "$.wave" }
191
+ - { name: wave_ordering_ok, from: "$.wave_ordering_ok" }
192
+ - { name: wave_problem, from: "$.wave_problem" }
193
+ - { name: unordered_ids, from: "$.unordered_ids" }
194
+ - { name: unordered_count, from: "$.unordered_count" }
186
195
 
187
196
  # Each refusal is a RAISE then a gate, not a conditional gate: the raise fires only on
188
197
  # the good state, so a value that never arrived (the script errored, the field was
@@ -199,6 +208,18 @@ workflow:
199
208
  on_false: stop
200
209
  on_false_message: "Change {{cr_id}} has already been MERGED — /appweaver finalize-delta folded it into spec.md and the context's domain-model.md, and the spec's Change History carries it: {{merge_evidence}}. The masters now describe behaviour that includes this change, so withdrawing it would leave them describing work no open change accounts for. A recall cannot un-merge, and this stage will not pretend otherwise. If the change should not have landed, finish it (/appweaver analyze-slice → close-delta) and then open a NEW change that reverses it with /appweaver specify — that keeps both the landing and the reversal in the ledger, which is what a reader of the spec needs to see."
201
210
 
211
+ # WAVE ORDERING IS A PRECONDITION OF THE FENCE BELOW, not a finding of it. The dependant
212
+ # test is `their wave > mine`, and a wave that cannot be read used to answer that as 1, so
213
+ # an unstamped change failed the test and was not counted — recall then withdrew the
214
+ # ground from under a change it could not see, which is the failure this stage exists to
215
+ # prevent. Waves have no ceiling, so unlike the merge fence there is no floor that makes
216
+ # an unordered neighbour harmless: every one of them holds recall until it is stamped.
217
+ - id: require_wave_ordering
218
+ action: verify_context
219
+ require_true: wave_ordering_ok
220
+ on_false: stop
221
+ on_false_message: "Recall REFUSED — the wave fence cannot order change {{cr_id}}, so it cannot tell you what was built on top of it: {{wave_problem}}. Withdrawing a change that something else was planned against is exactly what this stage refuses to do blind. A wave is stamped by a script; `**Wave**: [N]` is the unfilled template line, which means the stamp never ran on that document. Stamp each one — `.appweaver/scripts/bash/aw-spec-actions.sh cr-wave-derive --cr-id <CR-id> --stamp` derives the wave from that change's own `## Depends on` section and writes it — then re-run /appweaver recall-delta {{cr_id}}."
222
+
202
223
  - id: no_dependants_raise
203
224
  condition: "{{has_dependants}} == false"
204
225
  action: set
@@ -14,8 +14,13 @@
14
14
  # evidence sits in, and both are only ever proposals — an operator selects from the
15
15
  # list or types a path the kit has never seen.
16
16
  #
17
- # stack-list --suggest backend → select stack → ask source path → lane-declare
18
- # stack-list --suggest frontend → select stack → ask source path → lane-declare
17
+ # stack-list --suggest backend → select stack → stack-list --profile → ask source path → lane-declare
18
+ # stack-list --suggest frontend → select stack → stack-list --profile → ask source path → lane-declare
19
+ #
20
+ # THE SOURCE-PATH OPTIONS ARE BUILT AFTER THE STACK IS CHOSEN. Most profiles of a kind
21
+ # share a default root (six frontend stacks default to web/), so a list built before the
22
+ # choice cannot say which stack an option belongs to. The second stack-list call answers
23
+ # for the chosen stack alone.
19
24
  #
20
25
  # (none) IS A REAL ANSWER FOR EITHER LANE, and it is how a project says "I do not
21
26
  # have one of these". A backend-only project answers (none) to the UI lane; a
@@ -56,7 +61,7 @@
56
61
 
57
62
  name: aw-spec-select-stack
58
63
  description: Declare the project's lanes — each lane's STACK and its SOURCE PATH — and persist them to .appweaver/stacks.json, the single channel every later workflow resolves and gates on. The first command of both the greenfield and the brownfield sequence, and the sole writer of the lane model.
59
- version: 2.0.0
64
+ version: 2.3.0
60
65
  agent_agnostic: true
61
66
  dialect: aw-spec.orchestrator/v1
62
67
 
@@ -93,9 +98,7 @@ workflow:
93
98
  - { name: api_options, from: "$.select_options" }
94
99
  - { name: api_ids, from: "$.ids" }
95
100
  - { name: api_preselect, from: "$.preselect" }
96
- - { name: api_root_options, from: "$.root_options" }
97
- - { name: api_detected, from: "$.detected_profile" }
98
- - { name: api_detected_root, from: "$.detected_root" }
101
+ - { name: api_detected, from: "$.detected_summary" }
99
102
  - { name: api_none_sentinel, from: "$.none_sentinel" }
100
103
 
101
104
  # 2. No profiles installed is a setup error, not an empty menu. Gated on the
@@ -114,7 +117,7 @@ workflow:
114
117
  # does a detected stack; a project with exactly one candidate auto-confirms.
115
118
  - id: select_api_stack
116
119
  action: user_select
117
- prompt: "Select the API (backend) stack — it owns the service lane. This repo's own files point at: {{api_detected}} (leading the list). (none) declares a project with no service lane at all, and stops the technical constitution half and the service rule files being authored:"
120
+ prompt: "Select the API (backend) stack — it owns the service lane. This repo's own files point at: {{api_detected}}. A detected stack leads the list. (none) declares a project with no service lane at all, and stops the technical constitution half and the service rule files being authored:"
118
121
  options_from_variable: api_options
119
122
  preselect_from: api_preselect
120
123
  single_option_confirm: true
@@ -133,14 +136,24 @@ workflow:
133
136
  condition: "{{chosen_api}} == {{api_none_sentinel}}"
134
137
  vars: { api_selected: "" }
135
138
 
136
- # 5. WHERE the service lane's source lives. Offered best-first — where the
137
- # evidence actually sits, then each profile's own default — and free text is
138
- # accepted, because a repo that matches no profile still has a source path.
139
- # The label after the path says why it is offered and is stripped on the way in.
139
+ # 5. WHERE the service lane's source lives. Offered best-first for the CHOSEN stack —
140
+ # where its evidence sits, then its own default, then where another stack's
141
+ # evidence sits — and free text is accepted, because a repo that matches no
142
+ # profile still has a source path. The label after the path says why it is
143
+ # offered and is stripped on the way in.
144
+ - id: list_api_roots
145
+ action: execute_script
146
+ condition: "{{api_selected}}"
147
+ script: { ref: actions }
148
+ subcommand: stack-list
149
+ args: ["--kind", "backend", "--suggest", "--profile", "{{chosen_api}}"]
150
+ outputs:
151
+ - { name: api_root_options, from: "$.root_options" }
152
+
140
153
  - id: ask_api_source_path
141
154
  action: ask
142
155
  condition: "{{api_selected}}"
143
- prompt: "Service lane SOURCE PATH — the subtree this lane owns, the only place implement may write, and the directory build/test/run act on. Pick one or type your own:"
156
+ prompt: "Service lane SOURCE PATH — the subtree this lane owns, where implement writes its code, and the directory build/test/run act on. Its tests go to the stack's test path beside it (tests/ next to src/ for .NET), shown in the summary. Pick one or type your own:"
144
157
  options_from_variable: api_root_options
145
158
  answer_var: api_source_path
146
159
 
@@ -159,6 +172,7 @@ workflow:
159
172
  - { name: api_lane_id, from: "$.lane_id" }
160
173
  - { name: api_profile, from: "$.profile" }
161
174
  - { name: api_root, from: "$.source_path" }
175
+ - { name: api_test_path, from: "$.test_path" }
162
176
  - { name: api_root_exists, from: "$.root_exists" }
163
177
 
164
178
  # ═══════════════ UI LANE ════════════════════════════════════════════════════
@@ -176,15 +190,13 @@ workflow:
176
190
  - { name: ui_options, from: "$.select_options" }
177
191
  - { name: ui_ids, from: "$.ids" }
178
192
  - { name: ui_preselect, from: "$.preselect" }
179
- - { name: ui_root_options, from: "$.root_options" }
180
- - { name: ui_detected, from: "$.detected_profile" }
181
- - { name: ui_detected_root, from: "$.detected_root" }
193
+ - { name: ui_detected, from: "$.detected_summary" }
182
194
  - { name: ui_none_sentinel, from: "$.none_sentinel" }
183
195
 
184
196
  # 8. Choose the UI stack.
185
197
  - id: select_ui_stack
186
198
  action: user_select
187
- prompt: "Select the UI (frontend) stack — it owns the web lane, and its root must not overlap the service lane's. This repo's own files point at: {{ui_detected}} (leading the list). (none) declares a backend-only project, and stops the UI constitution half and the frontend rule files being authored:"
199
+ prompt: "Select the UI (frontend) stack — it owns the web lane, and its root must not overlap the service lane's. This repo's own files point at: {{ui_detected}}. A detected stack leads the list. (none) declares a backend-only project, and stops the UI constitution half and the frontend rule files being authored:"
188
200
  options_from_variable: ui_options
189
201
  preselect_from: ui_preselect
190
202
  single_option_confirm: true
@@ -199,7 +211,16 @@ workflow:
199
211
  condition: "{{chosen_ui}} == {{ui_none_sentinel}}"
200
212
  vars: { ui_selected: "" }
201
213
 
202
- # 9. WHERE the UI lane's source lives.
214
+ # 9. WHERE the UI lane's source lives, offered for the chosen UI stack.
215
+ - id: list_ui_roots
216
+ action: execute_script
217
+ condition: "{{ui_selected}}"
218
+ script: { ref: actions }
219
+ subcommand: stack-list
220
+ args: ["--kind", "frontend", "--suggest", "--profile", "{{chosen_ui}}"]
221
+ outputs:
222
+ - { name: ui_root_options, from: "$.root_options" }
223
+
203
224
  - id: ask_ui_source_path
204
225
  action: ask
205
226
  condition: "{{ui_selected}}"
@@ -217,6 +238,7 @@ workflow:
217
238
  - { name: ui_lane_id, from: "$.lane_id" }
218
239
  - { name: ui_profile, from: "$.profile" }
219
240
  - { name: ui_root, from: "$.source_path" }
241
+ - { name: ui_test_path, from: "$.test_path" }
220
242
  - { name: ui_root_exists, from: "$.root_exists" }
221
243
 
222
244
  # ═══════════════ CHECK + REPORT ═════════════════════════════════════════════
@@ -257,7 +279,7 @@ workflow:
257
279
  - id: api_summary_lane
258
280
  action: set
259
281
  condition: "{{api_declared}}"
260
- vars: { api_summary: "{{api_lane_id}} ({{api_profile}}) at {{api_root}} — source path exists: {{api_root_exists}}" }
282
+ vars: { api_summary: "{{api_lane_id}} ({{api_profile}}) at {{api_root}}, tests at {{api_test_path}} — source path exists: {{api_root_exists}}" }
261
283
 
262
284
  - id: ui_summary_none
263
285
  action: set
@@ -265,7 +287,7 @@ workflow:
265
287
  - id: ui_summary_lane
266
288
  action: set
267
289
  condition: "{{ui_declared}}"
268
- vars: { ui_summary: "{{ui_lane_id}} ({{ui_profile}}) at {{ui_root}} — source path exists: {{ui_root_exists}}" }
290
+ vars: { ui_summary: "{{ui_lane_id}} ({{ui_profile}}) at {{ui_root}}, tests at {{ui_test_path}} — source path exists: {{ui_root_exists}}" }
269
291
 
270
292
  # 12. Read the finished declaration back through the reader every later workflow
271
293
  # uses, so what this run reports is what they will resolve.
@@ -287,10 +309,12 @@ outputs:
287
309
  ui_lane: "{{ui_summary}}"
288
310
  api_source_path: "{{api_root}}"
289
311
  ui_source_path: "{{ui_root}}"
312
+ api_test_path: "{{api_test_path}}"
313
+ ui_test_path: "{{ui_test_path}}"
290
314
  api_source_path_exists: "{{be_root_exists}}"
291
315
  ui_source_path_exists: "{{fe_root_exists}}"
292
- api_detected: "{{api_detected}} at {{api_detected_root}}"
293
- ui_detected: "{{ui_detected}} at {{ui_detected_root}}"
316
+ api_detected: "{{api_detected}}"
317
+ ui_detected: "{{ui_detected}}"
294
318
  prototype: "{{prototype}}"
295
- note: "Lanes persisted to .appweaver/stacks.json. Each lane carries a STACK and a SOURCE PATH, and both are load-bearing. The SOURCE PATH is ownership: implement's service lane writes 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 — overlapping paths 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 — empty on a greenfield repo with nothing to detect. A source path 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 source path, or drop a lane with (none); it is the only workflow that may write this file."
296
- next: "GREENFIELD: /appweaver select-domain → constitution → select-feature → specify. BROWNFIELD: /appweaver onboard — it reverse-engineers out of the source paths declared here and generates the project (no select-domain; onboard derives and activates it from the code). Run this workflow FIRST in either sequence: constitution reads the declaration to decide which halves to author, and onboard reads it to decide what to scan."
319
+ note: "Lanes persisted to .appweaver/stacks.json. Each lane carries a STACK and a SOURCE PATH, and both are load-bearing. The SOURCE PATH 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 source path 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 source path 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 source path, or drop a lane with (none); it is the only workflow that may write this file."
320
+ next: "GREENFIELD: /appweaver foundation — once per lane, to scaffold a compiling project into each source path declared here (skip it for a lane whose code already exists, or scaffold with the stack's own tooling) — then /appweaver select-domain → constitution → select-feature → specify. BROWNFIELD: /appweaver onboard — it reverse-engineers out of the source paths declared here and generates the project (no select-domain; onboard derives and activates it from the code). Run this workflow FIRST in either sequence: foundation unpacks into the source paths declared here, constitution reads the declaration to decide which halves to author, and onboard reads it to decide what to scan."
@@ -107,6 +107,14 @@ standards:
107
107
 
108
108
  vars:
109
109
  spec_all_pass: "false" # validation-loop guard, flipped by the validate query
110
+ nn_count: "0" # ratified NON-NEGOTIABLE principles, from principles-list.
111
+ # 0 = a project that ratified none, which skips the gate.
112
+ nn_titles: "" # those principles, for the judge's prompt
113
+ nn_compliant: "false" # per-clarification verdict. DEFAULTS TO REFUSE: the gate
114
+ # reads it, so a judge that never answered stops the run
115
+ # rather than passing an unexamined answer through.
116
+ nn_principle: "" # the principle a departing answer was judged against
117
+ nn_why: "" # what it requires and what the answer does instead
110
118
  checklist_total: "0" # bound by checklist-read
111
119
  checklist_passed: "0" # stays 0 when the checklist has no items to score
112
120
  cr_id: "" # set on the DELTA path only
@@ -520,6 +528,23 @@ workflow:
520
528
 
521
529
  # 6. Resolve [NEEDS CLARIFICATION] markers interactively (≤3). Non-interactive
522
530
  # runs answer with the first/recommended option (a documented default).
531
+ #
532
+ # THE DEFAULT IS WHY THIS IS GOVERNED. A non-interactive run answers with options[0],
533
+ # so whatever the extraction listed first becomes project policy unattended — and a
534
+ # clarification is asked at exactly the altitude where a ratified principle already
535
+ # has an answer. The constitution halves ARE in context here (build_context put them
536
+ # in the envelope, which include_standards: false does not touch), but nothing told
537
+ # the extraction that a principle outranks a question, and nothing checked the answer
538
+ # afterwards. Both halves of that are below: the ordering rule in the task, and the
539
+ # per-answer gate inside the loop.
540
+ - id: nn_principles
541
+ action: execute_script
542
+ script: { ref: actions }
543
+ subcommand: principles-list
544
+ outputs:
545
+ - { name: nn_count, from: "$.count" }
546
+ - { name: nn_titles, from: "$.titles_csv" }
547
+
523
548
  - id: extract_clarifications
524
549
  action: agent_query
525
550
  context: ["{{quality_target}}"]
@@ -528,6 +553,15 @@ workflow:
528
553
  Extract up to 3 unresolved "[NEEDS CLARIFICATION: <question>]" markers from
529
554
  the document in context (a feature spec, or one change's delta-spec), most
530
555
  scope/security/UX-impacting first. Return {"clarifications":[]} if there are none.
556
+
557
+ A RATIFIED PRINCIPLE IS NOT A QUESTION. The constitution halves in context carry
558
+ principles marked (NON-NEGOTIABLE). Where one of them already settles a marker,
559
+ it is not a clarification: DROP it from the list — the answer is the principle,
560
+ and the document is corrected to match it in the quality loop. Offering it as a
561
+ choice invites a project to vote on something already ratified.
562
+ For a marker that merely TOUCHES such a principle, every option you list must be
563
+ compatible with it, and the most compliant option must be FIRST — an unattended
564
+ run takes the first one.
531
565
  schema: { clarifications: [ { id: c1, question: "<the question>", options: ["<answer A>", "<answer B>", "<answer C>"] } ] }
532
566
  mock: { clarifications: [] }
533
567
  outputs:
@@ -545,6 +579,72 @@ workflow:
545
579
  options_from: "{{c.options}}"
546
580
  answer_key: "{{c.id}}"
547
581
  answer_var: c_answer
582
+
583
+ # THE ANSWER IS CHECKED BEFORE IT IS WRITTEN IN. Placed between the ask and the
584
+ # rewrite on purpose: once resolve_clar has folded the answer into a requirement it
585
+ # is ordinary spec prose, and catching it then means unpicking a document rather
586
+ # than declining an answer. Judged one answer at a time so the stop can quote the
587
+ # question that caused it.
588
+ #
589
+ # The agent judges ONLY against the principles nn_principles found — a citation
590
+ # outside that list is visibly wrong, which is the point of enumerating them with a
591
+ # script instead of asking one agent to both find the principle and object to it.
592
+ #
593
+ # ASKED IN THE POSITIVE, AND RESET FIRST. The gate below reads nn_compliant, so an
594
+ # answer the judge never delivered — a malformed reply, a step that did not run —
595
+ # leaves "false" standing and STOPS. Asking "does it contradict?" would invert that:
596
+ # the same silence would read as no contradiction found. The reset is what makes it
597
+ # per-iteration; vars outlive a loop pass, so without it the second clarification
598
+ # would inherit the first one's verdict.
599
+ - id: governance_reset
600
+ action: set
601
+ vars:
602
+ nn_compliant: "false"
603
+ nn_principle: ""
604
+ nn_why: ""
605
+
606
+ # Skipped where a project ratified no NON-NEGOTIABLE principle: with nothing to
607
+ # depart from there is nothing to ask, and the gate is skipped on the same
608
+ # condition rather than reading the "false" this reset just wrote.
609
+ - id: governance_check
610
+ condition: "{{nn_count}} != 0"
611
+ action: agent_query
612
+ context: ["{{quality_target}}"]
613
+ include_standards: false
614
+ task: |
615
+ The constitution halves in context ratify these NON-NEGOTIABLE principles:
616
+ {{nn_titles}}
617
+ A question was raised about this specification and has just been answered:
618
+ Question: {{c.question}}
619
+ Answer: {{c_answer}}
620
+ Is that answer COMPLIANT with those principles — that is, can the specification
621
+ be amended to say it without asserting something a principle forbids or omitting
622
+ something a principle requires?
623
+ Judge only against the principles listed above, and when it is not compliant
624
+ cite the one it departs from by its id. A principle the question does not engage
625
+ is not a departure, and neither is an answer a principle simply does not speak
626
+ to; where none is engaged the answer is compliant. Report a departure only where
627
+ you can state what the principle requires and what the answer does instead.
628
+ schema: { compliant: true, principle: "<id, or empty>", why: "<one sentence, or empty>" }
629
+ mock: { compliant: true, principle: "", why: "" }
630
+ outputs:
631
+ - { name: nn_compliant, from: "$.compliant" }
632
+ - { name: nn_principle, from: "$.principle" }
633
+ - { name: nn_why, from: "$.why" }
634
+
635
+ # FAIL CLOSED. The constitution's own Governance says a deviation with no named
636
+ # approver is a defect, not a decision — and this answer has no approver at all: it
637
+ # was settled in the run, by an operator picking from a menu or by a non-interactive
638
+ # default. Recording it and carrying on is how an unapproved departure from a
639
+ # NON-NEGOTIABLE principle reaches code, so the run stops here and the choice goes
640
+ # back to a human who can either comply or amend the constitution properly.
641
+ - id: governance_gate
642
+ condition: "{{nn_count}} != 0"
643
+ action: verify_context
644
+ require_true: nn_compliant
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."
647
+
548
648
  - id: resolve_clar
549
649
  action: agent_author
550
650
  context: ["{{quality_target}}"]
@@ -670,5 +770,5 @@ outputs:
670
770
  authored_document: "{{quality_target}}" # spec.md (bootstrap) | delta-spec.md (delta)
671
771
  checklist_file: "{{checklist_file}}"
672
772
  checklist_quality: "{{checklist_passed}}/{{checklist_total}} items passing"
673
- note: "Behaviour and structure were authored together: the spec says what the system must do, {{domain_model}} says what the domain IS. The aggregate root, its entities and its invariants (BR-n) live in the model and are cited from the spec by id — never restated there, because several specs act on one aggregate and a per-spec copy drifts. On the DELTA path neither master was touched: this run wrote changes/{{cr_id}}/delta-spec.md and {{delta_model}}, and that model delta sits BESIDE the master where every other open delta can see it while being authored. WAVE {{wave}} is its parallel-development level — deltas sharing a wave are independent, a higher wave is implemented only after the lower ones land; `cr-wave --cr-id {{cr_id}} --wave <n>` moves it. {{open_count}} change(s) are open on this feature."
773
+ note: "Behaviour and structure were authored together: the spec says what the system must do, {{domain_model}} says what the domain IS. The aggregate root, its entities and its invariants (BR-n) live in the model and are cited from the spec by id — never restated there, because several specs act on one aggregate and a per-spec copy drifts. On the DELTA path neither master was touched: this run wrote changes/{{cr_id}}/delta-spec.md and {{delta_model}}, and that model delta sits BESIDE the master where every other open delta can see it while being authored. WAVE {{wave}} is its parallel-development level — deltas sharing a wave are independent, a higher wave is implemented only after the lower ones land. The wave is DERIVED from the model delta's `## Depends on`: one above the highest delta named there, stamped into both halves as soon as it was authored ({{wave_message}}). To move it, correct that section and re-run `.appweaver/scripts/bash/aw-spec-actions.sh cr-wave-derive --cr-id {{cr_id}} --stamp`; finalize-delta refuses a change whose wave contradicts it. {{open_count}} change(s) are open on this feature."
674
774
  next: "/appweaver plan — BOOTSTRAP authors backend-plan.md from the spec + the domain model; a DELTA (this run opened change {{cr_id}}, wave {{wave}}) authors the delta plans for it. A change then flows: analyze-plan-delta → implement → build → analyze-slice-delta → finalize-delta → analyze-slice → close-delta (finalize-delta merges the delta into spec.md + domain-model.md — not the lane plans, which are derived from those two — but leaves the change open; analyze-slice verifies the slice against the merged master; close-delta archives the change and bumps the version). ⚠️ With more than one change open, plan, analyze-plan-delta and implement ASK which one they are for (pass the id as the argument to skip the prompt) and activate the answer; finalize-delta and close-delta still act on whichever change is active — so finish or close the others deliberately."
@@ -0,0 +1,141 @@
1
+ # =============================================================================
2
+ # aw-spec-ui-model (orchestrator) — Derive UI Model from Workflow Model
3
+ #
4
+ # Stage 3 of the 4-Stage Spec-Driven UI Generation Pipeline.
5
+ # Consumes `specs/<Feature>/ui/workflow-model.md` and maps workflows into concrete UI surfaces
6
+ # (Pages, Sub-Pages, Drawers, Modals, Dialogs), interaction states (Loading, Empty, Populated, Filtered, Error, Success),
7
+ # user actions, and surface transitions.
8
+ # Writes output artifact to `specs/<Feature>/ui/ui-model.md`.
9
+ #
10
+ # Launch:
11
+ # /appweaver ui-model [strategy] [guidance]
12
+ # =============================================================================
13
+
14
+ name: aw-spec-ui-model
15
+ description: Derive the UI Model (specs/<Feature>/ui/ui-model.md) from workflow-model.md, specifying UI surfaces (pages, dialogs, drawers, modals), routes, interaction states, actions, and navigation transitions.
16
+ version: 1.0.0
17
+ agent_agnostic: true
18
+ dialect: aw-spec.orchestrator/v1
19
+
20
+ inputs:
21
+ user_input:
22
+ description: Optional UI layout or surface guidance.
23
+ required: false
24
+ bind: USER_INPUT
25
+
26
+ scripts:
27
+ actions:
28
+ sh: .appweaver/scripts/bash/aw-spec-actions.sh
29
+ ps1: .appweaver/scripts/powershell/aw-spec-actions.ps1
30
+
31
+ vars:
32
+ feature: ""
33
+ feature_dir: ""
34
+ feature_slug: ""
35
+ spec_file: ""
36
+ spec_exists: "false"
37
+ workflow_model_file: ""
38
+ workflow_model_exists: "false"
39
+ active_change: ""
40
+ has_change: "false"
41
+ domain: ""
42
+ prototype_root: ""
43
+
44
+ workflow:
45
+
46
+ # 1. Resolve active feature and paths
47
+ - id: feature_paths
48
+ action: execute_script
49
+ on_error: continue
50
+ script: { ref: actions }
51
+ subcommand: feature-paths
52
+ outputs:
53
+ - { name: feature, from: "$.feature" }
54
+ - { name: feature_dir, from: "$.feature_dir" }
55
+ - { name: feature_slug, from: "$.feature_slug" }
56
+ - { name: spec_file, from: "$.spec_file" }
57
+ - { name: spec_exists, from: "$.spec_exists" }
58
+ - { name: domain, from: "$.domain" }
59
+ - { name: prototype_root, from: "$.prototype_root" }
60
+
61
+ - id: require_feature
62
+ action: verify_context
63
+ require_non_empty: feature_dir
64
+ on_empty: stop
65
+ on_empty_message: "No active feature selected. Run /appweaver select-feature first."
66
+
67
+ - id: require_spec
68
+ action: verify_context
69
+ require_true: spec_exists
70
+ on_false: stop
71
+ on_false_message: "No specification found for this feature. Run /appweaver specify first."
72
+
73
+ # 2. Check workflow-model.md existence
74
+ - id: check_workflow_model
75
+ action: execute_script
76
+ on_error: continue
77
+ script: { ref: actions }
78
+ subcommand: path-exists
79
+ args: ["--path", "{{feature_dir}}/ui/workflow-model.md"]
80
+ outputs:
81
+ - { name: workflow_model_exists, from: "$.exists" }
82
+
83
+ - id: require_workflow_model
84
+ action: verify_context
85
+ require_true: workflow_model_exists
86
+ on_false: stop
87
+ on_false_message: "No Workflow Model found at specs/{{feature}}/ui/workflow-model.md. Run /appweaver workflow-model first."
88
+
89
+ # 3. Assemble workflow model & template context
90
+ - id: ctx_ui_model
91
+ action: build_context
92
+ vars:
93
+ feature_name: "{{feature}}"
94
+ feature_dir: "{{feature_dir}}"
95
+ user_preferences: "{{USER_INPUT}}"
96
+ context:
97
+ - "{{feature_dir}}/ui/workflow-model.md"
98
+ - "{{spec_file}}"
99
+ - { path: ".appweaver/templates/ui-model-template.md", optional: true }
100
+ - { path: ".appweaver/memory/constitution.ui.md", optional: true }
101
+
102
+ # 4. Generate UI Model Artifact (specs/<Feature>/ui/ui-model.md)
103
+ - id: generate_ui_model
104
+ action: agent_generate
105
+ tools: "Read,Write,Edit,Bash"
106
+ permission_mode: acceptEdits
107
+ task: |
108
+ You are authoring Stage 3 (UI Model) of the 4-Stage Spec-Driven UI Pipeline for feature `{{feature_name}}`.
109
+
110
+ Target Artifact Path: `{{feature_dir}}/ui/ui-model.md`
111
+
112
+ Instructions:
113
+ 1. Read `specs/{{feature}}/ui/workflow-model.md` and `spec.md` in context.
114
+ 2. Translate EVERY workflow (WF-1, WF-2, ...) into explicit **UI Surfaces**:
115
+ - Determine the appropriate UI surface type for each workflow step: `Page`, `Sub-Page`, `Dialog`, `Drawer`, `Modal`, `View`.
116
+ - Map routes or component mount paths (e.g. `/{{feature_slug}}`, `/{{feature_slug}}/create`, modal dialog triggers).
117
+ - Define supported interaction states per surface (`Loading`, `Empty`, `Populated`, `Filtered / Searched`, `Validation Error`, `Submitting`, `Success`, `Error`).
118
+ - Specify interactive controls, form fields, action buttons, and table/card layouts.
119
+ - Map explicit navigation & transition triggers (`On Action -> Opens Surface X`).
120
+ 3. Remember the core rule: **Workflow != Single Screen**. A workflow can span multiple surfaces or reuse surfaces with state changes.
121
+ 4. Format output according to `.appweaver/templates/ui-model-template.md`.
122
+ 5. Write the authored markdown content to `{{feature_dir}}/ui/ui-model.md`.
123
+
124
+ User Guidance:
125
+ {{user_preferences}}
126
+
127
+ End your final reply with a summary and a ```json fence:
128
+ ```json
129
+ {
130
+ "status": "ok",
131
+ "feature": "{{feature_name}}",
132
+ "ui_model_path": "{{feature_dir}}/ui/ui-model.md"
133
+ }
134
+ ```
135
+ outputs:
136
+ - { name: result, from: "$" }
137
+
138
+ outputs:
139
+ feature: "{{feature}}"
140
+ ui_model_path: "{{feature_dir}}/ui/ui-model.md"
141
+ next: "Run /appweaver ui-prototype to generate the executable Next.js prototype application."