aw-spec-cli 0.1.19 → 0.1.21

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 (75) hide show
  1. package/assets/.appweaver/DESIGN-FORMAT.md +17 -9
  2. package/assets/.appweaver/ORCH-DIALECT.md +8 -3
  3. package/assets/.appweaver/orchestrator/modules/delta-spec.orch.yaml +26 -3
  4. package/assets/.appweaver/orchestrator/orchestrator.py +65 -3
  5. package/assets/.appweaver/orchestrator/rev-templates/delta-design-template.skeleton.md +1 -1
  6. package/assets/.appweaver/orchestrator/rev-templates/design-template.skeleton.md +4 -2
  7. package/assets/.appweaver/orchestrator/rev-templates/ui-delta-design-template.skeleton.md +1 -1
  8. package/assets/.appweaver/orchestrator/rev-templates/ui-design-template.skeleton.md +1 -1
  9. package/assets/.appweaver/orchestrator/workflows/aw-spec-adopt.orch.yaml +121 -42
  10. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan-delta.orch.yaml +7 -7
  11. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan.orch.yaml +145 -52
  12. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice-delta.orch.yaml +18 -10
  13. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice.orch.yaml +306 -28
  14. package/assets/.appweaver/orchestrator/workflows/aw-spec-build.orch.yaml +1 -1
  15. package/assets/.appweaver/orchestrator/workflows/aw-spec-characterize.orch.yaml +1 -1
  16. package/assets/.appweaver/orchestrator/workflows/aw-spec-close-delta.orch.yaml +1 -1
  17. package/assets/.appweaver/orchestrator/workflows/aw-spec-constitution.orch.yaml +4 -4
  18. package/assets/.appweaver/orchestrator/workflows/aw-spec-deploy.orch.yaml +1 -1
  19. package/assets/.appweaver/orchestrator/workflows/aw-spec-finalize-delta.orch.yaml +114 -30
  20. package/assets/.appweaver/orchestrator/workflows/aw-spec-foundation.orch.yaml +140 -36
  21. package/assets/.appweaver/orchestrator/workflows/aw-spec-implement.orch.yaml +385 -61
  22. package/assets/.appweaver/orchestrator/workflows/aw-spec-inventory.orch.yaml +217 -174
  23. package/assets/.appweaver/orchestrator/workflows/aw-spec-onboard.orch.yaml +407 -107
  24. package/assets/.appweaver/orchestrator/workflows/aw-spec-parity.orch.yaml +1 -1
  25. package/assets/.appweaver/orchestrator/workflows/aw-spec-plan.orch.yaml +49 -11
  26. package/assets/.appweaver/orchestrator/workflows/aw-spec-recall-delta.orch.yaml +1 -1
  27. package/assets/.appweaver/orchestrator/workflows/aw-spec-run.orch.yaml +1 -1
  28. package/assets/.appweaver/orchestrator/workflows/aw-spec-select-feature.orch.yaml +29 -23
  29. package/assets/.appweaver/orchestrator/workflows/aw-spec-select-stack.orch.yaml +47 -23
  30. package/assets/.appweaver/orchestrator/workflows/aw-spec-specify.orch.yaml +19 -11
  31. package/assets/.appweaver/scripts/bash/aw-inventory.sh +15 -0
  32. package/assets/.appweaver/scripts/bash/aw-spec-actions.sh +683 -92
  33. package/assets/.appweaver/scripts/bash/aw-xref.sh +3 -0
  34. package/assets/.appweaver/scripts/powershell/aw-inventory.ps1 +15 -0
  35. package/assets/.appweaver/scripts/powershell/aw-spec-actions.ps1 +625 -64
  36. package/assets/.appweaver/scripts/powershell/aw-xref.ps1 +3 -0
  37. package/assets/.appweaver/scripts/python/aw-crstate.py +194 -0
  38. package/assets/.appweaver/scripts/python/aw-foundation.py +151 -0
  39. package/assets/.appweaver/scripts/python/aw-inventory.py +1829 -63
  40. package/assets/.appweaver/scripts/python/aw-payload-parity.py +3 -0
  41. package/assets/.appweaver/scripts/python/aw-plan-coverage.py +423 -0
  42. package/assets/.appweaver/scripts/python/aw-stack.py +251 -28
  43. package/assets/.appweaver/scripts/python/aw-testlog.py +148 -0
  44. package/assets/.appweaver/scripts/python/aw-xref.py +500 -81
  45. package/assets/.appweaver/stack-profiles/dotnet-webapi.json +1 -0
  46. package/assets/.appweaver/stack-profiles/java-spring.json +1 -0
  47. package/assets/.appweaver/stack-profiles/node-express.json +1 -0
  48. package/assets/.appweaver/stack-profiles/python-fastapi.json +1 -0
  49. package/assets/.appweaver/templates/constitution-domain-template.md +17 -2
  50. package/assets/.appweaver/templates/delta-design-template.md +1 -1
  51. package/assets/.appweaver/templates/delta-domain-model-template.md +1 -1
  52. package/assets/.appweaver/templates/delta-spec-template.md +4 -3
  53. package/assets/.appweaver/templates/design-template.md +16 -11
  54. package/assets/.appweaver/templates/okf-concept-template.md +11 -1
  55. package/assets/.appweaver/templates/proposed-inventory-template.md +127 -0
  56. package/assets/.appweaver/templates/source-map-template.md +5 -3
  57. package/assets/.appweaver/templates/spec-template.md +17 -17
  58. package/assets/.appweaver/templates/ui-delta-design-template.md +1 -1
  59. package/assets/.appweaver/templates/ui-design-template.md +4 -4
  60. package/assets/foundations/dotnet/ModularMonolith/ModularMonolith.pack.txt +4489 -361
  61. package/assets/foundations/dotnet/VerticalSlice/VerticalSlice.pack.txt +2274 -125
  62. package/assets/guardrails/projects/dotnet-default-project/domain-rule-bindings.md +10 -6
  63. package/assets/guardrails/projects/dotnet-default-project/tech-rule-bindings.md +4 -0
  64. package/assets/guardrails/technology/dotnet-webapi/auditing.md +3 -1
  65. package/assets/guardrails/technology/dotnet-webapi/coding-standards.md +5 -3
  66. package/assets/guardrails/technology/dotnet-webapi/cqrs.md +5 -1
  67. package/assets/guardrails/technology/dotnet-webapi/domain-driven-design.md +13 -5
  68. package/assets/guardrails/technology/dotnet-webapi/examples/cqrs.md +2 -0
  69. package/assets/guardrails/technology/dotnet-webapi/examples/domain-driven-design.md +6 -2
  70. package/assets/guardrails/technology/dotnet-webapi/framework-api-map.md +70 -0
  71. package/assets/guardrails/technology/dotnet-webapi/technology-constitution.md +3 -0
  72. package/assets/guardrails/technology/dotnet-webapi/testing.md +10 -0
  73. package/assets/guardrails/technology/nextjs/ui-constitution.md +8 -1
  74. package/package.json +1 -1
  75. package/assets/.appweaver/orchestrator/.state/smoke.state.json +0 -32
