aw-spec-cli 0.1.19 → 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 (46) hide show
  1. package/assets/.appweaver/DESIGN-FORMAT.md +17 -9
  2. package/assets/.appweaver/orchestrator/modules/delta-spec.orch.yaml +4 -2
  3. package/assets/.appweaver/orchestrator/rev-templates/delta-design-template.skeleton.md +1 -1
  4. package/assets/.appweaver/orchestrator/rev-templates/design-template.skeleton.md +4 -2
  5. package/assets/.appweaver/orchestrator/rev-templates/ui-delta-design-template.skeleton.md +1 -1
  6. package/assets/.appweaver/orchestrator/rev-templates/ui-design-template.skeleton.md +1 -1
  7. package/assets/.appweaver/orchestrator/workflows/aw-spec-adopt.orch.yaml +121 -42
  8. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan-delta.orch.yaml +1 -1
  9. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan.orch.yaml +38 -4
  10. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice-delta.orch.yaml +1 -1
  11. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice.orch.yaml +41 -8
  12. package/assets/.appweaver/orchestrator/workflows/aw-spec-build.orch.yaml +1 -1
  13. package/assets/.appweaver/orchestrator/workflows/aw-spec-characterize.orch.yaml +1 -1
  14. package/assets/.appweaver/orchestrator/workflows/aw-spec-close-delta.orch.yaml +1 -1
  15. package/assets/.appweaver/orchestrator/workflows/aw-spec-deploy.orch.yaml +1 -1
  16. package/assets/.appweaver/orchestrator/workflows/aw-spec-finalize-delta.orch.yaml +26 -16
  17. package/assets/.appweaver/orchestrator/workflows/aw-spec-implement.orch.yaml +31 -9
  18. package/assets/.appweaver/orchestrator/workflows/aw-spec-inventory.orch.yaml +217 -174
  19. package/assets/.appweaver/orchestrator/workflows/aw-spec-onboard.orch.yaml +405 -105
  20. package/assets/.appweaver/orchestrator/workflows/aw-spec-parity.orch.yaml +1 -1
  21. package/assets/.appweaver/orchestrator/workflows/aw-spec-plan.orch.yaml +49 -11
  22. package/assets/.appweaver/orchestrator/workflows/aw-spec-recall-delta.orch.yaml +1 -1
  23. package/assets/.appweaver/orchestrator/workflows/aw-spec-run.orch.yaml +1 -1
  24. package/assets/.appweaver/orchestrator/workflows/aw-spec-select-feature.orch.yaml +29 -23
  25. package/assets/.appweaver/orchestrator/workflows/aw-spec-specify.orch.yaml +18 -10
  26. package/assets/.appweaver/scripts/bash/aw-inventory.sh +15 -0
  27. package/assets/.appweaver/scripts/bash/aw-spec-actions.sh +32 -16
  28. package/assets/.appweaver/scripts/bash/aw-xref.sh +3 -0
  29. package/assets/.appweaver/scripts/powershell/aw-inventory.ps1 +15 -0
  30. package/assets/.appweaver/scripts/powershell/aw-spec-actions.ps1 +25 -5
  31. package/assets/.appweaver/scripts/powershell/aw-xref.ps1 +3 -0
  32. package/assets/.appweaver/scripts/python/aw-inventory.py +1829 -63
  33. package/assets/.appweaver/scripts/python/aw-xref.py +500 -81
  34. package/assets/.appweaver/templates/delta-design-template.md +1 -1
  35. package/assets/.appweaver/templates/delta-domain-model-template.md +1 -1
  36. package/assets/.appweaver/templates/delta-spec-template.md +4 -3
  37. package/assets/.appweaver/templates/design-template.md +16 -11
  38. package/assets/.appweaver/templates/okf-concept-template.md +11 -1
  39. package/assets/.appweaver/templates/proposed-inventory-template.md +127 -0
  40. package/assets/.appweaver/templates/source-map-template.md +5 -3
  41. package/assets/.appweaver/templates/spec-template.md +17 -17
  42. package/assets/.appweaver/templates/ui-delta-design-template.md +1 -1
  43. package/assets/.appweaver/templates/ui-design-template.md +4 -4
  44. package/assets/foundations/dotnet/ModularMonolith/ModularMonolith.pack.txt +4489 -361
  45. package/assets/foundations/dotnet/VerticalSlice/VerticalSlice.pack.txt +2274 -125
  46. package/package.json +1 -1
@@ -36,9 +36,11 @@ behaviour
36
36
  FR/US/SC/CN/DC/IC
37
37
  ```
38
38
 
39
- - **One design per aggregate.** The four clean-architecture layers (Domain,
39
+ - **One design per feature.** The four clean-architecture layers (Domain,
40
40
  Application, API, Infrastructure) are **sections** of this one document, each at
41
- design level. Feature = aggregate; commands are user stories inside it. *(This is
41
+ design level. A feature is a capability scope the developer chooses — it may act on
42
+ several aggregates, and several features may act on one; its operations are user
43
+ stories inside it. *(This is
42
44
  the **greenfield** layout, authored from `design-template.md`. See the Brownfield
43
45
  variant below for reverse-engineered / existing-codebase designs.)*
44
46
  - **High-level, not code.** It carries entities/value-objects/events, behaviour as
@@ -170,11 +172,13 @@ the gate and the review able to check coverage, and what keeps CR impact analysi
170
172
 
171
173
  ## Feature granularity (why a command is a delta, not a new feature)
172
174
 
173
- A **feature is the largest slice whose code no other feature owns.** Because
174
- `implement` regenerates the whole feature's code coherently, a shared file
175
- (`src/Domain/Order.cs`) must have exactly **one** owning design. So for an aggregate
176
- with a shared entity, **feature = the aggregate** (`Order`), and individual commands
177
- (`CreateOrder`, `UpdateOrder`, …) are **user stories / deltas** on the same backend-plan.md:
175
+ A **feature is a capability scope the developer chooses** — `Order`, or
176
+ `PatientRegistration` beside `PatientMerge`. It may act on several aggregates, and
177
+ several features may act on one. The aggregates live above every feature: in the bounded
178
+ context's `domain-model.md` (one `## Aggregate:` section each) and in the OKF bundle (one
179
+ concept each, listing the features that act on it). An operation inside a feature's scope
180
+ (`CreateOrder`, `UpdateOrder`, …) is a **user story / delta** on that feature's
181
+ backend-plan.md:
178
182
 
