aw-spec-cli 0.1.19 → 0.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/assets/.appweaver/DESIGN-FORMAT.md +17 -9
  2. package/assets/.appweaver/ORCH-DIALECT.md +8 -3
  3. package/assets/.appweaver/orchestrator/modules/delta-spec.orch.yaml +26 -3
  4. package/assets/.appweaver/orchestrator/orchestrator.py +65 -3
  5. package/assets/.appweaver/orchestrator/rev-templates/delta-design-template.skeleton.md +1 -1
  6. package/assets/.appweaver/orchestrator/rev-templates/design-template.skeleton.md +4 -2
  7. package/assets/.appweaver/orchestrator/rev-templates/ui-delta-design-template.skeleton.md +1 -1
  8. package/assets/.appweaver/orchestrator/rev-templates/ui-design-template.skeleton.md +1 -1
  9. package/assets/.appweaver/orchestrator/workflows/aw-spec-adopt.orch.yaml +121 -42
  10. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan-delta.orch.yaml +7 -7
  11. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan.orch.yaml +145 -52
  12. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice-delta.orch.yaml +18 -10
  13. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice.orch.yaml +306 -28
  14. package/assets/.appweaver/orchestrator/workflows/aw-spec-build.orch.yaml +1 -1
  15. package/assets/.appweaver/orchestrator/workflows/aw-spec-characterize.orch.yaml +1 -1
  16. package/assets/.appweaver/orchestrator/workflows/aw-spec-close-delta.orch.yaml +1 -1
  17. package/assets/.appweaver/orchestrator/workflows/aw-spec-constitution.orch.yaml +4 -4
  18. package/assets/.appweaver/orchestrator/workflows/aw-spec-deploy.orch.yaml +1 -1
  19. package/assets/.appweaver/orchestrator/workflows/aw-spec-finalize-delta.orch.yaml +114 -30
  20. package/assets/.appweaver/orchestrator/workflows/aw-spec-foundation.orch.yaml +140 -36
  21. package/assets/.appweaver/orchestrator/workflows/aw-spec-implement.orch.yaml +385 -61
  22. package/assets/.appweaver/orchestrator/workflows/aw-spec-inventory.orch.yaml +217 -174
  23. package/assets/.appweaver/orchestrator/workflows/aw-spec-onboard.orch.yaml +407 -107
  24. package/assets/.appweaver/orchestrator/workflows/aw-spec-parity.orch.yaml +1 -1
  25. package/assets/.appweaver/orchestrator/workflows/aw-spec-plan.orch.yaml +49 -11
  26. package/assets/.appweaver/orchestrator/workflows/aw-spec-recall-delta.orch.yaml +1 -1
  27. package/assets/.appweaver/orchestrator/workflows/aw-spec-run.orch.yaml +1 -1
  28. package/assets/.appweaver/orchestrator/workflows/aw-spec-select-feature.orch.yaml +29 -23
  29. package/assets/.appweaver/orchestrator/workflows/aw-spec-select-stack.orch.yaml +47 -23
  30. package/assets/.appweaver/orchestrator/workflows/aw-spec-specify.orch.yaml +19 -11
  31. package/assets/.appweaver/scripts/bash/aw-inventory.sh +15 -0
  32. package/assets/.appweaver/scripts/bash/aw-spec-actions.sh +683 -92
  33. package/assets/.appweaver/scripts/bash/aw-xref.sh +3 -0
  34. package/assets/.appweaver/scripts/powershell/aw-inventory.ps1 +15 -0
  35. package/assets/.appweaver/scripts/powershell/aw-spec-actions.ps1 +625 -64
  36. package/assets/.appweaver/scripts/powershell/aw-xref.ps1 +3 -0
  37. package/assets/.appweaver/scripts/python/aw-crstate.py +194 -0
  38. package/assets/.appweaver/scripts/python/aw-foundation.py +151 -0
  39. package/assets/.appweaver/scripts/python/aw-inventory.py +1829 -63
  40. package/assets/.appweaver/scripts/python/aw-payload-parity.py +3 -0
  41. package/assets/.appweaver/scripts/python/aw-plan-coverage.py +423 -0
  42. package/assets/.appweaver/scripts/python/aw-stack.py +251 -28
  43. package/assets/.appweaver/scripts/python/aw-testlog.py +148 -0
  44. package/assets/.appweaver/scripts/python/aw-xref.py +500 -81
  45. package/assets/.appweaver/stack-profiles/dotnet-webapi.json +1 -0
  46. package/assets/.appweaver/stack-profiles/java-spring.json +1 -0
  47. package/assets/.appweaver/stack-profiles/node-express.json +1 -0
  48. package/assets/.appweaver/stack-profiles/python-fastapi.json +1 -0
  49. package/assets/.appweaver/templates/constitution-domain-template.md +17 -2
  50. package/assets/.appweaver/templates/delta-design-template.md +1 -1
  51. package/assets/.appweaver/templates/delta-domain-model-template.md +1 -1
  52. package/assets/.appweaver/templates/delta-spec-template.md +4 -3
  53. package/assets/.appweaver/templates/design-template.md +16 -11
  54. package/assets/.appweaver/templates/okf-concept-template.md +11 -1
  55. package/assets/.appweaver/templates/proposed-inventory-template.md +127 -0
  56. package/assets/.appweaver/templates/source-map-template.md +5 -3
  57. package/assets/.appweaver/templates/spec-template.md +17 -17
  58. package/assets/.appweaver/templates/ui-delta-design-template.md +1 -1
  59. package/assets/.appweaver/templates/ui-design-template.md +4 -4
  60. package/assets/foundations/dotnet/ModularMonolith/ModularMonolith.pack.txt +4489 -361
  61. package/assets/foundations/dotnet/VerticalSlice/VerticalSlice.pack.txt +2274 -125
  62. package/assets/guardrails/projects/dotnet-default-project/domain-rule-bindings.md +10 -6
  63. package/assets/guardrails/projects/dotnet-default-project/tech-rule-bindings.md +4 -0
  64. package/assets/guardrails/technology/dotnet-webapi/auditing.md +3 -1
  65. package/assets/guardrails/technology/dotnet-webapi/coding-standards.md +5 -3
  66. package/assets/guardrails/technology/dotnet-webapi/cqrs.md +5 -1
  67. package/assets/guardrails/technology/dotnet-webapi/domain-driven-design.md +13 -5
  68. package/assets/guardrails/technology/dotnet-webapi/examples/cqrs.md +2 -0
  69. package/assets/guardrails/technology/dotnet-webapi/examples/domain-driven-design.md +6 -2
  70. package/assets/guardrails/technology/dotnet-webapi/framework-api-map.md +70 -0
  71. package/assets/guardrails/technology/dotnet-webapi/technology-constitution.md +3 -0
  72. package/assets/guardrails/technology/dotnet-webapi/testing.md +10 -0
  73. package/assets/guardrails/technology/nextjs/ui-constitution.md +8 -1
  74. package/package.json +1 -1
  75. package/assets/.appweaver/orchestrator/.state/smoke.state.json +0 -32
