aw-spec-cli 0.1.23 → 0.1.24

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 (38) hide show
  1. package/assets/.appweaver/ORCH-DIALECT.md +3 -3
  2. package/assets/.appweaver/RULE-PARTITION.md +41 -28
  3. package/assets/.appweaver/orchestrator/orchestrator.py +92 -14
  4. package/assets/.appweaver/orchestrator/rev-templates/ai-rules.reference.md +3 -3
  5. package/assets/.appweaver/orchestrator/rev-templates/delta-design-template.skeleton.md +19 -12
  6. package/assets/.appweaver/orchestrator/rev-templates/design-template.skeleton.md +25 -17
  7. package/assets/.appweaver/orchestrator/rev-templates/domain-requirements.skeleton.md +2 -2
  8. package/assets/.appweaver/orchestrator/rev-templates/domain-rule-bindings.skeleton.md +10 -10
  9. package/assets/.appweaver/orchestrator/rev-templates/tech-rule-bindings.skeleton.md +28 -17
  10. package/assets/.appweaver/orchestrator/rev-templates/technical-constitution.skeleton.md +22 -24
  11. package/assets/.appweaver/orchestrator/rev-templates/technical-rule-file.skeleton.md +1 -1
  12. package/assets/.appweaver/orchestrator/rev-templates/ui-delta-design-template.skeleton.md +29 -19
  13. package/assets/.appweaver/orchestrator/rev-templates/ui-design-template.skeleton.md +27 -20
  14. package/assets/.appweaver/orchestrator/workflows/aw-spec-adopt.orch.yaml +184 -37
  15. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan-delta.orch.yaml +18 -0
  16. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan.orch.yaml +19 -0
  17. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice-delta.orch.yaml +18 -0
  18. package/assets/.appweaver/orchestrator/workflows/aw-spec-inventory.orch.yaml +55 -29
  19. package/assets/.appweaver/orchestrator/workflows/aw-spec-onboard.orch.yaml +650 -205
  20. package/assets/.appweaver/orchestrator/workflows/aw-spec-plan.orch.yaml +19 -0
  21. package/assets/.appweaver/orchestrator/workflows/aw-spec-select-feature.orch.yaml +5 -5
  22. package/assets/.appweaver/orchestrator/workflows/aw-spec-select-stack.orch.yaml +2 -2
  23. package/assets/.appweaver/orchestrator/workflows/aw-spec-ui-model.orch.yaml +13 -3
  24. package/assets/.appweaver/orchestrator/workflows/aw-spec-ui-prototype.orch.yaml +40 -1
  25. package/assets/.appweaver/orchestrator/workflows/aw-spec-workflow-model.orch.yaml +2 -1
  26. package/assets/.appweaver/scripts/bash/aw-rev-constitution.sh +1 -1
  27. package/assets/.appweaver/scripts/bash/aw-spec-actions.sh +69 -7
  28. package/assets/.appweaver/scripts/powershell/aw-rev-constitution.ps1 +1 -1
  29. package/assets/.appweaver/scripts/powershell/aw-spec-actions.ps1 +58 -2
  30. package/assets/.appweaver/scripts/python/aw-inventory.py +1809 -156
  31. package/assets/.appweaver/scripts/python/aw-rev-constitution.py +106 -145
  32. package/assets/.appweaver/scripts/python/aw-stack.py +45 -1
  33. package/assets/.appweaver/scripts/python/aw-xref.py +73 -2
  34. package/assets/.appweaver/scripts/python/validate_okf.py +10 -5
  35. package/assets/.appweaver/templates/okf-concept-template.md +10 -2
  36. package/assets/.appweaver/templates/proposed-inventory-template.md +100 -48
  37. package/assets/.appweaver/templates/source-map-template.md +14 -6
  38. package/package.json +1 -1
@@ -54,7 +54,9 @@
54
54
  # only, NO domain files — domain reaches design as spec ids.
55
55
  #
56
56
  # Adopt does NOT generate code and does NOT modify source — writes land under
57
- # specs/<Feature>/, .appweaver/memory/constitution*.md, and guardrails/projects/<project>/.
57
+ # specs/<Context>/<Feature>/, in the context's specs/<Context>/domain-model.md, and in the
58
+ # aggregate's OKF concept; the constitution halves and guardrails/projects/<project>/ are
59
+ # written only when the reconcile block below is enabled.
58
60
  # Fidelity checks spec/design ↔ CODE; the Adopt review is REPORTED for out-of-band
59
61
  # closure — it blocks only on structural failures (unmapped code / fabricated spec/design).
60
62
  # There is NO migration content here (characterize is an optional on-touch fence, not a
@@ -69,7 +71,13 @@
69
71
  # =============================================================================
70
72
 
71
73
  name: aw-spec-adopt
