aw-spec-cli 0.1.17 → 0.1.19

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 (53) hide show
  1. package/assets/.appweaver/DESIGN-FORMAT.md +45 -21
  2. package/assets/.appweaver/orchestrator/modules/delta-plan.orch.yaml +62 -14
  3. package/assets/.appweaver/orchestrator/modules/delta-spec.orch.yaml +6 -4
  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/design-template.skeleton.md +18 -11
  8. package/assets/.appweaver/orchestrator/rev-templates/ui-design-template.skeleton.md +5 -3
  9. package/assets/.appweaver/orchestrator/workflows/aw-spec-adopt.orch.yaml +298 -109
  10. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan.orch.yaml +161 -34
  11. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice.orch.yaml +125 -24
  12. package/assets/.appweaver/orchestrator/workflows/aw-spec-build.orch.yaml +18 -7
  13. package/assets/.appweaver/orchestrator/workflows/aw-spec-close-delta.orch.yaml +182 -39
  14. package/assets/.appweaver/orchestrator/workflows/aw-spec-finalize-delta.orch.yaml +300 -35
  15. package/assets/.appweaver/orchestrator/workflows/aw-spec-implement.orch.yaml +190 -31
  16. package/assets/.appweaver/orchestrator/workflows/aw-spec-onboard.orch.yaml +93 -8
  17. package/assets/.appweaver/orchestrator/workflows/aw-spec-plan.orch.yaml +34 -7
  18. package/assets/.appweaver/orchestrator/workflows/aw-spec-recall-delta.orch.yaml +319 -0
  19. package/assets/.appweaver/orchestrator/workflows/aw-spec-specify.orch.yaml +125 -24
  20. package/assets/.appweaver/scripts/bash/aw-spec-actions.sh +846 -35
  21. package/assets/.appweaver/scripts/bash/common.sh +28 -1
  22. package/assets/.appweaver/scripts/powershell/aw-spec-actions.ps1 +843 -46
  23. package/assets/.appweaver/scripts/python/aw-inventory.py +3 -2
  24. package/assets/.appweaver/templates/design-template.md +41 -30
  25. package/assets/.appweaver/templates/spec-template.md +7 -4
  26. package/assets/.appweaver/templates/ui-design-template.md +7 -3
  27. package/assets/guardrails/domain/healthcare/auditing.domain.md +3 -3
  28. package/assets/guardrails/domain/pharma/auditing.domain.md +3 -3
  29. package/assets/guardrails/technology/dotnet-webapi/api-versioning.md +1 -1
  30. package/assets/guardrails/technology/dotnet-webapi/auditing.md +1 -1
  31. package/assets/guardrails/technology/dotnet-webapi/authorization.md +1 -1
  32. package/assets/guardrails/technology/dotnet-webapi/clean-architecture.md +1 -1
  33. package/assets/guardrails/technology/dotnet-webapi/cqrs.md +1 -1
  34. package/assets/guardrails/technology/dotnet-webapi/data-classification.md +1 -1
  35. package/assets/guardrails/technology/dotnet-webapi/eventing.md +1 -1
  36. package/assets/guardrails/technology/dotnet-webapi/folder-structure.md +1 -1
  37. package/assets/guardrails/technology/dotnet-webapi/modular-monolith.md +1 -1
  38. package/assets/guardrails/technology/dotnet-webapi/persistence.md +1 -1
  39. package/assets/guardrails/technology/dotnet-webapi/vertical-slices.md +1 -1
  40. package/assets/guardrails/technology/dotnet-webapi/zero-trust.md +1 -1
  41. package/assets/guardrails/technology/nextjs/accessibility-implementation.md +1 -1
  42. package/assets/guardrails/technology/nextjs/api-integration.dotnet-webapi.md +1 -1
  43. package/assets/guardrails/technology/nextjs/api-integration.java-spring.md +1 -1
  44. package/assets/guardrails/technology/nextjs/auth-session.md +1 -1
  45. package/assets/guardrails/technology/nextjs/component-architecture.md +1 -1
  46. package/assets/guardrails/technology/nextjs/design-system.md +1 -1
  47. package/assets/guardrails/technology/nextjs/error-handling.md +1 -1
  48. package/assets/guardrails/technology/nextjs/forms-and-validation.md +1 -1
  49. package/assets/guardrails/technology/nextjs/i18n.md +1 -1
  50. package/assets/guardrails/technology/nextjs/performance-budgets.md +1 -1
  51. package/assets/guardrails/technology/nextjs/routing-navigation.md +1 -1
  52. package/assets/guardrails/technology/nextjs/state-management.md +1 -1
  53. 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,33 @@ 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 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.
