aw-spec-cli 0.1.20 → 0.1.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/assets/.appweaver/ORCH-DIALECT.md +8 -3
  2. package/assets/.appweaver/STACK-PROFILES.md +87 -8
  3. package/assets/.appweaver/orchestrator/modules/delta-plan.orch.yaml +30 -0
  4. package/assets/.appweaver/orchestrator/modules/delta-spec.orch.yaml +22 -1
  5. package/assets/.appweaver/orchestrator/orchestrator.py +65 -3
  6. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan-delta.orch.yaml +6 -6
  7. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan.orch.yaml +144 -52
  8. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice-delta.orch.yaml +78 -10
  9. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice.orch.yaml +283 -25
  10. package/assets/.appweaver/orchestrator/workflows/aw-spec-constitution.orch.yaml +4 -4
  11. package/assets/.appweaver/orchestrator/workflows/aw-spec-finalize-delta.orch.yaml +202 -40
  12. package/assets/.appweaver/orchestrator/workflows/aw-spec-foundation.orch.yaml +140 -36
  13. package/assets/.appweaver/orchestrator/workflows/aw-spec-implement.orch.yaml +557 -57
  14. package/assets/.appweaver/orchestrator/workflows/aw-spec-onboard.orch.yaml +3 -3
  15. package/assets/.appweaver/orchestrator/workflows/aw-spec-plan.orch.yaml +39 -0
  16. package/assets/.appweaver/orchestrator/workflows/aw-spec-recall-delta.orch.yaml +28 -7
  17. package/assets/.appweaver/orchestrator/workflows/aw-spec-select-stack.orch.yaml +47 -23
  18. package/assets/.appweaver/orchestrator/workflows/aw-spec-specify.orch.yaml +101 -1
  19. package/assets/.appweaver/orchestrator/workflows/aw-spec-ui-model.orch.yaml +141 -0
  20. package/assets/.appweaver/orchestrator/workflows/aw-spec-ui-prototype.orch.yaml +386 -0
  21. package/assets/.appweaver/orchestrator/workflows/aw-spec-workflow-model.orch.yaml +135 -0
  22. package/assets/.appweaver/scripts/bash/aw-spec-actions.sh +772 -216
  23. package/assets/.appweaver/scripts/bash/aw-xref.sh +4 -1
  24. package/assets/.appweaver/scripts/powershell/aw-spec-actions.ps1 +708 -209
  25. package/assets/.appweaver/scripts/powershell/aw-xref.ps1 +4 -1
  26. package/assets/.appweaver/scripts/python/aw-crstate.py +319 -0
  27. package/assets/.appweaver/scripts/python/aw-foundation.py +151 -0
  28. package/assets/.appweaver/scripts/python/aw-gaps.py +119 -0
  29. package/assets/.appweaver/scripts/python/aw-payload-parity.py +19 -0
  30. package/assets/.appweaver/scripts/python/aw-plan-coverage.py +503 -0
  31. package/assets/.appweaver/scripts/python/aw-plangate.py +216 -0
  32. package/assets/.appweaver/scripts/python/aw-principles.py +108 -0
  33. package/assets/.appweaver/scripts/python/aw-stack.py +257 -28
  34. package/assets/.appweaver/scripts/python/aw-testlog.py +148 -0
  35. package/assets/.appweaver/scripts/python/aw-xref.py +141 -4
  36. package/assets/.appweaver/stack-profiles/angular.json +26 -0
  37. package/assets/.appweaver/stack-profiles/blazor-wasm.json +38 -0
  38. package/assets/.appweaver/stack-profiles/dotnet-webapi.json +39 -0
  39. package/assets/.appweaver/stack-profiles/java-spring.json +20 -0
  40. package/assets/.appweaver/stack-profiles/nextjs.json +26 -0
  41. package/assets/.appweaver/stack-profiles/node-express.json +27 -0
  42. package/assets/.appweaver/stack-profiles/python-fastapi.json +16 -0
  43. package/assets/.appweaver/stack-profiles/react-vite.json +26 -0
  44. package/assets/.appweaver/stack-profiles/sveltekit.json +26 -0
  45. package/assets/.appweaver/stack-profiles/vue-vite.json +26 -0
  46. package/assets/.appweaver/templates/constitution-domain-template.md +17 -2
  47. package/assets/.appweaver/templates/ui-model-template.md +67 -0
  48. package/assets/.appweaver/templates/workflow-model-template.md +62 -0
  49. package/assets/guardrails/projects/dotnet-default-project/domain-rule-bindings.md +10 -6
  50. package/assets/guardrails/projects/dotnet-default-project/tech-rule-bindings.md +4 -0
  51. package/assets/guardrails/technology/dotnet-webapi/auditing.md +3 -1
  52. package/assets/guardrails/technology/dotnet-webapi/coding-standards.md +5 -3
  53. package/assets/guardrails/technology/dotnet-webapi/cqrs.md +5 -1
  54. package/assets/guardrails/technology/dotnet-webapi/domain-driven-design.md +13 -5
  55. package/assets/guardrails/technology/dotnet-webapi/examples/cqrs.md +2 -0
  56. package/assets/guardrails/technology/dotnet-webapi/examples/domain-driven-design.md +6 -2
  57. package/assets/guardrails/technology/dotnet-webapi/framework-api-map.md +114 -0
  58. package/assets/guardrails/technology/dotnet-webapi/technology-constitution.md +3 -0
  59. package/assets/guardrails/technology/dotnet-webapi/testing.md +10 -0
  60. package/assets/guardrails/technology/nextjs/ui-constitution.md +8 -1
  61. package/package.json +1 -1
  62. package/assets/.appweaver/orchestrator/.state/smoke.state.json +0 -32
