aw-spec-cli 0.1.20 → 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 (41) hide show
  1. package/assets/.appweaver/ORCH-DIALECT.md +8 -3
  2. package/assets/.appweaver/orchestrator/modules/delta-spec.orch.yaml +22 -1
  3. package/assets/.appweaver/orchestrator/orchestrator.py +65 -3
  4. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan-delta.orch.yaml +6 -6
  5. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan.orch.yaml +107 -48
  6. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice-delta.orch.yaml +17 -9
  7. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice.orch.yaml +265 -20
  8. package/assets/.appweaver/orchestrator/workflows/aw-spec-constitution.orch.yaml +4 -4
  9. package/assets/.appweaver/orchestrator/workflows/aw-spec-finalize-delta.orch.yaml +89 -15
  10. package/assets/.appweaver/orchestrator/workflows/aw-spec-foundation.orch.yaml +140 -36
  11. package/assets/.appweaver/orchestrator/workflows/aw-spec-implement.orch.yaml +354 -52
  12. package/assets/.appweaver/orchestrator/workflows/aw-spec-onboard.orch.yaml +3 -3
  13. package/assets/.appweaver/orchestrator/workflows/aw-spec-select-stack.orch.yaml +47 -23
  14. package/assets/.appweaver/orchestrator/workflows/aw-spec-specify.orch.yaml +1 -1
  15. package/assets/.appweaver/scripts/bash/aw-spec-actions.sh +651 -76
  16. package/assets/.appweaver/scripts/powershell/aw-spec-actions.ps1 +600 -59
  17. package/assets/.appweaver/scripts/python/aw-crstate.py +194 -0
  18. package/assets/.appweaver/scripts/python/aw-foundation.py +151 -0
  19. package/assets/.appweaver/scripts/python/aw-payload-parity.py +3 -0
  20. package/assets/.appweaver/scripts/python/aw-plan-coverage.py +423 -0
  21. package/assets/.appweaver/scripts/python/aw-stack.py +251 -28
  22. package/assets/.appweaver/scripts/python/aw-testlog.py +148 -0
  23. package/assets/.appweaver/stack-profiles/dotnet-webapi.json +1 -0
  24. package/assets/.appweaver/stack-profiles/java-spring.json +1 -0
  25. package/assets/.appweaver/stack-profiles/node-express.json +1 -0
  26. package/assets/.appweaver/stack-profiles/python-fastapi.json +1 -0
  27. package/assets/.appweaver/templates/constitution-domain-template.md +17 -2
  28. package/assets/guardrails/projects/dotnet-default-project/domain-rule-bindings.md +10 -6
  29. package/assets/guardrails/projects/dotnet-default-project/tech-rule-bindings.md +4 -0
  30. package/assets/guardrails/technology/dotnet-webapi/auditing.md +3 -1
  31. package/assets/guardrails/technology/dotnet-webapi/coding-standards.md +5 -3
  32. package/assets/guardrails/technology/dotnet-webapi/cqrs.md +5 -1
  33. package/assets/guardrails/technology/dotnet-webapi/domain-driven-design.md +13 -5
  34. package/assets/guardrails/technology/dotnet-webapi/examples/cqrs.md +2 -0
  35. package/assets/guardrails/technology/dotnet-webapi/examples/domain-driven-design.md +6 -2
  36. package/assets/guardrails/technology/dotnet-webapi/framework-api-map.md +70 -0
  37. package/assets/guardrails/technology/dotnet-webapi/technology-constitution.md +3 -0
  38. package/assets/guardrails/technology/dotnet-webapi/testing.md +10 -0
  39. package/assets/guardrails/technology/nextjs/ui-constitution.md +8 -1
  40. package/package.json +1 -1
  41. 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,9 +163,10 @@ 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
@@ -256,13 +292,55 @@ workflow:
256
292
  - { name: test_log, from: "$.log_tail" }
257
293
  - { name: test_matched, from: "$.matched" }
258
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" }
259
304
  - id: green_break
260
305
  action: break
