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
@@ -95,7 +95,7 @@ workflow:
95
95
  action: verify_context
96
96
  require_non_empty: feature_dir
97
97
  on_empty: stop
98
- on_empty_message: "No active feature. Run /appweaver select-feature first (pick or create the aggregate, e.g. Order), then re-run parity."
98
+ on_empty_message: "No active feature. Run /appweaver select-feature first (pick or create the feature, e.g. Order or PatientRegistration), then re-run parity."
99
99
 
100
100
  - id: verify_spec
101
101
  action: verify_context
@@ -164,6 +164,10 @@ vars:
164
164
  plan_mode: "bootstrap" # bootstrap | rederive | delta — reported, not branched on
165
165
  domain_model: ""
166
166
  domain_model_exists: "false"
167
+ owned_sections: "" # the model section(s) this feature answers for — feature-aggregates
168
+ shared_with_csv: "" # the other features answering for the same aggregate(s)
169
+ one_to_one: "true"
170
+ model_scope_plan: "" # which part of those sections the domain layer realises
167
171
  source_files: [] # the feature's real source files, from its OKF concept
168
172
  source_count: "0"
169
173
 
@@ -241,11 +245,42 @@ workflow:
241
245
  - { name: domain_model, from: "$.domain_model" }
242
246
  - { name: domain_model_exists, from: "$.domain_model_exists" }
243
247
 
248
+ # WHICH PART OF THAT MODEL THIS PLAN REALISES. The model is filed by aggregate, and a
249
+ # feature is a capability scope the developer chose: it may act on several aggregates,
250
+ # and several features may act on one. Resolved from the spec's Acts-on line, then the
251
+ # boundary map, then the feature name. on_error: continue — without an answer the
252
+ # prompt falls back to the model as a whole, which is what it read before.
253
+ - id: feature_aggregates
254
+ action: execute_script
255
+ on_error: continue
256
+ script: { ref: xref }
257
+ subcommand: feature-aggregates
258
+ args: ["--feature-dir", "{{feature_dir}}"]
259
+ outputs:
260
+ - { name: owned_sections, from: "$.owned_sections" }
261
+ - { name: shared_with_csv, from: "$.shared_with_csv" }
262
+ - { name: one_to_one, from: "$.one_to_one" }
263
+
264
+ # HOW MUCH OF THOSE SECTIONS THE DOMAIN LAYER CARRIES. A feature that alone answers for
265
+ # an aggregate realises its whole section. A feature that shares it realises only what
266
+ # its own requirements act on: the rest is a sibling feature's, and planning it here
267
+ # would give two plans the same element to realise. The engine has no conditional
268
+ # prompt text, so the rule is an ordered pair of sets handed to the prompt as one line.
269
+ - id: model_scope_shared
270
+ action: set
271
+ vars:
272
+ model_scope_plan: "{{owned_sections}}, which this feature SHARES with other features. Realise only the entities, value objects, states and BR-nnn this feature's requirements act on, under the model's names and boundary; an element of that section this feature does not use belongs to another feature and is left out — never redefined, renamed or narrowed"
273
+ - id: model_scope_sole
274
+ condition: "{{one_to_one}} == true"
275
+ action: set
276
+ vars:
277
+ model_scope_plan: "{{owned_sections}}. This feature is the only one that answers for it, so the domain layer realises that whole section"
278
+
244
279
  - id: require_feature
245
280
  action: verify_context
246
281
  require_non_empty: feature_dir
247
282
  on_empty: stop
248
- on_empty_message: "No active feature. Run /appweaver select-feature first (pick or create the aggregate, e.g. Order), then re-run plan."
283
+ on_empty_message: "No active feature. Run /appweaver select-feature first (pick or create the feature, e.g. Order or PatientRegistration), then re-run plan."
249
284
 
250
285
  - id: verify_spec
251
286
  action: verify_context
@@ -531,9 +566,10 @@ workflow:
531
566
  - THE SPEC AND THE DOMAIN MODEL ARE THE ONLY DOMAIN INPUTS. You have them plus the
532
567
  technical standards, and NO domain rule files. Realise what they state, and invent
533
568
  no domain rule they omit (an unavoidable new concept → §7 DA-n).
534
- - THE DOMAIN LAYER (§1) MUST FOLLOW THE CONTEXT'S DOMAIN MODEL EXACTLY: entities &
535
- value objects are its `## Aggregate:` section's entities (SAME names), invariants
536
- are its BR-nnn (cite each, never restate), the aggregate boundary is its Boundary
569
+ - THE DOMAIN LAYER (§1) MUST FOLLOW THE CONTEXT'S DOMAIN MODEL EXACTLY. The
570
+ section(s) this feature answers for: {{model_scope_plan}}. Within that scope,
571
+ entities & value objects are the section's entities (SAME names), invariants are
572
+ its BR-nnn (cite each, never restate), the aggregate boundary is its Boundary
537
573
  block, and states are the lifecycle it describes. The model — not the spec — is