72
- description: The JIT per-feature step of brownfield development — reverse-engineer ONE existing feature into a living spec.md, an aggregate section in the bounded context's shared domain-model.md, and backend-plan.md (+ frontend-plan.md where a frontend lane is declared and this slice owns part of its surface) + source-map.md (origin reverse-engineered, per-item confidence, code-derived plan template), then COMPLETE its shallow OKF concept (deepen it to the reversed reality + link the deep artifacts) and record its cross-slice edges. The spec cites BR-nnn by id and the model defines them, exactly as on the greenfield path, so a reversed slice is checkable by the same gates as any other. Fidelity gate is spec + model + plans vs code, and an invented entity, boundary or invariant is a fabrication exactly as an invented FR- is; the review is reported for out-of-band closure. The governance reconcile — carrying this feature's real evidence into the provisional rule files + constitution onboard wrote — is HELD (reconcile_governance=false), so an ordinary run does not touch the governance tier. A NEW feature is authored via the forward pipeline instead. Output is a normal feature for the CR + forward loop.
74
+ description: The JIT per-feature step of brownfield development — reverse-engineer ONE existing feature into a living spec.md, an aggregate section in the bounded context's shared domain-model.md, and backend-plan.md (+ frontend-plan.md where a frontend lane is declared and this slice owns part of its surface) + source-map.md (origin reverse-engineered, per-item confidence, code-derived plan template), then COMPLETE its shallow OKF concept (deepen it to the reversed reality + link the deep artifacts) and record its cross-slice edges. The spec cites BR-nnn by id and the model defines them, exactly as on the greenfield path, so a reversed slice is checkable by the same gates as any other. Fidelity gate is spec + model + plans vs code, and an invented entity, boundary or invariant is a fabrication exactly as an invented FR- is; the review is reported for out-of-band closure. Every reverse step is scoped to the feature's own operations from the confirmed boundary map, so two features over one aggregate never claim the same operation, and the spec continues the context's BR-nnn numbering from its domain model. Where a frontend lane is declared and this slice owns none of its surface, the feature records lanes=backend so later phases neither author nor demand a frontend plan for it. The governance reconcile — carrying this feature's real evidence into the provisional rule files + constitution onboard wrote — is HELD (reconcile_governance=false), so an ordinary run does not touch the governance tier. A NEW feature is authored via the forward pipeline instead. Output is a normal feature for the CR + forward loop.
75
+ # 2.1.0 — every reverse step is scoped to the FEATURE, not its aggregate: the boundary map's
76
+ # operation list (and a sibling feature's operations, which are not this one's)
77
+ # reaches the spec, model, plan, UI plan, source-map and fidelity steps. The spec step
78
+ # reads the context's domain model so BR-nnn ids continue the context's numbering.
79
+ # The UI plan is authored under the frontend rule set. A slice with no part of a
80
+ # declared frontend's surface records lanes=backend for the later phases.
73
81
  # 1.5.0 — user_input is now OPTIONAL: adopt CONSUMES the ACTIVE feature (select-feature's
74
82
  # .appweaver/feature.json) when no arg is passed, so onboard→select-feature→adopt needs
75
83
  # no re-typing; an explicit <Feature> still selects directly (and wins). One coalesced
@@ -79,7 +87,7 @@ description: The JIT per-feature step of brownfield development — reverse-engi
79
87
  # 1.3.0 — reverse design scaffolds from the DEFAULT `design-template` (onboard's code-derived
80
88
  # override) instead of the never-shipped `brownfield-design-template`, unifying the
81
89
  # whole brownfield pipeline on one code-derived template.
82
- version: 2.0.0
90
+ version: 2.1.0
83
91
  agent_agnostic: true
84
92
  dialect: aw-spec.orchestrator/v1
85
93
 
@@ -116,12 +124,24 @@ vars:
116
124
  bundle_dir: "knowledge" # where /appweaver onboard writes the OKF feature map (repo-root default)
117
125
  feature_name: "" # the aggregate to adopt: USER_INPUT if passed, else the active feature (set-feature canonicalises)
118
126
  concept_exists: "false"
119
- fe_declared: "false" # lane_readback runs on_error:continue, so both its bindings need a declared default
127
+ fe_declared: "false" # lane_readback runs on_error:continue, so its bindings need declared defaults
128
+ be_declared: "false"
129
+ fe_no_surface: "false" # a frontend lane is declared, this slice owns none of its surface, and a backend lane carries it
130
+ feature_lanes: "" # what record_lanes wrote — empty when the feature follows every declared lane
120
131
  fe_root: ""
121
132
  ui_lane: "false" # THIS slice reverses a UI half: a frontend lane is declared AND the slice owns UI files
122
133
  ui_count: "0" # how many UI files the concept attributes to this aggregate (aw-inventory ui_source_files)
123
134
  ui_source_files: ""
124
135
  frontend_plan: ""
136
+ # THE FEATURE'S SCOPE INSIDE ITS AGGREGATE, from feature-aggregates (the boundary map).
137
+ operations_note: "" # this feature's operations + the ones only a sibling feature owns, as one line
138
+ operations_csv: ""
139
+ one_to_one: "true"
140
+ owned_sections: ""
141
+ shared_with_csv: ""
142
+ model_scope_plan: "" # which part of the model section the backend plan realises
143
+ baseline_commit: "" # the inventory HEAD the boundary map was drawn at
144
+ baseline_time: ""
125
145
  ui_tpl_source: ""
126
146
  # THE LANE PLANS, under the names the forward pipeline reads. A reversed slice is a
127
147
  # NORMAL feature, so it carries backend-plan.md / frontend-plan.md like any other —
@@ -149,18 +169,6 @@ vars:
149
169
 
150
170
  workflow:
151
171
 
