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
@@ -11,9 +11,18 @@
11
11
  # "Slice" is the unit under analysis: the aggregate's code, its paired tests, and
12
12
  # its OKF knowledge concept. Three passes over it —
13
13
  # 1. BEHAVIOUR run the tests (paired to the spec's acceptance criteria by the
14
- # design docs); on red, fix up to 3×.
14
+ # design docs); on red, fix up to 3×, then stop — the passes below would
15
+ # be scoring code already known to be wrong. A slice with NOTHING to run
16
+ # (no test project, or a filter that selected zero tests) is a different
17
+ # state: no fix attempt can help, so it warns, every other pass runs, and
18
+ # the gate fails at the END with what they found.
15
19
  # 2. CONFORMANCE check the code against the domain gate overlays AND the bounded
16
20
  # context's domain-model.md (validate-only).
21
+ # 2b. COMPOSITION is every dependency the slice injects actually BOUND in the service
22
+ # lane's composition root? The one failure class the other passes cannot
23
+ # see: a container graph is not compile-checked, so an unregistered port
24
+ # builds green, passes unit tests that construct their subject directly,
25
+ # and fails on the first request. Fixed up to 2×.
17
26
  # 3. KNOWLEDGE refresh + review the slice's OKF concept (advisory).
18
27
  # Every fix made here is recorded in that concept, which is also where the slice's
19
28
  # file list is READ from (2.3.0 — `aw-xref concept-paths`, replacing the retired
@@ -62,7 +71,7 @@
62
71
 
63
72
  name: aw-spec-analyze-slice
