aw-spec-cli 0.1.18 → 0.1.20

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 (77) hide show
  1. package/assets/.appweaver/DESIGN-FORMAT.md +63 -31
  2. package/assets/.appweaver/orchestrator/modules/delta-plan.orch.yaml +62 -14
  3. package/assets/.appweaver/orchestrator/modules/delta-spec.orch.yaml +10 -6
  4. package/assets/.appweaver/orchestrator/modules/lane-gate.orch.yaml +9 -1
  5. package/assets/.appweaver/orchestrator/modules/tech-select.orch.yaml +21 -0
  6. package/assets/.appweaver/orchestrator/orchestrator.py +23 -1
  7. package/assets/.appweaver/orchestrator/rev-templates/delta-design-template.skeleton.md +1 -1
  8. package/assets/.appweaver/orchestrator/rev-templates/design-template.skeleton.md +22 -13
  9. package/assets/.appweaver/orchestrator/rev-templates/ui-delta-design-template.skeleton.md +1 -1
  10. package/assets/.appweaver/orchestrator/rev-templates/ui-design-template.skeleton.md +6 -4
  11. package/assets/.appweaver/orchestrator/workflows/aw-spec-adopt.orch.yaml +399 -131
  12. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan-delta.orch.yaml +1 -1
  13. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan.orch.yaml +199 -38
  14. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice-delta.orch.yaml +1 -1
  15. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice.orch.yaml +159 -25
  16. package/assets/.appweaver/orchestrator/workflows/aw-spec-build.orch.yaml +20 -9
  17. package/assets/.appweaver/orchestrator/workflows/aw-spec-characterize.orch.yaml +1 -1
  18. package/assets/.appweaver/orchestrator/workflows/aw-spec-close-delta.orch.yaml +183 -40
  19. package/assets/.appweaver/orchestrator/workflows/aw-spec-deploy.orch.yaml +1 -1
  20. package/assets/.appweaver/orchestrator/workflows/aw-spec-finalize-delta.orch.yaml +311 -36
  21. package/assets/.appweaver/orchestrator/workflows/aw-spec-implement.orch.yaml +221 -40
  22. package/assets/.appweaver/orchestrator/workflows/aw-spec-inventory.orch.yaml +217 -174
  23. package/assets/.appweaver/orchestrator/workflows/aw-spec-onboard.orch.yaml +494 -109
  24. package/assets/.appweaver/orchestrator/workflows/aw-spec-parity.orch.yaml +1 -1
  25. package/assets/.appweaver/orchestrator/workflows/aw-spec-plan.orch.yaml +83 -18
  26. package/assets/.appweaver/orchestrator/workflows/aw-spec-recall-delta.orch.yaml +319 -0
  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-specify.orch.yaml +143 -34
  30. package/assets/.appweaver/scripts/bash/aw-inventory.sh +15 -0
  31. package/assets/.appweaver/scripts/bash/aw-spec-actions.sh +878 -51
  32. package/assets/.appweaver/scripts/bash/aw-xref.sh +3 -0
  33. package/assets/.appweaver/scripts/bash/common.sh +28 -1
  34. package/assets/.appweaver/scripts/powershell/aw-inventory.ps1 +15 -0
  35. package/assets/.appweaver/scripts/powershell/aw-spec-actions.ps1 +868 -51
  36. package/assets/.appweaver/scripts/powershell/aw-xref.ps1 +3 -0
  37. package/assets/.appweaver/scripts/python/aw-inventory.py +1832 -65
  38. package/assets/.appweaver/scripts/python/aw-xref.py +500 -81
  39. package/assets/.appweaver/templates/delta-design-template.md +1 -1
  40. package/assets/.appweaver/templates/delta-domain-model-template.md +1 -1
  41. package/assets/.appweaver/templates/delta-spec-template.md +4 -3
  42. package/assets/.appweaver/templates/design-template.md +56 -40
  43. package/assets/.appweaver/templates/okf-concept-template.md +11 -1
  44. package/assets/.appweaver/templates/proposed-inventory-template.md +127 -0
  45. package/assets/.appweaver/templates/source-map-template.md +5 -3
  46. package/assets/.appweaver/templates/spec-template.md +24 -21
  47. package/assets/.appweaver/templates/ui-delta-design-template.md +1 -1
  48. package/assets/.appweaver/templates/ui-design-template.md +11 -7
  49. package/assets/foundations/dotnet/ModularMonolith/ModularMonolith.pack.txt +4489 -361
  50. package/assets/foundations/dotnet/VerticalSlice/VerticalSlice.pack.txt +2274 -125
  51. package/assets/guardrails/domain/healthcare/auditing.domain.md +3 -3
  52. package/assets/guardrails/domain/pharma/auditing.domain.md +3 -3
  53. package/assets/guardrails/technology/dotnet-webapi/api-versioning.md +1 -1
  54. package/assets/guardrails/technology/dotnet-webapi/auditing.md +1 -1
  55. package/assets/guardrails/technology/dotnet-webapi/authorization.md +1 -1
  56. package/assets/guardrails/technology/dotnet-webapi/clean-architecture.md +1 -1
  57. package/assets/guardrails/technology/dotnet-webapi/cqrs.md +1 -1
  58. package/assets/guardrails/technology/dotnet-webapi/data-classification.md +1 -1
  59. package/assets/guardrails/technology/dotnet-webapi/eventing.md +1 -1
  60. package/assets/guardrails/technology/dotnet-webapi/folder-structure.md +1 -1
  61. package/assets/guardrails/technology/dotnet-webapi/modular-monolith.md +1 -1
  62. package/assets/guardrails/technology/dotnet-webapi/persistence.md +1 -1
  63. package/assets/guardrails/technology/dotnet-webapi/vertical-slices.md +1 -1
  64. package/assets/guardrails/technology/dotnet-webapi/zero-trust.md +1 -1
  65. package/assets/guardrails/technology/nextjs/accessibility-implementation.md +1 -1
  66. package/assets/guardrails/technology/nextjs/api-integration.dotnet-webapi.md +1 -1
  67. package/assets/guardrails/technology/nextjs/api-integration.java-spring.md +1 -1
  68. package/assets/guardrails/technology/nextjs/auth-session.md +1 -1
  69. package/assets/guardrails/technology/nextjs/component-architecture.md +1 -1
  70. package/assets/guardrails/technology/nextjs/design-system.md +1 -1
  71. package/assets/guardrails/technology/nextjs/error-handling.md +1 -1
  72. package/assets/guardrails/technology/nextjs/forms-and-validation.md +1 -1
  73. package/assets/guardrails/technology/nextjs/i18n.md +1 -1
  74. package/assets/guardrails/technology/nextjs/performance-budgets.md +1 -1
  75. package/assets/guardrails/technology/nextjs/routing-navigation.md +1 -1
  76. package/assets/guardrails/technology/nextjs/state-management.md +1 -1
  77. package/package.json +1 -1