@@ -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.
@@ -60,7 +60,7 @@
60
60
  # risk of "fixing" the change's new tests back toward pre-change behaviour.
61
61
  # 2. A red analyze-slice still lands on an OPEN change, so the remedy is "fix and
62
62
  # re-run" rather than a NEW change to repair the one already shipped.
63
- # The downstream fence below still guards the MERGE (a change that breaks a dependent
63
+ # The regression fence below still guards the MERGE (a change that breaks a dependent or a co-owner
64
64
  # must not reach the master docs); close-delta guards nothing — it is the bookkeeping.
65
65
  #
66
66
  # Prereqs: run /appweaver implement (answering it with THIS change), then build, then
@@ -89,22 +89,38 @@
89
89
  # is one close-delta could never archive, so it must not reach the masters.
90
90
  #
91
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:
92
+ # before the regression fence, because they are free while that one runs a test suite:
93
93
  #
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.
@@ -121,8 +137,8 @@
121
137
  # =============================================================================
122
138
 
123
139
  name: aw-spec-finalize-delta
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
140
+ 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 regression fence over the whole blast radius (fence_green) — the transitive REFERRER closure and the features that CO-OWN the aggregates this delta touches, which hold no edge to it but whose code is in the same files — writing a triage report and asking 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.
141
+ version: 2.5.0
126
142
  agent_agnostic: true
127
143
  dialect: aw-spec.orchestrator/v1
128
144
 
@@ -149,22 +165,42 @@ vars:
149
165
  has_changes: "false" # from cr-list — the DIRECTORY's answer to "is anything open", not the ledger's
150
166
  version: "" # the CURRENT version — unchanged by this stage
151
167
  next_version: "" # what close-delta will bump to; the number the Change History rows carry
152
- downstream_count: "0" # transitive + placeholder-filtered dependents (blast radius); drives the gate
168
+ downstream_count: "0" # transitive + placeholder-filtered dependents THAT HAVE CODE; drives the gate
153
169
  downstream_csv: "" # the blast-radius names, fed to the scoped fence's --also and shown to the human
170
+ unbuilt_count: "0" # dependants that are planned and not implemented: real, with nothing to re-test
171
+ unbuilt_csv: ""
172
+ reached_count: "0" # the whole closure — what this change reaches, built or not
173
+ co_owner_count: "0" # features that CO-OWN the aggregates this delta touches
174
+ co_owner_csv: "" # their names, fed to the same fence as a second --also
175
+ co_owner_aggs: "" # the aggregates the co-owner search was scoped to
176
+ co_owner_source: "" # `delta` (the change's own header) or `feature-owned` (the fallback)
177
+ blast_any: "" # truthy when EITHER half of the blast radius is non-empty
154
178
  fence_ok: "true" # default green so the gate is a no-op when nothing downstream