64
73
  description: The design↔code conformance gate — run the slice's tests against its acceptance criteria, check the code against the domain gate overlays AND the bounded context's domain-model.md (every invariant the slice's own aggregate section states must be enforced as written; one another aggregate owns is reported, not blocking), verify the change did not break any referring aggregate (the OKF referrer blast radius) and write a triage report before stopping if it did, refresh + review its OKF concept, and record every fix in it. Sits after finalize-delta on the change path, so the two masters it scores against already carry the change, and before close-delta, so a red gate leaves the change open and recoverable.
65
- version: 2.4.0
74
+ version: 2.6.0
66
75
  agent_agnostic: true
67
76
  dialect: aw-spec.orchestrator/v1
68
77
 
@@ -87,7 +96,33 @@ standards:
87
96
 
88
97
  vars:
89
98
  test_ok: "false"
99
+ # TWO WAYS PASS 1 IS NOT GREEN, and they are not the same failure. `tests_red` is a suite
100
+ # that RAN and failed — the fix loop's business, and a stop. `tests_missing` is a slice
101
+ # with nothing to run (no test project, or a slice filter that selected zero tests):
102
+ # nothing to repair, so the loop leaves at once, the run carries on through every other
103
+ # pass, and the gate fails at the end with what they found. See PASS 1.
104
+ tests_red: "false"
105
+ tests_missing: "false"
106
+ tests_present: "true"
107
+ slice_testable: "true"
108
+ not_green: "false"
109
+ test_excerpt: ""
110
+ test_failing_lanes: ""
111
+ test_no_setup: "false"
112
+ test_setup_reason: ""
113
+ zero_match: "false"
90
114
  conformance_ok: "false"
115
+ # Composition pass (PASS 2b). Defaults to PASSED, because a project with no service lane
116
+ # has no container to check — the flag is lowered only where a backend lane is declared.
117
+ wiring_ok: "true"
118
+ unbound: ""
119
+ be_declared: "false"
120
+ be_lane_id: ""
121
+ be_source_path: ""
122
+ wiring_notes: ""
123
+ # The integrity fence blocks through this, not through `integrity_ok` — a fence skipped
124
+ # because there were no tests to run must not read as a broken referrer.
125
+ integrity_gate_ok: "true"
91
126
  # Invariants the model states under ANOTHER aggregate's section that this slice appears
92
127
  # to touch — reported, never blocking, since this slice does not own them. Defaulted so
93
128
  # the gate message and the outputs read a real value on a run that skipped the pass.
@@ -128,15 +163,16 @@ workflow:
128
163
  script: { ref: actions }
129
164
  subcommand: feature-paths
130
165
  outputs:
131
- - { name: feature, from: "$.feature" }
132
- - { name: feature_dir, from: "$.feature_dir" }
133
- - { name: spec_file, from: "$.spec_file" }
166
+ - { name: feature, from: "$.feature" }
167
+ - { name: feature_dir, from: "$.feature_dir" }
168
+ - { name: spec_file, from: "$.spec_file" }
169
+ - { name: backend_plan, from: "$.backend_plan" } # the ports + §4.3 wiring the composition pass reads
134
170
 
135
171
  - id: require_feature
136
172
  action: verify_context
137
173
  require_non_empty: feature_dir
138
174
  on_empty: stop
139
- on_empty_message: "No active feature. Run /appweaver select-feature first (pick or create the aggregate, e.g. Order), then re-run analyze-slice."
175
+ on_empty_message: "No active feature. Run /appweaver select-feature first (pick or create the feature, e.g. Order or PatientRegistration), then re-run analyze-slice."
140
176
 
141
177
  # The bounded context's DOMAIN MODEL — in every pass's context beside the spec, and the
142
178
  # authority the conformance pass scores against. Since the model split the spec no longer
@@ -162,6 +198,38 @@ workflow:
162
198
  - { name: domain_model, from: "$.domain_model" }
163
199
  - { name: domain_model_exists, from: "$.domain_model_exists" }
164
200
 
201
+ # WHICH PART OF THAT MODEL THIS GATE SCORES. The model is filed by aggregate, the specs
202
+ # tree by feature. For a feature that is one capability over a shared aggregate
203
+ # (PatientRegistration over Patient) no section carries the feature's name, and scoring
204
+ # "the BR-nnn under this feature's section" found none and PASSED — every invariant the
205
+ # feature relies on silently demoted to a non-blocking note. Resolved from the spec's
206
+ # Acts-on line, then the boundary map, then the feature name.
207
+ - id: feature_aggregates
208
+ action: execute_script
209
+ script: { ref: xref }
210
+ subcommand: feature-aggregates
211
+ args: ["--feature-dir", "{{feature_dir}}"]
212
+ outputs:
213
+ - { name: owned_sections, from: "$.owned_sections" }
214
+ - { name: one_to_one, from: "$.one_to_one" }
215
+
216
+ # WHICH OF THOSE INVARIANTS BLOCK. A feature that alone answers for an aggregate owns
217
+ # its section outright, and every BR-nnn under it has to be enforced in this code. A
218
+ # feature that shares the aggregate with other features — whatever it is named —
219
+ # answers only for what its spec cites: the rest of that section is the other features'
220
+ # code, and blocking on it would fail each of them for work another one does. The
221
+ # engine has no conditional prompt text, so the rule is chosen by an ordered pair of
222
+ # sets and handed to the pass as one sentence.
223
+ - id: model_scope_capability
224
+ action: set
225
+ vars:
226
+ model_scope_rule: "this feature SHARES those sections with other features, so BLOCKING covers only the BR-nnn this feature's spec CITES from them; any other BR-nnn stated there belongs to another feature and is a model_notes entry, not a violation"
227
+ - id: model_scope_owned
228
+ condition: "{{one_to_one}} == true"
229
+ action: set
230
+ vars:
231
+ model_scope_rule: "this feature is the ONLY one that answers for that section, so BLOCKING covers EVERY BR-nnn stated there, whether or not the spec cites it"
232
+
165
233
  # Is a change OPEN on this feature? Read for NAVIGATION only — see the vars note. Safe
166
234
  # on the bootstrap path: with no active_version.json, version-read returns
167
235
  # active_change="" / has_change=false and exits 0, so nothing here is conditional on it.
@@ -224,13 +292,55 @@ workflow:
224
292
  - { name: test_log, from: "$.log_tail" }
225
293
  - { name: test_matched, from: "$.matched" }
226
294
  - { name: test_scoped, from: "$.scoped" }
295
+ # The lines that NAME the failure, per failing lane, with the command and exit
296
+ # code that produced them — what the fix step reads. A positional tail can be
297
+ # entirely green while the failure sits above it.
298
+ - { name: test_excerpt, from: "$.failing_excerpt" }
299
+ - { name: test_failing_lanes, from: "$.failing_lanes" }
300
+ # "No suite ran, and here is why" — no test script, no runner, no test project.
301
+ # Distinct from a red suite, and from a scoped filter that matched nothing.
302
+ - { name: test_no_setup, from: "$.no_setup" }
303
+ - { name: test_setup_reason, from: "$.setup_reason" }
227
304
  - id: green_break
228
305
  action: break
229
306
  condition: "{{test_ok}} == true"
307
+ # WAS A SUITE ACTUALLY RUN? Two ways it was not, and both are STRUCTURAL: there is no
308
+ # failing test to repair, and three fix attempts against one only spend model passes
309
+ # to re-discover the same emptiness.
310
+ # • `no_setup` — test-run read the runner's own words: no test script wired up, no
311
+ # runner installed, no test project. It carries the reason.
312
+ # • `matched=false` on a SCOPED run — a .NET slice filter that selected zero tests.
313
+ # Only meaningful when scoped, since `matched` reads a .NET-shaped summary line;
314
+ # a failing jest run says "Tests: 3 failed" and would otherwise read as empty.
315
+ # The engine has no compound conditions, so each AND is an ordered assignment —
316
+ # `not_green` carries "the run was not green" into both tests.
317
+ - id: flag_not_green
318
+ action: set
319
+ vars: { not_green: "true" }
320
+ - id: flag_no_setup
321
+ condition: "{{test_no_setup}} == true"
322
+ action: set
323
+ vars: { tests_missing: "{{not_green}}" }
324
+ - id: flag_zero_match
325
+ condition: "{{test_scoped}} == true"
326
+ action: set
327
+ vars: { zero_match: "true" }
328
+ - id: unflag_zero_match
329
+ condition: "{{test_matched}} == true"
330
+ action: set
331
+ vars: { zero_match: "false" }
332
+ - id: flag_tests_missing
333
+ condition: "{{zero_match}} == true"
334
+ action: set
335
+ vars: { tests_missing: "{{not_green}}" }
336
+ - id: missing_break
337
+ action: break
338
+ condition: "{{tests_missing}} == true"
230
339
  - id: ctx_fix
231
340
  action: build_context
232
341
  vars:
233
342
  test_log: "{{test_log}}"
343
+ test_excerpt: "{{test_excerpt}}"
234
344
  context:
235
345
  - { path: "{{spec_file}}" }
236
346
  - { path: "{{domain_model}}", optional: true }
@@ -240,29 +350,64 @@ workflow:
240
350
  tools: "Read,Write,Edit,Bash"
241
351
  permission_mode: acceptEdits
242
352
  task: |
243
- Tests failed. Make them pass by fixing the code (or a genuinely wrong
353
+ Tests failed (tool {{test_tool}}, failing lane(s) {{test_failing_lanes}}; each
354
+ excerpt below is labelled with the lane, its command and its exit code).
355
+ Make them pass by fixing the code (or a genuinely wrong
244
356
  test) in the files in context, keeping behaviour aligned with the spec's
245
357
  acceptance criteria (spec in context). Where the spec cites an invariant
246
358
  by id (BR-nnn), the bounded context's domain-model.md in context carries
247
359
  its text and the entity names it is stated over — read the rule there
248
- rather than inferring it from the id. Test log:
360
+ rather than inferring it from the id.
361
+ FIX WHAT THE LOG ACTUALLY SAYS. The failing lines are below; if they do not
362
+ show a real failure, say so and change nothing rather than inventing a repair.
363
+ Failing lines:
364
+ {{test_excerpt}}
365
+ Log tail (context, may be from a lane that passed):
249
366
  {{test_log}}
250
367
  outputs:
251
368
  - { name: fixed_files, from: "$.files" }
252
369
 
253
- - id: test_gate
370
+ # THE RED GATE — a suite that RAN and failed, after 3 fix attempts. It stops here, and
371
+ # deliberately: the passes below score the code against the model, the container and the
372
+ # referrers, and running them over code whose own tests fail reports findings nobody can
373
+ # act on until the tests are green. The structural case does NOT come here (see below).
374
+ - id: flag_tests_red
375
+ condition: "{{test_ok}} != true"
376
+ action: set
377
+ vars: { tests_red: "true" }
378
+ - id: unflag_tests_red
379
+ condition: "{{tests_missing}} == true"
380
+ action: set
381
+ vars: { tests_red: "false", slice_testable: "false", tests_present: "false" }
382
+ - id: red_gate
254
383
  action: verify_context
255
- require_true: test_ok
256
- on_false: stop
257
- on_false_message: >
384
+ require_value: tests_red
385
+ equals: "false"
386
+ on_mismatch: stop
387
+ on_mismatch_message: >
258
388
  Slice tests not green after 3 fix attempts (tool={{test_tool}}, scoped={{test_scoped}},
259
- matched={{test_matched}}). Read those two flags before the log. scoped=true means a
260
- per-feature name filter ran, and matched=false then means it selected ZERO tests —
261
- the test names do not contain the aggregate name — which the fence treats as a
262
- failure, not a vacuous pass. scoped=false means this stack has no per-test selector,
263
- so the lane's whole declared suite ran and every failure in it counts here, including
264
- ones this feature did not cause; matched is not meaningful in that case. Last log:
265
- {{test_log}}
389
+ matched={{test_matched}}). scoped=true means a per-feature name filter ran; scoped=false
390
+ means this stack has no per-test selector, so the lane's whole declared suite ran and
391
+ every failure in it counts here, including ones this feature did not cause.
392
+ Failing lines (lane(s) {{test_failing_lanes}}):
393
+ {{test_excerpt}}
394
+ Last log: {{test_log}}
395
+
396
+ # THE STRUCTURAL CASE — nothing to run. It WARNS and carries on, so the passes below
397
+ # still report what they find; the gate fails at the end, once they have. Before this,
398
+ # a slice with no tests stopped here and the conformance, composition, integrity and
399
+ # knowledge passes never executed on any run.
400
+ - id: warn_no_tests
401
+ action: verify_context
402
+ require_true: tests_present
403
+ on_false: continue
404
+ on_false_message: >
405
+ WARNING — this slice has NO TESTS TO RUN (tool={{test_tool}}, scoped={{test_scoped}},
406
+ matched={{test_matched}}). Reason: {{test_setup_reason}} — or, where that is blank,
407
+ the slice filter selected zero tests because no test name carries the aggregate's
408
+ name. Nothing failed and nothing passed. PASS 1 proves nothing on this run, and the integrity fence below
409
+ is skipped for the same reason — every other pass still runs, and this gate fails at
410
+ the end with what they found.
266
411
 
267
412
  # ═══════ PASS 2 — DOMAIN CONFORMANCE: code vs the gate overlay checklist (validation) ═══════
268
413
  # With tests green, scan the feature's CODE against the domain gate overlays (the
@@ -316,8 +461,9 @@ workflow:
316
461
  authority on what the domain IS. The spec cites invariants by id; this document
317
462
  states them. It describes the WHOLE context while this gate scores ONE feature,
318
463
  so scope matters:
319
- • BLOCKING — every BR-nnn stated under `## Aggregate: {{feature}}`, the section
320
- this slice owns. The rule has to be enforced somewhere in this code, and the
464
+ • BLOCKING — the invariants of the section(s) this feature answers for,
465
+ {{owned_sections}}: {{model_scope_rule}}. Each blocking rule has to be
466
+ enforced somewhere in this code, and the
321
467
  enforcement has to say what the model says. An owned invariant with no
322
468
  enforcement, or one enforced with a different threshold, condition or
323
469
  direction than the model states, is a `violations` entry with the BR id as
@@ -326,7 +472,7 @@ workflow:
326
472
  • BLOCKING — the ubiquitous language of that same section. An entity, field or
327
473
  state the code names differently from the model is a violation; the model's
328
474
  name is the right one.
329
- • NOT BLOCKING — anything the model states under ANOTHER aggregate's section.
475
+ • NOT BLOCKING — anything the model states under any section not named above.
330
476
  This slice does not own those and often should not enforce them. Report each
331
477
  one this code appears to touch as a `model_notes` entry, naming the owning
332
478
  aggregate, and carry on.
@@ -335,8 +481,8 @@ workflow:
335
481
  invariant's text from its id, and never treat a rule the model does not state
336
482
  as one.
337
483
 
338
- ok=true ONLY when no checklist rule is violated AND no invariant this aggregate
339
- owns is unenforced or contradicted. `model_notes` never affects ok.
484
+ ok=true ONLY when no checklist rule is violated AND no BLOCKING invariant above
485
+ is unenforced or contradicted. `model_notes` never affects ok.
340
486
  outputs:
341
487
  - { name: conformance_ok, from: "$.ok" }
342
488
  - { name: conformance_violations, from: "$.violations" }
@@ -352,8 +498,8 @@ workflow:
352
498
  Fix these domain-conformance violations in the feature's code (in context),
353
499
  keeping the design's behaviour intact. A violation whose `rule` is an overlay
354
500
  rule is fixed against the gate checklist in the standards; one whose `rule` is a
355
- BR-nnn is fixed against the text domain-model.md states for it under
356
- `## Aggregate: {{feature}}` — enforce what the model says, where the code
501
+ BR-nnn is fixed against the text domain-model.md states for it under its section
502
+ among {{owned_sections}} — enforce what the model says, where the code
357
503
  already owns that decision, and name entities and fields the way the model names
358
504
  them. Do NOT weaken or delete a test to hide a violation, and do NOT edit
359
505
  domain-model.md or the spec to match the code: this is a gate on the code, and a
@@ -369,6 +515,99 @@ workflow:
369
515
  on_false: stop
370
516
  on_false_message: "Code fails domain conformance after 2 fix attempts (violations: {{conformance_violations}}). Each entry is either a gate-checklist rule or an invariant (BR-nnn) the aggregate's own section of {{domain_model}} states and this code does not enforce as written. Both are constraints plan/implement must satisfy — fix the code, or if the constraint itself is wrong, correct it upstream (/appweaver specify → plan) rather than editing the model to match what was built. Invariants owned by other aggregates in this context were reported, not enforced here: {{model_notes}}"
371
517
 
518
+ # ═══════ PASS 2b — COMPOSITION: is everything the slice injects actually BOUND? ═══════
519
+ # The one failure class both other passes are blind to. A dependency-injection graph is
520
+ # not compile-checked, so a port that is injected and never registered builds green, runs
521
+ # every unit test green (they construct their subject directly), and fails on the first
522
+ # request that needs it. The mock run shipped exactly that: six of seven handlers took
523
+ # ports the composition root never bound.
524
+ #
525
+ # It reads FILES, so the query declares tools — the composition root is shared
526
+ # infrastructure and is often not among the slice's own concept paths. Registration by
527
+ # assembly scan or convention counts; assuming one without finding it does not.
528
+ - id: lane_resolve
529
+ action: execute_script
530
+ on_error: continue
531
+ script: { ref: actions }
532
+ subcommand: lane-resolve
533
+ outputs:
534
+ - { name: be_declared, from: "$.backend_declared" }
535
+ - { name: be_lane_id, from: "$.backend_lane_id" }
536
+ - { name: be_source_path, from: "$.backend_source_path" }
537
+ - id: wiring_arm
538
+ condition: "{{be_declared}} == true"
539
+ action: set
540
+ vars: { wiring_ok: "false" }
541
+
542
+ - id: wiring_loop
543
+ condition: "{{be_declared}} == true"
544
+ action: loop
545
+ while: "{{wiring_ok}} != true"
546
+ max_iterations: 2
547
+ steps:
548
+ - id: ctx_wiring
549
+ action: build_context
550
+ context:
551
+ - { path: "{{backend_plan}}", optional: true } # §2.1 ports + §4.1 adapters + §4.3 wiring
552
+ context_from: all_paths
553
+ - id: check_wiring
554
+ action: agent_query
555
+ tools: "Read,Grep"
556
+ schema: { ok: true, unbound: [ { dependency: "<injected type>", injected_by: "<file:type>", checked: "<the registration file(s) read>" } ], notes: [ "<a binding worth a second look>" ] }
557
+ mock: { ok: true, unbound: [], notes: [] }
558
+ task: |
559
+ WOULD THIS SLICE RUN? Check the service lane's composition against its code.
560
+ The lane is `{{be_lane_id}}` and its source path is `{{be_source_path}}`; the
561
+ backend plan is in context, and its §2.1 Ports, §4.1 Persistence and §4.3 Wiring
562
+ say what has to be bound.
563
+
564
+ 1. Collect every dependency this slice's own types take by INJECTION — handler,
565
+ endpoint, adapter and service constructors, and the ports the plan names.
566
+ 2. Read the lane's composition root: wherever this stack registers services (a
567
+ Program/Startup file, a registration/module/configuration class, a container
568
+ module, an assembly scan). Open the files; do not infer from names.
569
+ 3. For each dependency, say whether the container binds it — an explicit
570
+ registration, an assembly scan or convention you have CONFIRMED covers that
571
+ type, or the framework itself providing it (a logger, a clock, an options
572
+ object, the HTTP context). A scan you assumed rather than found is not a
573
+ binding.
574
+
575
+ Every dependency with no binding is an `unbound` entry, naming where it is
576
+ injected and which registration file(s) you read. A binding that exists but whose
577
+ LIFETIME contradicts what it holds — a singleton capturing a scoped unit of work,
578
+ a scoped dependency in a singleton — goes in `notes`, which never affects ok.
579
+ Judge only this slice's types; another feature's unbound port is not this gate's.
580
+ ok=true only when every dependency this slice injects is bound.
581
+ outputs:
582
+ - { name: wiring_ok, from: "$.ok" }
583
+ - { name: unbound, from: "$.unbound" }
584
+ - { name: wiring_notes, from: "$.notes" }
585
+ - id: wiring_break
586
+ action: break
587
+ condition: "{{wiring_ok}} == true"
588
+ - id: fix_wiring
589
+ action: agent_generate
590
+ tools: "Read,Write,Edit,Bash"
591
+ permission_mode: acceptEdits
592
+ task: |
593
+ Bind these dependencies in the `{{be_lane_id}}` lane's composition root, under
594
+ `{{be_source_path}}`: {{unbound}}
595
+ Register each one the way this project already registers its neighbours — same
596
+ file, same helper, same lifetime convention — and bind the implementation the
597
+ plan's §4.1 / §4.3 names, never a new one you invent. Change no behaviour: this
598
+ is wiring, not design. If a dependency has NO implementation to bind because
599
+ nothing was ever written for it, do not stub one — leave it and say so, since
600
+ that is a generation gap /appweaver implement closes.
601
+ Then re-run the slice's tests (Bash) to confirm they stay green.
602
+ outputs:
603
+ - { name: wiring_fixes, from: "$.files" }
604
+
605
+ - id: wiring_gate
606
+ action: verify_context
607
+ require_true: wiring_ok
608
+ on_false: stop
609
+ on_false_message: "The service lane's container does not bind everything this slice injects, after 2 fix attempts: {{unbound}}. A dependency-injection graph is not compile-checked, so this builds green and fails on the first request that needs it. Bind each one in the lane's composition root under {{be_source_path}} — or, if nothing was ever written to bind, run /appweaver implement to generate it. Lifetime notes (not blocking): {{wiring_notes}}"
610
+
372
611
  # ═══════ INTEGRITY — did this feature's change break any aggregate that USES it? ═══════