152
- # 0. TECHNICAL rule files → sticky standards (configuration, not hardcoded). tech-select
153
- # reads guardrails/projects/<project>/tech-rule-bindings.md under "## /aw-spec-adopt" and
154
- # loads that reference set into the sticky standards. Loaded FIRST so it is present for
155
- # reverse_design (include_standards:true) — the reversed SPEC step still turns it off
156
- # (include_standards:false), keeping design domain-free (rule partition). No domain rule
157
- # file here — the tech map lists only technical files. No blocking gate: an unconfigured
158
- # project soft-continues (constitution.technical.md still carries the tech base).
159
- - id: tech_select
160
- action: include
161
- module: .appweaver/orchestrator/modules/tech-select.orch.yaml
162
- with: { command: aw-spec-adopt }
163
-
164
172
  # 1. Resolve the aggregate to adopt. TWO ways in (arg wins over active):
165
173
  # a) `/appweaver adopt <Feature>` — the explicit arg selects directly.
166
174
  # b) `/appweaver adopt` (no arg) — CONSUME the feature select-feature already
@@ -253,6 +261,34 @@ workflow:
253
261
  - { name: aggregate_sections, from: "$.sections" }
254
262
  - { name: aggregate_source, from: "$.source" }
255
263
  - { name: aggregate_primary, from: "$.primary" } # the concept this feature's OKF work lands on
264
+ # THE FEATURE'S SCOPE INSIDE THE AGGREGATE. The concept lists every file of the
265
+ # aggregate, and two features over one aggregate share them — only the boundary map's
266
+ # operation list tells their scopes apart. operations_note names this feature's
267
+ # operations and the ones only a sibling owns, as a line every reverse step quotes.
268
+ - { name: operations_note, from: "$.operations_note" }
269
+ - { name: operations_csv, from: "$.operations_csv" }
270
+ - { name: one_to_one, from: "$.one_to_one" }
271
+ - { name: owned_sections, from: "$.owned_sections" }
272
+ - { name: shared_with_csv, from: "$.shared_with_csv" }
273
+ # The inventory HEAD the map was drawn at — the source-map's baseline commit and the
274
+ # concept's timestamp. Empty time where git cannot resolve the commit.
275
+ - { name: baseline_commit, from: "$.baseline_commit" }
276
+ - { name: baseline_time, from: "$.baseline_time" }
277
+
278
+ # HOW MUCH OF THE MODEL SECTION THE BACKEND PLAN REALISES — the rule the forward `plan`
279
+ # applies. A feature that alone answers for its aggregate realises the whole section; one
280
+ # that shares it realises only what its own operations act on, or two plans would realise
281
+ # the same element. The engine has no conditional prompt text, so the rule is an ordered
282
+ # pair of sets handed to the prompt as one line.
283
+ - id: model_scope_shared
284
+ action: set
285
+ vars:
286
+ model_scope_plan: "{{owned_sections}}, which this feature SHARES with {{shared_with_csv}}. Realise only the entities, value objects, states and BR-nnn this feature's operations act on, under the model's names and boundary; an element of that section only another feature's operations use belongs to that feature's plan and is left out — never redefined, renamed or narrowed"
287
+ - id: model_scope_sole
288
+ condition: "{{one_to_one}} == true"
289
+ action: set
290
+ vars:
291
+ model_scope_plan: "{{owned_sections}}. This feature is the only one that answers for it, so the plan realises that whole section"
256
292
 
257
293
  # 1a. One-time baseline: refuse to clobber an already-adopted / authored spec.
258
294
  # After adoption, changes go through the CR delta loop, not adopt.
@@ -329,6 +365,20 @@ workflow:
329
365
  on_false: stop
330
366
  on_false_message: "No active governance for adopt to work against — run /appweaver onboard first. onboard establishes the PROJECT constitution + guardrails/projects/<project>/ rule set (provisional-from-scan) and activates it. Reversing '{{feature_name}}' without that scaffold would author the spec and design against no code-derived rules at all, and leave them with no governance home."
331
367
 
368
+ # TECHNICAL rule files → sticky standards (configuration, not hardcoded). tech-select
369
+ # reads guardrails/projects/<project>/tech-rule-bindings.md under "## /aw-spec-adopt" and
370
+ # loads that reference set into the sticky standards for reverse_plan
371
+ # (include_standards:true) — the reversed SPEC and MODEL steps turn it off
372
+ # (include_standards:false), keeping them free of architecture (rule partition). No domain
373
+ # rule file here — the tech map lists only technical files. No blocking gate: an
374
+ # unconfigured project soft-continues (constitution.technical.md still carries the tech
375
+ # base). Placed after every gate that can stop the run, so no selection box is shown for a
376
+ # run that stops anyway.
377
+ - id: tech_select
378
+ action: include
379
+ module: .appweaver/orchestrator/modules/tech-select.orch.yaml
380
+ with: { command: aw-spec-adopt }
381
+
332
382
  # 2. Scaffold the living docs. The SPEC reuses the forward spec-template (it is
333
383
  # tech-agnostic WHAT/WHY — no architectural bias). The BACKEND PLAN scaffolds from the
334
384
  # DEFAULT `design-template` — which /appweaver onboard REPLACED with the repo's
@@ -405,6 +455,7 @@ workflow:
405
455
  outputs:
406
456
  - { name: fe_declared, from: "$.frontend_declared" }
407
457
  - { name: fe_root, from: "$.frontend_root" }
458
+ - { name: be_declared, from: "$.backend_declared" }
408
459
  - id: ui_lane_off
409
460
  action: set
410
461
  vars: { ui_lane: "false" }
@@ -417,6 +468,40 @@ workflow:
417
468
  condition: "{{ui_count}} == 0"
418
469
  vars: { ui_lane: "false" }
419
470
 
471
+ # 2c. RECORD A SERVICE-ONLY SLICE, so the later phases know it is one. A declared
472
+ # frontend lane whose surface this slice does not touch is a DECISION adopt just
473
+ # made, and nothing downstream can re-derive it: `plan` would otherwise read the repo's
474
+ # declaration, find no frontend-plan.md, and author one from the spec for screens that
475
+ # do not exist, and `analyze-plan` would stop on the missing document. feature-lanes
476
+ # writes `lanes: [backend]` into this feature's active_version.json, and
477
+ # `lane-resolve --feature-dir` reports the frontend lane as not declared FOR THIS
478
+ # FEATURE. Recorded only where a backend lane carries the slice — with none declared
479
+ # there is no lane to narrow to. `feature-lanes --clear` undoes it when the feature
480
+ # gains a UI. Four ordered sets, because the engine has no compound conditions.
481
+ - id: fe_no_surface_off
482
+ action: set
483
+ vars: { fe_no_surface: "false" }
484
+ - id: fe_no_surface_declared
485
+ action: set
486
+ condition: "{{fe_declared}}"
487
+ vars: { fe_no_surface: "true" }
488
+ - id: fe_no_surface_has_ui
489
+ action: set
490
+ condition: "{{ui_count}} != 0"
491
+ vars: { fe_no_surface: "false" }
492
+ - id: fe_no_surface_no_backend
493
+ action: set
494
+ condition: "{{be_declared}} != true"
495
+ vars: { fe_no_surface: "false" }
496
+ - id: record_lanes
497
+ condition: "{{fe_no_surface}} == true"
498
+ action: execute_script
499
+ script: { ref: actions }
500
+ subcommand: feature-lanes
501
+ args: ["--feature-dir", "{{feature_dir}}", "--set", "backend"]
502
+ outputs:
503
+ - { name: feature_lanes, from: "$.lanes" }
504
+
420
505
  # Scaffold the LANE's plan from ITS template pair (--lane frontend resolves
421
506
  # ui-design-template -> frontend-plan.md), the twin of scaffold_plan above. onboard
422
507
  # GENERATED that override from this repo's own UI source, so the reversed frontend plan
@@ -445,7 +530,7 @@ workflow:
445
530
  action: verify_context
446
531
  require_true: ui_tpl_override
447
532
  on_false: stop
448
- on_false_message: "A frontend lane is declared ({{fe_root}}) and '{{feature_name}}' owns {{ui_count}} UI file(s), but no CODE-DERIVED UI design template is installed at .appweaver/templates/overrides/ui-design-template.md — only the kit's greenfield template, which would impose a section structure this app's UI did not choose. onboard GENERATES the override from the UI source — declare the lane with /appweaver select-stack and re-run /appweaver onboard, then adopt this feature again."
533
+ on_false_message: "A frontend lane is declared ({{fe_root}}) and '{{feature_name}}' owns {{ui_count}} UI file(s), but no CODE-DERIVED UI design template is installed at .appweaver/templates/overrides/ui-design-template.md — only the kit's greenfield template, which would impose a section structure this app's UI did not choose. onboard GENERATES the override from the UI source, together with the lane's constitution half and rule set, when it first sees the lane declared. Run /appweaver onboard {{context}} — naming an onboarded context re-runs its governance pass, which authors the missing lane tier — then adopt this feature again."
449
534
 
450
535
  # 3. Pre-execution hooks.
451
536
  - id: resolve_pre_hooks
@@ -474,6 +559,7 @@ workflow:
474
559
  context:
475
560
  - { path: "{{spec_file}}" } # the scaffolded template = section structure + the modelling vocabulary
476
561
  - { path: "{{concept_path}}" } # the OKF concept: source_files, seam, open_questions
562
+ - { path: "{{domain_model}}" } # the CONTEXT's model — the BR-nnn ids already issued, which this spec cites and continues
477
563
  - { path: .appweaver/memory/constitution.domain.md, optional: true } # the domain frame
478
564
  - { path: .appweaver/memory/constitution.ui.md, optional: true } # the frontend lane's UX-* base — absent on a backend-only repo, which onboard leaves unwritten by design
479
565
  - id: reverse_spec
@@ -498,26 +584,40 @@ workflow:
498
584
  whatever it is named: its user stories are that capability's operations and no
499
585
  others. Sharing a name with an aggregate does not make it that aggregate's only
500
586
  feature — other features may act on the same aggregate. Keep US-1.
501
- - Each command / endpoint / use-case the code exposes is a USER STORY (US-n),
587
+ - THE FEATURE'S SCOPE. {{operations_note}} The source files below are the whole
588
+ aggregate's, and a sibling feature over it shares them — so an operation that
589
+ belongs only to another feature is in this code and is still NOT this spec's: no
590
+ user story, requirement or success criterion for it. A read both features call is
591
+ this feature's too.
592
+ - Each of THIS feature's commands / endpoints / use-cases is a USER STORY (US-n),
502
593
  never a separate feature. Derive its Given/When/Then from the code's paths.