179
183
  - `CreateOrder` = US-1; `UpdateOrder` = US-2.
180
184
  - Adding `UpdateOrder` is an additive CR: a **delta-spec** (`+US-2`), the change's own
@@ -192,8 +196,12 @@ with a shared entity, **feature = the aggregate** (`Order`), and individual comm
192
196
  version (`v1.0 → v1.1`). The two are deliberately separate so a red `analyze-slice`
193
197
  between them is still "fix and re-run" rather than a change that already shipped.
194
198
 
195
- Two command-level features would both claim `src/Domain/Order.cs` and collide —
196
- which is exactly why the aggregate is the boundary.
199
+ **Shared aggregate code is extended, never regenerated.** When two features act on one
200
+ aggregate, its root, entities and repository (`src/Domain/Order.cs`) are realised by
201
+ both plans: each names only the members its feature needs, and `implement` adds those
202
+ members to the existing file instead of writing it afresh. What a feature owns outright
203
+ is its own use-cases, endpoints and handlers. The invariants are the model's, so two
204
+ features never disagree about them.
197
205
 
198
206
  ## Migration status
199
207
 
@@ -121,8 +121,10 @@ workflow:
121
121
  to the standard governing every such behaviour is a UX- delta. A bound the SERVICE
122
122
  must meet stays a CN-, because the service-side gates trace CN- and never read the
123
123
  Experience Standards.