@@ -40,10 +40,10 @@
40
40
  # drift. What this stage must NOT do is hide that by merging a delta plan: the plan would
41
41
  # then agree with the spec without any run of `plan` ever having derived it.
42
42
  #
43
- # ✅ THE DETECTION IS BUILT (2026-09-14). analyze-plan leaves `.aw-plan-gate.json` when it
44
- # PASSES — the sha256 of spec.md, the context's domain-model.md and each lane plan it read —
45
- # and `implement` reads it through plan-gate-check before any lane generates, refusing a
46
- # master run whose plans are no longer the ones the gate passed. So after this merge the
43
+ # ✅ THE DETECTION IS BUILT (2026-09-14). Every master-mode analyze-plan run leaves
44
+ # `.aw-plan-gate.json` — the sha256 of spec.md, the context's domain-model.md and each lane
45
+ # plan it read, and its verdict — and `implement` reads it through plan-gate-check before any
46
+ # lane generates, refusing a master run whose plans are no longer the ones the gate checked. So after this merge the
47
47
  # plans are not merely "visibly behind": the next `implement` on the master path says so and
48
48
  # stops, naming `plan main`. A feature with no receipt has never been gated since the receipt
49
49
  # existed and is reported, never refused.
@@ -94,17 +94,33 @@
94
94
  # REBASE. The delta carries the master version it was authored against (`**Base version**:`,
95
95
  # stamped by cr-setup). If the master has moved, the delta's MODIFIED items name a BEFORE
96
96
  # spec.md no longer says, and merging them is last-writer-wins over work that landed.
97
+ # cr-rebase runs FIRST and re-stamps that header — and `**Base context version**` — when
98
+ # the move is one this delta survives: the masters moved, and everything that landed in
99
+ # between touched OTHER aggregates than the ones this delta names. Nothing else in the kit
100
+ # could ever write those headers, so before this the only way past the refusal was a hand
101
+ # edit the authoring prompts forbid. Where a landed change DOES share an aggregate, the
102
+ # stamp is left alone and the refusal below names it.
97
103
  #
98
104
  # MERGED-NOT-CLOSED. The masters can move WITHOUT the version moving, because this stage
99
- # merges and close-delta bumps. In that window spec.md carries a change the number does not
100
- # reflect, so a sibling sharing the same base_version compares EQUAL and would merge onto
101
- # masters that had already shifted. The version alone is not a proxy for "the masters
102
- # moved"; the id appearing in spec.md is the other half of the answer.
105
+ # merges and close-delta bumps. In that window the masters carry a change the number does
106
+ # not reflect, so a sibling sharing the same base_version compares EQUAL and would merge
107
+ # onto masters that had already shifted. The version alone is not a proxy for "the masters
108
+ # moved"; a Change History row naming the change is the other half of the answer. That test
109
+ # lives in aw-crstate — one definition for this fence, cr-recall-check and close-delta —
110
+ # and it reads the CR CELL of a row, not the id anywhere in the file, which used to fire on
111
+ # an id quoted in prose. It looks at the context model too, which every feature shares, so
112
+ # a SIBLING feature's merged-unclosed change is visible; a sibling counts only where it
113
+ # touched an aggregate this delta also names.
103
114
  #
104
115
  # PREDECESSOR. A lower wave is committed to landing first, and `plan` put exactly those
105
- # deltas in this one's context as the base it was planned on top of. `depends_on` is
106
- # stamped nowhere, so the wave is the only dependency signal that exists — and it is the
107
- # conservative one, since same-wave changes are independent by definition.
116
+ # deltas in this one's context as the base it was planned on top of. Same-wave changes are
117
+ # independent by definition, so the wave is the conservative signal.
118
+ #
119
+ # DECLARED DEPENDENCIES vs THE WAVE. The wave is a number; `## Depends on` in the change's
120
+ # own delta documents is what that number is supposed to encode. specify derives the wave
121
+ # from that section, and this stage re-checks it (cr-wave-derive): a change naming a
122
+ # predecessor at the same or a higher wave contradicts itself, and the predecessor fence
123
+ # above — which sees only numbers — would let the two merge in either order.
108
124
  #
109
125
  # All three are RAISE-then-VERIFY and fail-closed: each flag is raised only by the good
110
126
  # state, so a check that did not run leaves the merge refused rather than waved through.
@@ -153,6 +169,17 @@ vars:
153
169
  downstream_csv: "" # the blast-radius names, fed to the scoped fence's --also and shown to the human
154
170
  fence_ok: "true" # default green so the gate is a no-op when nothing downstream
155
171
  gate_decision: "clean" # audit: 'clean' unless a RED fence was deliberately overridden