503
594
  - System Requirements in EARS (THE SYSTEM SHALL / WHEN / IF…THEN / WHILE /
504
595
  WHERE), each atomic + testable, derived from observed behaviour.
505
596
  - Always-true invariants the code enforces get stable BR-nnn ids, CITED here and
506
- DEFINED in the domain model — see the next bullet.
597
+ DEFINED in the domain model — see the next bullet. The context's domain-model.md
598
+ is in context: still the template on the context's first adopted feature, and on
599
+ a later one it already defines aggregates and BR-nnn ids that other specs cite. A
600
+ rule the model ALREADY defines is cited by ITS id. A rule it lacks takes the next
601
+ id after the highest BR-nnn the model carries (BR-001 on an unfilled model), and
602
+ the next step defines it there. Never cite an id the model uses for a DIFFERENT
603
+ rule — not even when a code comment names that id: the model's numbering wins,
604
+ and the code's citation is recorded as drift.
507
605
  - THE DOMAIN MODEL IS NOT THIS DOCUMENT'S. Fill "### Domain Model Reference" with
508
606
  the aggregate's NAME and the ids of the invariants this spec relies on
509
607
  (BR-nnn), and nothing more. Do NOT write the ubiquitous language, the aggregate
510
608
  boundary, the entity/value-object split, the lifecycle states, or the TEXT of any
511
- invariant: all of that lives in the bounded context's domain-model.md, which the
512
- NEXT step reverses out of this same code, and several specs act on one aggregate
513
- so a per-spec copy drifts. Cite by id; never restate.
609
+ invariant: all of that lives in the bounded context's domain-model.md (in
610
+ context), which the NEXT step fills or extends from this same code, and several
611
+ specs act on one aggregate so a per-spec copy drifts. Cite by id; never restate.
514
612
  - DOMAIN LIFT — where the CODE enforces them, lift into the three sections
515
613
  (cite the source file): Constraints & NFRs (CN-, e.g. retention /
516
614
  immutability / append-only audit the code implements), Data Classification
517
615
  (DC-, sensitive fields + where they may/mustn't appear), Integration
518
616
  Contracts (IC-, external services/feeds the code calls). "None (as of v1.0)"
519
617
  if the code shows none — never delete a section.
520
- - EXPERIENCE LIFT — only where this slice HAS a UI. Fill Experience Scope from
618
+ - EXPERIENCE LIFT — only where this slice HAS a UI. Here that is {{ui_lane}} (true
619
+ = a frontend lane is declared and this aggregate owns UI files, listed below —
620
+ read them). Fill Experience Scope from
521
621
  the surface the code exposes (UI actors, channels, and the device / browser /
522
622
  assistive-technology floors the UI source actually targets), and lift the
523
623
  project-wide standards that UI already holds into Experience Standards (UX-n),
@@ -545,6 +645,8 @@ workflow:
545
645
  OBLIGATION is an IC- requirement, never the adapter design).
546
646
 