@@ -30,14 +30,23 @@
30
30
  # item merged into the spec is carried by the spec until the frontend plan is re-derived from
31
31
  # it — not dropped, re-derived.
32
32
  #
33
- # ⌛ THE RE-DERIVATION IS NOT BUILT YET, and this stage is where that shows. `plan`
34
- # self-detects on plan-EXISTS, so a run against a merged master authors another DELTA plan
35
- # rather than refreshing the master ("to regenerate a master, delete it first"). Until a
36
- # refresh mode lands, the master plans stand at their bootstrap content while spec.md and
37
- # domain-model.md move ahead of them — visible, because analyze-plan gates the masters against
38
- # exactly these two documents and reports the drift. What this stage must NOT do is hide that
39
- # by merging a delta plan: the plan would then agree with the spec without any run of `plan`
40
- # ever having derived it.
33
+ # THE RE-DERIVATION IS `plan main`. After this merge the masters carry the change and the
34
+ # lane plans do not, so the next step for the plans is to be derived again from the two
35
+ # documents this stage just moved: `/appweaver plan main` asks to re-derive them, carries
36
+ # the recorded architectural decisions and workarounds forward, and rewrites whatever the
37
+ # spec and model no longer support. Until that is run the master plans stand at their last
38
+ # authored content while spec.md and domain-model.md have moved ahead of them — visible,
39
+ # because analyze-plan gates the masters against exactly these two documents and reports the
40
+ # drift. What this stage must NOT do is hide that by merging a delta plan: the plan would
41
+ # then agree with the spec without any run of `plan` ever having derived it.
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
47
+ # plans are not merely "visibly behind": the next `implement` on the master path says so and
48
+ # stops, naming `plan main`. A feature with no receipt has never been gated since the receipt
49
+ # existed and is reported, never refused.
41
50
  #
42
51
  # specify → plan → analyze-plan-delta → implement → build → analyze-slice-delta
43
52
  # → finalize-delta ← you are here
@@ -65,16 +74,64 @@
65
74
  # that describes an intention.
66
75
  # finalize-delta edits ONLY spec.md and domain-model.md; it generates no code.
67
76
  #