261
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"
262
339
  - id: ctx_fix
263
340
  action: build_context
264
341
  vars:
265
342
  test_log: "{{test_log}}"
343
+ test_excerpt: "{{test_excerpt}}"
266
344
  context:
267
345
  - { path: "{{spec_file}}" }
268
346
  - { path: "{{domain_model}}", optional: true }
@@ -272,29 +350,64 @@ workflow:
272
350
  tools: "Read,Write,Edit,Bash"
273
351
  permission_mode: acceptEdits
274
352
  task: |
275
- 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
276
356
  test) in the files in context, keeping behaviour aligned with the spec's
277
357
  acceptance criteria (spec in context). Where the spec cites an invariant
278
358
  by id (BR-nnn), the bounded context's domain-model.md in context carries
279
359
  its text and the entity names it is stated over — read the rule there
280
- 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):
281
366
  {{test_log}}
282
367
  outputs:
283
368
  - { name: fixed_files, from: "$.files" }
284
369
 
285
- - 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
286
383
  action: verify_context
287
- require_true: test_ok
288
- on_false: stop
289
- on_false_message: >
384
+ require_value: tests_red
385
+ equals: "false"
386
+ on_mismatch: stop
387
+ on_mismatch_message: >
290
388
  Slice tests not green after 3 fix attempts (tool={{test_tool}}, scoped={{test_scoped}},
291
- matched={{test_matched}}). Read those two flags before the log. scoped=true means a
292
- per-feature name filter ran, and matched=false then means it selected ZERO tests —
293
- the test names do not contain the aggregate name — which the fence treats as a
294
- failure, not a vacuous pass. scoped=false means this stack has no per-test selector,
295
- so the lane's whole declared suite ran and every failure in it counts here, including
296
- ones this feature did not cause; matched is not meaningful in that case. Last log:
297
- {{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.
298
411
 
299
412
  # ═══════ PASS 2 — DOMAIN CONFORMANCE: code vs the gate overlay checklist (validation) ═══════
300
413
  # With tests green, scan the feature's CODE against the domain gate overlays (the
@@ -402,6 +515,99 @@ workflow:
402
515
  on_false: stop
403
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}}"
404
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
+
405
611
  # ═══════ INTEGRITY — did this feature's change break any aggregate that USES it? ═══════
406
612
  # The slice is green + domain-conformant on its own. But a change to a feature can break
407
613
  # any OTHER aggregate that references it. Ask the OKF Cross-References index "who refers to
@@ -422,7 +628,12 @@ workflow:
422
628
  - { name: referrer_count, from: "$.referrer_closure_count" }
423
629
  - { name: referrer_names, from: "$.referrer_closure" }
424
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.
425
635
  - id: run_integrity
636
+ condition: "{{tests_missing}} == false"
426
637
  action: execute_script
427
638
  on_error: continue
428
639
  script: { ref: actions }
@@ -431,6 +642,12 @@ workflow:
431
642
  outputs:
432
643
  - { name: integrity_ok, from: "$.fence_green" }
433
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}}" }
434
651
 
435
652
  # On a RED fence, author a durable triage report BEFORE stopping — a broken referrer
436
653
  # means someone must go fix ANOTHER slice, so this is a record, not scrollback (mirrors
@@ -450,7 +667,7 @@ workflow:
450
667
  context_from: all_paths
451
668
  - id: write_broken_referrers
452
669
  action: agent_author
453
- condition: "{{integrity_ok}} != true"
670
+ condition: "{{integrity_gate_ok}} == false"
454
671
  include_standards: false # a triage report, not generated code
455
672
  output_target: { type: file, path: "{{feature_dir}}/broken-referrers.md" }
456
673
  task: |
@@ -488,7 +705,7 @@ workflow:
488
705
 
489
706
  - id: integrity_gate
490
707
  action: verify_context
491
- require_true: integrity_ok
708
+ require_true: integrity_gate_ok
492
709
  on_false: stop
493
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}}"
494
711
 