547
647
  Source files to read (the slice's code): {{source_files}}
648
+ UI source files (the Experience lift's evidence; empty where the aggregate has no UI —
649
+ a sibling feature's UI folder is not this feature's surface): {{ui_source_files}}
548
650
 
549
651
  # ══════════ REVERSE DOMAIN MODEL — the CONTEXT's structure, from the CODE ══════
550
652
  # The greenfield split, applied to the reverse: the spec says what the system DOES, the
@@ -596,6 +698,16 @@ workflow:
596
698
  (owns / references by id / out of scope), entities, value objects, domain events,
597
699
  lifecycle states, and every always-true invariant the code enforces as a stable
598
700
  BR-nnn with its RULE TEXT. The spec cites those ids and never restates them.
701
+ - THE SECTION MODELS THE WHOLE AGGREGATE, as its code has it — every entity, value
702
+ object, lifecycle state, event and invariant the root carries, including the parts
703
+ only another feature's operations exercise. {{operations_note}} Structure belongs
704
+ to the aggregate, not to one capability: a later feature over it cites the ids
705
+ already here instead of defining its part a second time. `Serving specs` names the
706
+ features that have a spec so far, this one included — never one that has none yet.
707
+ - NUMBERING. Define every BR-nnn the reversed spec cites under exactly the id it
708
+ cites. An invariant the spec does not cite takes the next free id after the highest
709
+ in the model — or the id the code's own comments give it, where that id is free.
710
+ An id the model already uses is never given to a different rule.
599
711
  - IF THE MODEL ALREADY HAS CONTENT, this is the context's SECOND (or later) adopted
600
712
  slice. ADD any of this feature's aggregates not already there as a new section and
601
713
  EXTEND the ubiquitous language. Do NOT rewrite, renumber or "tidy" the aggregates
@@ -672,10 +784,15 @@ workflow:
672
784
  Domain/Application/API/Infrastructure layering. A component the code has that
673
785
  fits no textbook layer (a UI tier, a worker, a shared library) gets its own
674
786
  section; a layer the code lacks does NOT appear.
787
+ - THE FEATURE'S SCOPE. {{operations_note}} The source files below are the whole
788
+ aggregate's, shared with any sibling feature over it: the plan's operation
789
+ components realise THIS feature's operations only, and a component that serves only
790
+ another feature's operation is left out even though its file is listed.
675
791
  - KEEP THE NEUTRAL ANCHORS (the template pins these): §1.2 is the domain/data
676
- MODEL and MUST match the CONTEXT'S DOMAIN MODEL exactly — the same entity and
677
- value-object NAMES, the same boundary, the same lifecycle states — with
678
- cross-slice References-by-id kept here. Every BR-nnn the model defines gets a
792
+ MODEL and follows the CONTEXT'S DOMAIN MODEL exactly within this feature's part of
793
+ it — {{model_scope_plan}}. Within that part: the same entity and value-object NAMES,
794
+ the same boundary, the same lifecycle states — with cross-slice References-by-id
795
+ kept here. Every BR-nnn in that part of the model gets a
679
796
  named ENFORCEMENT POINT in this plan: cite it by id and NEVER restate what it
680
797
  says, because the model is the only place that text lives. One §2 subsection is
681
798
  the EXTERNAL INTEGRATIONS table (heading contains "Integration") realising the
@@ -701,6 +818,17 @@ workflow:
701
818
  # NOT its parent: backend-plan.md realises the spec's FR/BR/CN-, frontend-plan.md
702
819
  # realises the spec's UX- and the user stories' interaction, and it CONSUMES the backend
703
820
  # plan's contracts rather than restating them. Both are in context for exactly that reason.
821
+ # THE FRONTEND'S RULES REPLACE THE BACKEND'S before the lane's plan is authored — the
822
+ # swap the forward `plan` makes at the same boundary. Sticky standards only accumulate,
823
+ # so without it the UI plan was authored under the service rule set and none of the
824
+ # lane's own. `## /aw-spec-plan-ui` is the set the forward plan authors this same
825
+ # document under, so a reversed frontend plan and a planned one answer to one rule set.
826
+ - id: tech_select_ui
827
+ condition: "{{ui_lane}} == true"
828
+ action: include
829
+ module: .appweaver/orchestrator/modules/tech-select.orch.yaml
830
+ with: { command: aw-spec-plan-ui, replace: "true" }
831
+
704
832
  - id: ctx_reverse_ui_plan
705
833
  condition: "{{ui_lane}} == true"
706
834
  action: build_context
@@ -728,6 +856,10 @@ workflow:
728
856
  a target to bend it toward. An inconsistent app is recorded as inconsistent; a
729
857
  surface with no loading states is recorded as having none, NEVER as needing them.
730
858
  Rules:
859
+ - THE FEATURE'S SCOPE. {{operations_note}} The UI files below are the aggregate's
860
+ whole surface: a screen, component or action that serves only another feature's
861
+ operation — typically that feature's own UI folder — is that feature's. Where this
862
+ feature's screen hosts it, cite it as the sibling's; never plan it here.
731
863
  - STAY IN THE LANE. Realise the spec's Experience Standards (UX-) and the
732
864
  interaction its User Stories imply. The backend plan (in context) supplies
733
865
  the CONTRACTS this surface consumes — cite them; do NOT restate, re-derive or
@@ -776,8 +908,9 @@ workflow:
776
908
  (certain | inferred). EVERY source file MUST appear — the Fidelity gate
777
909
  checks full coverage both ways.
778
910
  - 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:
911
+ code on it. {{operations_note}} Where its `features:` names features besides
912
+ '{{feature_name}}', a file that serves ONLY one of those sibling capabilities (only
913
+ their operations) is not this feature's:
781
914
  record it under Unmapped / open as `sibling: <Feature>`, never as a mapped row.
782
915
  The mapped rows ARE this feature's file list — later commands hand exactly those
783
916
  files to their fix loops.
@@ -791,7 +924,8 @@ workflow:
791
924
  analysis (not a migration item).
792
925
  - Record as-built quirks captured verbatim (candidate deviations), and any
793
926
  unmapped code / unanchored spec ids.
794
- - Baseline commit = the inventory HEAD.
927
+ - Baseline commit: `{{baseline_commit}}` — the inventory HEAD the boundary map was
928
+ drawn at. Write `unknown` when that is empty; never guess one.
795
929
  Source files to map: {{source_files}}
796
930
 
797
931
  # ═══════ FIDELITY — do spec + model + plans describe the CODE? ══════════════
@@ -817,7 +951,7 @@ workflow:
817
951
  - "{{concept_path}}"
818
952
  include_standards: false
819
953
  tools: Read # the check is documents vs CODE; with no code it degrades to doc vs doc and certifies fabrication as ok
820
- schema: { ok: true, report_needed: false, blocking_gaps: [ { severity: "high", artefact: "<source file / spec id / model element / plan element>", kind: "unmapped-code | fabricated-spec | fabricated-model | fabricated-plan", detail: "<why>" } ], review_items: [ { kind: "inferred | as-built-quirk | missing-invariant | confidence", where: "<spec/model/plan/source-map location>", note: "<what to confirm out of band>" } ] }
954
+ schema: { ok: true, report_needed: false, blocking_gaps: [ { severity: "high", artefact: "<source file / spec id / model element / plan element>", kind: "unmapped-code | fabricated-spec | fabricated-model | fabricated-plan | foreign-operation", detail: "<why>" } ], review_items: [ { kind: "inferred | as-built-quirk | missing-invariant | confidence", where: "<spec/model/plan/source-map location>", note: "<what to confirm out of band>" } ] }
821
955
  mock: { ok: true, report_needed: false, blocking_gaps: [], review_items: [] }
822
956
  task: |
823
957
  FIDELITY CHECK — do the reversed documents actually describe the CODE
@@ -836,6 +970,14 @@ workflow:
836
970
  value object the code does not have, an aggregate boundary it does not draw,
837
971
  a lifecycle state it never enters, or a BR-nnn whose rule nothing enforces.
838
972
  An anemic slice reversing as an anemic model is CORRECT, not a gap.
973
+ (3) foreign-operation: a user story, requirement, plan element or UI-plan element
974
+ that realises an operation belonging ONLY to another feature over the same
975
+ aggregate. {{operations_note}} Its code is real, so it is not a fabrication —
976
+ but two specs owning one operation is the overlap the boundary map exists to
977
+ prevent, and every later change to it would land in the wrong feature.
978
+ - THE MODEL SECTION COVERS THE WHOLE AGGREGATE. An entity, state, event or invariant
979
+ that only a sibling feature's operations exercise is NOT fabricated when the code
980
+ has it: the section models the aggregate, not this feature's slice of it.
839
981
  - ONLY THIS FEATURE'S SECTIONS ARE THIS RUN'S. domain-model.md is the CONTEXT's and
840
982
  may already carry sections from earlier adopted slices — judge ONLY
841
983
  {{aggregate_sections}}. Do not report a sibling aggregate as unmapped because this
@@ -892,7 +1034,7 @@ workflow:
892
1034
  action: verify_context
893
1035
  require_true: fidelity_ok
894
1036
  on_false: stop
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."
1037
+ 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. A `foreign-operation` gap means a document claims an operation the boundary map gives only to another feature; remove it here — it belongs to that feature's adopt."
896
1038
 
897
1039
  # ═══════════ OKF CONCEPT — COMPLETE the shallow scan concept (deep reverse) ═══
898
1040
  # onboard wrote this aggregate's OKF concept SHALLOW (a discovery stub — enough to pick the
@@ -937,12 +1079,14 @@ workflow:
937
1079
  points at the deep truth. Keep every link another feature's adopt added.
938
1080
  - UNION `source_files` with the source-map's mapped files (grouped by the code's OWN
939
1081
  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.
1082
+ capability's. The boundary map was drawn at the baseline commit, whose time is
1083
+ [{{baseline_time}}]: set `timestamp` to that value, and where the brackets are empty
1084
+ (git could not resolve the commit) leave `timestamp` exactly as it is.
941
1085
  - 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.
1086
+ includes '{{feature_name}}'; stamp `generated_by: aw-spec-adopt`; and add
1087
+ '{{feature_name}}' to `adopted_features:` (a block list, like `features:`), keeping
1088
+ every feature already listed there. A feature in `features:` and not in
1089
+ `adopted_features:` is still the shallow stub — never mark the whole concept done.
946
1090
  - Resolve any `open_questions` the deep reverse answered; keep the rest.
947
1091
  - Do NOT touch the concept's Depends-On / Cross-References sections (xref_record
948
1092
  maintains those next), and do NOT edit any OTHER aggregate concept.