68
- # Launch: /appweaver finalize-delta [strategy]
77
+ # WHICH CHANGE IS MERGED IS ASKED, NEVER INFERRED. MANY changes may be open at once, and
78
+ # active_version.json's `active_change` is only the most recently opened or activated — every
79
+ # /appweaver specify overwrites it. Taking that field as "the" change is how the delta built
80
+ # by implement stays open while a freshly-opened stub is merged into spec.md: cr-setup
81
+ # scaffolds delta-spec.md from the template the moment it opens a change, so an empty stub
82
+ # passes an exists check. This stage therefore opens with the same list → pick → activate
83
+ # sequence as plan, implement, analyze-plan-delta and recall-delta, and the argument doubles
84
+ # as the answer (`finalize-delta CR-…` skips the prompt).
85
+ #
86
+ # THE DIRECTORY IS THE AUTHORITY for what is open — cr-list reads changes/<id>/ and reports
87
+ # `tracked` separately, so a change whose ledger row was lost is still offered. cr-activate
88
+ # then REFUSES it, which is the outcome that belongs here: a change the ledger does not carry
89
+ # is one close-delta could never archive, so it must not reach the masters.
90
+ #
91
+ # THE MERGE FENCE — THREE REFUSALS, read off disk in one cr-merge-check call and placed
92
+ # before the downstream fence, because they are free while that one runs a test suite:
93
+ #
94
+ # REBASE. The delta carries the master version it was authored against (`**Base version**:`,
95
+ # stamped by cr-setup). If the master has moved, the delta's MODIFIED items name a BEFORE
96
+ # spec.md no longer says, and merging them is last-writer-wins over work that landed.
97
+ #
98
+ # 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.
103
+ #
104
+ # 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.
108
+ #
109
+ # All three are RAISE-then-VERIFY and fail-closed: each flag is raised only by the good
110
+ # state, so a check that did not run leaves the merge refused rather than waved through.
111
+ #
112
+ # NO LOCK. A lock guards CONCURRENT runs; this pipeline is operator-driven and sequential,
113
+ # and the three refusals above cover the sequential case completely. A lockfile would add a
114
+ # stale-lock failure mode to guard a scenario the kit does not have.
115
+ #
116
+ # THE MASTERS ARE SNAPSHOTTED to changes/<id>/pre-merge/ immediately before the rewrite —
117
+ # the pipeline's own record of what they said beforehand, archived with the change.
118
+ #
119
+ # Launch: /appweaver finalize-delta [CR-id] (a bare id skips the prompt)
69
120
  # Prereq: an OPEN change whose delta was implemented.
70
121
  # =============================================================================
71
122
 
72
123
  name: aw-spec-finalize-delta
73
- description: Merge the open change's delta-spec into the feature's spec.md and its model delta into the bounded context's domain-model.md — one aggregate section at a time, only the aggregates the delta names — in a single transaction, and append the spec's Change History row. The lane plans are NOT merged — backend-plan.md and frontend-plan.md are derived from these two documents and are re-derived by /appweaver plan main. Gated first by a SCOPED downstream fence over the transitive blast radius (fence_green), which writes a triage report and asks Abort/Override on red. Leaves the change OPEN — analyze-slice runs next against the merged masters, and close-delta archives the change and bumps the version.
74
- version: 2.0.0
124
+ description: Merge ONE chosen open change's delta-spec into the feature's spec.md and its model delta into the bounded context's domain-model.md — one aggregate section at a time, only the aggregates the delta names — in a single transaction, and append the spec's Change History row. ASKS which change (a bare CR id as the argument skips the prompt) and activates it, rather than taking whichever was last opened. Fenced by three hard refusals read off disk before anything is written — a moved base_version (rebase first), another change merged but not yet closed, and an open LOWER-wave predecessor — then by a SCOPED downstream fence over the transitive blast radius (fence_green), which writes a triage report and asks Abort/Override on red. Snapshots both masters to changes/<id>/pre-merge/ immediately before the rewrite. The lane plans are NOT merged — backend-plan.md and frontend-plan.md are derived from these two documents and are re-derived by /appweaver plan main. Leaves the change OPEN — analyze-slice runs next against the merged masters, and close-delta archives the change and bumps the version.
125
+ version: 2.4.0
75
126
  agent_agnostic: true
76
127
  dialect: aw-spec.orchestrator/v1
77
128
 
129
+ inputs:
130
+ user_input:
131
+ description: "Optional CR id of the change to merge. A bare id selects it and skips the prompt; anything else is ignored and the menu is shown."
132
+ required: false
133
+ bind: USER_INPUT
134
+
78
135
  scripts:
79
136
  actions:
80
137
  sh: .appweaver/scripts/bash/aw-spec-actions.sh
@@ -88,7 +145,8 @@ standards:
88
145
 