155
179
  gate_decision: "clean" # audit: 'clean' unless a RED fence was deliberately overridden
180
+ # The change's own `## Depends on` vs its wave — the fence that keeps the number and the
181
+ # prose from drifting apart. Defaults green, so a run that cannot read them is not blocked.
182
+ wave_ok: "true"
183
+ wave_required: ""
184
+ wave_message: ""
185
+ # The automatic rebase (cr-rebase). Defaults say "nothing moved, nothing stamped",
186
+ # so a step that could not run leaves the existing refusal to speak.
187
+ rebase_clean: "false"
188
+ rebase_stamped: ""
189
+ rebase_conflicts: ""
190
+ rebase_message: ""
156
191
  domain_model: "" # specs/<Context>/domain-model.md — the second merge target
157
192
  domain_model_exists: "false"
158
193
  delta_model: "" # specs/<Context>/domain-model.<id>.delta.md — the model half of the change
159
194
  delta_model_exists: "false" # false ⇒ a behaviour-only change: the spec merge is the whole merge
160
195
  model_orphan: "false" # a model delta whose context has no master to merge it into
161
196
  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
197
+ # THE MERGE FENCE. All four flags default DOWN and are raised only by the good state, so
163
198
  # a cr-merge-check that errored or a field that was renamed leaves the merge refused.
164
199
  # Fail-closed is the only safe default in front of an in-place edit of the masters.
165
200
  rebase_ok: "false"
166
201
  merged_open_ok: "false"
167
202
  no_blockers: "false"
203
+ wave_ordering_ok: "false"
168
204
  rebase_needed: "true" # the raw readings, defaulted to the blocking value for the
169
205
  has_merged_open: "true" # same reason — an unanswered question is not a clean answer
170
206
  has_blockers: "true"
@@ -173,7 +209,11 @@ vars:
173
209
  blocker_ids: ""
174
210
  blocker_count: "0"
175
211
  merged_open_ids: ""
176
- merge_wave: "1"
212
+ wave_problem: "" # which changes could not be ordered, and why that ends the check
213
+ unordered_ids: ""
214
+ unordered_count: "0"
215
+ merge_wave: "" # EMPTY, not 1: an unreadable wave is not the floor, and the
216
+ # gate below refuses rather than letting it pass as one
177
217
  snapshot_dir: "" # changes/<id>/pre-merge/ — the before-image recall-delta restores
178
218
  snapshot_count: "0"
179
219
  # THE OKF OVERVIEW'S SUBJECT. The Overview step selects a `## Aggregate:` section BY
@@ -352,6 +392,28 @@ workflow:
352
392
  # NO on_error: continue. A gate that cannot run must not be a gate that passes, and the
353
393
  # raise-then-verify below is fail-closed for the same reason: each flag is raised ONLY
354
394
  # by the good state, so a field that never arrived leaves it down and the merge refuses.
395
+ # AUTO-REBASE FIRST, and only where it is EARNED. `**Base version**` was written once by
396
+ # cr-setup and nothing could ever update it — every other site reads it, and every
397
+ # authoring prompt forbids the model from writing it — so a moved master left the operator
398
+ # hand-editing the delta-spec in the middle of an automated loop. cr-rebase re-stamps it
399
+ # (and `**Base context version**`, which until now nothing read at all) when the masters
400
+ # moved and NOTHING THAT LANDED IN BETWEEN touched the aggregates this delta names: then
401
+ # the delta still describes the document it was written against, and the stamp is
402
+ # bookkeeping. A landed change sharing an aggregate, or one whose model delta cannot be
403
+ # read, leaves the stamp alone — and the refusal below fires with the overlap named.
404
+ # It runs BEFORE cr-merge-check so that check reads the stamp this step just settled.
405
+ - id: auto_rebase
406
+ action: execute_script
407
+ on_error: continue
408
+ script: { ref: actions }
409
+ subcommand: cr-rebase
410
+ args: ["--cr-id", "{{active_change}}", "--feature-dir", "{{feature_dir}}", "--stamp"]
411
+ outputs:
412
+ - { name: rebase_clean, from: "$.clean" }
413
+ - { name: rebase_stamped, from: "$.stamped" }
414
+ - { name: rebase_conflicts, from: "$.conflict_ids" }
415
+ - { name: rebase_message, from: "$.message" }
416
+
355
417
  - id: merge_check