124
- - The new operation is a NEW USER STORY — assign the next id after the highest US-n
125
- in the master — never a new feature/aggregate. Record the regression risk (shared
124
+ - The new operation is a NEW USER STORY of THIS feature — assign the next id after
125
+ the highest US-n in the master. Whether a capability is its own feature is the
126
+ developer's decision, made when they chose this feature; never open, name or
127
+ imply another feature or a new aggregate here. Record the regression risk (shared
126
128
  behaviour that must still hold).
127
129
  - STRUCTURE IS NOT YOURS TO WRITE HERE. A new entity or value object, a changed
128
130
  aggregate boundary, a new domain event, or a new or amended invariant (BR-n) is a
@@ -40,7 +40,7 @@
40
40
  -->
41
41
  # Delta Backend Plan — [CR-ID]: [TITLE]
42
42
 
43
- **Feature (aggregate slice)**: [Feature]
43
+ **Feature**: [Feature]
44
44
 
45
45
  **Lane**: backend
46
46
 
@@ -53,7 +53,7 @@
53
53
  -->
54
54
  # Design: [AGGREGATE]
55
55
 
56
- **Aggregate (feature)**: `[Aggregate]`
56
+ **Feature**: `[Feature]`
57
57
 
58
58
  **Plan home**: `specs/[Context]/[Feature]/backend-plan.md`
59
59
 
@@ -81,7 +81,9 @@
81
81
  aggregate boundary and the BR-nnn invariants; where it does not, the spec
82
82
  carries that structure alone. A domain rule neither states is recorded as a
83
83
  §5 assumption, never smuggled in.
84
- - Feature = aggregate. A later requirement is a DELTA on this document.
84
+ - One feature = one plan. A feature is the capability scope the developer chose
85
+ and may act on several aggregates, as several features may act on one. A later
86
+ requirement on this feature is a DELTA on this document.
85
87
 
86
88
  STRUCTURE FOLLOWS THIS PROJECT'S CODE
87
89
  - [GENERATED_STRUCTURE_NOTE] — form: one or two sentences naming the
@@ -37,7 +37,7 @@
37
37
  -->
38
38
  # Delta Frontend Plan — [CR-ID]: [TITLE]
39
39
 
40
- **Feature (aggregate slice)**: [Feature]
40
+ **Feature**: [Feature]
41
41
 
42
42
  **Base version**: [vX.Y]
43
43
 
@@ -59,7 +59,7 @@
59
59
  -->
60
60
  # UI Design: [AGGREGATE]
61
61
 
62
- **Aggregate (feature)**: `[Aggregate]`
62
+ **Feature**: `[Feature]`
63
63
 
64
64
  **Plan home**: `specs/[Context]/[Feature]/frontend-plan.md`
65
65
 
@@ -189,7 +189,7 @@ workflow:
189
189
  action: verify_context
190
190
  require_non_empty: feature_name
191
191
  on_empty: stop
192
- on_empty_message: "No feature to adopt. Either run /appweaver select-feature first (pick the aggregate from the onboard feature map), then /appweaver adopt; or name it directly: /appweaver adopt <Feature>."
192
+ on_empty_message: "No feature to adopt. Either run /appweaver select-feature first (pick the feature from the boundary map onboard laid out), then /appweaver adopt; or name it directly: /appweaver adopt <Feature>."
193
193
 
194
194
  # WHICH BOUNDED CONTEXT THIS FEATURE LANDS IN, and a refusal to guess — the same gate
195
195
  # select-feature carries, and needed here for the same reason: this stage also creates a
@@ -234,6 +234,26 @@ workflow:
234
234
  - { name: spec_file, from: "$.spec_file" }
235
235
  - { name: spec_exists, from: "$.spec_exists" }
236
236
 
237
+ # WHICH AGGREGATE THIS FEATURE ACTS ON. Usually the same name — one aggregate, one
238
+ # feature, the 1:1 default — but not when the boundary map split an aggregate into
239
+ # capabilities (PatientRegistration and PatientMerge over Patient). The reverse steps
240
+ # below write and judge the model section BY THIS NAME, and every later command finds
241
+ # the feature's part of the model the same way; writing it under the feature's name
242
+ # instead defines one root twice as soon as a sibling feature is adopted. No spec
243
+ # exists yet at this point, so it resolves from the confirmed boundary map, and a
244
+ # feature the map does not split resolves to its own name — exactly what this
245
+ # workflow always used.
246
+ - id: feature_aggregates
247
+ action: execute_script
248
+ script: { ref: xref }
249
+ subcommand: feature-aggregates
250
+ args: ["--feature-dir", "{{feature_dir}}"]
251
+ outputs:
252
+ - { name: aggregates_csv, from: "$.aggregates_csv" }
253
+ - { name: aggregate_sections, from: "$.sections" }
254
+ - { name: aggregate_source, from: "$.source" }
255
+ - { name: aggregate_primary, from: "$.primary" } # the concept this feature's OKF work lands on
256
+
237
257
  # 1a. One-time baseline: refuse to clobber an already-adopted / authored spec.
238
258
  # After adoption, changes go through the CR delta loop, not adopt.
239
259
  # set-feature returns spec_exists as a JSON boolean; the condition path
@@ -247,14 +267,16 @@ workflow:
247
267
  on_empty: stop
248
268
  on_empty_message: "specs/ already has a spec.md for {{feature_name}} — adopt is a one-time baseline. For a change to an adopted slice use the CR delta loop (/appweaver specify → plan → analyze-plan-delta → implement → build → analyze-slice-delta → finalize-delta → analyze-slice → close-delta); to re-adopt from scratch, remove the feature dir first."
249
269
 
250
- # 1b. Resolve this aggregate's OKF bundle concept → its source_files (the code
251
- # that IS the slice) + module dir. The INDEX adopt reverse-engineers FROM;
252
- # the code itself is the source of truth (read during authoring).
270
+ # 1b. Resolve the OKF concept of the aggregate this feature acts on → its source_files
271
+ # + module dir. The bundle is the PROJECT's knowledge and a concept is an aggregate,
272
+ # so a feature that is one capability over a shared aggregate reads the SHARED
273
+ # concept, which lists every capability's code; the source-map below narrows it to
274
+ # this feature's. The code itself is the source of truth (read during authoring).
253
275
  - id: concept
254
276
  action: execute_script
255
277
  script: { ref: inventory }
256
278
  subcommand: concept
257
- args: ["--bundle", "{{bundle_dir}}", "--aggregate", "{{feature_name}}"]
279
+ args: ["--bundle", "{{bundle_dir}}", "--aggregate", "{{aggregate_primary}}"]
258
280
  outputs:
259
281
  - { name: concept_exists, from: "$.exists" }
260
282
  - { name: concept_path, from: "$.concept_path" }
@@ -274,7 +296,7 @@ workflow:
274
296
  action: verify_context
275
297
  require_true: concept_exists
276
298
  on_false: stop
277
- on_false_message: "No feature '{{feature_name}}' in the onboard feature map under {{bundle_dir}}. If you have not onboarded, run /appweaver onboard first, then use the exact feature name from {{bundle_dir}}/aggregates/index.md. If '{{feature_name}}' is a NEW feature (no existing code), do NOT adopt it — author it through the forward pipeline (/appweaver specify → plan → implement), which onboard already made brownfield-ready."
299
+ on_false_message: "No concept for the aggregate '{{aggregate_primary}}' — the one feature '{{feature_name}}' acts on — in the onboard bundle under {{bundle_dir}}. If its bounded context has not been onboarded, run /appweaver onboard <Context> first; {{bundle_dir}}/aggregates/index.md lists each aggregate with the features that act on it. If '{{feature_name}}' is a NEW feature (no existing code), do NOT adopt it — author it through the forward pipeline (/appweaver specify → plan → implement), which onboard already made brownfield-ready."
278
300
 
279
301
  # Governance prerequisite: the reverse steps below are authored AGAINST the project
280
302
  # onboard established (the constitution halves + guardrails/projects/<project>/ rule set).
@@ -291,6 +313,16 @@ workflow:
291
313
  - { name: gov_domain, from: "$.domain" }
292
314
  - { name: rulemap_exists, from: "$.rulemap_exists" }
293
315
 
316
+ # ⌛ OPEN, now that onboard runs once per BOUNDED CONTEXT: this gate asks whether
317
+ # ANY context has been onboarded, and it needs to ask whether THIS FEATURE'S
318
+ # context has. The narrower question is already answered next door — require_concept
319
+ # stops when the feature has no OKF concept, and a feature only has one once its
320
+ # context was onboarded — so the sequence is safe today and the gap is in what the
321
+ # message says, not in what it lets through. Tightening it properly means resolving
322
+ # the feature's context from the concept's `bounded_context:` stamp and naming that
323
+ # context in the refusal. Related and unresolved: `fidelity_gate` is per feature and
324
+ # has no context-level equivalent — a context-wide one would fail a whole context on
325
+ # a single mismapped feature.
294
326
  - id: require_governance
295
327
  action: verify_context
296
328
  require_true: rulemap_exists
@@ -451,7 +483,7 @@ workflow:
451
483
  output_target: { type: file, path: "{{spec_file}}" }
452
484
  task: |
453
485
  Reverse-engineer the MASTER specification (the target spec.md in context)
454
- for the aggregate '{{feature_name}}' FROM ITS CODE. The bundle concept (in
486
+ for the feature '{{feature_name}}' FROM ITS CODE. The bundle concept (in
455
487
  context) is the INDEX — its `source_files` list the code that IS this slice;
456
488
  READ those files (under {{module_dir}}, listed below) as the SOURCE OF TRUTH.
457
489
  The concept is a summary; the CODE is authoritative — if they disagree, the
@@ -459,7 +491,13 @@ workflow:
459
491
 
460
492
  Capture what the code ACTUALLY DOES, as-built (bugs/quirks included), in the
461
493
  template's vocabulary:
462
- - The feature IS the aggregate; set the Aggregate field, keep US-1.
494
+ - THIS FEATURE ACTS ON {{aggregates_csv}}. Write exactly that on the Domain Model
495
+ Reference's `**Acts on aggregate(s)**:` line, each name in backticks — every later
496
+ command finds this feature's part of the domain model by reading that line. The
497
+ feature is the capability scope the developer confirmed in the boundary map,
498
+ whatever it is named: its user stories are that capability's operations and no
499
+ others. Sharing a name with an aggregate does not make it that aggregate's only
500
+ feature — other features may act on the same aggregate. Keep US-1.
463
501
  - Each command / endpoint / use-case the code exposes is a USER STORY (US-n),
464
502
  never a separate feature. Derive its Given/When/Then from the code's paths.
465
503
  - System Requirements in EARS (THE SYSTEM SHALL / WHEN / IF…THEN / WHILE /
@@ -501,7 +539,7 @@ workflow:
501
539
  rule the code does not exhibit. Behaviour that is clearly present is
502
540
  "(certain)".
503
541
  - Reference the source-map: note that legacy-artefact ↔ requirement links
504
- live in `specs/{{feature_name}}/source-map.md` (authored next).
542
+ live in `{{feature_dir}}/source-map.md` (authored next).
505
543
  - This is v1.0: fill the Change History row; keep US-1's "Added: v1.0".
506
544
  - Business-facing + testable; NO implementation mechanics (an external interop
507
545
  OBLIGATION is an IC- requirement, never the adapter design).
@@ -538,20 +576,32 @@ workflow:
538
576
  output_target: { type: file, path: "{{domain_model}}" }
539
577
  task: |
540
578
  Reverse-engineer the BOUNDED CONTEXT's DOMAIN MODEL (the target domain-model.md in
541
- context) for the aggregate '{{feature_name}}', FROM ITS CODE. The reversed spec (in
542
- context) names the aggregate and cites the invariant ids; this document DEFINES them.
543
- READ the source files listed below — the code is the SOURCE OF TRUTH.
579
+ context) for the aggregate(s) the feature '{{feature_name}}' acts on —
580
+ {{aggregates_csv}} — FROM ITS CODE. The reversed spec (in context) names them and
581
+ cites the invariant ids; this document DEFINES them. READ the source files listed
582
+ below — the code is the SOURCE OF TRUTH.
544
583
 
545
- - ADD ONE `## Aggregate: <Name>` SECTION for this feature's aggregate: its boundary
584
+ - THE SECTION IS NAMED AFTER THE AGGREGATE, NEVER THE FEATURE: {{aggregate_sections}},
585
+ exactly. Every later command finds this feature's part of the model by that
586
+ heading. Where the feature and the aggregate share a name the two are the same
587
+ thing; where they differ, a section headed '{{feature_name}}' is a defect — the
588
+ next feature over the same aggregate would define its root a second time.
589
+ - IF THAT SECTION ALREADY EXISTS, another feature over the same aggregate was adopted
590
+ first. EXTEND it and never write a second one: add `{{feature_name}}` to its
591
+ `Serving specs` line, and add only what THIS feature's code evidences that the
592
+ section lacks — an entity, a lifecycle state, a BR-nnn numbered after the highest
593
+ one already there. Leave what the earlier feature wrote exactly as it is.
594
+ - OTHERWISE ADD ONE `## Aggregate: <Name>` SECTION per aggregate, `{{feature_name}}`
595
+ on its `Serving specs` line: its boundary
546
596
  (owns / references by id / out of scope), entities, value objects, domain events,
547
597
  lifecycle states, and every always-true invariant the code enforces as a stable
548
598
  BR-nnn with its RULE TEXT. The spec cites those ids and never restates them.
549
599
  - IF THE MODEL ALREADY HAS CONTENT, this is the context's SECOND (or later) adopted
550
- slice. ADD this aggregate as a new section and EXTEND the ubiquitous language. Do
551
- NOT rewrite, renumber or "tidy" the aggregates already there — other specs cite
552
- their ids, and a renumbered BR breaks every citation silently. Add
553
- `{{feature_name}}` to the `Serving specs` line of any existing aggregate this
554
- slice also acts on.
600
+ slice. ADD any of this feature's aggregates not already there as a new section and
601
+ EXTEND the ubiquitous language. Do NOT rewrite, renumber or "tidy" the aggregates
602
+ already there — other specs cite their ids, and a renumbered BR breaks every
603
+ citation silently. Add `{{feature_name}}` to the `Serving specs` line of any
604
+ existing aggregate this slice also acts on.
555
605
  - MODEL WHAT THE CODE EXHIBITS, NOT WHAT GOOD DESIGN WOULD WANT. Classify entities
556
606
  vs value objects by whether the code gives the thing an IDENTITY; draw the
557
607
  boundary where the code actually draws it (what it loads and saves together).
@@ -670,7 +720,7 @@ workflow:
670
720
  task: |
671
721
  Reverse-engineer the FRONTEND LANE's master plan (the target frontend-plan.md
672
722
  in context — it is this project's code-derived UI plan template) for the
673
- aggregate '{{feature_name}}', FROM ITS UI CODE. Fill the template; preserve its
723
+ feature '{{feature_name}}', FROM ITS UI CODE. Fill the template; preserve its
674
724
  section order.
675
725
 
676
726
  THE UI CODE IS THE SOURCE OF TRUTH — describe the surface AS-BUILT, quirks
@@ -718,13 +768,19 @@ workflow:
718
768
  output_target: { type: file, path: "{{feature_dir}}/source-map.md" }
719
769
  task: |
720
770
  Author the source-map sidecar (the target in context) from the template,
721
- for aggregate '{{feature_name}}'. It is the SOURCE ANCHOR of the traceability
771
+ for the feature '{{feature_name}}'. It is the SOURCE ANCHOR of the traceability
722
772
  spine — it keeps spec.md / backend-plan.md clean while recording provenance here.
723
773
  - One row per legacy artefact in the bundle concept's `source_files` (read
724
774
  them under {{module_dir}}): map each to the spec ids (FR/BR/US/CN/DC/IC)