89
146
  vars:
90
147
  active_change: ""
91
- has_change: "false"
148
+ cr_choice: "" # the LABEL the pick returns ("CR-… · wave 2 · title"); scripts take its first token
149
+ has_changes: "false" # from cr-list — the DIRECTORY's answer to "is anything open", not the ledger's
92
150
  version: "" # the CURRENT version — unchanged by this stage
93
151
  next_version: "" # what close-delta will bump to; the number the Change History rows carry
94
152
  downstream_count: "0" # transitive + placeholder-filtered dependents (blast radius); drives the gate
@@ -101,6 +159,34 @@ vars:
101
159
  delta_model_exists: "false" # false ⇒ a behaviour-only change: the spec merge is the whole merge
102
160
  model_orphan: "false" # a model delta whose context has no master to merge it into
103
161
  model_master_ok: "true" # lowered by model_orphan_raise; the gate reads only this
162
+ # THE MERGE FENCE. All three flags default DOWN and are raised only by the good state, so
163
+ # a cr-merge-check that errored or a field that was renamed leaves the merge refused.
164
+ # Fail-closed is the only safe default in front of an in-place edit of the masters.
165
+ rebase_ok: "false"
166
+ merged_open_ok: "false"
167
+ no_blockers: "false"
168
+ rebase_needed: "true" # the raw readings, defaulted to the blocking value for the
169
+ has_merged_open: "true" # same reason — an unanswered question is not a clean answer
170
+ has_blockers: "true"
171
+ base_version: ""
172
+ current_version: ""
173
+ blocker_ids: ""
174
+ blocker_count: "0"
175
+ merged_open_ids: ""
176
+ merge_wave: "1"
177
+ snapshot_dir: "" # changes/<id>/pre-merge/ — the before-image recall-delta restores
178
+ snapshot_count: "0"
179
+ # THE OKF OVERVIEW'S SUBJECT. The Overview step selects a `## Aggregate:` section BY
180
+ # NAME, so it needs the aggregate, which is not always the feature: a feature that is
181
+ # one capability over a shared aggregate (PatientRegistration over Patient) has no
182
+ # section of its own name. delta_aggregates is what the change's own model delta
183
+ # declares; it is empty for a behaviour-only change, which falls back to the aggregate
184
+ # the feature answers for (feature-aggregates — the spec's Acts-on line).
185
+ delta_aggregates: ""
186
+ overview_scope: ""
187
+ overview_section_ok: "false"
188
+ overview_written: "false"
189
+ overview_reason: ""
104
190
  backend_plan: "" # READ, never written here — the OKF edges are parsed out of it
105
191
  backend_plan_exists: "false"
106
192
 
@@ -126,7 +212,7 @@ workflow:
126
212
  action: verify_context
127
213
  require_non_empty: feature_dir
128
214
  on_empty: stop
129
- on_empty_message: "No active feature. Run /appweaver select-feature first (pick or create the aggregate, e.g. Order), then re-run finalize-delta."
215
+ 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."
130
216
 
131
217
  - id: verify_spec
132
218
  action: verify_context
@@ -134,24 +220,64 @@ workflow:
134
220
  on_false: stop
135
221
  on_false_message: "No spec.md for the current feature — there is no master to merge into. Run /appweaver specify first."
136
222
 