184
+ delta_aggregates: ""
185
+ overview_scope: ""
186
+ overview_section_ok: "false"
187
+ overview_written: "false"
188
+ overview_reason: ""
104
189
  backend_plan: "" # READ, never written here — the OKF edges are parsed out of it
105
190
  backend_plan_exists: "false"
106
191
 
@@ -134,24 +219,64 @@ workflow:
134
219
  on_false: stop
135
220
  on_false_message: "No spec.md for the current feature — there is no master to merge into. Run /appweaver specify first."
136
221
 
137
- # 1b. Read the OPEN change (opened by cr). finalize-delta needs a change to close.
222
+ # 1b. WHICH CHANGE IS BEING MERGED. The evidence is changes/<CR-id>/ on disk; a closed
223
+ # change has moved to changes/archive/. No synthetic rows: `main` is not a change and
224
+ # there is nothing to open here, so the menu is the real open changes and nothing else.
225
+ # `matched_label` resolves a CR id passed as the argument to the whole option string,
226
+ # which is how a bare `finalize-delta CR-…` skips the prompt.
227
+ - id: cr_list
228
+ action: execute_script
229
+ script: { ref: actions }
230
+ subcommand: cr-list
231
+ args: ["--feature-dir", "{{feature_dir}}", "--preselect", "{{USER_INPUT}}"]
232
+ outputs:
233
+ - { name: change_options, from: "$.select_options" }
234
+ - { name: matched_label, from: "$.matched_label" }
235
+ - { name: has_changes, from: "$.has_changes" }
236
+ - { name: change_count, from: "$.count" }
237
+
238
+ - id: require_changes
239
+ action: verify_context
240
+ require_true: has_changes
241
+ on_false: stop
242
+ 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."
243
+
244
+ # single_option_confirm so a feature with exactly one open change still confirms the id
245
+ # out loud. This stage edits the two documents that DEFINE the slice, and picking
246
+ # silently is how the wrong change is merged on a feature that has two open.
247
+ - id: pick_change
248
+ action: user_select
249
+ 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."
250
+ options_from_variable: change_options
251
+ preselect_from: matched_label
252
+ single_option_confirm: true
253
+ outputs:
254
+ - { name: cr_choice, from: selection }
255
+
256
+ # Make the chosen change the ACTIVE one — analyze-slice and close-delta still read
257
+ # active_change, so the stages after this one must follow the same delta. The activate
258
+ # also VALIDATES: it refuses an id the ledger does not carry and stops the run, which is
259
+ # the right outcome for a merge target close-delta could never archive.
260
+ - id: activate
261
+ action: execute_script
262
+ script: { ref: actions }
263
+ subcommand: cr-activate
264
+ args: ["--cr-id", "{{cr_choice}}", "--feature-dir", "{{feature_dir}}"]
265
+ outputs:
266
+ - { name: active_change, from: "$.active_change" }
267
+
268
+ # 1b-i. The VERSION spine, read after the pick and used for the Change History row only.
269
+ # This call is no longer the selector — `active_change` comes from the pick above —
270
+ # and version-read moves nothing, so what comes back is the current number.
138
271
  - id: read_change
139
272
  action: execute_script
140
273
  script: { ref: actions }
141
274
  subcommand: version-read
142
275
  args: ["--feature-dir", "{{feature_dir}}"]
143
276
  outputs:
144
- - { name: active_change, from: "$.active_change" }
145
- - { name: has_change, from: "$.has_change" }
146
277
  - { name: version, from: "$.version" }