356
418
  action: execute_script
357
419
  script: { ref: actions }
@@ -367,6 +429,10 @@ workflow:
367
429
  - { name: has_merged_open, from: "$.has_merged_open" }
368
430
  - { name: merged_open_ids, from: "$.merged_open_ids" }
369
431
  - { name: merge_wave, from: "$.wave" }
432
+ - { name: wave_ordering_ok, from: "$.wave_ordering_ok" }
433
+ - { name: wave_problem, from: "$.wave_problem" }
434
+ - { name: unordered_ids, from: "$.unordered_ids" }
435
+ - { name: unordered_count, from: "$.unordered_count" }
370
436
 
371
437
  - id: rebase_ok_raise
372
438
  condition: "{{rebase_needed}} == false"
@@ -376,7 +442,7 @@ workflow:
376
442
  action: verify_context
377
443
  require_true: rebase_ok
378
444
  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}}."
445
+ 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."
380
446
 
381
447
  - id: merged_open_ok_raise
382
448
  condition: "{{has_merged_open}} == false"
@@ -386,7 +452,21 @@ workflow:
386
452
  action: verify_context
387
453
  require_true: merged_open_ok
388
454
  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}}."
455
+ on_false_message: "finalize-delta REFUSED — a change has already been MERGED into a master but not yet CLOSED, so the masters carry it while the version still reads {{current_version}}: {{merged_open_ids}}. The feature named beside it is where it lives — domain-model.md is shared, so a SIBLING feature'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, under ITS OWN feature: select that feature (/appweaver select-feature), then /appweaver analyze-slice and /appweaver close-delta on it, which bumps the version — then rebase this one against the new number and re-run /appweaver finalize-delta {{active_change}}."
456
+
457
+ # WAVE ORDERING IS A PRECONDITION OF THE FENCE BELOW, not a finding of it. The predecessor
458
+ # test is `their wave < mine`, and a wave that cannot be read used to answer that question
459
+ # as 1 — the floor — so an unstamped delta posed as a predecessor to everything above it,
460
+ # and an unstamped SUBJECT compared against nothing at all and sailed through. Neither
461
+ # showed up as a refusal, because both look exactly like "no blockers found".
462
+ # It is narrow by construction: nothing sits below the floor, so a change AT wave 1 is
463
+ # never held up by a neighbour nobody stamped. Above it, the fence says so instead of
464
+ # guessing.
465
+ - id: require_wave_ordering
466
+ action: verify_context
467
+ require_true: wave_ordering_ok
468
+ on_false: stop
469
+ on_false_message: "finalize-delta REFUSED — the wave fence cannot order change {{active_change}}, so it cannot tell you whether a predecessor is still open: {{wave_problem}}. A wave is stamped by a script and read by both merge gates; `**Wave**: [N]` is the unfilled template line, which means the stamp never ran on that document. Stamp each one — `.appweaver/scripts/bash/aw-spec-actions.sh cr-wave-derive --cr-id <CR-id> --stamp` derives the wave from that change's own `## Depends on` section and writes it — then re-run /appweaver finalize-delta {{active_change}}. A change in another feature is stamped under that feature (/appweaver select-feature first)."
390
470
 
391
471
  - id: no_blockers_raise
392
472
  condition: "{{has_blockers}} == false"
@@ -396,21 +476,48 @@ workflow:
396
476
  action: verify_context
397
477
  require_true: no_blockers
398
478
  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."
479
+ 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. 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. THE LOWER-WAVE CHANGES: {{blocker_ids}}. This scan is context-wide, so one of them may belong to a feature you have never opened — and finalize-delta resolves a change under the ACTIVE feature. Select the feature named beside a change (/appweaver select-feature) before running /appweaver finalize-delta <CR-id> → analyze-slice → close-delta on it; 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."
480
+
481
+ # THE DECLARED DEPENDENCIES AND THE WAVE MUST AGREE. The fence above orders changes by
482
+ # their wave NUMBER; this one checks that number against what the change's own documents
483
+ # SAY under `## Depends on`. Before this, the two could diverge silently: an author named
484
+ # a predecessor in prose, cr-setup left the wave at the predecessor's own level, and the
485
+ # fence above then read the two as independent and let them merge in either order.
486
+ - id: wave_declared_check
487
+ action: execute_script
488
+ on_error: continue
489
+ script: { ref: actions }
490
+ subcommand: cr-wave-derive
491
+ args: ["--cr-id", "{{active_change}}"]
492
+ outputs:
493
+ - { name: wave_ok, from: "$.ok" }
494
+ - { name: wave_required, from: "$.required_wave" }
495
+ - { name: wave_message, from: "$.message" }
496
+ - id: require_wave_matches_depends
497
+ action: verify_context
498
+ require_true: wave_ok
499
+ on_false: stop
500
+ 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."
400
501
 