538
574
  where those facts live; the spec cites them by id. This document records WHERE
539
575
  each invariant is ENFORCED, never what it says. Elaborate with domain events /
@@ -631,10 +667,11 @@ workflow:
631
667
  textbook layer gets its own section; a layer the feature does not need does NOT
632
668
  appear.
633
669
  - KEEP THE NEUTRAL ANCHORS (the template pins them): §1.2 is the domain/data MODEL
634
- and MUST follow the CONTEXT'S DOMAIN MODEL EXACTLY — entities & value objects are
635
- its aggregate section's entities (SAME names), invariants are its BR-nnn (cite
636
- each and record WHERE each is enforced), the boundary is its Boundary block,
637
- states are its lifecycle; cross-slice references-by-id live here. One §2
670
+ and MUST follow the CONTEXT'S DOMAIN MODEL EXACTLY for the section(s) this feature
671
+ answers for — {{model_scope_plan}}. Entities & value objects are the section's
672
+ entities (SAME names), invariants are its BR-nnn (cite each and record WHERE each
673
+ is enforced), the boundary is its Boundary block, states are its lifecycle;
674
+ cross-slice references-by-id live here. One §2
638
675
  subsection is the EXTERNAL INTEGRATIONS table (its heading contains "Integration")
639
676
  — one port/adapter row per IC-, realising the obligation the spec named.
640
677
  - REALISE THE LIFTED CONSTRAINTS (each cites its spec id): CN- → structural ones
@@ -704,9 +741,10 @@ workflow:
704
741
  A row whose id is gone is deleted along with what it justified. A spec id or model
705
742
  invariant with NO plan element is a GAP — realise it, in the shape the rest of the
706
743
  document already uses.
707
- - THE DOMAIN LAYER (§1) MUST FOLLOW THE CONTEXT'S DOMAIN MODEL EXACTLY: entities &
708
- value objects are its `## Aggregate:` section's entities (SAME names), invariants
709
- are its BR-nnn (cite each, never restate), the aggregate boundary is its Boundary
744
+ - THE DOMAIN LAYER (§1) MUST FOLLOW THE CONTEXT'S DOMAIN MODEL EXACTLY. The
745
+ section(s) this feature answers for: {{model_scope_plan}}. Within that scope,
746
+ entities & value objects are the section's entities (SAME names), invariants are
747
+ its BR-nnn (cite each, never restate), the aggregate boundary is its Boundary
710
748
  block, and states are the lifecycle it describes. Where the model has MOVED since
711
749
  this plan was written, the model is right and the plan is stale.
712
750
  - CARRY THE LEDGERS FORWARD. §7 DA-n architectural decisions and the WA-n
@@ -121,7 +121,7 @@ workflow:
121
121
  action: verify_context
122
122
  require_non_empty: feature_dir
123
123
  on_empty: stop
124
- on_empty_message: "No active feature. Run /appweaver select-feature first (pick or create the aggregate, e.g. Order), then re-run recall-delta."
124
+ on_empty_message: "No active feature. Run /appweaver select-feature first (pick or create the feature, e.g. Order or PatientRegistration), then re-run recall-delta."
125
125
 
126
126
  # 2. WHICH CHANGES ARE OPEN — the evidence is changes/<CR-id>/ on disk. No synthetic
127
127
  # rows: `main` is not a change and there is nothing to open here, so the menu is the
@@ -64,7 +64,7 @@ workflow:
64
64
  action: verify_context
65
65
  require_non_empty: feature_dir
66
66
  on_empty: stop
67
- on_empty_message: "No active feature. Run /appweaver select-feature first (pick or create the aggregate, e.g. Order), then re-run run."
67
+ on_empty_message: "No active feature. Run /appweaver select-feature first (pick or create the feature, e.g. Order or PatientRegistration), then re-run run."
68
68
 
69
69
  # Require the SERVICE LANE and its source path. Migrations belong to the backend
70
70
  # lane by definition, and run-migrate searches that lane's root for the project
@@ -1,17 +1,21 @@
1
1
  # =============================================================================
2
2
  # aw-spec-select-feature (orchestrator) — pick or create the active FEATURE
3
3
  #
4
- # feature = AGGREGATE (e.g. Order). This is the FIRST step of the spec-anchored
5
- # pipeline: it fixes the aggregate boundary BEFORE any spec/design exists, so the
6
- # feature's single spec.md + backend-plan.md and every command (CreateOrder/UpdateOrder =
7
- # user stories) hang off one directory. It replaces guessing the aggregate out of prose in
8
- # `specify` — that is why finding #1 (feature=aggregate) is enforced here, not hoped for.
4
+ # A FEATURE IS A CAPABILITY SCOPE THE DEVELOPER CHOOSES (e.g. Order, or
5
+ # PatientRegistration). It may act on several aggregates, and several features may act on
6
+ # one: the aggregates live in the bounded context's domain model and the OKF bundle, above
7
+ # every feature, and the spec names the ones this feature acts on. This is the FIRST step
8
+ # of the spec-anchored pipeline: it fixes the feature's scope BEFORE any spec/plan exists,
9
+ # so its single spec.md + backend-plan.md and every operation (CreateOrder/UpdateOrder =
10
+ # user stories) hang off one directory.
9
11
  #