@@ -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
 
@@ -90,7 +90,7 @@ the raw object, not its JSON string.
90
90
  | `set` | engine | assign vars (`vars:` or decl-style `parameters:`); raw-object aware |
91
91
  | `build_context` | engine | REPLACE the per-request envelope: `system:`/`context:` file lists (+ `context_from: <var>` holding paths a prior generation reported, + `vars:`) |
92
92
  | `load_standards` | engine | F4 — append to the **sticky standards set** prepended to every system block (dedup, `optional:`, `files:`/`files_from_variable`). This is the byte-stable, prompt-cacheable prefix |
93
- | `execute_script` | engine | F1 — run a `scripts:` ref (`subcommand:` + `args:`; sh/ps1 via `--os`); stdout's last line = JSON, bound via `outputs[]`; `on_error: continue` tolerated; cached across resumes; runs for real even in mock transport |
93
+ | `execute_script` | engine | F1 — run a `scripts:` ref (`subcommand:` + `args:`; sh/ps1 via `--os`); stdout's last line = JSON, bound via `outputs[]`; `on_error: continue` tolerates a script that ran and exited non-zero (its outputs bind empty) — never an undeclared ref, which the pre-flight refuses before step 1; cached across resumes; runs for real even in mock transport |
94
94
  | `verify_context` | engine | F2 — gates: `required_files` (+`allow_missing`), `require_non_empty` (+`on_empty[_message]`), `require_true` (+`on_false[_message]`), `require_value`+`equals` (+`on_mismatch[_message]`). `stop` raises a terminal stop |