@@ -545,6 +762,26 @@ workflow:
545
762
  - { name: okf_ok, from: "$.ok" }
546
763
  - { name: okf_orphans, from: "$.orphan_count" }
547
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
+
548
785
  - id: resolve_post_hooks
549
786
  action: execute_script
550
787
  script: { ref: actions }
@@ -570,7 +807,15 @@ outputs:
570
807
  # Reported by the conformance pass and never gated — surfaced so a real cross-aggregate
571
808
  # leak is visible rather than only living in one query's answer.
572
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}}"
573
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}}"
574
819
  referrer_count: "{{referrer_count}}"
575
820
  referrer_blast_radius: "{{referrer_names}}"
576
821
  okf_concept: "{{okf_concept}}"
@@ -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
@@ -40,10 +40,10 @@
40
40
  # drift. What this stage must NOT do is hide that by merging a delta plan: the plan would
41
41
  # then agree with the spec without any run of `plan` ever having derived it.
42
42
  #
43
- # ✅ THE DETECTION IS BUILT (2026-09-14). analyze-plan leaves `.aw-plan-gate.json` when it
44
- # PASSES — the sha256 of spec.md, the context's domain-model.md and each lane plan it read —
45
- # and `implement` reads it through plan-gate-check before any lane generates, refusing a
46
- # master run whose plans are no longer the ones the gate passed. So after this merge the
43
+ # ✅ THE DETECTION IS BUILT (2026-09-14). Every master-mode analyze-plan run leaves
44
+ # `.aw-plan-gate.json` — the sha256 of spec.md, the context's domain-model.md and each lane
45
+ # plan it read, and its verdict — and `implement` reads it through plan-gate-check before any
46
+ # lane generates, refusing a master run whose plans are no longer the ones the gate checked. So after this merge the
47
47
  # plans are not merely "visibly behind": the next `implement` on the master path says so and
48
48
  # stops, naming `plan main`. A feature with no receipt has never been gated since the receipt
49
49
  # existed and is reported, never refused.
@@ -94,17 +94,33 @@
94
94
  # REBASE. The delta carries the master version it was authored against (`**Base version**:`,
95
95
  # stamped by cr-setup). If the master has moved, the delta's MODIFIED items name a BEFORE
96
96
  # spec.md no longer says, and merging them is last-writer-wins over work that landed.
97
+ # cr-rebase runs FIRST and re-stamps that header — and `**Base context version**` — when
98
+ # the move is one this delta survives: the masters moved, and everything that landed in
99
+ # between touched OTHER aggregates than the ones this delta names. Nothing else in the kit
100
+ # could ever write those headers, so before this the only way past the refusal was a hand
101
+ # edit the authoring prompts forbid. Where a landed change DOES share an aggregate, the
102
+ # stamp is left alone and the refusal below names it.
97
103
  #
98
104
  # MERGED-NOT-CLOSED. The masters can move WITHOUT the version moving, because this stage
99
- # merges and close-delta bumps. In that window spec.md carries a change the number does not
100
- # reflect, so a sibling sharing the same base_version compares EQUAL and would merge onto
101
- # masters that had already shifted. The version alone is not a proxy for "the masters
102
- # moved"; the id appearing in spec.md is the other half of the answer.
105
+ # merges and close-delta bumps. In that window the masters carry a change the number does
106
+ # not reflect, so a sibling sharing the same base_version compares EQUAL and would merge
107
+ # onto masters that had already shifted. The version alone is not a proxy for "the masters
108
+ # moved"; a Change History row naming the change is the other half of the answer. That test
109
+ # lives in aw-crstate — one definition for this fence, cr-recall-check and close-delta —
110
+ # and it reads the CR CELL of a row, not the id anywhere in the file, which used to fire on
111
+ # an id quoted in prose. It looks at the context model too, which every feature shares, so
112
+ # a SIBLING feature's merged-unclosed change is visible; a sibling counts only where it
113
+ # touched an aggregate this delta also names.
103
114
  #
104
115
  # PREDECESSOR. A lower wave is committed to landing first, and `plan` put exactly those