137
- # 1b. Read the OPEN change (opened by cr). finalize-delta needs a change to close.
223
+ # 1b. WHICH CHANGE IS BEING MERGED. The evidence is changes/<CR-id>/ on disk; a closed
224
+ # change has moved to changes/archive/. No synthetic rows: `main` is not a change and
225
+ # there is nothing to open here, so the menu is the real open changes and nothing else.
226
+ # `matched_label` resolves a CR id passed as the argument to the whole option string,
227
+ # which is how a bare `finalize-delta CR-…` skips the prompt.
228
+ - id: cr_list
229
+ action: execute_script
230
+ script: { ref: actions }
231
+ subcommand: cr-list
232
+ args: ["--feature-dir", "{{feature_dir}}", "--preselect", "{{USER_INPUT}}"]
233
+ outputs:
234
+ - { name: change_options, from: "$.select_options" }
235
+ - { name: matched_label, from: "$.matched_label" }
236
+ - { name: has_changes, from: "$.has_changes" }
237
+ - { name: change_count, from: "$.count" }
238
+
239
+ - id: require_changes
240
+ action: verify_context
241
+ require_true: has_changes
242
+ on_false: stop
243
+ on_false_message: "No open change on this feature — nothing to finalize. Run /appweaver specify → plan → analyze-plan-delta → implement first. A change that already shipped was archived by /appweaver close-delta and is under changes/archive/, where a merge does not reach it."
244
+
245
+ # single_option_confirm so a feature with exactly one open change still confirms the id
246
+ # out loud. This stage edits the two documents that DEFINE the slice, and picking
247
+ # silently is how the wrong change is merged on a feature that has two open.
248
+ - id: pick_change
249
+ action: user_select
250
+ prompt: "Which open change is being MERGED into spec.md and the context's domain-model.md? Merge the change that was implemented and passed analyze-slice-delta — a change opened but never authored carries an empty delta-spec, and merging it writes nothing while consuming the stage. Deltas sharing a wave are independent; a higher wave is built on top of the lower ones, so merge those first."
251
+ options_from_variable: change_options
252
+ preselect_from: matched_label
253
+ single_option_confirm: true
254
+ outputs:
255
+ - { name: cr_choice, from: selection }
256
+
257
+ # Make the chosen change the ACTIVE one — analyze-slice and close-delta still read
258
+ # active_change, so the stages after this one must follow the same delta. The activate
259
+ # also VALIDATES: it refuses an id the ledger does not carry and stops the run, which is
260
+ # the right outcome for a merge target close-delta could never archive.
261
+ - id: activate
262
+ action: execute_script
263
+ script: { ref: actions }
264
+ subcommand: cr-activate
265
+ args: ["--cr-id", "{{cr_choice}}", "--feature-dir", "{{feature_dir}}"]
266
+ outputs:
267
+ - { name: active_change, from: "$.active_change" }
268
+
269
+ # 1b-i. The VERSION spine, read after the pick and used for the Change History row only.
270
+ # This call is no longer the selector — `active_change` comes from the pick above —
271
+ # and version-read moves nothing, so what comes back is the current number.
138
272
  - id: read_change
139
273
  action: execute_script
140
274
  script: { ref: actions }
141
275
  subcommand: version-read
142
276
  args: ["--feature-dir", "{{feature_dir}}"]
143
277
  outputs:
144
- - { name: active_change, from: "$.active_change" }
145
- - { name: has_change, from: "$.has_change" }
146
278
  - { name: version, from: "$.version" }
147
279
  - { name: next_version, from: "$.next_version" }
148
280
 
149
- - id: require_change
150
- action: verify_context
151
- require_true: has_change
152
- on_false: stop
153
- on_false_message: "No open change on this feature — nothing to finalize. Run /appweaver specify → plan → analyze-plan-delta → implement first."
154
-
155
281
  # 1b-ii. The change's own documents, from the ONE resolver that knows the layout: the
156
282
  # delta-spec sits at FEATURE level (changes/<id>/) and the model delta at CONTEXT
157
283
  # level (specs/<Context>/domain-model.<id>.delta.md), beside the master it merges
@@ -179,7 +305,7 @@ workflow:
179
305
  action: verify_context
180
306
  require_true: delta_spec_exists
181
307
  on_false: stop
182
- on_false_message: "Change {{active_change}} has no delta-spec.md at {{delta_spec}} — nothing to merge into spec.md. Run /appweaver specify to author the change, or /appweaver recall-delta to withdraw it."
308
+ on_false_message: "Change {{active_change}} has no delta-spec.md at {{delta_spec}} — nothing to merge into spec.md. Run /appweaver specify to author the change, or /appweaver recall-delta {{active_change}} to withdraw it (its documents are archived, the version does not move, and code implement already generated is left alone)."
183
309
 
184
310
  # A model delta with no master to merge it into. specify creates domain-model.md the
185
311
  # first time it models a context, so this is a context whose master was moved or
@@ -204,6 +330,74 @@ workflow:
204
330
  on_false: stop
205
331
  on_false_message: "Change {{active_change}} carries a model delta ({{delta_model}}) but its bounded context has no domain-model.md master at {{domain_model}}. Spec and model merge in one transaction, so this stage will not merge the spec half alone. Restore the context's domain-model.md (it is what specify creates when it first models the context) and re-run /appweaver finalize-delta."
206
332
 