725
775
  and the backend-plan element (§/name) that now describe it, with CONFIDENCE
726
776
  (certain | inferred). EVERY source file MUST appear — the Fidelity gate
727
777
  checks full coverage both ways.
778
+ - THE CONCEPT IS THE AGGREGATE'S ('{{aggregate_primary}}') and lists every feature's
779
+ code on it. Where its `features:` names features besides '{{feature_name}}', a
780
+ file that serves ONLY one of those sibling capabilities is not this feature's:
781
+ record it under Unmapped / open as `sibling: <Feature>`, never as a mapped row.
782
+ The mapped rows ARE this feature's file list — later commands hand exactly those
783
+ files to their fix loops.
728
784
  - Where this slice has a UI half, its files map the same way, to the UX-/US-
729
785
  they realise and the frontend-plan.md section (§/name) that describes them —
730
786
  one spine, both lanes. UI files to map: {{ui_source_files}}
@@ -772,17 +828,24 @@ workflow:
772
828
  route it describes that the UI code does not have is a fabricated-plan, and a
773
829
  UI file with no home in the source-map is unmapped-code. Classify findings:
774
830
  - BLOCKING (structural — set ok=false if any):
775
- (1) unmapped-code: a source file with NO home in the source-map;
831
+ (1) unmapped-code: a source file with NO home in the source-map — one recorded
832
+ there as `sibling: <Feature>` has a home;
776
833
  (2) fabricated-spec / fabricated-model / fabricated-plan: a spec id, model