105
- # deltas in this one's context as the base it was planned on top of. `depends_on` is
106
- # stamped nowhere, so the wave is the only dependency signal that exists — and it is the
107
- # conservative one, since same-wave changes are independent by definition.
116
+ # deltas in this one's context as the base it was planned on top of. Same-wave changes are
117
+ # independent by definition, so the wave is the conservative signal.
118
+ #
119
+ # DECLARED DEPENDENCIES vs THE WAVE. The wave is a number; `## Depends on` in the change's
120
+ # own delta documents is what that number is supposed to encode. specify derives the wave
121
+ # from that section, and this stage re-checks it (cr-wave-derive): a change naming a
122
+ # predecessor at the same or a higher wave contradicts itself, and the predecessor fence
123
+ # above — which sees only numbers — would let the two merge in either order.
108
124
  #
109
125
  # All three are RAISE-then-VERIFY and fail-closed: each flag is raised only by the good
110
126
  # state, so a check that did not run leaves the merge refused rather than waved through.
@@ -153,6 +169,17 @@ vars:
153
169
  downstream_csv: "" # the blast-radius names, fed to the scoped fence's --also and shown to the human
154
170
  fence_ok: "true" # default green so the gate is a no-op when nothing downstream
155
171
  gate_decision: "clean" # audit: 'clean' unless a RED fence was deliberately overridden
172
+ # The change's own `## Depends on` vs its wave — the fence that keeps the number and the
173
+ # prose from drifting apart. Defaults green, so a run that cannot read them is not blocked.
174
+ wave_ok: "true"
175
+ wave_required: ""
176
+ wave_message: ""
177
+ # The automatic rebase (cr-rebase). Defaults say "nothing moved, nothing stamped",
178
+ # so a step that could not run leaves the existing refusal to speak.
179
+ rebase_clean: "false"
180
+ rebase_stamped: ""
181
+ rebase_conflicts: ""
182
+ rebase_message: ""
156
183
  domain_model: "" # specs/<Context>/domain-model.md — the second merge target
157
184
  domain_model_exists: "false"
158
185
  delta_model: "" # specs/<Context>/domain-model.<id>.delta.md — the model half of the change
@@ -352,6 +379,28 @@ workflow:
352
379
  # NO on_error: continue. A gate that cannot run must not be a gate that passes, and the
353
380
  # raise-then-verify below is fail-closed for the same reason: each flag is raised ONLY
354
381
  # by the good state, so a field that never arrived leaves it down and the merge refuses.
382
+ # AUTO-REBASE FIRST, and only where it is EARNED. `**Base version**` was written once by
383
+ # cr-setup and nothing could ever update it — every other site reads it, and every
384
+ # authoring prompt forbids the model from writing it — so a moved master left the operator
385
+ # hand-editing the delta-spec in the middle of an automated loop. cr-rebase re-stamps it
386
+ # (and `**Base context version**`, which until now nothing read at all) when the masters
387
+ # moved and NOTHING THAT LANDED IN BETWEEN touched the aggregates this delta names: then
388
+ # the delta still describes the document it was written against, and the stamp is
389
+ # bookkeeping. A landed change sharing an aggregate, or one whose model delta cannot be
390
+ # read, leaves the stamp alone — and the refusal below fires with the overlap named.
391
+ # It runs BEFORE cr-merge-check so that check reads the stamp this step just settled.
392
+ - id: auto_rebase
393
+ action: execute_script
394
+ on_error: continue
395
+ script: { ref: actions }
396
+ subcommand: cr-rebase
397
+ args: ["--cr-id", "{{active_change}}", "--feature-dir", "{{feature_dir}}", "--stamp"]
398
+ outputs:
399
+ - { name: rebase_clean, from: "$.clean" }
400
+ - { name: rebase_stamped, from: "$.stamped" }
401
+ - { name: rebase_conflicts, from: "$.conflict_ids" }
402
+ - { name: rebase_message, from: "$.message" }
403
+
355
404
  - id: merge_check