172
+ # The change's own `## Depends on` vs its wave — the fence that keeps the number and the
173
+ # prose from drifting apart. Defaults green, so a run that cannot read them is not blocked.
174
+ wave_ok: "true"
175
+ wave_required: ""
176
+ wave_message: ""
177
+ # The automatic rebase (cr-rebase). Defaults say "nothing moved, nothing stamped",
178
+ # so a step that could not run leaves the existing refusal to speak.
179
+ rebase_clean: "false"
180
+ rebase_stamped: ""
181
+ rebase_conflicts: ""
182
+ rebase_message: ""
156
183
  domain_model: "" # specs/<Context>/domain-model.md — the second merge target
157
184
  domain_model_exists: "false"
158
185
  delta_model: "" # specs/<Context>/domain-model.<id>.delta.md — the model half of the change
@@ -176,11 +203,12 @@ vars:
176
203
  merge_wave: "1"
177
204
  snapshot_dir: "" # changes/<id>/pre-merge/ — the before-image recall-delta restores
178
205
  snapshot_count: "0"
179
- # THE OKF OVERVIEW'S SUBJECT. The kit's working assumption is feature == aggregate; the
180
- # Overview step is the one place it is load-bearing rather than cosmetic, because it
181
- # selects a `## Aggregate:` section BY NAME. delta_aggregates is what the change's own
182
- # model delta declares, so the common case is exact rather than assumed; it is empty for
183
- # a behaviour-only change, which falls back to the feature name.
206
+ # THE OKF OVERVIEW'S SUBJECT. The Overview step selects a `## Aggregate:` section BY
207
+ # NAME, so it needs the aggregate, which is not always the feature: a feature that is
208
+ # one capability over a shared aggregate (PatientRegistration over Patient) has no
209
+ # section of its own name. delta_aggregates is what the change's own model delta
210
+ # declares; it is empty for a behaviour-only change, which falls back to the aggregate
211
+ # the feature answers for (feature-aggregates — the spec's Acts-on line).
184
212
  delta_aggregates: ""
185
213
  overview_scope: ""
186
214
  overview_section_ok: "false"
@@ -211,7 +239,7 @@ workflow:
211
239
  action: verify_context
212
240
  require_non_empty: feature_dir
213
241
  on_empty: stop
214
- on_empty_message: "No active feature. Run /appweaver select-feature first (pick or create the aggregate, e.g. Order), then re-run finalize-delta."
242
+ on_empty_message: "No active feature. Run /appweaver select-feature first (pick or create the feature, e.g. Order or PatientRegistration), then re-run finalize-delta."
215
243
 
216
244
  - id: verify_spec
217
245
  action: verify_context
@@ -351,6 +379,28 @@ workflow:
351
379
  # NO on_error: continue. A gate that cannot run must not be a gate that passes, and the
352
380
  # raise-then-verify below is fail-closed for the same reason: each flag is raised ONLY
353
381
  # by the good state, so a field that never arrived leaves it down and the merge refuses.
382
+ # AUTO-REBASE FIRST, and only where it is EARNED. `**Base version**` was written once by
383
+ # cr-setup and nothing could ever update it — every other site reads it, and every
384
+ # authoring prompt forbids the model from writing it — so a moved master left the operator
385
+ # hand-editing the delta-spec in the middle of an automated loop. cr-rebase re-stamps it
386
+ # (and `**Base context version**`, which until now nothing read at all) when the masters
387
+ # moved and NOTHING THAT LANDED IN BETWEEN touched the aggregates this delta names: then
388
+ # the delta still describes the document it was written against, and the stamp is
389
+ # bookkeeping. A landed change sharing an aggregate, or one whose model delta cannot be
390
+ # read, leaves the stamp alone — and the refusal below fires with the overlap named.
391
+ # It runs BEFORE cr-merge-check so that check reads the stamp this step just settled.
392
+ - id: auto_rebase
393
+ action: execute_script
394
+ on_error: continue
395
+ script: { ref: actions }
396
+ subcommand: cr-rebase
397
+ args: ["--cr-id", "{{active_change}}", "--feature-dir", "{{feature_dir}}", "--stamp"]
398
+ outputs:
399
+ - { name: rebase_clean, from: "$.clean" }
400
+ - { name: rebase_stamped, from: "$.stamped" }
401
+ - { name: rebase_conflicts, from: "$.conflict_ids" }
402
+ - { name: rebase_message, from: "$.message" }
403
+
354
404
  - id: merge_check
355
405
  action: execute_script
356
406
  script: { ref: actions }
@@ -375,7 +425,7 @@ workflow:
375
425
  action: verify_context
376
426
  require_true: rebase_ok
377
427
  on_false: stop
378
- on_false_message: "finalize-delta REFUSED — change {{active_change}} needs a REBASE. It was authored against master version {{base_version}}, and the feature now stands at {{current_version}}: another change finalized and closed while this one was open. Its MODIFIED items name a BEFORE that spec.md no longer says, so merging now would overwrite work that already landed. Rebase: run /appweaver analyze-plan-delta {{active_change}} against the current master, fix what it reports, re-stamp the delta's `**Base version**:` to {{current_version}}, then re-run /appweaver finalize-delta {{active_change}}."
428
+ on_false_message: "finalize-delta REFUSED — change {{active_change}} needs a REBASE. It was authored against master version {{base_version}}, and the feature now stands at {{current_version}}: another change finalized and closed while this one was open. Its MODIFIED items name a BEFORE that spec.md no longer says, so merging now would overwrite work that already landed. The automatic re-stamp did not apply, because what landed in between touches this delta's own aggregates: {{rebase_message}}. Rebase for real: run /appweaver analyze-plan-delta {{active_change}} against the current master, fix what it reports — then `.appweaver/scripts/bash/aw-spec-actions.sh cr-rebase --cr-id {{active_change}} --stamp` re-stamps `**Base version**` and `**Base context version**` once the overlap with {{rebase_conflicts}} is resolved, and /appweaver finalize-delta {{active_change}} can run again."
379
429
 
380
430
  - id: merged_open_ok_raise
381
431
  condition: "{{has_merged_open}} == false"