@@ -1188,6 +1332,9 @@ outputs:
1188
1332
  bounded_context: "{{context}}"
1189
1333
  ui_lane: "{{ui_lane}}"
1190
1334
  ui_count: "{{ui_count}}"
1335
+ feature_lanes: "{{feature_lanes}}" # empty = every declared lane; [backend] = a service-only slice in a repo with a frontend
1336
+ operations: "{{operations_csv}}" # this feature's operations, from the boundary map
1337
+ baseline_commit: "{{baseline_commit}}"
1191
1338
  source_map: "{{feature_dir}}/source-map.md"
1192
1339
  feature: "{{feature_name}}"
1193
1340
  # What the feature acts on. Equal to the feature on a 1:1 slice; not on a feature that
@@ -1204,4 +1351,4 @@ outputs:
1204
1351
  composed_file: "{{composed_file}}"
1205
1352
  technical_reconcile_ok: "{{technical_ok}}"
1206
1353
  domain_reconcile_ok: "{{domain_ok}}"
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."
1354
+ 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. In the second case, where a frontend lane IS declared, the feature records lanes=[backend] in its active_version.json (feature_lanes shows it), so plan and the analyze gates treat the frontend lane as absent for THIS feature; run `aw-spec-actions feature-lanes --clear` if it later gains a UI. 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."
@@ -77,6 +77,7 @@ vars:
77
77
  lanes_declared: "false"
78
78
  be_declared: "false"
79
79
  fe_declared: "false"
80
+ fe_excluded: "false" # this feature records no surface in the declared frontend lane (feature-lanes)
80
81
  has_changes: "false"
81
82
  cr_choice: ""
82
83
  cr_id: ""
@@ -149,6 +150,23 @@ workflow:
149
150
  - { name: backend_plan_exists, from: "$.backend_plan_exists" }
150
151
  - { name: frontend_plan_exists, from: "$.frontend_plan_exists" }
151
152
 