356
405
  action: execute_script
357
406
  script: { ref: actions }
@@ -376,7 +425,7 @@ workflow:
376
425
  action: verify_context
377
426
  require_true: rebase_ok
378
427
  on_false: stop
379
- on_false_message: "finalize-delta REFUSED — change {{active_change}} needs a REBASE. It was authored against master version {{base_version}}, and the feature now stands at {{current_version}}: another change finalized and closed while this one was open. Its MODIFIED items name a BEFORE that spec.md no longer says, so merging now would overwrite work that already landed. Rebase: run /appweaver analyze-plan-delta {{active_change}} against the current master, fix what it reports, re-stamp the delta's `**Base version**:` to {{current_version}}, then re-run /appweaver finalize-delta {{active_change}}."
428
+ on_false_message: "finalize-delta REFUSED — change {{active_change}} needs a REBASE. It was authored against master version {{base_version}}, and the feature now stands at {{current_version}}: another change finalized and closed while this one was open. Its MODIFIED items name a BEFORE that spec.md no longer says, so merging now would overwrite work that already landed. The automatic re-stamp did not apply, because what landed in between touches this delta's own aggregates: {{rebase_message}}. Rebase for real: run /appweaver analyze-plan-delta {{active_change}} against the current master, fix what it reports — then `.appweaver/scripts/bash/aw-spec-actions.sh cr-rebase --cr-id {{active_change}} --stamp` re-stamps `**Base version**` and `**Base context version**` once the overlap with {{rebase_conflicts}} is resolved, and /appweaver finalize-delta {{active_change}} can run again."
380
429
 
381
430
  - id: merged_open_ok_raise
382
431
  condition: "{{has_merged_open}} == false"
@@ -386,7 +435,7 @@ workflow:
386
435
  action: verify_context
387
436
  require_true: merged_open_ok
388
437
  on_false: stop
389
- on_false_message: "finalize-delta REFUSED — {{merged_open_ids}} has already been MERGED into spec.md but not yet CLOSED, so the masters carry it while the version still reads {{current_version}}. Change {{active_change}} was authored against those pre-merge masters and the version comparison cannot see the difference. Finish the merged change first: /appweaver analyze-slice then /appweaver close-delta {{merged_open_ids}}, which bumps the version — then rebase this one against the new number and re-run /appweaver finalize-delta {{active_change}}."
438
+ on_false_message: "finalize-delta REFUSED — {{merged_open_ids}} has already been MERGED into a master but not yet CLOSED, so the masters carry it while the version still reads {{current_version}}. That change may belong to ANOTHER feature in this context: domain-model.md is shared, so a sibling's merge moves a master this delta was authored against, and it is counted here when it touched an aggregate this delta also names. Change {{active_change}} was authored against those pre-merge masters and the version comparison cannot see the difference. Finish the merged change first: /appweaver analyze-slice then /appweaver close-delta {{merged_open_ids}}, which bumps the version — then rebase this one against the new number and re-run /appweaver finalize-delta {{active_change}}."
390
439
 
391
440
  - id: no_blockers_raise
392
441
  condition: "{{has_blockers}} == false"
@@ -396,7 +445,28 @@ workflow:
396
445
  action: verify_context
397
446
  require_true: no_blockers
398
447
  on_false: stop