95
95
  | `agent_generate` | fresh agent | demo contract (code work): agent DECIDES file paths per the loaded standards, writes with its tools, reports `files[]` in the trailing ```json fence. `sdk-apply` = read-only generation + invoking agent applies |
96
96
  | `agent_review` | fresh agent | payload-only review of `review_of_var`/`review_of` files (inlined into THIS request, never sticky) → `{pass, issues}` |
@@ -159,8 +159,13 @@ orchestrator.py <workflow.orch.yaml>
159
159
  ## Validation & smoke
160
160
 
161
161
  - `--validate` every workflow/module (unknown actions/strategies, duplicate
162
- ids, script refs + files on disk, `output_target.path`, module paths,
163
- unproduced `{{vars}}`, dialect header).
162
+ ids, script refs + files on disk — including the refs of every included
163
+ module, which resolve against the caller's `scripts:` — `output_target.path`,
164
+ module paths, unproduced `{{vars}}`, dialect header).
165
+ - Every run starts with the same checks. A workflow with an ERROR is not
166
+ started: the engine exits 1 with `status: stopped` and the defects in the
167
+ reason, before any step runs and before a checkpoint response is ingested.
168
+ Warnings never block.
164
169
  - `workflows/_smoke.orch.yaml` exercises every construct with no model and no
165
170
  writes outside `.smoke-out`: run it `--non-interactive` (CI) and with
166
171
  `--checkpoint` (protocol walk-through).
@@ -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
@@ -138,7 +140,7 @@ workflow:
138
140
  touches the same aggregate or the same FR ids, say so under "Other open deltas"
139
141
  in Impact. If THIS change only makes sense after one of them lands, say so in the
140
142
  Wave rationale — that is a wave dependency, and the wave must then be RAISED above
141
- that delta's (report it; `cr-wave` applies it). Do not silently assume their
143
+ that delta's — name it under `## Depends on` and the wave is raised from that. Do not silently assume their
142
144
  changes: until merged they are proposals, not the master.
143
145
  - Do NOT edit the Wave / Created / Base version headers — scripts stamp those.
144
146
  Change request: {{USER_INPUT}}
@@ -181,4 +183,25 @@ workflow:
181
183
  entry MUST sit in a LOWER wave than this delta ({{wave}}). "None" when it stands
182
184
  alone. Reading another delta is NOT depending on it — only assuming its changes is.
183
185
  - Do NOT edit the Wave / Created / Base context version headers — scripts stamp those.
186
+ Naming a delta under "## Depends on" is what RAISES this change's wave, immediately
187
+ after you write it; you never set the number yourself.
184
188
  Change request: {{USER_INPUT}}
189
+
190
+ # 4. THE WAVE THE DOCUMENTS THEMSELVES IMPLY. cr-setup stamped a provisional wave before
191
+ # either half existed — the highest already open, which is right for a change that
192
+ # stands alone and wrong for one that builds on another: it lands at its predecessor's
193
+ # wave, where the blocker fence reads the two as independent and lets them merge in
194
+ # either order. Now that "## Depends on" is written, that declaration decides: the wave
195
+ # is raised to one above the highest delta named there, in BOTH halves, so the number
196
+ # and the prose cannot drift apart. A change that names none keeps what it has, and one
197
+ # already sitting higher is left alone. The merge fence re-checks the same rule.
198
+ - id: derive_wave
199
+ action: execute_script
200
+ on_error: continue
201
+ script: { ref: actions }
202
+ subcommand: cr-wave-derive
203
+ args: ["--cr-id", "{{cr_id}}", "--stamp"]
204
+ outputs:
205
+ - { name: wave, from: "$.wave" }
206
+ - { name: wave_depends, from: "$.depends_on" }
207
+ - { name: wave_message, from: "$.message" }
@@ -1458,6 +1458,39 @@ def _refs_in(v):
1458
1458
  return found
1459
1459
 
1460
1460
 