153
+ # THIS FEATURE'S LANES. lane_resolve above read the REPO's declaration; one feature may
154
+ # own no part of a lane's surface — adopt records `lanes: [backend]` in
155
+ # active_version.json for a service-only slice in a repo that also has a UI. Re-read with
156
+ # --feature-dir so a lane the feature records no surface in is absent here: no delta plan
157
+ # is expected of it. With no feature resolved the argument is dropped and the repo's
158
+ # answer stands.
159
+ - id: feature_lanes
160
+ action: execute_script
161
+ on_error: continue
162
+ script: { ref: actions }
163
+ subcommand: lane-resolve
164
+ args: ["--feature-dir", "{{feature_dir}}"]
165
+ outputs:
166
+ - { name: be_declared, from: "$.backend_declared" }
167
+ - { name: fe_declared, from: "$.frontend_declared" }
168
+ - { name: fe_excluded, from: "$.frontend_feature_excluded" }
169
+
152
170
  - id: require_feature
153
171
  action: verify_context
154
172
  require_non_empty: feature_dir
@@ -119,6 +119,7 @@ vars:
119
119
  lanes_declared: "false"
120
120
  be_declared: "false"
121
121
  fe_declared: "false"
122
+ fe_excluded: "false" # this feature records no surface in the declared frontend lane (feature-lanes)
122
123
  # PER-LANE PRESENCE. A lane is CHECKED when it is declared and its plan document is on
123
124
  # disk, and MISSING when it is declared and the document is not. The engine has no
124
125
  # compound conditions, so each AND is one ordered assignment taking the lane's own
@@ -190,6 +191,23 @@ workflow:
190
191
  - { name: backend_plan_exists, from: "$.backend_plan_exists" }
191
192
  - { name: frontend_plan_exists, from: "$.frontend_plan_exists" }
192
193
 
194
+ # THIS FEATURE'S LANES. lane_resolve above read the REPO's declaration; one feature may
195
+ # own no part of a lane's surface — adopt records `lanes: [backend]` in
196
+ # active_version.json for a service-only slice in a repo that also has a UI. Re-read with
197
+ # --feature-dir so a lane the feature records no surface in is absent here: its missing
198
+ # plan is not a gap this gate stops on. With no feature resolved the argument is dropped
199
+ # and the repo's answer stands.
200
+ - id: feature_lanes
201
+ action: execute_script
202
+ on_error: continue
203
+ script: { ref: actions }
204
+ subcommand: lane-resolve
205
+ args: ["--feature-dir", "{{feature_dir}}"]
206
+ outputs:
207
+ - { name: be_declared, from: "$.backend_declared" }
208
+ - { name: fe_declared, from: "$.frontend_declared" }
209
+ - { name: fe_excluded, from: "$.frontend_feature_excluded" }
210
+
193
211
  # The BOUNDED CONTEXT's domain model — where the aggregate root, its entities and its
194
212
  # invariants (BR-n) live. The spec cites them by id and the plans record where each is
195
213
  # ENFORCED, so check 4 has nothing to check without it in context.
@@ -773,6 +791,7 @@ outputs:
773
791
  bounded_context: "{{context}}"
774
792
  backend_plan: "{{backend_plan}}" # empty when no backend lane is declared
775
793
  frontend_plan: "{{frontend_plan}}" # empty when no frontend lane is declared
794
+ frontend_excluded: "{{fe_excluded}}" # true = a frontend lane is declared and this feature records no surface in it (feature-lanes)
776
795
  domain_model: "{{domain_model}}"
777
796
  conformant: "{{analyze_ok}}"
778
797
  gaps: "{{analyze_gaps}}"
@@ -115,6 +115,7 @@ vars:
115
115
  # ordered assignment; the engine has no compound conditions.
116
116
  be_declared: "false"
117
117
  fe_declared: "false"
118
+ fe_excluded: "false" # this feature records no surface in the declared frontend lane (feature-lanes)
118
119
  be_gated: "false"
119
120
  fe_gated: "false"
120
121
  any_lane: "false"
@@ -193,6 +194,23 @@ workflow:
193
194
  - { name: backend_plan, from: "$.backend_plan" }
194
195
  - { name: frontend_plan, from: "$.frontend_plan" }
195
196
 
197
+ # THIS FEATURE'S LANES. lane_resolve above read the REPO's declaration; one feature may
198
+ # own no part of a lane's surface — adopt records `lanes: [backend]` in
199
+ # active_version.json for a service-only slice in a repo that also has a UI. Re-read with
200
+ # --feature-dir so a lane the feature records no surface in is absent here: its code is
201
+ # not gated against a plan it never had. With no feature resolved the argument is dropped
202
+ # and the repo's answer stands.
203
+ - id: feature_lanes
204
+ action: execute_script
205
+ on_error: continue
206
+ script: { ref: actions }
207
+ subcommand: lane-resolve
208
+ args: ["--feature-dir", "{{feature_dir}}"]
209
+ outputs:
210
+ - { name: be_declared, from: "$.backend_declared" }
211
+ - { name: fe_declared, from: "$.frontend_declared" }
212
+ - { name: fe_excluded, from: "$.frontend_feature_excluded" }
213
+
196
214
  - id: require_feature
197
215
  action: verify_context
198
216
  require_non_empty: feature_dir