10
12
  # The pickable list is TWO merged sources (deduped by name): specs/ sub-folders
11
- # (features already scaffolded/adopted) AND the OKF bundle's Aggregate concepts —
12
- # the brownfield feature map `/appweaver onboard` reverse-engineers. So in a
13
- # brownfield repo you pick a real discovered code feature BEFORE anything is under
14
- # specs/; on greenfield (no bundle) the list is specs-only, exactly as before.
13
+ # (features already scaffolded/adopted) AND the features the OKF bundle's concepts are
14
+ # known to serve — from the boundary map the developer confirmed, else from each
15
+ # concept's `features:` list. So in a brownfield repo you pick a real discovered code
16
+ # feature BEFORE anything is under specs/. An aggregate is never offered as a feature
17
+ # nobody defined: on greenfield every feature already has its specs/ directory, so the
18
+ # list is specs-only.
15
19
  #
16
20
  # SCOPED BY THE ACTIVE BOUNDED CONTEXT. When .appweaver/context.json names one, both
17
21
  # the listing and the creation happen inside specs/<Context>/ — so a context shows
@@ -22,7 +26,7 @@
22
26
  # ZERO LLM: only scripts + one select + (conditionally) one ask. No generation,
23
27
  # no standards, no domain-select. Pure, deterministic orchestration/governance.
24
28
  #
25
- # list-features (specs/ ∪ OKF aggregates) → user_select (existing | "+ create new") → [ask name] → set-feature
29
+ # list-features (specs/ ∪ features the OKF concepts serve) → user_select (existing | "+ create new") → [ask name] → set-feature
26
30
  #
27
31
  # Persists .appweaver/feature.json {"feature_directory":"specs/<Feature>"} so every
28
32
  # later command operates on it: the forward loop (specify/plan/analyze-plan/implement/
@@ -36,14 +40,14 @@
36
40
  # =============================================================================
37
41
 
38
42
  name: aw-spec-select-feature
39
- description: Pick an existing feature (aggregate) or create a new one, and store it in .appweaver/feature.json so every AW-Spec command operates on it. Zero LLM — scripts + one selection only.
43
+ description: Pick an existing feature (a capability scope you choose) or create a new one, and store it in .appweaver/feature.json so every AW-Spec command operates on it. Zero LLM — scripts + one selection only.
40
44
  version: 1.0.0
41
45
  agent_agnostic: true
42
46
  dialect: aw-spec.orchestrator/v1
43
47
 
44
48
  inputs:
45
49
  user_input:
46
- description: Optional feature (aggregate) name to create or select directly, e.g. Order.
50
+ description: Optional feature name to create or select directly, e.g. Order or PatientRegistration.
47
51
  required: false
48
52
  bind: USER_INPUT
49
53
 
@@ -85,11 +89,12 @@ workflow:
85
89
  on_false_message: >
86
90
  {{scope_message}}
87
91
 
88
- # 1. Enumerate selectable aggregates from TWO merged sources (deduped by name):
89
- # specs/ sub-folders (already scaffolded/adopted) AND the OKF bundle's
90
- # Aggregate concepts — the brownfield feature map onboard reverse-engineers,
91
- # so a not-yet-adopted code feature is pickable before its specs/ dir exists.
92
- # On greenfield (no bundle) the OKF source is empty → specs-only, as before.
92
+ # 1. Enumerate selectable FEATURES from TWO merged sources (deduped by name):
93
+ # specs/ sub-folders (already scaffolded/adopted) AND the features the OKF
94
+ # bundle's aggregate concepts serve — the boundary map first, else each concept's
95
+ # `features:` list — so a not-yet-adopted code feature is pickable before its
96
+ # specs/ dir exists. A concept greenfield implement wrote contributes nothing:
97
+ # its features all have specs/ dirs, and its own name is an aggregate.
93
98
  # `select_options` carries the "+ create new" sentinel and (when USER_INPUT
94
99
  # names a not-yet feature) that name injected — a direct arg is one-list select.
95
100
  - id: list_features
@@ -108,7 +113,7 @@ workflow:
108
113
  # no prompt; the lone sentinel at bootstrap (no features yet) auto-confirms.
109
114
  - id: pick
110
115
  action: user_select
111
- prompt: "Select a feature (the aggregate, e.g. Order) in {{feature_base}}/ — or create a new one:"
116
+ prompt: "Select a feature (a capability scope, e.g. Order or PatientRegistration) in {{feature_base}}/ — or create a new one:"
112
117
  options_from_variable: select_options