777
834
  element or plan element with NO basis in the code (reverse-engineering must
778
835
  not invent behaviour or structure). For the MODEL specifically: an entity or
779
836
  value object the code does not have, an aggregate boundary it does not draw,
780
837
  a lifecycle state it never enters, or a BR-nnn whose rule nothing enforces.
781
838
  An anemic slice reversing as an anemic model is CORRECT, not a gap.
782
- - ONLY ONE AGGREGATE SECTION IS THIS RUN'S. domain-model.md is the CONTEXT's and
783
- may already carry sections from earlier adopted slices — judge ONLY the aggregate
784
- '{{feature_name}}' reversed here. Do not report a sibling aggregate as unmapped
785
- because this slice's code does not contain it.
839
+ - ONLY THIS FEATURE'S SECTIONS ARE THIS RUN'S. domain-model.md is the CONTEXT's and
840
+ may already carry sections from earlier adopted slices — judge ONLY
841
+ {{aggregate_sections}}. Do not report a sibling aggregate as unmapped because this
842
+ slice's code does not contain it. Where one of those sections' `Serving specs`
843
+ lists an EARLIER feature as well, the aggregate is shared: judge what this run
844
+ ADDED to it, and do not score the earlier feature's entities, states or
845
+ invariants as fabricated because this slice's code does not touch them.
846
+ - ALSO BLOCKING (fabricated-model): a `## Aggregate:` section this run created under
847
+ a name that is not one of {{aggregates_csv}} — most often '{{feature_name}}', the
848
+ feature's own name, used as if it were the aggregate's.
786
849
  - REVIEW (does NOT block ok — these are the Adopt review worklist, closed out
787
850
  of band): items marked "(inferred)"; as-built quirks the code shows but the
788
851
  spec under-states; invariants the code enforces that may be missing/weak;
@@ -804,7 +867,7 @@ workflow:
804
867
  context: ["{{spec_file}}", "{{domain_model}}", "{{backend_plan}}", "{{feature_dir}}/source-map.md"]
805
868
  output_target: { type: file, path: "{{feature_dir}}/adopt-review-report.md" }
806
869
  task: |
807
- Write an Adopt Review report (the target) for aggregate '{{feature_name}}' —
870
+ Write an Adopt Review report (the target) for the feature '{{feature_name}}' (acting on {{aggregates_csv}}) —
808
871
  the out-of-band worklist for this reverse-engineered baseline. Markdown:
809
872
  - "# Adopt Review — {{feature_name}}" + today's date + "origin: reverse-engineered".