147
278
  - { name: next_version, from: "$.next_version" }
148
279
 
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
280
  # 1b-ii. The change's own documents, from the ONE resolver that knows the layout: the
156
281
  # delta-spec sits at FEATURE level (changes/<id>/) and the model delta at CONTEXT
157
282
  # level (specs/<Context>/domain-model.<id>.delta.md), beside the master it merges
@@ -179,7 +304,7 @@ workflow:
179
304
  action: verify_context
180
305
  require_true: delta_spec_exists
181
306
  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."
307
+ 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
308
 
184
309
  # A model delta with no master to merge it into. specify creates domain-model.md the
185
310
  # first time it models a context, so this is a context whose master was moved or
@@ -204,6 +329,74 @@ workflow:
204
329
  on_false: stop
205
330
  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
331
 
332
+ # 1b-iii. THE MERGE FENCE — the three refusals that make a parallel merge safe, all read
333
+ # off disk in ONE call, and all placed BEFORE the downstream fence because they are
334
+ # free while that one runs a test suite. Refusing cheaply first is the whole ordering.
335
+ #
336
+ # REBASE: the delta carries the master version it was authored against (cr-setup stamps
337
+ # `**Base version**:` when the change opens). If the master has moved since, the
338
+ # delta's MODIFIED items name a BEFORE the master no longer says, and merging them is
339
+ # last-writer-wins over work that already landed.
340
+ #
341
+ # MERGED-NOT-CLOSED: the masters can move WITHOUT the version moving, because this
342
+ # stage merges and close-delta bumps. Between the two, spec.md carries a change the
343
+ # number does not yet reflect — so a sibling delta sharing the same base_version would
344
+ # compare EQUAL and merge onto masters that had already shifted. The id in spec.md is
345
+ # the proof, the same evidence recall-delta's merged-refusal reads.
346
+ #
347
+ # PREDECESSOR: a lower wave is committed to landing first, and `plan` put exactly those
348
+ # deltas in this one's context as the base it was planned on top of. Merging out of
349
+ # wave order folds in behaviour assuming a predecessor the masters do not carry.
350
+ #
351
+ # NO on_error: continue. A gate that cannot run must not be a gate that passes, and the
352
+ # raise-then-verify below is fail-closed for the same reason: each flag is raised ONLY
353
+ # by the good state, so a field that never arrived leaves it down and the merge refuses.
354
+ - id: merge_check
355
+ action: execute_script
356
+ script: { ref: actions }
357
+ subcommand: cr-merge-check
358
+ args: ["--cr-id", "{{active_change}}", "--feature-dir", "{{feature_dir}}"]
359
+ outputs:
360
+ - { name: rebase_needed, from: "$.rebase_needed" }
361
+ - { name: base_version, from: "$.base_version" }
362
+ - { name: current_version, from: "$.current_version" }
363
+ - { name: has_blockers, from: "$.has_blockers" }
364
+ - { name: blocker_ids, from: "$.blocker_ids" }
365
+ - { name: blocker_count, from: "$.blocker_count" }
366
+ - { name: has_merged_open, from: "$.has_merged_open" }
367
+ - { name: merged_open_ids, from: "$.merged_open_ids" }
368
+ - { name: merge_wave, from: "$.wave" }
369
+
370
+ - id: rebase_ok_raise
371
+ condition: "{{rebase_needed}} == false"
372
+ action: set
373
+ vars: { rebase_ok: "true" }
374
+ - id: require_rebase
375
+ action: verify_context
376
+ require_true: rebase_ok
377
+ 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}}."
379
+
380
+ - id: merged_open_ok_raise
381
+ condition: "{{has_merged_open}} == false"
382
+ action: set
383
+ vars: { merged_open_ok: "true" }
384
+ - id: require_no_merged_open
385
+ action: verify_context
386
+ require_true: merged_open_ok
387
+ 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}}."
389
+
390
+ - id: no_blockers_raise
391
+ condition: "{{has_blockers}} == false"
392
+ action: set
393
+ vars: { no_blockers: "true" }
394
+ - id: require_no_blockers
395
+ action: verify_context
396
+ require_true: no_blockers
397
+ 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."
399
+
207
400
  # 1c. DOWNSTREAM GATE (hard, with a REVIEWED override). Who depends on this aggregate —