@@ -385,7 +435,7 @@ workflow:
385
435
  action: verify_context
386
436
  require_true: merged_open_ok
387
437
  on_false: stop
388
- on_false_message: "finalize-delta REFUSED — {{merged_open_ids}} has already been MERGED into spec.md but not yet CLOSED, so the masters carry it while the version still reads {{current_version}}. Change {{active_change}} was authored against those pre-merge masters and the version comparison cannot see the difference. Finish the merged change first: /appweaver analyze-slice then /appweaver close-delta {{merged_open_ids}}, which bumps the version — then rebase this one against the new number and re-run /appweaver finalize-delta {{active_change}}."
438
+ on_false_message: "finalize-delta REFUSED — {{merged_open_ids}} has already been MERGED into a master but not yet CLOSED, so the masters carry it while the version still reads {{current_version}}. That change may belong to ANOTHER feature in this context: domain-model.md is shared, so a sibling's merge moves a master this delta was authored against, and it is counted here when it touched an aggregate this delta also names. Change {{active_change}} was authored against those pre-merge masters and the version comparison cannot see the difference. Finish the merged change first: /appweaver analyze-slice then /appweaver close-delta {{merged_open_ids}}, which bumps the version — then rebase this one against the new number and re-run /appweaver finalize-delta {{active_change}}."
389
439
 
390
440
  - id: no_blockers_raise
391
441
  condition: "{{has_blockers}} == false"
@@ -395,7 +445,28 @@ workflow:
395
445
  action: verify_context
396
446
  require_true: no_blockers
397
447
  on_false: stop
398
- on_false_message: "finalize-delta REFUSED — change {{active_change}} is at wave {{merge_wave}} and {{blocker_count}} open change(s) sit at a LOWER wave: {{blocker_ids}}. A lower wave is committed to landing FIRST, and this delta was planned with those in its context as the base it builds on — merging it now folds in behaviour that assumes a predecessor the masters do not yet carry. Merge and close the lower-wave change(s) first (/appweaver finalize-delta <CR-id> → analyze-slice → close-delta), then rebase and re-run this one. Same-wave changes are independent and are not counted here; if this delta does NOT in fact build on them, /appweaver cr-wave can correct its wave."
448
+ on_false_message: "finalize-delta REFUSED — change {{active_change}} is at wave {{merge_wave}} and {{blocker_count}} open change(s) sit at a LOWER wave: {{blocker_ids}}. A lower wave is committed to landing FIRST, and this delta was planned with those in its context as the base it builds on — merging it now folds in behaviour that assumes a predecessor the masters do not yet carry. Merge and close the lower-wave change(s) first (/appweaver finalize-delta <CR-id> → analyze-slice → close-delta), then rebase and re-run this one. Same-wave changes are independent and are not counted here; if this delta does NOT in fact build on them, correct its `## Depends on` section (naming only the deltas it truly builds on) and re-run /appweaver specify's wave derivation — `.appweaver/scripts/bash/aw-spec-actions.sh cr-wave-derive --cr-id {{active_change}} --stamp` applies it directly."
449
+
450
+ # THE DECLARED DEPENDENCIES AND THE WAVE MUST AGREE. The fence above orders changes by
451
+ # their wave NUMBER; this one checks that number against what the change's own documents
452
+ # SAY under `## Depends on`. Before this, the two could diverge silently: an author named
453
+ # a predecessor in prose, cr-setup left the wave at the predecessor's own level, and the
454
+ # fence above then read the two as independent and let them merge in either order.
455
+ - id: wave_declared_check
456
+ action: execute_script
457
+ on_error: continue
458
+ script: { ref: actions }
459
+ subcommand: cr-wave-derive
460
+ args: ["--cr-id", "{{active_change}}"]
461
+ outputs:
462
+ - { name: wave_ok, from: "$.ok" }
463
+ - { name: wave_required, from: "$.required_wave" }
464
+ - { name: wave_message, from: "$.message" }
465
+ - id: require_wave_matches_depends
466
+ action: verify_context
467
+ require_true: wave_ok
468
+ on_false: stop
469
+ on_false_message: "finalize-delta REFUSED — change {{active_change}} contradicts its own `## Depends on`: {{wave_message}}. The blocker fence above compares wave NUMBERS, so it would let the two merge in either order and this delta could land before the change it says it builds on. Either raise this change to wave {{wave_required}} (`.appweaver/scripts/bash/aw-spec-actions.sh cr-wave-derive --cr-id {{active_change}} --stamp`, which reads the same section), or correct `## Depends on` in its delta documents if it does not in fact build on them."
399
470
 
400
471
  # 1c. DOWNSTREAM GATE (hard, with a REVIEWED override). Who depends on this aggregate —
401
472
  # its OKF Cross-References, TRANSITIVELY (C→B→A all count) and placeholder-filtered
@@ -700,16 +771,15 @@ workflow:
700
771
  # the context has no model: there would be nothing in context to distil, and a
701
772
  # query with no source invents one. Best-effort; an empty answer is a no-op.
702
773
  # WHICH AGGREGATE SECTION(S) THIS CHANGE ACTUALLY TOUCHED, read off the model delta's
703
- # own `## Aggregate:` headings rather than assumed from the feature name. The kit's
704
- # working assumption is feature == aggregate, and it holds today — but the Overview step
705
- # below is the one place that assumption is LOAD-BEARING rather than cosmetic: it selects
706
- # a section by name, and a feature whose name is not an aggregate name selects nothing,
707
- # producing an empty answer that the writer then declines. Driving it off the delta's
708
- # declared aggregates makes the common case exact and the uncommon case visible.
774
+ # own `**Aggregates touched**:` header rather than assumed from the feature name. The Overview
775
+ # step below selects a section by name, and a feature whose name is not an aggregate
776
+ # name — one capability over a shared aggregate — selects nothing, producing an empty
777
+ # answer that the writer then declines. Driving it off the delta's declared aggregates
778
+ # makes a model change exact.
709
779
  #