810
873
  - "verdict:" line — "BLOCKING ✗ · <n>" if blocking gaps exist, else "REVIEW ○ · <m> item(s)".
@@ -829,7 +892,7 @@ workflow:
829
892
  action: verify_context
830
893
  require_true: fidelity_ok
831
894
  on_false: stop
832
- on_false_message: "Fidelity FAILED for {{feature_name}} — the reversed documents do not match the code (blocking gaps: {{blocking_gaps}}). See specs/{{feature_name}}/adopt-review-report.md. Fix the unmapped code / remove fabricated items (re-read the source, refine spec.md, {{domain_model}}, backend-plan.md and source-map.md), then re-run /appweaver adopt (after removing the dir) or resolve out of band. A `fabricated-model` gap means the reverse gave this slice structure its code does not have — an entity, a boundary, a lifecycle state or a BR-nnn nothing enforces; the fix is to record the domain as thin as the code really is, never to leave it flattering."
895
+ on_false_message: "Fidelity FAILED for {{feature_name}} — the reversed documents do not match the code (blocking gaps: {{blocking_gaps}}). See {{feature_dir}}/adopt-review-report.md. Fix the unmapped code / remove fabricated items (re-read the source, refine spec.md, {{domain_model}}, backend-plan.md and source-map.md), then re-run /appweaver adopt (after removing the dir) or resolve out of band. A `fabricated-model` gap means the reverse gave this slice structure its code does not have — an entity, a boundary, a lifecycle state or a BR-nnn nothing enforces; the fix is to record the domain as thin as the code really is, never to leave it flattering."
833
896
 
834
897
  # ═══════════ OKF CONCEPT — COMPLETE the shallow scan concept (deep reverse) ═══
835
898
  # onboard wrote this aggregate's OKF concept SHALLOW (a discovery stub — enough to pick the
@@ -854,29 +917,37 @@ workflow:
854
917
  tools: "Read,Write,Edit"
855
918
  permission_mode: acceptEdits
856
919
  task: |
857
- COMPLETE the OKF aggregate concept for '{{feature_name}}' at {{concept_path}} — onboard
858
- wrote it SHALLOW (a discovery stub from the whole-repo scan). The deep reverse has now
920
+ COMPLETE the OKF concept of the aggregate '{{aggregate_primary}}' at {{concept_path}} —
921
+ the aggregate this run adopted the feature '{{feature_name}}' over. onboard wrote it
922
+ SHALLOW (a discovery stub from the whole-repo scan). The deep reverse has now
859
923
  produced a TRUSTWORTHY spec.md + domain-model.md + backend-plan.md + source-map.md (in
860
924
  context); deepen the
861
925
  concept to match and LINK those artifacts. Follow the OKF concept template in context.
926
+ THE CONCEPT IS THE AGGREGATE'S, NOT THE FEATURE'S. Where its `features:` names more
927
+ than one feature, another feature's adopt may already have deepened it, and nothing
928
+ that run wrote is this run's to remove — reconcile ADDITIVELY.
862
929
  RECONCILE, do NOT rewrite:
863
930
  - DEEPEN the responsibilities + the owned entities / value objects / domain events /
864
931
  commands (user stories) / API endpoints / persistence to match the reversed spec,
865
932
  model and plan — the ENTITIES and VALUE OBJECTS come from domain-model.md, which is
866
933
  the authority on them; correct any drift the shallow scan got wrong (the deep docs
867
- win).
868
- - LINK the deep artifacts: add bundle links to specs/{{feature_name}}/spec.md,
934
+ win). A sibling capability's commands and endpoints stay where they are.
935
+ - LINK the deep artifacts: add bundle links to {{feature_dir}}/spec.md,
869
936
  backend-plan.md, source-map.md and the context's domain-model.md so the concept
870
- points at the deep truth.
871
- - Refresh `source_files` from the source-map (grouped by the code's OWN folders, per
872
- OKF), and set `timestamp` to the baseline (inventory HEAD) commit time.
873
- - Update frontmatter: keep `origin: reverse-engineered`; mark the concept ADOPTED /
874
- deep-reversed (no longer a shallow scan stub) and stamp `generated_by: aw-spec-adopt`.
937
+ points at the deep truth. Keep every link another feature's adopt added.
938
+ - UNION `source_files` with the source-map's mapped files (grouped by the code's OWN
939
+ folders, per OKF) — never drop a file already listed, which may be a sibling
940
+ capability's — and set `timestamp` to the baseline (inventory HEAD) commit time.
941
+ - Update frontmatter: keep `origin: reverse-engineered`; make sure `features:`
942
+ includes '{{feature_name}}'; stamp `generated_by: aw-spec-adopt`; and record that
943
+ '{{feature_name}}' is ADOPTED / deep-reversed. Where several features act on the
944
+ aggregate, record WHICH of them are adopted rather than marking the whole concept
945
+ done — the others are still shallow.
875
946
  - Resolve any `open_questions` the deep reverse answered; keep the rest.
876
947
  - Do NOT touch the concept's Depends-On / Cross-References sections (xref_record
877
948
  maintains those next), and do NOT edit any OTHER aggregate concept.