373
612
  # The slice is green + domain-conformant on its own. But a change to a feature can break
374
613
  # any OTHER aggregate that references it. Ask the OKF Cross-References index "who refers to
@@ -389,7 +628,12 @@ workflow:
389
628
  - { name: referrer_count, from: "$.referrer_closure_count" }
390
629
  - { name: referrer_names, from: "$.referrer_closure" }
391
630
 
631
+ # Skipped when PASS 1 found nothing to run: this is the same test command over a wider
632
+ # scope, so with no tests it would report the same emptiness a second time and fail a
633
+ # fence over it. `integrity_gate_ok` is what blocks, and it stays true for a fence that
634
+ # never ran — `integrity_ok` keeps reporting what actually happened.
392
635
  - id: run_integrity
636
+ condition: "{{tests_missing}} == false"
393
637
  action: execute_script
394
638
  on_error: continue
395
639
  script: { ref: actions }
@@ -398,6 +642,12 @@ workflow:
398
642
  outputs:
399
643
  - { name: integrity_ok, from: "$.fence_green" }
400
644
  - { name: integrity_log, from: "$.log_tail" }
645
+ # Ordered assignment again: a fence that is not green blocks UNLESS it was skipped, and
646
+ # `tests_missing` carries exactly that (it reads "true" only when the fence did not run).
647
+ - id: integrity_verdict
648
+ condition: "{{integrity_ok}} != true"
649
+ action: set
650
+ vars: { integrity_gate_ok: "{{tests_missing}}" }
401
651
 