710
780
  # A BEHAVIOUR-ONLY CHANGE HAS NO MODEL DELTA, so this reports none and the step falls
711
- # back to the feature name — which is correct, not a degradation: there is no model
712
- # change to distil and the concept's existing Overview stands.
781
+ # back to the aggregate the feature answers for — which is correct, not a degradation:
782
+ # there is no model change to distil and the concept's existing Overview stands.
713
783
  - id: cr_aggregates
714
784
  action: execute_script
715
785
  on_error: continue # informational — a missing model delta is the ordinary case
@@ -720,9 +790,19 @@ workflow:
720
790
  - { name: delta_aggregates, from: "$.aggregates_csv" }
721
791
  - { name: delta_aggregate_list, from: "$.aggregates" }
722
792
 
793
+ # The fallback subject: the aggregate this feature answers for, which is the feature
794
+ # itself on a 1:1 slice and the shared aggregate on a capability over one.
795
+ - id: feature_aggregates
796
+ action: execute_script
797
+ script: { ref: xref }
798
+ subcommand: feature-aggregates
799
+ args: ["--feature-dir", "{{feature_dir}}"]
800
+ outputs:
801
+ - { name: owned_csv, from: "$.owned_csv" }
802
+
723
803
  - id: overview_scope_feature
724
804
  action: set
725
- vars: { overview_scope: "{{feature}}" }
805
+ vars: { overview_scope: "{{owned_csv}}" }
726
806
  - id: overview_scope_delta
727
807
  condition: "{{delta_aggregates}} != "
728
808
  action: set
@@ -820,7 +900,11 @@ outputs:
820
900
  # the change by close-delta. What an un-merge would restore from.
821
901
  pre_merge_snapshot: "{{snapshot_dir}}"
822
902
  base_version: "{{base_version}}"
903
+ # What the automatic rebase did before the fence read the stamp — empty when the delta was
904
+ # already current, and the reason it could not when a landed change shared its aggregates.
905
+ rebased: "{{rebase_stamped}}"
906
+ rebase_note: "{{rebase_message}}"
823
907
  model_merged: "{{delta_model_exists}}" # true ⇒ domain-model.md was the second target of this merge.
824
908
  # false = a behaviour-only change: the spec merge was the
825
909
  # whole merge, not a half of one.
826
- next: "Delta MERGED into spec.md, and into the context's domain-model.md one aggregate section at a time where the change carried a model delta (model_merged={{delta_model_exists}} — false means a behaviour-only change, so the spec merge was the whole merge). The lane plans were NOT touched — they are DERIVED from these two masters, so backend-plan.md / frontend-plan.md now stand BEHIND the spec and model at their last authored content. That drift starts here and lasts until /appweaver plan main re-derives them, carrying the recorded decisions (DA-n) and workarounds (WA-n) forward and rewriting what the spec and model no longer support. analyze-plan is not what closes it and cannot repair it while this change is open: with a change open it runs READ-ONLY, reporting the drift into plan-analyze-report.md without rewriting either plan. The full gate — check, repair, stop — comes back once the last change is closed, so the order is close-delta, then plan main, then analyze-plan. OKF edges reconciled ({{xref_edge_count}} dep(s)), {{downstream_count}} downstream slice(s) fenced (gate: {{gate_decision}}). The concept Overview was distilled for aggregate(s) `{{overview_scope}}` — taken from what this change's model delta DECLARES it touched, not assumed from the feature name (empty on a behaviour-only change, which falls back to the feature). section_found={{overview_section_ok}}, written={{overview_written}}{{overview_reason}} — written=false is normal for a behaviour-only change (nothing to distil, the existing Overview stands), but section_found=false means domain-model.md has no `## Aggregate:` section under that name and the concept's Overview was left as it was: check that the feature name matches the aggregate the model defines. Change {{active_change}} is STILL OPEN and the version is still {{version}}. Next: /appweaver analyze-slice — it now reads masters that carry this change, and a red result is still recoverable because nothing has been archived. Then /appweaver close-delta archives the change and bumps the version."
910
+ next: "Delta MERGED into spec.md, and into the context's domain-model.md one aggregate section at a time where the change carried a model delta (model_merged={{delta_model_exists}} — false means a behaviour-only change, so the spec merge was the whole merge). The lane plans were NOT touched — they are DERIVED from these two masters, so backend-plan.md / frontend-plan.md now stand BEHIND the spec and model at their last authored content. That drift starts here and lasts until /appweaver plan main re-derives them, carrying the recorded decisions (DA-n) and workarounds (WA-n) forward and rewriting what the spec and model no longer support. analyze-plan is not what closes it and cannot repair it while this change is open: with a change open it runs READ-ONLY, reporting the drift into plan-analyze-report.md without rewriting either plan. The full gate — check, repair, stop — comes back once the last change is closed, so the order is close-delta, then plan main, then analyze-plan. OKF edges reconciled ({{xref_edge_count}} dep(s)), {{downstream_count}} downstream slice(s) fenced (gate: {{gate_decision}}). The concept Overview was distilled for aggregate(s) `{{overview_scope}}` — taken from what this change's model delta DECLARES it touched, not assumed from the feature name (empty on a behaviour-only change, which falls back to the aggregate the feature answers for). section_found={{overview_section_ok}}, written={{overview_written}}{{overview_reason}} — written=false is normal for a behaviour-only change (nothing to distil, the existing Overview stands), but section_found=false means domain-model.md has no `## Aggregate:` section under that name and the concept's Overview was left as it was: check that the spec's Acts on aggregate(s) line names an aggregate the model defines. Change {{active_change}} is STILL OPEN and the version is still {{version}}. Next: /appweaver analyze-slice — it now reads masters that carry this change, and a red result is still recoverable because nothing has been archived. Then /appweaver close-delta archives the change and bumps the version."
@@ -12,9 +12,26 @@
12
12
  # Interactive confirmations (proceed / overwrite / reference sample) ride the
13
13
  # checkpoint channel under EVERY strategy.
14
14
  #