113
118
  preselect_from: USER_INPUT
114
119
  single_option_confirm: true
@@ -120,9 +125,10 @@ workflow:
120
125
  action: ask
121
126
  condition: "{{chosen}} == {{new_sentinel}}"
122
127
  prompt: >
123
- Name the new feature — this is the AGGREGATE root (e.g. Order, Invoice,
124
- Customer), NOT a single command. Operations like CreateOrder / UpdateOrder
125
- become user stories inside it:
128
+ Name the new feature — the capability scope you want to specify and build as
129
+ one unit (e.g. Order, PatientRegistration). It may act on several aggregates, and
130
+ other features may act on the same ones; its spec names them. Each operation in
131
+ its scope (e.g. CreateOrder / UpdateOrder) becomes a user story inside it:
126
132
  answer_var: typed_name
127
133
 
128
134
  # 4. Coalesce the target name. The engine has no compound conditions, so this
@@ -136,7 +142,7 @@ workflow:
136
142
  condition: "{{chosen}} == {{new_sentinel}}"
137
143
  vars: { target_name: "{{typed_name}}" }
138
144
 
139
- # 5. Create-or-select the aggregate dir + persist it as the active feature.
145
+ # 5. Create-or-select the feature dir + persist it as the active feature.
140
146
  - id: set_feature
141
147
  action: execute_script
142
148
  script: { ref: actions }
@@ -157,5 +163,5 @@ outputs:
157
163
  context: "{{context}}"
158
164
  created: "{{created}}"
159
165
  spec_exists: "{{spec_exists}}"
160
- note: "Active feature set in .appweaver/feature.json — the aggregate boundary for its single spec.md + backend-plan.md. `context` names the bounded context it was created in, and is empty on a repo with no active context (the flat specs/<Feature> layout, which keeps working unchanged)."
166
+ note: "Active feature set in .appweaver/feature.json — the capability scope that owns its single spec.md + backend-plan.md. `context` names the bounded context it was created in, and is empty on a repo with no active context (the flat specs/<Feature> layout, which keeps working unchanged)."
161
167
  next: "/appweaver specify <what this feature does> (authors {{feature_dir}}/spec.md; each operation = a user story)"
@@ -1,8 +1,9 @@
1
1
  # =============================================================================
2
2
  # aw-spec-specify (orchestrator) — author the SPEC for the ACTIVE feature
3
3
  #
4
- # Spec-anchored pipeline, spec phase. feature = AGGREGATE (picked/created by
5
- # /appweaver select-feature, which sets .appweaver/feature.json). specify does
4
+ # Spec-anchored pipeline, spec phase. The feature is a capability scope the developer
5
+ # picked/created with /appweaver select-feature (.appweaver/feature.json); it may act on
6
+ # several aggregates, and several features may act on one. specify does
6
7
  # NOT guess or create the feature — it CONSUMES the active one, and is phase-pure
7
8
  # (spec side only; the single backend-plan.md is the `design` phase).
8
9
  #
@@ -163,7 +164,7 @@ workflow:
163
164
  action: verify_context
164
165
  require_non_empty: feature_dir
165
166
  on_empty: stop
166
- on_empty_message: "No active feature. Run /appweaver select-feature first (pick or create the aggregate, e.g. Order), then re-run specify."
167
+ on_empty_message: "No active feature. Run /appweaver select-feature first (pick or create the feature, e.g. Order or PatientRegistration), then re-run specify."
167
168
 
168
169
  # 1b. Resolve the BOUNDED CONTEXT's domain model — the canonical home of the aggregate
169
170
  # root, its entities and its invariants, which this spec references and never
@@ -325,13 +326,17 @@ workflow:
325
326
  action: agent_author
326
327
  output_target: { type: file, path: "{{spec_file}}" }
327
328
  task: |
328
- Author the MASTER specification for this aggregate by filling out the
329
+ Author the MASTER specification for the feature `{{feature}}` by filling out the
329
330
  template in context. This is the first user story, US-1. Base everything on:
330
331
 
331
332
  {{USER_INPUT}}
332
333
 
333
334
  Rules for the content you generate:
334
- - The feature IS the aggregate slice; set the Aggregate field and keep US-1's id.
335
+ - The feature is a CAPABILITY SCOPE the developer chose; set the Feature field to
336
+ `{{feature}}` and keep US-1's id. It may act on several aggregates, and other
337
+ features may act on the same ones: name every aggregate its requirements act on
338
+ in the Domain Model Reference's `**Acts on aggregate(s)**:` line, each in
339
+ backticks — every later command finds this feature's part of the model there.
335
340
  - Preserve the template's section order and headings.
336
341
  - SPECIFY IS THE ONLY PHASE THAT READS THE DOMAIN RULES. Design and implement
337
342
  work from the spec + the domain model, never the domain docs — so this pair is
@@ -436,11 +441,14 @@ workflow:
436
441
  section per root: its boundary (owns / references by id / out of scope), entities,