402
652
  # On a RED fence, author a durable triage report BEFORE stopping — a broken referrer
403
653
  # means someone must go fix ANOTHER slice, so this is a record, not scrollback (mirrors
@@ -417,7 +667,7 @@ workflow:
417
667
  context_from: all_paths
418
668
  - id: write_broken_referrers
419
669
  action: agent_author
420
- condition: "{{integrity_ok}} != true"
670
+ condition: "{{integrity_gate_ok}} == false"
421
671
  include_standards: false # a triage report, not generated code
422
672
  output_target: { type: file, path: "{{feature_dir}}/broken-referrers.md" }
423
673
  task: |
@@ -455,7 +705,7 @@ workflow:
455
705
 
456
706
  - id: integrity_gate
457
707
  action: verify_context
458
- require_true: integrity_ok
708
+ require_true: integrity_gate_ok
459
709
  on_false: stop
460
710
  on_false_message: "Integrity check failed — this feature's change breaks a test in an aggregate that REFERS to it. Referrer blast radius ({{referrer_count}}): {{referrer_names}}. Triage written to {{feature_dir}}/broken-referrers.md — resolve each in the referrer's own slice (or reconsider this feature's contract change), then re-run. Last log: {{integrity_log}}"
461
711
 
@@ -512,6 +762,26 @@ workflow:
512
762
  - { name: okf_ok, from: "$.ok" }