333
+ # 1b-iii. THE MERGE FENCE — the three refusals that make a parallel merge safe, all read
334
+ # off disk in ONE call, and all placed BEFORE the downstream fence because they are
335
+ # free while that one runs a test suite. Refusing cheaply first is the whole ordering.
336
+ #
337
+ # REBASE: the delta carries the master version it was authored against (cr-setup stamps
338
+ # `**Base version**:` when the change opens). If the master has moved since, the
339
+ # delta's MODIFIED items name a BEFORE the master no longer says, and merging them is
340
+ # last-writer-wins over work that already landed.
341
+ #
342
+ # MERGED-NOT-CLOSED: the masters can move WITHOUT the version moving, because this
343
+ # stage merges and close-delta bumps. Between the two, spec.md carries a change the
344
+ # number does not yet reflect — so a sibling delta sharing the same base_version would
345
+ # compare EQUAL and merge onto masters that had already shifted. The id in spec.md is
346
+ # the proof, the same evidence recall-delta's merged-refusal reads.
347
+ #
348
+ # PREDECESSOR: a lower wave is committed to landing first, and `plan` put exactly those
349
+ # deltas in this one's context as the base it was planned on top of. Merging out of
350
+ # wave order folds in behaviour assuming a predecessor the masters do not carry.
351
+ #
352
+ # NO on_error: continue. A gate that cannot run must not be a gate that passes, and the
353
+ # raise-then-verify below is fail-closed for the same reason: each flag is raised ONLY
354
+ # by the good state, so a field that never arrived leaves it down and the merge refuses.
355
+ - id: merge_check
356
+ action: execute_script
357
+ script: { ref: actions }
358
+ subcommand: cr-merge-check
359
+ args: ["--cr-id", "{{active_change}}", "--feature-dir", "{{feature_dir}}"]
360
+ outputs:
361
+ - { name: rebase_needed, from: "$.rebase_needed" }
362
+ - { name: base_version, from: "$.base_version" }
363
+ - { name: current_version, from: "$.current_version" }
364
+ - { name: has_blockers, from: "$.has_blockers" }
365
+ - { name: blocker_ids, from: "$.blocker_ids" }
366
+ - { name: blocker_count, from: "$.blocker_count" }
367
+ - { name: has_merged_open, from: "$.has_merged_open" }
368
+ - { name: merged_open_ids, from: "$.merged_open_ids" }
369
+ - { name: merge_wave, from: "$.wave" }
370
+
371
+ - id: rebase_ok_raise
372
+ condition: "{{rebase_needed}} == false"
373
+ action: set
374
+ vars: { rebase_ok: "true" }
375
+ - id: require_rebase
376
+ action: verify_context
377
+ require_true: rebase_ok
378
+ on_false: stop
379
+ 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}}."
380
+
381
+ - id: merged_open_ok_raise
382
+ condition: "{{has_merged_open}} == false"
383
+ action: set
384
+ vars: { merged_open_ok: "true" }
385
+ - id: require_no_merged_open
386
+ action: verify_context
387
+ require_true: merged_open_ok
388
+ on_false: stop
389
+ 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}}."
390
+
391
+ - id: no_blockers_raise
392
+ condition: "{{has_blockers}} == false"
393
+ action: set
394
+ vars: { no_blockers: "true" }
395
+ - id: require_no_blockers
396
+ action: verify_context
397
+ require_true: no_blockers
398
+ on_false: stop
399
+ 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."
400
+
207
401
  # 1c. DOWNSTREAM GATE (hard, with a REVIEWED override). Who depends on this aggregate —
208
402
  # its OKF Cross-References, TRANSITIVELY (C→B→A all count) and placeholder-filtered
209
403
  # (the "_(none observed)_" row is NOT a dependent) = the real blast radius. If any
@@ -347,6 +541,29 @@ workflow:
347
541
  action: dispatch_hooks
348
542
  hooks_from_variable: before_hooks
349
543
 
544
+ # 2b. SNAPSHOT THE MASTERS — the last thing before they are rewritten, and after every
545
+ # gate including the downstream override, so the copy is taken only by the attempt
546
+ # that actually merges. finalize-delta may be attempted more than once (a red fence
547
+ # aborts it), and a snapshot from an aborted attempt would claim to be the before-image
548
+ # of a merge that never happened.
549
+ #
550
+ # This is the ONLY record of what the masters said beforehand that belongs to the
551
+ # pipeline rather than to git, and it travels with the change: changes/<id>/ is what
552
+ # cr-archive moves, so a closed change carries the before-image of the documents it
553
+ # moved, in the archive, for as long as the repo exists.
554
+ #
555
+ # ⌛ The RESTORE is not built. recall-delta still refuses a merged change outright
556
+ # rather than un-merging from this copy — the snapshot is what makes that buildable,
557
+ # and the rebase gate above is what will answer whether restoring is still safe.
558
+ - id: snapshot_masters
559
+ action: execute_script
560
+ script: { ref: actions }
561
+ subcommand: cr-snapshot
562
+ args: ["--cr-id", "{{active_change}}", "--feature-dir", "{{feature_dir}}"]
563
+ outputs:
564
+ - { name: snapshot_dir, from: "$.dir" }
565
+ - { name: snapshot_count, from: "$.count" }
566
+
350
567
  # 3. THE MERGE: the change's two halves into the two masters, in place.