878
- Append a one-line `log.md` entry (bundle root) under today's date noting '{{feature_name}}'
879
- was deep-adopted. Write ONLY {{concept_path}} (+ its dir index.md / bundle log.md if a
949
+ Append a one-line `log.md` entry (bundle root) under today's date noting the feature
950
+ '{{feature_name}}' of '{{aggregate_primary}}' was deep-adopted. Write ONLY {{concept_path}} (+ its dir index.md / bundle log.md if a
880
951
  link or description changed). Report what you deepened.
881
952
  outputs:
882
953
  - { name: concept_completed, from: "$.files" }
@@ -886,14 +957,17 @@ workflow:
886
957
  # (its External integrations table, IC-); record them into the shared OKF bundle — a
887
958
  # Cross-References row in every
888
959
  # SIBLING aggregate this slice uses (their inventory concepts already exist, so this
889
- # resolves them rather than stubbing), plus this slice's own Depends-On. Same
960
+ # resolves them rather than stubbing), plus the Depends-On of the aggregate this
961
+ # feature acts on — the union over every feature on that aggregate, so adopting one
962
+ # capability never erases another's edges. Keyed by --feature-dir so record resolves
963
+ # the aggregate itself rather than being told a feature name. Same
890
964
  # reconcile-on-write helper as forward implement, stamped origin reverse-engineered,
891
965
  # additive to the inventory bundle. Runs only after fidelity passes.
892
966
  - id: xref_record
893
967
  action: execute_script
894
968
  script: { ref: xref }
895
969
  subcommand: record
896
- args: ["--using", "{{feature_name}}", "--design", "{{backend_plan}}", "--bundle", "{{bundle_dir}}", "--origin", "reverse-engineered"]
970
+ args: ["--feature-dir", "{{feature_dir}}", "--design", "{{backend_plan}}", "--bundle", "{{bundle_dir}}", "--origin", "reverse-engineered"]
897
971
  outputs:
898
972
  - { name: xref_wrote, from: "$.wrote" }
899
973
  - { name: xref_edge_count, from: "$.edge_count" }
@@ -1115,7 +1189,12 @@ outputs:
1115
1189
  ui_lane: "{{ui_lane}}"
1116
1190
  ui_count: "{{ui_count}}"
1117
1191
  source_map: "{{feature_dir}}/source-map.md"
1118
- aggregate: "{{feature_name}}"
1192
+ feature: "{{feature_name}}"
1193
+ # What the feature acts on. Equal to the feature on a 1:1 slice; not on a feature that
1194
+ # is one capability over a shared aggregate. aggregate_source says which of the spec,
1195
+ # the boundary map or the feature name answered.
1196
+ aggregate: "{{aggregates_csv}}"
1197
+ aggregate_source: "{{aggregate_source}}"
1119
1198
  source_count: "{{source_count}}"
1120
1199
  review_items: "{{review_items}}"
1121
1200
  concept_completed: "{{concept_completed}}"
@@ -1125,4 +1204,4 @@ outputs:
1125
1204
  composed_file: "{{composed_file}}"
1126
1205
  technical_reconcile_ok: "{{technical_ok}}"
1127
1206
  domain_reconcile_ok: "{{domain_ok}}"
1128
- next: "Adopted {{feature_name}} (spec.md + backend-plan.md + source-map.md, v1.0) and ADDED its aggregate section to the bounded context `{{context}}`'s shared {{domain_model}} — the spec cites BR-nnn by id, the model carries their text, so this slice is checkable by the same gates as a greenfield one and analyze-plan's check 4 now has a model to check against. Other features in this context keep their own aggregate sections untouched. COMPLETED its OKF concept (deepened + linked the deep artifacts; {{xref_edge_count}} cross-slice edge(s) recorded). UI HALF: ui_lane={{ui_lane}} — where it is true this slice also has frontend-plan.md, reversed from the {{ui_count}} UI file(s) the feature map attributes to it; where it is false the repo declared no frontend lane, or this aggregate owns no part of its surface, and a service-only adoption is the correct outcome rather than a gap. The SHARED UI surface (app shell, layout, design system) belongs to no slice and stays in {{bundle_dir}}/architecture/frontend.md. NOTE — governance reconcile is currently HELD (reconcile_governance=false): this run did NOT modify the constitution halves or the guardrails/projects/{{gov_domain}}/ rule set, so they keep onboard's provisional scaffold. Close any items in specs/{{feature_name}}/adopt-review-report.md out of band. The slice is now a NORMAL feature: refine via /appweaver analyze-plan, then drive every change through the CR delta loop (/appweaver specify → plan → analyze-plan-delta → implement → build → analyze-slice-delta → finalize-delta → analyze-slice → close-delta). If a change edits untested legacy behaviour with blast radius, characterize just that surface first (optional on-touch fence). To restore governance reconcile, set reconcile_governance to \"true\" in the workflow vars."
1207
+ next: "Adopted {{feature_name}} (spec.md + backend-plan.md + source-map.md, v1.0) and wrote {{aggregate_sections}} — the model section(s) for the aggregate(s) it acts on, resolved from the {{aggregate_source}} — in the bounded context `{{context}}`'s shared {{domain_model}}. The spec cites BR-nnn by id, the model carries their text, so this slice is checkable by the same gates as a greenfield one and analyze-plan's check 4 now has a model to check against. A section another feature over the same aggregate wrote first was EXTENDED, never duplicated; every other section is untouched. COMPLETED the OKF concept of {{aggregate_primary}} (deepened + linked the deep artifacts; {{xref_edge_count}} cross-slice edge(s) recorded). UI HALF: ui_lane={{ui_lane}} — where it is true this slice also has frontend-plan.md, reversed from the {{ui_count}} UI file(s) the feature map attributes to it; where it is false the repo declared no frontend lane, or this aggregate owns no part of its surface, and a service-only adoption is the correct outcome rather than a gap. The SHARED UI surface (app shell, layout, design system) belongs to no slice and stays in {{bundle_dir}}/architecture/frontend.md. NOTE — governance reconcile is currently HELD (reconcile_governance=false): this run did NOT modify the constitution halves or the guardrails/projects/{{gov_domain}}/ rule set, so they keep onboard's provisional scaffold. Close any items in {{feature_dir}}/adopt-review-report.md out of band. The slice is now a NORMAL feature: refine via /appweaver analyze-plan, then drive every change through the CR delta loop (/appweaver specify → plan → analyze-plan-delta → implement → build → analyze-slice-delta → finalize-delta → analyze-slice → close-delta). If a change edits untested legacy behaviour with blast radius, characterize just that surface first (optional on-touch fence). To restore governance reconcile, set reconcile_governance to \"true\" in the workflow vars."
@@ -153,7 +153,7 @@ workflow:
153
153
  action: verify_context
154
154
  require_non_empty: feature_dir
155
155
  on_empty: stop
156
- on_empty_message: "No active feature. Run /appweaver select-feature first (pick or create the aggregate, e.g. Order), then re-run analyze-plan-delta."
156
+ on_empty_message: "No active feature. Run /appweaver select-feature first (pick or create the feature, e.g. Order or PatientRegistration), then re-run analyze-plan-delta."
157
157
 
158
158
  - id: verify_spec
159
159
  action: verify_context
@@ -122,6 +122,10 @@ vars:
122
122
  be_plan_ctx: ""
123
123
  fe_plan_ctx: ""
124
124
  domain_model: ""
125
+ owned_sections: "" # the model section(s) this feature answers for — feature-aggregates
126
+ shared_with_csv: ""
127
+ one_to_one: "true"
128
+ model_scope_plan: "" # which part of those sections check 4(a) holds the plan to
125
129
 
126
130
  workflow:
127
131
 
@@ -175,11 +179,40 @@ workflow:
175
179
  - { name: domain_model, from: "$.domain_model" }
176
180
  - { name: domain_model_exists, from: "$.domain_model_exists" }
177
181
 
182
+ # WHICH PART OF THAT MODEL CHECK 4 HOLDS THE PLAN TO. A feature is a capability scope the
183
+ # developer chose — it may act on several aggregates, and several features may act on
184
+ # one — so "the plan matches its aggregate's section" is only exact for a feature that
185
+ # alone answers for that aggregate. Resolved from the spec's Acts-on line, then the
186
+ # boundary map, then the feature name.
187
+ - id: feature_aggregates
188
+ action: execute_script
189
+ on_error: continue
190
+ script: { ref: xref }
191
+ subcommand: feature-aggregates
192
+ args: ["--feature-dir", "{{feature_dir}}"]
193
+ outputs:
194
+ - { name: owned_sections, from: "$.owned_sections" }
195
+ - { name: shared_with_csv, from: "$.shared_with_csv" }
196
+ - { name: one_to_one, from: "$.one_to_one" }
197
+
198
+ # A shared section is realised by several plans, each covering what its own feature
199
+ # acts on; flagging every element one plan leaves out would fail each of them for work
200
+ # another feature's plan carries. Ordered pair of sets — no conditional prompt text.
201
+ - id: model_scope_shared
202
+ action: set
203
+ vars:
204
+ model_scope_plan: "{{owned_sections}}, which this feature SHARES with other features: every element the plan realises must carry the model's name and sit inside the model's boundary, but an element of the section the plan leaves out is NOT a gap unless this feature's requirements act on it — another feature's plan realises it"
205
+ - id: model_scope_sole
206
+ condition: "{{one_to_one}} == true"
207
+ action: set
208
+ vars:
209
+ model_scope_plan: "{{owned_sections}}. This feature is the only one that answers for it, so the plan's domain layer must realise that whole section"
210
+
178
211
  - id: require_feature
179
212
  action: verify_context
180
213
  require_non_empty: feature_dir
181
214
  on_empty: stop
182
- on_empty_message: "No active feature. Run /appweaver select-feature first (pick or create the aggregate, e.g. Order), then re-run analyze-plan."
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 analyze-plan."
183
216
 
184
217
  - id: verify_spec
185
218
  action: verify_context
@@ -388,9 +421,10 @@ workflow:
388
421
 
389
422
  CHECK 4 — the plans → domain-model.md (when the model is in context; skip if it
390
423
  is not):
391
- (a) the backend plan's DOMAIN layer matches the model EXACTLY — entities and
392
- value objects are its aggregate section's entities (SAME names), the boundary is
393
- its Boundary block, states are the lifecycle it describes;
424
+ (a) the backend plan's DOMAIN layer matches the model EXACTLY. The section(s)
425
+ this feature answers for: {{model_scope_plan}}. Entities and value objects carry
426
+ the section's names (SAME names), the boundary is its Boundary block, states are
427
+ the lifecycle it describes;
394
428
  (b) EVERY invariant (BR-nnn) the spec cites has a named ENFORCEMENT POINT in the
395
429
  backend plan. An invariant nothing enforces is a HIGH gap; a plan that RESTATES
396
430
  what an invariant says instead of citing it by id is a gap too — the model is the
@@ -182,7 +182,7 @@ workflow:
182
182
  action: verify_context
183
183
  require_non_empty: feature_dir
184
184
  on_empty: stop
185
- on_empty_message: "No active feature. Run /appweaver select-feature first (pick the aggregate, e.g. Order), then re-run analyze-slice-delta."
185
+ on_empty_message: "No active feature. Run /appweaver select-feature first (pick the feature, e.g. Order or PatientRegistration), then re-run analyze-slice-delta."
186
186
 
187
187
  # 3. WHICH CHANGE. Same selection contract as plan, analyze-plan-delta and implement:
188
188
  # cr-list enumerates the open changes (evidence = changes/<CR-id>/ on disk), a bare CR