513
763
  - { name: okf_orphans, from: "$.orphan_count" }
514
764
 
765
+ # THE LAST GATE — a slice with no tests fails HERE, not at PASS 1, so everything the
766
+ # other passes found has already been reported and any fix they made is on disk. It sits
767
+ # before the after-hooks, because an extension wired to `after_analyze_slice` runs on a
768
+ # slice this gate has passed.
769
+ - id: untested_gate
770
+ action: verify_context
771
+ require_true: tests_present
772
+ on_false: stop
773
+ on_false_message: >
774
+ The slice was analysed but NOT tested — it has no tests to run (tool={{test_tool}},
775
+ scoped={{test_scoped}}, matched={{test_matched}}). Reason: {{test_setup_reason}}.
776
+ So PASS 1 and the integrity fence proved nothing. Everything above ran: domain
777
+ conformance={{conformance_ok}}, composition={{wiring_ok}}, OKF index={{okf_ok}}.
778
+ A lane whose test command is not wired up is fixed in that lane — add the script or
779
+ the runner its declared command expects, or correct the command in
780
+ .appweaver/stacks.json. Otherwise, generate the missing tests —
781
+ /appweaver implement writes a paired test per plan element, under the test path the
782
+ lane declares — or, if the tests exist, name them so each carries its aggregate's
783
+ name, which is what the slice filter selects on. Then re-run /appweaver analyze-slice.
784
+
515
785
  - id: resolve_post_hooks