208
401
  # its OKF Cross-References, TRANSITIVELY (C→B→A all count) and placeholder-filtered
209
402
  # (the "_(none observed)_" row is NOT a dependent) = the real blast radius. If any
@@ -347,6 +540,29 @@ workflow:
347
540
  action: dispatch_hooks
348
541
  hooks_from_variable: before_hooks
349
542
 
543
+ # 2b. SNAPSHOT THE MASTERS — the last thing before they are rewritten, and after every
544
+ # gate including the downstream override, so the copy is taken only by the attempt
545
+ # that actually merges. finalize-delta may be attempted more than once (a red fence
546
+ # aborts it), and a snapshot from an aborted attempt would claim to be the before-image
547
+ # of a merge that never happened.
548
+ #
549
+ # This is the ONLY record of what the masters said beforehand that belongs to the
550
+ # pipeline rather than to git, and it travels with the change: changes/<id>/ is what
551
+ # cr-archive moves, so a closed change carries the before-image of the documents it
552
+ # moved, in the archive, for as long as the repo exists.
553
+ #
554
+ # ⌛ The RESTORE is not built. recall-delta still refuses a merged change outright
555
+ # rather than un-merging from this copy — the snapshot is what makes that buildable,
556
+ # and the rebase gate above is what will answer whether restoring is still safe.
557
+ - id: snapshot_masters
558
+ action: execute_script
559
+ script: { ref: actions }
560
+ subcommand: cr-snapshot
561
+ args: ["--cr-id", "{{active_change}}", "--feature-dir", "{{feature_dir}}"]
562
+ outputs:
563
+ - { name: snapshot_dir, from: "$.dir" }
564
+ - { name: snapshot_count, from: "$.count" }
565
+
350
566
  # 3. THE MERGE: the change's two halves into the two masters, in place.
351
567
  # Context is fed via build_context — agent_generate reads the ENVELOPE, not a
352
568
  # step-level context: key (unlike agent_author/agent_query which read both).
@@ -399,8 +615,13 @@ workflow:
399
615
  that placeholder; a section the delta does not touch keeps its "None" verbatim.
400
616
  - Apply every MODIFIED item as an in-place edit of the existing item; apply every
401
617
  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.
618
+ - Append a Change History row for {{active_change}} (date + one-line summary), with
619
+ the CR id in its CR cell. Its Version cell is the literal text `(pending)` — write
620
+ that, not a number. THE VERSION DOES NOT EXIST YET: close-delta issues it, and it
621
+ stamps this cell at the same instant it stamps the headers. A number written here
622
+ would be a prediction, and a change that merged but never closed — an abandoned
623
+ run, a red analyze-slice — would leave spec.md asserting a version nothing issued.
624
+ Do not invent a version anywhere, including in this cell.
404
625
  To domain-model.md — ONLY where it and a model delta are both in context:
405
626
  - THE MERGE UNIT IS ONE `## Aggregate: <Name>` SECTION, and the ONLY sections you may
406
627
  open are the ones the model delta's "Aggregates touched" header names. Every other
@@ -434,9 +655,9 @@ workflow:
434
655
  version it bumps to — the model's row and its version land together or the row
435
656
  carries a number nothing else agrees with.
436
657
  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.
658
+ {{version}} and does not move here. close-delta stamps it, and the Change History
659
+ row's `(pending)` cell, at the moment it bumps active_version.json, so every carrier
660
+ of the number changes in one step by one mechanism. Do not invent a version anywhere.
440
661
  Preserve every unrelated section of BOTH masters byte-for-byte. Do NOT touch the
441
662
  delta documents (close-delta archives them later), do NOT open or edit any lane plan,
442
663
  and do NOT touch any source code. Report the files you changed.
@@ -478,6 +699,35 @@ workflow:
478
699
  # splice (the Cross-References rows other slices own are untouched). Skipped where
479
700
  # the context has no model: there would be nothing in context to distil, and a