351
568
  # Context is fed via build_context — agent_generate reads the ENVELOPE, not a
352
569
  # step-level context: key (unlike agent_author/agent_query which read both).
@@ -399,8 +616,13 @@ workflow:
399
616
  that placeholder; a section the delta does not touch keeps its "None" verbatim.
400
617
  - Apply every MODIFIED item as an in-place edit of the existing item; apply every
401
618
  REMOVED item by removing it. Keep the rest of spec.md VERBATIM.
402
- - Append a Change History row for {{active_change}} (date + one-line summary).
403
- Its Version cell is {{next_version}} — the version this change ships as.
619
+ - Append a Change History row for {{active_change}} (date + one-line summary), with
620
+ the CR id in its CR cell. Its Version cell is the literal text `(pending)` — write
621
+ that, not a number. THE VERSION DOES NOT EXIST YET: close-delta issues it, and it
622
+ stamps this cell at the same instant it stamps the headers. A number written here
623
+ would be a prediction, and a change that merged but never closed — an abandoned
624
+ run, a red analyze-slice — would leave spec.md asserting a version nothing issued.
625
+ Do not invent a version anywhere, including in this cell.
404
626
  To domain-model.md — ONLY where it and a model delta are both in context:
405
627
  - THE MERGE UNIT IS ONE `## Aggregate: <Name>` SECTION, and the ONLY sections you may
406
628
  open are the ones the model delta's "Aggregates touched" header names. Every other
@@ -434,9 +656,9 @@ workflow:
434
656
  version it bumps to — the model's row and its version land together or the row
435
657
  carries a number nothing else agrees with.
436
658
  Leave the `**Version**:` HEADER of spec.md exactly as it is — it still reads
437
- {{version}} and does not move here. close-delta stamps it to {{next_version}} at the
438
- moment it bumps active_version.json, so every carrier of the number changes in one
439
- step by one mechanism. Do not invent a version anywhere.
659
+ {{version}} and does not move here. close-delta stamps it, and the Change History
660
+ row's `(pending)` cell, at the moment it bumps active_version.json, so every carrier
661
+ of the number changes in one step by one mechanism. Do not invent a version anywhere.
440
662
  Preserve every unrelated section of BOTH masters byte-for-byte. Do NOT touch the
441
663
  delta documents (close-delta archives them later), do NOT open or edit any lane plan,
442
664
  and do NOT touch any source code. Report the files you changed.
@@ -478,6 +700,44 @@ workflow:
478
700
  # splice (the Cross-References rows other slices own are untouched). Skipped where
479
701
  # the context has no model: there would be nothing in context to distil, and a
480
702
  # query with no source invents one. Best-effort; an empty answer is a no-op.
703
+ # WHICH AGGREGATE SECTION(S) THIS CHANGE ACTUALLY TOUCHED, read off the model delta's
704
+ # own `## Aggregate:` headings rather than assumed from the feature name. The Overview
705
+ # step below selects a section by name, and a feature whose name is not an aggregate
706
+ # name — one capability over a shared aggregate — selects nothing, producing an empty
707
+ # answer that the writer then declines. Driving it off the delta's declared aggregates
708
+ # makes a model change exact.
709
+ #
710
+ # A BEHAVIOUR-ONLY CHANGE HAS NO MODEL DELTA, so this reports none and the step falls
711
+ # back to the aggregate the feature answers for — which is correct, not a degradation:
712
+ # there is no model change to distil and the concept's existing Overview stands.
713
+ - id: cr_aggregates
714
+ action: execute_script
715
+ on_error: continue # informational — a missing model delta is the ordinary case
716
+ script: { ref: actions }
717
+ subcommand: cr-overlap
718
+ args: ["--cr-id", "{{active_change}}", "--feature-dir", "{{feature_dir}}"]
719
+ outputs:
720
+ - { name: delta_aggregates, from: "$.aggregates_csv" }
721
+ - { name: delta_aggregate_list, from: "$.aggregates" }
722
+
723
+ # The fallback subject: the aggregate this feature answers for, which is the feature
724
+ # itself on a 1:1 slice and the shared aggregate on a capability over one.
725
+ - id: feature_aggregates
726
+ action: execute_script
727
+ script: { ref: xref }
728
+ subcommand: feature-aggregates
729
+ args: ["--feature-dir", "{{feature_dir}}"]
730
+ outputs:
731
+ - { name: owned_csv, from: "$.owned_csv" }
732
+
733
+ - id: overview_scope_feature
734
+ action: set
735
+ vars: { overview_scope: "{{owned_csv}}" }
736
+ - id: overview_scope_delta
737
+ condition: "{{delta_aggregates}} != "
738
+ action: set
739
+ vars: { overview_scope: "{{delta_aggregates}}" }
740
+
481
741
  # Re-scope the envelope first. build_context REPLACES it and it PERSISTS across the