401
502
  # 1c. DOWNSTREAM GATE (hard, with a REVIEWED override). Who depends on this aggregate —
402
503
  # its OKF Cross-References, TRANSITIVELY (C→B→A all count) and placeholder-filtered
403
504
  # (the "_(none observed)_" row is NOT a dependent) = the real blast radius. If any
404
505
  # slice does, this change could ripple into it, so run a SCOPED regression fence over
405
- # this feature + its downstream closure (--scope slice --also <closure>) BEFORE the
506
+ # this feature + its blast radius (--scope slice --also <referrers> --also <co-owners>) BEFORE the
406
507
  # doc merge, so a block leaves the repo untouched. The fence binds fence_GREEN (ran
407
508
  # AND matched >=1), not raw ok, so an empty / zero-match run is RED, never a vacuous
408
509
  # pass. If RED we do NOT hard-stop blindly: we WRITE a triage report, then surface the
409
- # failing log + dependent slices and ASK the developer — Abort (a real downstream
510
+ # failing log + affected slices and ASK the developer — Abort (a real downstream
410
511
  # break: fix it first) or, having reviewed the log (pre-existing / flaky / unrelated),
411
512
  # deliberately Override and close anyway. Non-interactive / CI runs take the FIRST
412
513
  # option (Abort), so automation never silently overrides. Skipped when nothing depends
413
- # on this slice (count 0) or the fence is green.
514
+ # on this slice and nothing co-owns its aggregates, or the fence is green.
515
+ # THE FENCE RUNS TESTS, so it takes the BUILT half of the closure. Edges are read off
516
+ # the lane PLAN, not off code, so a dependant can be fully planned and hold none:
517
+ # selecting it would hand the fence a slice with no tests, and the fence counts a
518
+ # zero-match run as RED. An unbuilt dependant is still reported — it is a real
519
+ # dependant with nothing to re-test yet, which is a different thing from being
520
+ # unaffected, and it is what /appweaver implement on that feature will settle.
414
521
  - id: xref_impact
415
522
  action: execute_script
416
523
  on_error: continue
@@ -418,16 +525,49 @@ workflow:
418
525
  subcommand: list
419
526
  args: ["--feature-dir", "{{feature_dir}}"]
420
527
  outputs:
421
- - { name: downstream_count, from: "$.referrer_closure_count" } # transitive + placeholder-filtered
422
- - { name: downstream_csv, from: "$.referrer_closure_csv" } # blast-radius names → --also + display
528
+ - { name: downstream_count, from: "$.built_count" } # transitive, placeholder-filtered, HAS CODE
529
+ - { name: downstream_csv, from: "$.built_closure_csv" } # blast-radius names → --also + display
530
+ - { name: unbuilt_count, from: "$.unbuilt_count" } # dependants planned but never implemented
531
+ - { name: unbuilt_csv, from: "$.unbuilt_closure_csv" }
532
+ - { name: reached_count, from: "$.referrer_closure_count" } # the whole closure, for the report
533
+
534
+ # THE OTHER HALF OF THE BLAST RADIUS. A referrer reaches this aggregate through an edge;
535
+ # a CO-OWNER has no edge to it because it is not a user of the aggregate — it is one of
536
+ # the capabilities the aggregate is made of, and its code is in the very files this merge
537
+ # rewrites. `Encounter.cs` is extended by every feature claiming Encounter, so a change to
538
+ # it can break a co-owner while every referrer still passes. Scoped to the aggregates THIS
539
+ # delta names, so a feature owning two aggregates does not drag the second one's co-owners
540
+ # through a fence they cannot have been affected by.
541
+ - id: xref_co_owners
542
+ action: execute_script
543
+ on_error: continue
544
+ script: { ref: xref }
545
+ subcommand: co-owners
546
+ args: ["--cr", "{{active_change}}", "--feature-dir", "{{feature_dir}}"]
547
+ outputs:
548
+ - { name: co_owner_count, from: "$.count" }
549
+ - { name: co_owner_csv, from: "$.features_csv" }
550
+ - { name: co_owner_aggs, from: "$.aggregates_csv" }
551
+ - { name: co_owner_source, from: "$.source" }
552
+
553
+ # The fence runs when EITHER half is non-empty. Two ordered assignments rather than one
554
+ # condition, because the dialect has no compound conditions.
555
+ - id: blast_from_referrers
556
+ condition: "{{downstream_count}} != 0"
557
+ action: set
558
+ vars: { blast_any: "true" }
559
+ - id: blast_from_co_owners
560
+ condition: "{{co_owner_count}} != 0"
561
+ action: set
562
+ vars: { blast_any: "true" }
423
563
 