437
442
  value objects, domain events, and its invariants as stable BR-nnn ids. The spec
438
443
  cites those ids; this document defines them.
439
- - IF THE MODEL ALREADY HAS CONTENT, this is the context's SECOND (or later) feature.
440
- ADD this feature's aggregate as a new section and EXTEND the ubiquitous language.
441
- Do NOT rewrite, renumber or "tidy" the aggregates already there — other specs cite
442
- their ids, and a renumbered BR breaks every citation silently. Add `{{feature}}` to
443
- the `Serving specs` line of any existing aggregate this feature also acts on.
444
+ - IF THE MODEL ALREADY HAS CONTENT, this is the context's SECOND (or later) feature,
445
+ and the aggregates it acts on may already be there. An aggregate that already has
446
+ a section is EXTENDED in place — add what this feature's requirements need, and add
447
+ `{{feature}}` to its `Serving specs` line — never duplicated, and never given a
448
+ second section named after the feature. Only an aggregate the model does not have
449
+ yet gets a new section. EXTEND the ubiquitous language. Do NOT rewrite, renumber
450
+ or "tidy" what is already there — other specs cite those ids, and a renumbered BR
451
+ breaks every citation silently.
444
452
  - DDD IS THE RATIFIED MODELLING STANDARD (NON-NEGOTIABLE → the modelling-methodology
445
453
  standard in the sticky standards, wired per-project via tech-rule-bindings
446
454
  "## /aw-spec-specify", plus the domain's own DDD overlay): a consistent ubiquitous
@@ -11,6 +11,21 @@
11
11
  # [--service-root <p>] [--ui-root <p>] (the declared lane paths:
12
12
  # backend signals come from outside the UI subtree, frontend
13
13
  # signals from inside it, so neither carries the other's files)
14
+ # aw-inventory.sh propose [--root <dir>] [--service-root <p>] [--ui-root <p>]
15
+ # [--strategy structure|reference|persistence] [--out <md>] [--map <json>]
16
+ # (Pass A+B+C: writes the editable specs/proposed-inventory.md
17
+ # and the machine map onboard reads)
18
+ # aw-inventory.sh apply [--root <dir>] [--proposal <md>] [--map <json>]
19
+ # (validates the operator's edits to the proposal and
20
+ # regenerates the machine map; reports the row it cannot resolve)
21
+ # aw-inventory.sh scaffold-context --context <Name> [--root <dir>] [--map <json>]
22
+ # (creates specs/<Context>/<Feature>/ for every feature the map
23
+ # assigns to that context — directories only, no documents)
24
+ # aw-inventory.sh concept --bundle <dir> --aggregate <Name>
25
+ # aw-inventory.sh list-aggregates --bundle <dir> [--context <Name>] [--root <dir>] [--map <json>]
26
+ # (--context drops concepts stamped with a DIFFERENT bounded
27
+ # context; unstamped concepts are always kept. `pickable` offers
28
+ # each concept as the features the map puts on it)
14
29
  # aw-inventory.sh validate --bundle <dir> [--repo-root <dir>]
15
30
  set -euo pipefail
16
31
  DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@@ -42,7 +42,7 @@
42
42
  # Always exits 0 — the workflow's verify_context owns the stop)
43
43
  # lane-probe [--kind <k>] [--hint <text>] -> {declared, declared_profile, hint_profile, hint_terms[], signals[], any, evidence}
44
44
  # (the FILE signals for "does this lane exist"; resolve-tech carries the rule signal)
45
- # list-features [--preselect <name>] [--bundle <dir>] -> {features[], names[], count, select_options[], sentinel} (specs/ ∪ OKF aggregates)
45
+ # list-features [--preselect <name>] [--bundle <dir>] -> {features[], names[], count, select_options[], sentinel} (specs/ ∪ features the OKF concepts serve)
46
46
  # set-feature --feature <name> -> {feature, feature_dir, spec_file, existed, created, spec_exists}
47
47
  # version-read [--feature-dir <path>] -> {feature, version, active_change, exists, bootstrap_pending}
48
48
  # version-init [--feature-dir <path>] [--bootstrap] -> {…, created, bootstrap_pending} (create active_version.json v1.0; --bootstrap marks a FIRST draft in progress)
@@ -1206,6 +1206,10 @@ cmd_model_scaffold() {
1206
1206
  # reverse-engineers (the discovered aggregates NOT yet under specs/). This is
1207
1207
  # why select-feature can offer real code features in a brownfield repo before
1208
1208
  # anything is adopted, instead of an empty specs/ list.
1209
+ # BOTH sources are scoped to the ACTIVE BOUNDED CONTEXT: source 1 by enumerating the
1210
+ # context's own directory, source 2 by the `bounded_context:` key onboard stamps on
1211
+ # every concept it writes. onboard runs once per context, so an unfiltered bundle
1212
+ # offers a neighbouring context's features to a picker that would scaffold them here.
1209
1213
  # On greenfield (no bundle) source 2 is empty → identical to the old specs-only
1210
1214
  # behaviour. `select_options` = merged names + (an injected --preselect that is not
1211
1215
  # yet a feature) + a "create new" sentinel, so select-feature can offer add-new and
@@ -1223,16 +1227,24 @@ cmd_list_features() {
1223
1227
  local repo_root; repo_root=$(get_repo_root)
1224
1228
  [ -n "$bundle" ] || bundle="knowledge"
1225
1229
  case "$bundle" in /*) ;; *) bundle="$repo_root/$bundle" ;; esac
1226
- # Best-effort OKF aggregate list from the shared python core (single source of
1227
- # truth for OKF parsing; the ps1 twin calls the SAME script). Empty on greenfield.
1228
- local okf_json="{}" invpy="$SCRIPT_DIR/../python/aw-inventory.py"
1229
- if [ -d "$bundle" ] && [ -f "$invpy" ] && command -v python3 >/dev/null 2>&1; then
1230
- okf_json=$(python3 "$invpy" list-aggregates --bundle "$bundle" 2>/dev/null) || okf_json="{}"
1231
- fi
1232
1230
  # Features are enumerated from the ACTIVE BOUNDED CONTEXT's directory when one is
1233
1231
  # set, and from specs/ itself when none is — so a context scopes the picker to its
1234
1232
  # own features and a pre-context repo lists exactly what it always did.
1235
1233
  local base_rel; base_rel=$(feature_base_rel "$repo_root")
1234
+ # The OKF half is scoped the same way. onboard runs once per bounded context and
1235
+ # stamps each concept it writes, so a bundle that has seen two runs holds two
1236
+ # contexts' aggregates; unfiltered, standing inside one context would offer the
1237
+ # other's features and scaffold them into the wrong tree. Empty when no context is
1238
+ # active, which keeps the flat layout listing everything.
1239
+ local active_ctx=""
1240
+ [ "$base_rel" != "specs" ] && active_ctx="${base_rel##*/}"
1241
+ # Best-effort OKF aggregate list from the shared python core (single source of
1242
+ # truth for OKF parsing AND for the context filter; the ps1 twin calls the SAME
1243
+ # script with the same argument). Empty on greenfield.
1244
+ local okf_json="{}" invpy="$SCRIPT_DIR/../python/aw-inventory.py"
1245
+ if [ -d "$bundle" ] && [ -f "$invpy" ] && command -v python3 >/dev/null 2>&1; then
1246
+ okf_json=$(python3 "$invpy" list-aggregates --bundle "$bundle" --root "$repo_root" --context "$active_ctx" 2>/dev/null) || okf_json="{}"
1247
+ fi
1236
1248
  AW_SPECS="$repo_root/$base_rel" AW_BASE_REL="$base_rel" AW_PRESELECT="$preselect" AW_OKF="$okf_json" python3 - <<'PY'
1237
1249
  import json, os, glob
1238
1250
  base = os.environ["AW_SPECS"]
@@ -1293,9 +1305,13 @@ if os.path.isdir(base):
1293
1305
  "version": ver,
1294
1306
  "active_change": active_change,
1295
1307
  })