15
+ # A SCAFFOLD LANDS IN A LANE. The output directory is the SOURCE PATH of the lane being
16
+ # scaffolded, read from .appweaver/stacks.json — never the repository root and never a
17
+ # directory named on the command line. One bundle is one lane's project, so a repo with a
18
+ # service and a UI is scaffolded by running this twice, once per lane, and each run's files
19
+ # land in the subtree that lane owns. That is what makes every later phase's "write only
20
+ # into your lane" rule true of the scaffold as well as of generated code: implement,
21
+ # build, test and run all resolve the same path this unpacked into.
22
+ #
23
+ # The lane's TEST PATH is its second subtree, and for most backend stacks it is a sibling
24
+ # of the source path rather than a directory inside it. A pack carrying its own test
25
+ # project is unpacked like everything else and then has that tree lifted onto the lane's
26
+ # test path — see .appweaver/scripts/python/aw-foundation.py for when that is a no-op.
27
+ #
28
+ # Lanes must therefore be declared FIRST: /appweaver select-stack is the sole writer of
29
+ # stacks.json and the first command of both sequences.
30
+ #
15
31
  # Launch via the parametric skill: /appweaver foundation [strategy]
16
- # Interactive (no args): LISTS the available foundations to pick from, then ASKS
17
- # for the app (project) name — both deterministic, no LLM. Fast path still works:
32
+ # Interactive (no args): ASKS which lane to scaffold, LISTS the foundations available
33
+ # for that lane's ecosystem, then ASKS for the app (project) name — all deterministic,
34
+ # no LLM. Fast path still works:
18
35
  # /appweaver foundation agent VerticalSlice AppWeaver.Health
19
36
  #
20
37
  # Headless:
@@ -25,14 +42,14 @@
25
42
  # =============================================================================
26
43
 
27
44
  name: aw-spec-foundation
28
- description: Scaffold a complete, compiling project — pick from the available foundation bundles and enter the app name, then unpack the verified bundle and rename its sentinel to your app name. Selection is zero-LLM (list + pick + ask).
29
- version: 1.1.0
45
+ description: Scaffold a complete, compiling project INTO A DECLARED LANE — pick the lane, pick from the foundation bundles available for its ecosystem, enter the app name, and the verified bundle is unpacked into that lane's source path with its sentinel renamed. Selection is zero-LLM (list + pick + ask).
46
+ version: 1.2.0
30
47
  agent_agnostic: true
31
48
  dialect: aw-spec.orchestrator/v1
32
49
 
33
50
  inputs:
34
51
  user_input:
35
- description: "Optional fast path: FoundationName [AppName] [OutputDirectory]. Omit to pick a foundation from the list and be asked for the app name."
52
+ description: "Optional fast path: FoundationName [AppName]. Omit to pick a foundation from the list and be asked for the app name. There is no output-directory argument — the scaffold goes into the chosen lane's source path."
36
53
  required: false
37
54
  bind: USER_INPUT
38
55
 
@@ -50,21 +67,83 @@ vars:
50
67
  stash_found: "false" # true only when the reference flow stashed a DbContext
51
68
  app: "" # resolved app name: fast-path 2nd token, else the ask below
52
69
  app_typed: "" # app name entered interactively (empty on the fast path)
70
+ out_ok: "true" # cleared when a third fast-path token names an output directory
53
71
 
54
72
  workflow:
55
73
 
56
- # 1a. LIST the available foundations (deterministic foundations/<ecosystem>/<Name>/<Name>.pack.txt
57
- # scan — no LLM), narrowed to the ecosystems the declared lanes belong to. A
58
- # bundle is a whole compiling project in one ecosystem's build system, so the
59
- # list a Spring repo is shown and the list a .NET repo is shown are different
60
- # lists; `all_foundations` keeps the unfiltered set for the gate message below.
61
- # A direct "Foundation App [out]" argument is parsed here too as an optional
62
- # fast path (preselect_name / preselect_app / out_dir).
74
+ # 0a. THE LANE MODEL, which is where this scaffold is going. Read-only: select-stack is
75
+ # the sole writer, and a scaffold has no business declaring the lane it fills.
76
+ - id: lanes
77
+ action: execute_script
78
+ on_error: continue # never crash raw on an absent stacks.json — the gate explains it
79
+ script: { ref: actions }
80
+ subcommand: lane-resolve
81
+ outputs:
82
+ - { name: lane_options, from: "$.lane_options" }
83
+ - { name: lane_count, from: "$.lane_count" }
84
+
85
+ - id: require_lanes
86
+ action: verify_context
87
+ require_non_empty: lane_options
88
+ on_empty: stop
89
+ on_empty_message: >
90
+ /appweaver foundation cannot run: no lane is declared in .appweaver/stacks.json.
91
+ A scaffold is unpacked into the SOURCE PATH of the lane it belongs to — that is what
92
+ keeps a project's files inside the subtree every later phase compiles, tests and
93
+ generates into — so there has to be a lane to scaffold before there is a scaffold.
94
+ Run /appweaver select-stack first: it declares each lane's stack and source path, and
95
+ is the first command of both the greenfield and the brownfield sequence.
96
+
97
+ # 0b. WHICH lane. One bundle is one lane's project, so a fullstack repo runs this once
98
+ # per lane rather than unpacking a tree that spans both. A single declared lane
99
+ # auto-confirms; the option carries its own explanation and resolves below.
100
+ - id: pick_lane
101
+ action: user_select
102
+ prompt: "Which lane is this scaffold for? The bundle is unpacked into that lane's source path, and its tests into the lane's test path — nothing lands outside them. Scaffold the other lane by running /appweaver foundation again."
103
+ options_from_variable: lane_options
104
+ single_option_confirm: true
105
+ outputs:
106
+ - { name: chosen_lane, from: selection }
107
+
108
+ # 0c. Resolve the chosen lane to the two paths this run writes into, plus the ecosystem
109
+ # that narrows the bundle list. `lane-resolve` strips the option's label itself, so
110
+ # the selection goes back in as the id it already carries.
111
+ - id: lane_target
112
+ action: execute_script
113
+ script: { ref: actions }
114
+ subcommand: lane-resolve
115
+ args: ["--id", "{{chosen_lane}}"]
116
+ outputs:
117
+ - { name: lane_found, from: "$.found" }
118
+ - { name: lane_id, from: "$.lane_id" }
119
+ - { name: lane_root, from: "$.source_path" }
120
+ - { name: lane_test_path, from: "$.test_path" }
121
+ - { name: lane_ecosystem, from: "$.ecosystem" }
122
+ - { name: lane_language, from: "$.language" }
123
+
124
+ - id: lane_resolved
125
+ action: verify_context
126
+ require_true: lane_found
127
+ on_false: stop
128
+ on_false_message: "No lane matches the selection `{{chosen_lane}}` in .appweaver/stacks.json. Re-run /appweaver select-stack to re-declare the lanes, then re-run foundation."
129
+
130
+ - id: lane_has_root
131
+ action: verify_context
132
+ require_non_empty: lane_root
133
+ on_empty: stop
134
+ on_empty_message: "Lane `{{lane_id}}` declares no source path in .appweaver/stacks.json, and the source path is where this scaffold is unpacked. Re-run /appweaver select-stack and give the lane the directory it owns."
135
+
136
+ # 1a. LIST the foundations available for THAT LANE's ecosystem (deterministic
137
+ # foundations/<ecosystem>/<Name>/<Name>.pack.txt scan — no LLM). A bundle is a whole
138
+ # compiling project in one ecosystem's build system, so a Node UI lane is not offered
139
+ # a .NET bundle even in a repo that also declares a .NET service; `all_foundations`
140
+ # keeps the unfiltered set for the gate message below. A direct "Foundation App"
141
+ # argument is parsed here too as an optional fast path.
63
142
  - id: list_foundations