399
- on_false_message: "finalize-delta REFUSED — change {{active_change}} is at wave {{merge_wave}} and {{blocker_count}} open change(s) sit at a LOWER wave: {{blocker_ids}}. A lower wave is committed to landing FIRST, and this delta was planned with those in its context as the base it builds on — merging it now folds in behaviour that assumes a predecessor the masters do not yet carry. Merge and close the lower-wave change(s) first (/appweaver finalize-delta <CR-id> → analyze-slice → close-delta), then rebase and re-run this one. Same-wave changes are independent and are not counted here; if this delta does NOT in fact build on them, /appweaver cr-wave can correct its wave."
448
+ on_false_message: "finalize-delta REFUSED — change {{active_change}} is at wave {{merge_wave}} and {{blocker_count}} open change(s) sit at a LOWER wave: {{blocker_ids}}. A lower wave is committed to landing FIRST, and this delta was planned with those in its context as the base it builds on — merging it now folds in behaviour that assumes a predecessor the masters do not yet carry. Merge and close the lower-wave change(s) first (/appweaver finalize-delta <CR-id> → analyze-slice → close-delta), then rebase and re-run this one. Same-wave changes are independent and are not counted here; if this delta does NOT in fact build on them, correct its `## Depends on` section (naming only the deltas it truly builds on) and re-run /appweaver specify's wave derivation — `.appweaver/scripts/bash/aw-spec-actions.sh cr-wave-derive --cr-id {{active_change}} --stamp` applies it directly."
449
+
450
+ # THE DECLARED DEPENDENCIES AND THE WAVE MUST AGREE. The fence above orders changes by
451
+ # their wave NUMBER; this one checks that number against what the change's own documents
452
+ # SAY under `## Depends on`. Before this, the two could diverge silently: an author named
453
+ # a predecessor in prose, cr-setup left the wave at the predecessor's own level, and the
454
+ # fence above then read the two as independent and let them merge in either order.
455
+ - id: wave_declared_check
456
+ action: execute_script
457
+ on_error: continue
458
+ script: { ref: actions }
459
+ subcommand: cr-wave-derive
460
+ args: ["--cr-id", "{{active_change}}"]
461
+ outputs:
462
+ - { name: wave_ok, from: "$.ok" }
463
+ - { name: wave_required, from: "$.required_wave" }
464
+ - { name: wave_message, from: "$.message" }
465
+ - id: require_wave_matches_depends
466
+ action: verify_context
467
+ require_true: wave_ok
468
+ on_false: stop
469
+ on_false_message: "finalize-delta REFUSED — change {{active_change}} contradicts its own `## Depends on`: {{wave_message}}. The blocker fence above compares wave NUMBERS, so it would let the two merge in either order and this delta could land before the change it says it builds on. Either raise this change to wave {{wave_required}} (`.appweaver/scripts/bash/aw-spec-actions.sh cr-wave-derive --cr-id {{active_change}} --stamp`, which reads the same section), or correct `## Depends on` in its delta documents if it does not in fact build on them."
400
470
 
401
471
  # 1c. DOWNSTREAM GATE (hard, with a REVIEWED override). Who depends on this aggregate —
402
472
  # its OKF Cross-References, TRANSITIVELY (C→B→A all count) and placeholder-filtered
@@ -701,7 +771,7 @@ workflow:
701
771
  # the context has no model: there would be nothing in context to distil, and a
702
772
  # query with no source invents one. Best-effort; an empty answer is a no-op.
703
773
  # WHICH AGGREGATE SECTION(S) THIS CHANGE ACTUALLY TOUCHED, read off the model delta's
704
- # own `## Aggregate:` headings rather than assumed from the feature name. The Overview
774
+ # own `**Aggregates touched**:` header rather than assumed from the feature name. The Overview
705
775
  # step below selects a section by name, and a feature whose name is not an aggregate
706
776
  # name — one capability over a shared aggregate — selects nothing, producing an empty
707
777
  # answer that the writer then declines. Driving it off the delta's declared aggregates
@@ -830,6 +900,10 @@ outputs:
830
900
  # the change by close-delta. What an un-merge would restore from.
831
901
  pre_merge_snapshot: "{{snapshot_dir}}"
832
902
  base_version: "{{base_version}}"
903
+ # What the automatic rebase did before the fence read the stamp — empty when the delta was
904
+ # already current, and the reason it could not when a landed change shared its aggregates.
905
+ rebased: "{{rebase_stamped}}"
906
+ rebase_note: "{{rebase_message}}"
833
907
  model_merged: "{{delta_model_exists}}" # true ⇒ domain-model.md was the second target of this merge.
834
908
  # false = a behaviour-only change: the spec merge was the
835
909
  # whole merge, not a half of one.