1296
- # Merge OKF-discovered aggregates NOT already scaffolded under specs/ (a specs/
1297
- # folder is the adopted/created form of the same aggregate — it wins, no dup).
1298
- for agg in (okf.get("aggregates") or []):
1308
+ # Merge the OKF-discovered FEATURES not already scaffolded under specs/ (a specs/
1309
+ # folder is the adopted/created form of the same feature — it wins, no dup).
1310
+ # `pickable` is each aggregate concept offered as the features the map puts on it, so
1311
+ # an aggregate split into capabilities is never itself offered as a feature to pick.
1312
+ # An EMPTY `pickable` is an answer — nothing to offer, e.g. a greenfield bundle whose
1313
+ # features all have specs/ dirs — so only its absence falls back to the raw aggregates.
1314
+ for agg in (okf["pickable"] if "pickable" in okf else okf.get("aggregates") or []):
1299
1315
  nm = (agg.get("name") or "").strip()
1300
1316
  if not nm or nm.lower() in seen:
1301
1317
  continue
@@ -1328,26 +1344,26 @@ PY
1328
1344
  }
1329
1345
 
1330
1346
  # ─────────────────────────────── set-feature ───────────────────────────────
1331
- # Create-or-select an aggregate under specs/ and persist it as the active feature
1332
- # (.appweaver/feature.json). feature = aggregate: the name is normalized to a
1333
- # PascalCase identifier (e.g. "order management" -> OrderManagement). Idempotent:
1347
+ # Create-or-select a feature under specs/ and persist it as the active feature
1348
+ # (.appweaver/feature.json). The name is the developer's capability scope, normalized to
1349
+ # a PascalCase identifier (e.g. "patient registration" -> PatientRegistration). Idempotent:
1334
1350
  # selecting an existing feature just re-points feature.json; a new one is created.