516
786
  action: execute_script
517
787
  script: { ref: actions }
@@ -537,7 +807,15 @@ outputs:
537
807
  # Reported by the conformance pass and never gated — surfaced so a real cross-aggregate
538
808
  # leak is visible rather than only living in one query's answer.
539
809
  model_notes: "{{model_notes}}"
810
+ # The composition pass. `true` with no backend lane declared — there is no container.
811
+ # `wiring_notes` are lifetime observations, reported and never gated.
812
+ wiring_ok: "{{wiring_ok}}"
813
+ wiring_notes: "{{wiring_notes}}"
540
814
  test_tool: "{{test_tool}}"
815
+ # false when the slice had no tests to run — PASS 1 and the integrity fence proved
816
+ # nothing on this run, and the gate failed at the end saying so.
817
+ tests_present: "{{tests_present}}"
818
+ tests_matched: "{{test_matched}}"
541
819
  referrer_count: "{{referrer_count}}"
542
820
  referrer_blast_radius: "{{referrer_names}}"
543
821
  okf_concept: "{{okf_concept}}"
@@ -82,7 +82,7 @@ workflow:
82
82
  action: verify_context
83
83
  require_non_empty: feature_dir
84
84
  on_empty: stop
85
- on_empty_message: "No active feature. Run /appweaver select-feature first (pick or create the aggregate, e.g. Order), then re-run build."
85
+ on_empty_message: "No active feature. Run /appweaver select-feature first (pick or create the feature, e.g. Order or PatientRegistration), then re-run build."
86
86
 