482
742
  # steps that follow, so without this the Overview query would still be holding the
483
743
  # merge's payload — the delta-spec and the model delta among it — and would distil the
@@ -491,13 +751,20 @@ workflow:
491
751
  condition: "{{domain_model_exists}} == true"
492
752
  action: agent_query
493
753
  include_standards: false
494
- schema: { overview: "<2-4 sentences of prose>" }
495
- mock: { overview: "" }
754
+ schema: { overview: "<2-4 sentences of prose>", section_found: true }
755
+ mock: { overview: "", section_found: false }
496
756
  task: |
497
- Write the OKF knowledge-concept Overview for the aggregate `{{feature}}`, from the
498
- merged domain-model.md in context. That document models the WHOLE bounded context —
499
- read ONLY the `## Aggregate: {{feature}}` section (and any `## Cross-Aggregate
500
- Invariants` naming it); the other aggregates belong to other concepts.
757
+ Write the OKF knowledge-concept Overview for the aggregate `{{overview_scope}}`, from
758
+ the merged domain-model.md in context. That document models the WHOLE bounded context
759
+ — read ONLY the `## Aggregate: {{overview_scope}}` section (and any `## Cross-Aggregate
760
+ Invariants` naming it); the other aggregates belong to other concepts. Where more than
761
+ one aggregate is named above, this change touched several: write the Overview for the
762
+ FIRST one, which is the concept this feature owns.
763
+
764
+ Set section_found=false if domain-model.md has no `## Aggregate:` section under that
765
+ name, and answer with an empty overview. Do NOT substitute a different section: the
766
+ name is how the concept and the model are keyed to each other, and describing the
767
+ wrong aggregate is worse than describing none.
501
768
  2-4 sentences of PROSE: name the aggregate ROOT and what the aggregate IS; state the
502
769
  INVARIANTS it guards (its BR-n rules); state its TRANSACTION BOUNDARY — what it OWNS
503
770
  inside the boundary vs what it only REFERENCES by id. Plain prose only: no headings,
@@ -505,7 +772,8 @@ workflow:
505
772
  (those live in the Depends-On / Cross-References tables). Describe only what the model
506
773
  states — invent nothing. If the section is absent, answer with an empty string.
507
774
  outputs:
508
- - { name: okf_overview, from: "$.overview" }
775
+ - { name: okf_overview, from: "$.overview" }
776
+ - { name: overview_section_ok, from: "$.section_found" }
509
777
 
510
778
  - id: okf_overview_write
511
779
  condition: "{{domain_model_exists}} == true"
@@ -514,6 +782,9 @@ workflow:
514
782
  script: { ref: xref }
515
783
  subcommand: concept-overview
516
784
  args: ["--feature-dir", "{{feature_dir}}", "--text", "{{okf_overview}}"]
785
+ outputs:
786
+ - { name: overview_written, from: "$.written" }
787
+ - { name: overview_reason, from: "$.reason" }
517
788
 
518
789
  # 4c. Anchor the OKF bundle's freshness journal to the current commit (log.md, OKF §7).
519
790
  # Best-effort + git-aware: a no-op when there is no bundle or no commit, so it never
@@ -555,7 +826,11 @@ outputs:
555
826
  gate_decision: "{{gate_decision}}"
556
827
  xref_edge_count: "{{xref_edge_count}}"
557
828
  domain_model: "{{domain_model}}"
829
+ # The before-image of both masters, taken immediately before the rewrite and archived with
830
+ # the change by close-delta. What an un-merge would restore from.
831
+ pre_merge_snapshot: "{{snapshot_dir}}"
832
+ base_version: "{{base_version}}"
558
833
  model_merged: "{{delta_model_exists}}" # true ⇒ domain-model.md was the second target of this merge.
559
834
  # false = a behaviour-only change: the spec merge was the
560
835
  # whole merge, not a half of one.
561
- 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, and ⌛ re-deriving a MASTER plan is not built yet (plan self-detects on plan-exists and would author another delta), so backend-plan.md / frontend-plan.md stand at their last authored content and analyze-plan will report the drift against the merged spec and model. OKF edges reconciled ({{xref_edge_count}} dep(s)), {{downstream_count}} downstream slice(s) fenced (gate: {{gate_decision}}). 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."
836
+ 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."