1335
1351
  cmd_set_feature() {
1336
1352
  local raw=""
1337
1353
  while [ $# -gt 0 ]; do
1338
1354
  case "$1" in
1339
1355
  --feature) raw="$2"; shift 2 ;;
1340
- --help|-h) echo "set-feature --feature <name> (create-or-select the aggregate under specs/)"; return 0 ;;
1356
+ --help|-h) echo "set-feature --feature <name> (create-or-select the feature under specs/)"; return 0 ;;
1341
1357
  *) echo "set-feature: unknown arg '$1'" >&2; return 1 ;;
1342
1358
  esac
1343
1359
  done
1344
1360
  [ -n "$raw" ] || { echo "set-feature: --feature is required" >&2; return 1; }
1345
1361
 
1346
- # Normalize to a PascalCase aggregate identifier (feature = aggregate).
1362
+ # Normalize to a PascalCase identifier.
1347
1363
  local cleaned; cleaned=$(echo "$raw" | sed -E 's/[^A-Za-z0-9]+/ /g; s/^ +| +$//g')
1348
1364
  local feature; feature=$(_pascal "$cleaned")
1349
1365
  if ! [[ "$feature" =~ ^[A-Za-z][A-Za-z0-9]*$ ]]; then
1350
- echo "set-feature: '$raw' is not a valid feature name (need a letter-led alphanumeric aggregate, e.g. Order)" >&2
1366
+ echo "set-feature: '$raw' is not a valid feature name (need a letter-led alphanumeric name, e.g. Order or PatientRegistration)" >&2
1351
1367
  return 1
1352
1368
  fi
1353
1369
 
@@ -10,6 +10,9 @@
10
10
  # aw-xref.sh concept-upsert --aggregate X [--source-files a,b,c] [--description D] [--origin ...] [--root DIR]
11
11
  # aw-xref.sh concept-overview --aggregate X (--text PROSE | --text-file F) [--root DIR]
12
12
  # aw-xref.sh concept-paths --aggregate X | --feature-dir <dir> [--root DIR] [--bundle knowledge]
13
+ # aw-xref.sh feature-aggregates --feature-dir <dir> [--root DIR]
14
+ # (the aggregate(s) a feature acts on: its spec's Acts-on line,
15
+ # then the boundary map, then the feature name itself)
13
16
  set -euo pipefail
14
17
  DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
15
18
  exec python3 "$DIR/../python/aw-xref.py" "$@"
@@ -10,6 +10,21 @@
10
10
  # [--service-root <p>] [--ui-root <p>] (the declared lane paths:
11
11
  # backend signals come from outside the UI subtree, frontend
12
12
  # signals from inside it, so neither carries the other's files)
13
+ # aw-inventory.ps1 propose [--root <dir>] [--service-root <p>] [--ui-root <p>]
14
+ # [--strategy structure|reference|persistence] [--out <md>] [--map <json>]
15
+ # (Pass A+B+C: writes the editable specs/proposed-inventory.md
16
+ # and the machine map onboard reads)
17
+ # aw-inventory.ps1 apply [--root <dir>] [--proposal <md>] [--map <json>]
18
+ # (validates the operator's edits to the proposal and
19
+ # regenerates the machine map; reports the row it cannot resolve)
20
+ # aw-inventory.ps1 scaffold-context --context <Name> [--root <dir>] [--map <json>]
21
+ # (creates specs/<Context>/<Feature>/ for every feature the map
22
+ # assigns to that context — directories only, no documents)
23
+ # aw-inventory.ps1 concept --bundle <dir> --aggregate <Name>
24
+ # aw-inventory.ps1 list-aggregates --bundle <dir> [--context <Name>] [--root <dir>] [--map <json>]
25
+ # (--context drops concepts stamped with a DIFFERENT bounded
26
+ # context; unstamped concepts are always kept. `pickable` offers
27
+ # each concept as the features the map puts on it)
13
28
  # aw-inventory.ps1 validate --bundle <dir> [--repo-root <dir>]
14
29
  $ErrorActionPreference = "Stop"
15
30
  $core = Join-Path $PSScriptRoot "..\python\aw-inventory.py"