87
87
  # Require A LANE — not a KIND of lane. build-compile runs EVERY declared lane's own
88
88
  # build command, so a two-lane project compiles both (the case the old first-match-wins
@@ -106,7 +106,7 @@ workflow:
106
106
  action: verify_context
107
107
  require_non_empty: feature_dir
108
108
  on_empty: stop
109
- on_empty_message: "No active feature. Run /appweaver select-feature first (pick or create the aggregate, e.g. Order), then re-run characterize."
109
+ on_empty_message: "No active feature. Run /appweaver select-feature first (pick or create the feature, e.g. Order or PatientRegistration), then re-run characterize."
110
110
 
111
111
  - id: verify_spec
112
112
  action: verify_context
@@ -136,7 +136,7 @@ workflow:
136
136
  action: verify_context
137
137
  require_non_empty: feature_dir
138
138
  on_empty: stop
139
- on_empty_message: "No active feature. Run /appweaver select-feature first (pick or create the aggregate, e.g. Order), then re-run close-delta."
139
+ on_empty_message: "No active feature. Run /appweaver select-feature first (pick or create the feature, e.g. Order or PatientRegistration), then re-run close-delta."
140
140
 
141
141
  # 2. WHICH CHANGE IS BEING CLOSED. The evidence is changes/<CR-id>/ on disk; a change