480
701
  # query with no source invents one. Best-effort; an empty answer is a no-op.
702
+ # 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.
709
+ #
710
+ # 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.
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
+ - id: overview_scope_feature
724
+ action: set
725
+ vars: { overview_scope: "{{feature}}" }
726
+ - id: overview_scope_delta
727
+ condition: "{{delta_aggregates}} != "
728
+ action: set
729
+ vars: { overview_scope: "{{delta_aggregates}}" }
730
+
481
731
  # Re-scope the envelope first. build_context REPLACES it and it PERSISTS across the
482
732
  # steps that follow, so without this the Overview query would still be holding the
483
733
  # merge's payload — the delta-spec and the model delta among it — and would distil the
@@ -491,13 +741,20 @@ workflow:
491
741
  condition: "{{domain_model_exists}} == true"
492
742
  action: agent_query
493
743
  include_standards: false
494
- schema: { overview: "<2-4 sentences of prose>" }
495
- mock: { overview: "" }
744
+ schema: { overview: "<2-4 sentences of prose>", section_found: true }
745
+ mock: { overview: "", section_found: false }
496
746
  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.
747
+ Write the OKF knowledge-concept Overview for the aggregate `{{overview_scope}}`, from
748
+ the merged domain-model.md in context. That document models the WHOLE bounded context
749
+ — read ONLY the `## Aggregate: {{overview_scope}}` section (and any `## Cross-Aggregate
750
+ Invariants` naming it); the other aggregates belong to other concepts. Where more than
751
+ one aggregate is named above, this change touched several: write the Overview for the
752
+ FIRST one, which is the concept this feature owns.
753
+
754
+ Set section_found=false if domain-model.md has no `## Aggregate:` section under that
755
+ name, and answer with an empty overview. Do NOT substitute a different section: the
756
+ name is how the concept and the model are keyed to each other, and describing the
757
+ wrong aggregate is worse than describing none.
501
758
  2-4 sentences of PROSE: name the aggregate ROOT and what the aggregate IS; state the
502
759
  INVARIANTS it guards (its BR-n rules); state its TRANSACTION BOUNDARY — what it OWNS
503
760
  inside the boundary vs what it only REFERENCES by id. Plain prose only: no headings,
@@ -505,7 +762,8 @@ workflow:
505
762
  (those live in the Depends-On / Cross-References tables). Describe only what the model
506
763
  states — invent nothing. If the section is absent, answer with an empty string.
507
764
  outputs:
508
- - { name: okf_overview, from: "$.overview" }
765
+ - { name: okf_overview, from: "$.overview" }
766
+ - { name: overview_section_ok, from: "$.section_found" }
509
767
 
510
768
  - id: okf_overview_write
511
769
  condition: "{{domain_model_exists}} == true"
@@ -514,6 +772,9 @@ workflow:
514
772
  script: { ref: xref }
515
773
  subcommand: concept-overview
516
774
  args: ["--feature-dir", "{{feature_dir}}", "--text", "{{okf_overview}}"]
775
+ outputs:
776
+ - { name: overview_written, from: "$.written" }
777
+ - { name: overview_reason, from: "$.reason" }
517
778
 
518
779
  # 4c. Anchor the OKF bundle's freshness journal to the current commit (log.md, OKF §7).
519
780
  # Best-effort + git-aware: a no-op when there is no bundle or no commit, so it never
@@ -555,7 +816,11 @@ outputs:
555
816
  gate_decision: "{{gate_decision}}"
556
817
  xref_edge_count: "{{xref_edge_count}}"
557
818
  domain_model: "{{domain_model}}"
819
+ # The before-image of both masters, taken immediately before the rewrite and archived with
820
+ # the change by close-delta. What an un-merge would restore from.
821
+ pre_merge_snapshot: "{{snapshot_dir}}"
822
+ base_version: "{{base_version}}"
558
823
  model_merged: "{{delta_model_exists}}" # true ⇒ domain-model.md was the second target of this merge.
559
824
  # false = a behaviour-only change: the spec merge was the
560
825
  # 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."
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."