@@ -684,6 +684,8 @@ function Invoke-LaneProbe {
684
684
  # (case-insensitive) — (1) specs/ sub-folders (already scaffolded/adopted) and
685
685
  # (2) the OKF bundle's Aggregate concepts (the brownfield feature map, resolved by
686
686
  # the SAME shared python core the bash twin calls). Empty source 2 on greenfield.
687
+ # Both are scoped to the ACTIVE BOUNDED CONTEXT — source 1 by the directory listed,
688
+ # source 2 by the `bounded_context:` key onboard stamps on every concept it writes.
687
689
  # ─────────────────────── context.json (bounded context) ───────────────────
688
690
  # Twin of the bash read_context_json_context_directory / feature_base_rel pair. A
689
691
  # bounded context is the container a feature lives in — specs/<Context>/<Feature>/ —
@@ -861,13 +863,31 @@ function Invoke-ListFeatures {
861
863
  if (-not $bundle) { $bundle = 'knowledge' }
862
864
  if (-not [System.IO.Path]::IsPathRooted($bundle)) { $bundle = Join-Path $repo $bundle }
863
865
 
866
+ # The OKF half is scoped to the active bounded context the same way. onboard runs
867
+ # once per context and stamps each concept it writes, so a bundle that has seen two
868
+ # runs holds two contexts' aggregates; unfiltered, standing inside one context would
869
+ # offer the other's features and scaffold them into the wrong tree. Empty when no
870
+ # context is active, which keeps the flat layout listing everything.
871
+ $activeCtx = ''
872
+ if ($baseRel -ne 'specs') { $activeCtx = ($baseRel -split '/')[-1] }
873
+
864
874
  # Best-effort OKF aggregate list (brownfield feature map). Empty on greenfield.
865
875
  $okfAggs = @()
866
876
  $py = Get-Py; $invpy = Join-Path $PSScriptRoot '..' 'python' 'aw-inventory.py'
867
877
  if ($py -and (Test-Path $bundle) -and (Test-Path $invpy)) {
868
878
  try {
869
- $okfRaw = & $py $invpy list-aggregates --bundle $bundle 2>$null
870
- if ($okfRaw) { $okfAggs = @((($okfRaw | ConvertFrom-Json).aggregates)) }
879
+ # --context only when one is active: Windows PowerShell drops an empty-string
880
+ # argument to a native command, which would leave --context with no value.
881
+ $ctxArgs = @(); if ($activeCtx) { $ctxArgs = @('--context', $activeCtx) }
882
+ $okfRaw = & $py $invpy list-aggregates --bundle $bundle --root $repo @ctxArgs 2>$null
883
+ if ($okfRaw) {
884
+ # The FEATURES the map puts on each concept, not the aggregates themselves —
885
+ # the same list the bash twin reads.
886
+ $okfJson = $okfRaw | ConvertFrom-Json
887
+ # An EMPTY `pickable` is an answer (nothing to offer); only its absence
888
+ # falls back to the raw aggregates.
889
+ $okfAggs = if ($okfJson.PSObject.Properties.Name -contains 'pickable') { @($okfJson.pickable) } else { @($okfJson.aggregates) }
890
+ }
871
891
  } catch { $okfAggs = @() }
872
892
  }
873
893
 
@@ -901,7 +921,7 @@ function Invoke-ListFeatures {
901
921
  }
902
922
  }
903
923
  }
904
- # Merge OKF-discovered aggregates not already scaffolded under specs/.
924
+ # Merge the OKF-discovered FEATURES not already scaffolded under specs/.
905
925
  foreach ($agg in $okfAggs) {
906
926
  $nm = "$($agg.name)".Trim()
907
927
  if (-not $nm -or $seen.ContainsKey($nm.ToLower())) { continue }
@@ -944,11 +964,11 @@ function Invoke-SetFeature {
944
964
  }
945
965
  if (-not $raw) { Write-Error 'set-feature: --feature is required'; exit 1 }
946
966
 
947
- # Normalize to a PascalCase aggregate identifier (feature = aggregate).
967
+ # Normalize to a PascalCase identifier.
948
968
  $cleaned = ($raw -replace '[^A-Za-z0-9]+', ' ').Trim()
949
969
  $feature = ConvertTo-Pascal -In $cleaned
950
970
  if ($feature -notmatch '^[A-Za-z][A-Za-z0-9]*$') {
951
- Write-Error "set-feature: '$raw' is not a valid feature name (need a letter-led alphanumeric aggregate, e.g. Order)"; exit 1
971
+ Write-Error "set-feature: '$raw' is not a valid feature name (need a letter-led alphanumeric name, e.g. Order or PatientRegistration)"; exit 1
952
972
  }
953
973
 
954
974
  $repo = Get-RepoRoot
@@ -9,6 +9,9 @@
9
9
  # aw-xref.ps1 concept-upsert --aggregate X [--source-files a,b,c] [--description D] [--origin ...] [--root DIR]
10
10
  # aw-xref.ps1 concept-overview --aggregate X (--text PROSE | --text-file F) [--root DIR]
11
11
  # aw-xref.ps1 concept-paths --aggregate X | --feature-dir <dir> [--root DIR] [--bundle knowledge]
12
+ # aw-xref.ps1 feature-aggregates --feature-dir <dir> [--root DIR]
13
+ # (the aggregate(s) a feature acts on: its spec's Acts-on line,
14
+ # then the boundary map, then the feature name itself)
12
15
  $ErrorActionPreference = "Stop"
13
16
  $core = Join-Path $PSScriptRoot "..\python\aw-xref.py"
14
17
  & python3 $core @args