424
564
  - id: run_downstream_fence
425
- condition: "{{downstream_count}} != 0"
565
+ condition: "{{blast_any}}"
426
566
  action: execute_script
427
567
  on_error: continue
428
568
  script: { ref: actions }
429
569
  subcommand: test-run
430
- args: ["--feature-dir", "{{feature_dir}}", "--scope", "slice", "--also", "{{downstream_csv}}"]
570
+ args: ["--feature-dir", "{{feature_dir}}", "--scope", "slice", "--also", "{{downstream_csv}}", "--also", "{{co_owner_csv}}"]
431
571
  outputs:
432
572
  - { name: fence_ok, from: "$.fence_green" }
433
573
  - { name: fence_tool, from: "$.tool" }
@@ -441,11 +581,14 @@ workflow:
441
581
  # only when nothing depends on this slice (no fence ran, nothing to report).
442
582
  - id: ctx_broken_downstream
443
583
  action: build_context
444
- condition: "{{downstream_count}} != 0"
584
+ condition: "{{blast_any}}"
445
585
  vars:
446
586
  active_change: "{{active_change}}"
447
587
  downstream_count: "{{downstream_count}}"
448
588
  downstream_csv: "{{downstream_csv}}"
589
+ co_owner_count: "{{co_owner_count}}"
590
+ co_owner_csv: "{{co_owner_csv}}"
591
+ co_owner_aggs: "{{co_owner_aggs}}"
449
592
  fence_ok: "{{fence_ok}}"
450
593
  fence_log: "{{fence_log}}"
451
594
  context:
@@ -456,14 +599,27 @@ workflow:
456
599
  - { path: "{{delta_backend_plan}}", optional: true }
457
600
  - id: write_broken_downstream
458
601
  action: agent_author
459
- condition: "{{downstream_count}} != 0"
602
+ condition: "{{blast_any}}"
460
603
  include_standards: false # a triage report, not generated code
461
604
  output_target: { type: file, path: "{{feature_dir}}/changes/{{active_change}}/broken-downstream.md" }
462
605
  task: |
463
606
  Author the DOWNSTREAM report for change {{active_change}}: the result of re-running this