1461
+ def _module_script_refs(mpath, base_dir, seen):
1462
+ """(step id, script ref) for every execute_script step an included module runs,
1463
+ following nested includes. Modules that cannot be read contribute nothing — the
1464
+ include check already warns about a module missing from disk."""
1465
+ path = next((c for c in (mpath, os.path.join(base_dir, mpath)) if c and os.path.exists(c)), "")
1466
+ if not path or path in seen:
1467
+ return []
1468
+ seen.add(path)
1469
+ try:
1470
+ with open(path, encoding="utf-8") as fh:
1471
+ mwf = load_yaml(fh.read())
1472
+ except Exception:
1473
+ return []
1474
+ out = []
1475
+
1476
+ def walk(ss):
1477
+ for s in ss or []:
1478
+ if not isinstance(s, dict):
1479
+ continue
1480
+ p = s.get("parameters") or s
1481
+ if s.get("action") == "execute_script":
1482
+ ref = (p.get("script") or {}).get("ref")
1483
+ if ref:
1484
+ out.append((s.get("id") or "step", ref))
1485
+ elif s.get("action") == "include" and s.get("module"):
1486
+ out.extend(_module_script_refs(s["module"], base_dir, seen))
1487
+ elif s.get("action") == "loop":
1488
+ walk(s.get("steps"))
1489
+
1490
+ walk((mwf or {}).get("workflow"))
1491
+ return out
1492
+
1493
+
1461
1494
  def validate_workflow(wf, base_dir):
1462
1495
  errors, warnings = [], []
1463
1496
  if not isinstance(wf, dict):
@@ -1551,6 +1584,13 @@ def validate_workflow(wf, base_dir):
1551
1584
  errors.append(f"{loc}: include needs 'module'")
1552
1585
  elif not (os.path.exists(m) or os.path.exists(os.path.join(base_dir, m))):
1553
1586
  warnings.append(f"{loc}: include module not found on disk: {m}")
1587
+ elif scripts:
1588
+ # A module declares no scripts: of its own — its refs resolve against
1589
+ # THIS workflow's map, so an undeclared one is this workflow's defect.
1590
+ for mid, ref in _module_script_refs(m, base_dir, set()):
1591
+ if ref not in scripts:
1592
+ errors.append(f"{loc}: included step {mid} uses script ref "
1593
+ f"'{ref}', not declared in top-level scripts:")
1554
1594
  if action == "agent_query" and not step.get("outputs"):
1555
1595
  warnings.append(f"{loc}: agent_query has no outputs[] to bind")
1556
1596
  if action == "agent_author" and not (p.get("output_target") or {}).get("path"):
@@ -1682,6 +1722,31 @@ def main(argv=None):
1682
1722
  print(f"\n{opts.workflow}: {len(errors)} error(s), {len(warnings)} warning(s)")
1683
1723
  return 1 if errors else 0
1684
1724
 
1725
+ # ---- pre-flight: a workflow --validate rejects is never started ----------
1726
+ # An undeclared script ref, an unknown action or a duplicate step id is a defect in
1727
+ # the workflow file, and no run can recover from it — `on_error: continue` covers a
1728
+ # script that RAN and failed, not one the workflow never declared. Found mid-run, it
1729
+ # strands the operator after the questions were answered; found here, nothing has
1730
+ # happened yet. Runs BEFORE the checkpoint ingest, so a refused resume leaves the
1731
+ # pending response on disk for the run after the fix. Warnings do not block.
1732
+ try:
1733
+ with open(opts.workflow, encoding="utf-8") as fh:
1734
+ wf = load_yaml(fh.read())
1735
+ errors, _ = validate_workflow(wf, os.path.dirname(os.path.abspath(opts.workflow)))
1736
+ except Exception as e:
1737
+ wf, errors = None, [f"parse: {e}"]
1738
+ if errors:
1739
+ reason = (f"workflow {opts.workflow} has {len(errors)} defect(s) and was not started: "
1740
+ + "; ".join(errors)
1741
+ + " — fix the workflow file (python3 .appweaver/orchestrator/orchestrator.py "
1742
+ "<workflow> --validate lists them) and re-run.")
1743
+ if opts.json:
1744
+ print(json.dumps({"status": "stopped", "reason": reason, "errors": errors}, indent=2))
1745
+ else:
1746
+ print("\n=== STOPPED ===")
1747
+ print(reason)
1748
+ return 1
1749
+
1685
1750
  # ---- checkpoint mode: load prior state + ingest the agent's response ----
1686
1751
  # (before profile/strategy resolution, so a bare `--checkpoint` resume
1687
1752
  # restores the original run's profile/strategy/model/os and prefills)
@@ -1787,9 +1852,6 @@ def main(argv=None):
1787
1852
  print("ERROR: --transport real needs --strategy (or --agent-profile / --checkpoint)")
1788
1853
  return 2
1789
1854
 
1790
- with open(opts.workflow, encoding="utf-8") as fh:
1791
- wf = load_yaml(fh.read())
1792
-
1793
1855
  orch = Orchestrator(wf, opts)
1794
1856
  try:
1795
1857
  result = orch.run()
@@ -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."