142
142
  # already closed has moved to changes/archive/. On the BOOTSTRAP path (implement →
@@ -80,7 +80,7 @@
80
80
 
81
81
  name: aw-spec-constitution
82
82
  description: Create or update the split project constitution — constitution.technical.md (technology base) + constitution.ui.md (the frontend lane's base, only where a lane exists) + constitution.domain.md (active domain addenda), each version-bumped with a Sync Impact Report, then composed into constitution.md. Project-level, run-once/amend — not a per-feature phase.
83
- version: 2.4.0
83
+ version: 2.5.0
84
84
  agent_agnostic: true
85
85
  dialect: aw-spec.orchestrator/v1
86
86
 
@@ -561,9 +561,9 @@ workflow:
561
561
  invent principles, and do NOT reach for a rule file the map did not supply. If NO
562
562
  domain rule file was loaded, say so in the Sync Impact Report and keep the principles
563
563
  to what the user input actually supports.
564
- - Author ONLY the domain layer: the adopted modelling standard, the domain
565
- principles, domain folder additions, regulatory posture & security addenda, and
566
- domain governance overrides. For each domain principle, state which technical
564
+ - Author ONLY the domain layer: the project identity, the adopted modelling
565
+ standard, the domain principles, domain folder additions, regulatory posture &
566
+ security addenda, and domain governance overrides. For each domain principle, state which technical
567
567
  principle it EXTENDS. Do NOT restate the technical half's principles or the base
568
568
  governance — reference them. Where constitution.technical.md is absent from
569
569
  context the project declared no service lane and there is no technical half to
@@ -36,7 +36,7 @@ workflow:
36
36
  action: verify_context
37
37
  require_non_empty: feature_dir
38
38
  on_empty: stop
39
- on_empty_message: "No active feature. Run /appweaver select-feature first (pick or create the aggregate, e.g. Order), then re-run deploy."
39
+ on_empty_message: "No active feature. Run /appweaver select-feature first (pick or create the feature, e.g. Order or PatientRegistration), then re-run deploy."
40
40
 
41
41
  # Pre-deploy hooks (e.g. a final smoke/build gate configured by the project).
42
42
  - id: resolve_pre_hooks