464
- change against the aggregates that DEPEND ON this feature. finalize-delta does NOT fix a
465
- downstream slice (that is the downstream slice's own job). Downstream blast radius
466
- ({{downstream_count}}): {{downstream_csv}}. The fence result was fence_green={{fence_ok}}
607
+ change against everything it could have broken. finalize-delta does NOT fix a
608
+ downstream slice (that is the downstream slice's own job).
609
+ THE BLAST RADIUS HAS TWO HALVES and they fail for different reasons — say which is which
610
+ wherever the report names a slice:
611
+ - DEPENDENTS ({{downstream_count}}): {{downstream_csv}} — aggregates that USE this one
612
+ through a Cross-Reference edge. A failure here is a broken CONTRACT between two slices.
613
+ - NOT YET BUILT ({{unbuilt_count}}): {{unbuilt_csv}} — aggregates that also depend on this
614
+ one and hold no code yet, so the fence had nothing to run for them. Edges come from the
615
+ lane PLAN, so these are real dependants whose contract with this aggregate has never
616
+ been exercised. Say so plainly: they were NOT tested, which is not the same as passing,
617
+ and /appweaver implement on those features is what settles them.
618
+ - CO-OWNERS ({{co_owner_count}}): {{co_owner_csv}} — features that share the aggregate(s)
619
+ this change touches ({{co_owner_aggs}}). They hold no edge to it: their code is IN the
620
+ files this merge rewrites, so a failure here is this change breaking a sibling
621
+ capability of the same aggregate, not a contract between two of them.
622
+ Either list may be empty. The fence result was fence_green={{fence_ok}}
467
623
  (true = every downstream test passed; false = at least one failed). Test log:
468
624
  {{fence_log}}
469
625
  Always write the file (it is regenerated on every finalize-delta run so a green close never
@@ -471,8 +627,9 @@ workflow:
471
627
 
472
628
  IF fence_green is TRUE (all passed): write a SHORT all-clear — the heading
473
629
  `# Broken Downstream — change {{active_change}}`, then a few lines stating that the
474
- {{downstream_count}} slice(s) that use this feature ({{downstream_csv}}) were re-run against
475
- this change and all passed, so nothing downstream broke. No per-slice sections; invent nothing.
630
+ {{downstream_count}} dependent slice(s) ({{downstream_csv}}) and {{co_owner_count}} co-owner
631
+ feature(s) ({{co_owner_csv}}) were re-run against this change and all passed, so nothing it
632
+ could reach broke. No per-slice sections; invent nothing.
476
633
 
477
634
  IF fence_green is FALSE (something failed): write the triage record —
478
635
  - `# Broken Downstream — change {{active_change}}` and a one-line statement: aggregates that
@@ -480,10 +637,11 @@ workflow:
480
637
  finalize-delta's downstream gate. Note it is the record a human reads while deciding Abort vs
481
638
  Override, and if the change is Overridden it is archived with the change as the record of
482
639
  what was knowingly closed over.
483
- - A SUMMARY line: total downstream slices in the blast radius, and how many have a failing test.
484
- - One `##` section per DOWNSTREAM slice that has a failing test — attribute a failure to a
485
- slice by matching the failing test names in the log to the slice's name. Under it, on their
486
- own labelled lines: the downstream aggregate, the failing test(s) named in the log, what
640
+ - A SUMMARY line: total slices in the blast radius (dependents + co-owners), and how many have a failing test.
641
+ - One `##` section per slice with a failing test, from EITHER half — attribute a failure to a
642
+ slice by matching the failing test names in the log to the slice's name, and label the
643
+ section `dependent` or `co-owner`. Under it, on their
644
+ own labelled lines: the aggregate or feature, the failing test(s) named in the log, what
487
645
  broke (as far as the log shows), and a RESOLUTION line — either this change broke the
488
646
  downstream slice's expected contract (fix it in the downstream slice's OWN CR, or re-baseline
489
647
  its golden-master) or the failure is pre-existing / flaky / unrelated (a candidate for a
@@ -701,7 +859,7 @@ workflow:
701
859
  # the context has no model: there would be nothing in context to distil, and a
702
860
  # query with no source invents one. Best-effort; an empty answer is a no-op.
703
861
  # 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
862
+ # own `**Aggregates touched**:` header rather than assumed from the feature name. The Overview
705
863
  # step below selects a section by name, and a feature whose name is not an aggregate
706
864
  # name — one capability over a shared aggregate — selects nothing, producing an empty
707
865
  # answer that the writer then declines. Driving it off the delta's declared aggregates
@@ -830,6 +988,10 @@ outputs:
830
988
  # the change by close-delta. What an un-merge would restore from.
831
989
  pre_merge_snapshot: "{{snapshot_dir}}"
832
990
  base_version: "{{base_version}}"
991
+ # What the automatic rebase did before the fence read the stamp — empty when the delta was
992
+ # already current, and the reason it could not when a landed change shared its aggregates.
993
+ rebased: "{{rebase_stamped}}"
994
+ rebase_note: "{{rebase_message}}"
833
995
  model_merged: "{{delta_model_exists}}" # true ⇒ domain-model.md was the second target of this merge.
834
996
  # false = a behaviour-only change: the spec merge was the
835
997
  # whole merge, not a half of one.