64
143
  action: execute_script
65
144
  script: { ref: actions }
66
145
  subcommand: foundation-list
67
- args: ["--args", "{{USER_INPUT}}"]
146
+ args: ["--args", "{{USER_INPUT}}", "--ecosystem", "{{lane_ecosystem}}"]
68
147
  outputs:
69
148
  - { name: foundations, from: "$.foundations" }
70
149
  - { name: foundation_count, from: "$.count" }
@@ -72,23 +151,41 @@ workflow:
72
151
  - { name: lane_ecosystems, from: "$.lane_ecosystems" }
73
152
  - { name: preselect_name, from: "$.preselect_name" }
74
153
  - { name: preselect_app, from: "$.preselect_app" }
75
- - { name: out_dir, from: "$.out_dir" }
154
+ - { name: rejected_out, from: "$.rejected_out" }
155
+
156
+ # 1b. A third fast-path token used to be an output directory. Refused by name rather
157
+ # than ignored: silently dropping a path argument is how someone believes they
158
+ # scaffolded a sandbox and finds they filled their service lane.
159
+ - id: out_dir_given
160
+ action: set
161
+ condition: "{{rejected_out}} != "
162
+ vars: { out_ok: "" }
163
+
164
+ - id: no_out_dir
165
+ action: verify_context
166
+ require_true: out_ok
167
+ on_false: stop
168
+ on_false_message: >
169
+ `{{rejected_out}}` was given as an output directory, and foundation has none to give.
170
+ A scaffold is unpacked into the source path of the lane it is for — here lane
171
+ `{{lane_id}}` at `{{lane_root}}` — so that everything it writes is inside the subtree
172
+ that lane owns. Re-run as `/appweaver foundation {{preselect_name}} {{preselect_app}}`.
173
+ To scaffold somewhere else, declare a lane there with /appweaver select-stack and
174
+ pick that lane.
76
175
 
77
176
  - id: have_foundations
78
177
  action: verify_context
79
178
  require_non_empty: foundations
80
179
  on_empty: stop
81
180
  on_empty_message: >
82
- No foundation bundle is available for this repository. Bundles present:
83
- {{all_foundations}}; the ecosystem(s) this repository declares in
84
- .appweaver/stacks.json: {{lane_ecosystems}}. A foundation is a complete project in
85
- one ecosystem's languages and build system, so one from another ecosystem is not a
86
- scaffold for this repo — it is a second, foreign project beside it. Either add a
87
- bundle under foundations/<ecosystem>/<Name>/<Name>.pack.txt for your stack (see the
88
- pack scripts in that directory), or scaffold with the stack's own tooling (Spring
89
- Initializr, `npm create`, `cargo new`, …) and declare the result with
90
- /appweaver select-stack — implement generates INTO a project tree and does not
91
- require that the kit be the thing that created it.
181
+ No foundation bundle ships for the `{{lane_id}}` lane's ecosystem ({{lane_ecosystem}}).
182
+ Bundles present: {{all_foundations}}. A foundation is a complete project in one
183
+ ecosystem's languages and build system, so one from another ecosystem is not a
184
+ scaffold for this lane — it is a second, foreign project inside it. Either add a
185
+ bundle under foundations/{{lane_ecosystem}}/<Name>/<Name>.pack.txt (see the pack
186
+ scripts in that directory), or scaffold `{{lane_root}}` with the stack's own tooling
187
+ (Spring Initializr, `npm create`, `cargo new`, …) — implement generates INTO a
188
+ project tree and does not require that the kit be the thing that created it.
92
189
 
93
190
  # 1b. PICK the foundation. A matching fast-path arg auto-selects; a lone
94
191
  # foundation auto-confirms; otherwise the user chooses from the list.
@@ -128,14 +225,13 @@ workflow:
128
225
  action: execute_script
129
226
  script: { ref: actions }
130
227
  subcommand: foundation-locate
131
- args: ["--name", "{{fname}}", "--app", "{{app}}", "--out", "{{out_dir}}"]
228
+ args: ["--name", "{{fname}}", "--app", "{{app}}", "--out", "{{lane_root}}"]
132
229
  outputs:
133
230
  - { name: locate_ok, from: "$.ok" }
134
231
  - { name: locate_msg, from: "$.reason" }
135
232
  - { name: available, from: "$.available" }
136
233
  - { name: fname, from: "$.name" }
137
234
  - { name: app, from: "$.app" }
138
- - { name: out_dir, from: "$.out_dir" }
139
235
  - { name: sentinel, from: "$.sentinel" }
140
236
  - { name: file_count, from: "$.file_count" }
141
237
 
@@ -151,7 +247,9 @@ workflow:
151
247
  prompt: |
152
248
  Foundation: {{fname}} (sentinel {{sentinel}}, {{file_count}} files)
153
249
  App Name: {{app}}
154
- Output: {{out_dir}}
250
+ Lane: {{lane_id}} — {{lane_language}} ({{lane_ecosystem}})
251
+ Source: {{lane_root}} <- the whole bundle unpacks here
252
+ Tests: {{lane_test_path}} <- a test tree the bundle carries is lifted here
155
253
  Rename {{sentinel}} -> {{app}} (dotted/underscored/hyphen/lower forms).
156
254
  Unchanged: 'Projects' sample domain + AppWeaver.* framework packages. Proceed?
157
255
  options: ["yes", "no"]
@@ -169,7 +267,7 @@ workflow:
169
267
  action: execute_script
170
268
  script: { ref: actions }
171
269
  subcommand: foundation-unpack
172
- args: ["--name", "{{fname}}", "--app", "{{app}}", "--out", "{{out_dir}}", "--dry-run"]
270
+ args: ["--name", "{{fname}}", "--app", "{{app}}", "--out", "{{lane_root}}", "--dry-run"]
173
271
  outputs:
174
272
  - { name: has_collisions, from: "$.has_collisions" }
175
273
 
@@ -193,9 +291,11 @@ workflow:
193
291
  action: execute_script
194
292
  script: { ref: actions }
195
293
  subcommand: foundation-unpack
196
- args: ["--name", "{{fname}}", "--app", "{{app}}", "--out", "{{out_dir}}", "--force", "{{overwrite}}"]
294
+ args: ["--name", "{{fname}}", "--app", "{{app}}", "--out", "{{lane_root}}", "--test-path", "{{lane_test_path}}", "--force", "{{overwrite}}"]
197
295
  outputs:
198
- - { name: written, from: "$.written" }
296
+ - { name: written, from: "$.written" }
297
+ - { name: tests_moved, from: "$.tests_moved" }
298
+ - { name: tests_message, from: "$.tests_message" }
199
299
 
200
300
  # 5. Optional reference sample.
201
301
  - id: ask_reference
@@ -209,7 +309,7 @@ workflow:
209
309
  condition: "{{want_reference}} == yes"
210
310
  script: { ref: actions }
211
311
  subcommand: foundation-stash-dbcontext
212
- args: ["--out", "{{out_dir}}"]
312
+ args: ["--out", "{{lane_root}}"]
213
313
  outputs:
214
314
  - { name: stash_found, from: "$.found" }
215
315
  - { name: db_context, from: "$.dbcontext" }
@@ -220,7 +320,7 @@ workflow:
220
320
  condition: "{{want_reference}} == yes"
221
321
  script: { ref: actions }
222
322
  subcommand: foundation-unpack
223
- args: ["--name", "{{fname}}", "--app", "{{app}}", "--out", "{{out_dir}}", "--reference"]
323
+ args: ["--name", "{{fname}}", "--app", "{{app}}", "--out", "{{lane_root}}", "--test-path", "{{lane_test_path}}", "--reference"]
224
324
  outputs:
225
325
  - { name: ref_written, from: "$.written" }
226
326
 
@@ -248,7 +348,7 @@ workflow:
248
348
  action: execute_script
249
349
  script: { ref: actions }
250
350
  subcommand: foundation-verify
251
- args: ["--out", "{{out_dir}}", "--sentinel", "{{sentinel}}"]
351
+ args: ["--out", "{{lane_root}}", "--sentinel", "{{sentinel}}"]
252
352
  outputs:
253
353
  - { name: clean, from: "$.clean" }
254
354
  - { name: residue, from: "$.residue" }
@@ -256,10 +356,14 @@ workflow:
256
356
  outputs:
257
357
  foundation: "{{fname}}"
258
358
  app: "{{app}}"
259
- output_dir: "{{out_dir}}"
359
+ lane: "{{lane_id}}"
360
+ source_path: "{{lane_root}}"
361
+ test_path: "{{lane_test_path}}"
260
362
  files_written: "{{written}}"
363
+ tests_moved: "{{tests_moved}}"
364
+ tests_note: "{{tests_message}}"
261
365
  reference_files: "{{ref_written}}"
262
366
  sentinel_clean: "{{clean}}"
263
367
  sentinel_residue: "{{residue}}"
264
- note: "Foundation scaffolded into {{out_dir}} ({{written}} file(s), sentinel renamed to {{app}}). Configure your AppWeaver.* NuGet feed and run the AppHost to confirm the scaffold starts before building on it."
265
- next: "/appweaver select-feature (pick the feature to build; every stage from specify onward reads that active feature and stops without it. If this workspace has governed projects under guardrails/projects/, run /appweaver select-domain first — it is one-time and optional.)"
368
+ note: "Foundation scaffolded into the {{lane_id}} lane at {{lane_root}} ({{written}} file(s), sentinel renamed to {{app}}); {{tests_message}}. Everything written is inside the subtree that lane owns, which is where implement generates and build/test/run act. Configure your AppWeaver.* NuGet feed and run the AppHost to confirm the scaffold starts before building on it."
369
+ next: "Another lane still to scaffold? Run /appweaver foundation again and pick it — one bundle fills one lane. Otherwise /appweaver select-feature (pick the feature to build; every stage from specify onward reads that active feature and stops without it. If this workspace has governed projects under guardrails/projects/, run /appweaver select-domain first — it is one-time and optional.)"