aw-spec-cli 0.1.20 → 0.1.22

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 (62) hide show
  1. package/assets/.appweaver/ORCH-DIALECT.md +8 -3
  2. package/assets/.appweaver/STACK-PROFILES.md +87 -8
  3. package/assets/.appweaver/orchestrator/modules/delta-plan.orch.yaml +30 -0
  4. package/assets/.appweaver/orchestrator/modules/delta-spec.orch.yaml +22 -1
  5. package/assets/.appweaver/orchestrator/orchestrator.py +65 -3
  6. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan-delta.orch.yaml +6 -6
  7. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan.orch.yaml +144 -52
  8. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice-delta.orch.yaml +78 -10
  9. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice.orch.yaml +283 -25
  10. package/assets/.appweaver/orchestrator/workflows/aw-spec-constitution.orch.yaml +4 -4
  11. package/assets/.appweaver/orchestrator/workflows/aw-spec-finalize-delta.orch.yaml +202 -40
  12. package/assets/.appweaver/orchestrator/workflows/aw-spec-foundation.orch.yaml +140 -36
  13. package/assets/.appweaver/orchestrator/workflows/aw-spec-implement.orch.yaml +557 -57
  14. package/assets/.appweaver/orchestrator/workflows/aw-spec-onboard.orch.yaml +3 -3
  15. package/assets/.appweaver/orchestrator/workflows/aw-spec-plan.orch.yaml +39 -0
  16. package/assets/.appweaver/orchestrator/workflows/aw-spec-recall-delta.orch.yaml +28 -7
  17. package/assets/.appweaver/orchestrator/workflows/aw-spec-select-stack.orch.yaml +47 -23
  18. package/assets/.appweaver/orchestrator/workflows/aw-spec-specify.orch.yaml +101 -1
  19. package/assets/.appweaver/orchestrator/workflows/aw-spec-ui-model.orch.yaml +141 -0
  20. package/assets/.appweaver/orchestrator/workflows/aw-spec-ui-prototype.orch.yaml +386 -0
  21. package/assets/.appweaver/orchestrator/workflows/aw-spec-workflow-model.orch.yaml +135 -0
  22. package/assets/.appweaver/scripts/bash/aw-spec-actions.sh +772 -216
  23. package/assets/.appweaver/scripts/bash/aw-xref.sh +4 -1
  24. package/assets/.appweaver/scripts/powershell/aw-spec-actions.ps1 +708 -209
  25. package/assets/.appweaver/scripts/powershell/aw-xref.ps1 +4 -1
  26. package/assets/.appweaver/scripts/python/aw-crstate.py +319 -0
  27. package/assets/.appweaver/scripts/python/aw-foundation.py +151 -0
  28. package/assets/.appweaver/scripts/python/aw-gaps.py +119 -0
  29. package/assets/.appweaver/scripts/python/aw-payload-parity.py +19 -0
  30. package/assets/.appweaver/scripts/python/aw-plan-coverage.py +503 -0
  31. package/assets/.appweaver/scripts/python/aw-plangate.py +216 -0
  32. package/assets/.appweaver/scripts/python/aw-principles.py +108 -0
  33. package/assets/.appweaver/scripts/python/aw-stack.py +257 -28
  34. package/assets/.appweaver/scripts/python/aw-testlog.py +148 -0
  35. package/assets/.appweaver/scripts/python/aw-xref.py +141 -4
  36. package/assets/.appweaver/stack-profiles/angular.json +26 -0
  37. package/assets/.appweaver/stack-profiles/blazor-wasm.json +38 -0
  38. package/assets/.appweaver/stack-profiles/dotnet-webapi.json +39 -0
  39. package/assets/.appweaver/stack-profiles/java-spring.json +20 -0
  40. package/assets/.appweaver/stack-profiles/nextjs.json +26 -0
  41. package/assets/.appweaver/stack-profiles/node-express.json +27 -0
  42. package/assets/.appweaver/stack-profiles/python-fastapi.json +16 -0
  43. package/assets/.appweaver/stack-profiles/react-vite.json +26 -0
  44. package/assets/.appweaver/stack-profiles/sveltekit.json +26 -0
  45. package/assets/.appweaver/stack-profiles/vue-vite.json +26 -0
  46. package/assets/.appweaver/templates/constitution-domain-template.md +17 -2
  47. package/assets/.appweaver/templates/ui-model-template.md +67 -0
  48. package/assets/.appweaver/templates/workflow-model-template.md +62 -0
  49. package/assets/guardrails/projects/dotnet-default-project/domain-rule-bindings.md +10 -6
  50. package/assets/guardrails/projects/dotnet-default-project/tech-rule-bindings.md +4 -0
  51. package/assets/guardrails/technology/dotnet-webapi/auditing.md +3 -1
  52. package/assets/guardrails/technology/dotnet-webapi/coding-standards.md +5 -3
  53. package/assets/guardrails/technology/dotnet-webapi/cqrs.md +5 -1
  54. package/assets/guardrails/technology/dotnet-webapi/domain-driven-design.md +13 -5
  55. package/assets/guardrails/technology/dotnet-webapi/examples/cqrs.md +2 -0
  56. package/assets/guardrails/technology/dotnet-webapi/examples/domain-driven-design.md +6 -2
  57. package/assets/guardrails/technology/dotnet-webapi/framework-api-map.md +114 -0
  58. package/assets/guardrails/technology/dotnet-webapi/technology-constitution.md +3 -0
  59. package/assets/guardrails/technology/dotnet-webapi/testing.md +10 -0
  60. package/assets/guardrails/technology/nextjs/ui-constitution.md +8 -1
  61. package/package.json +1 -1
  62. 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.
@@ -101,9 +136,11 @@ vars:
101
136
  # Integrity pass (referrer blast radius) defaults — safe values if xref/test-run is
102
137
  # skipped or errors, so the gate reads a real verdict rather than an empty var.
103
138
  integrity_ok: "false"
104
- referrer_csv: ""
139
+ referrer_csv: "" # referrers THAT HAVE CODE — what the fence can actually run
105
140
  referrer_count: "0"
106
141
  referrer_names: ""
142
+ unbuilt_referrers: "" # referrers that are planned and not implemented: nothing to run
143
+ unbuilt_count: "0"
107
144
  # Pass-3 (OKF) reporting defaults — the knowledge pass is advisory, so these stay
108
145
  # readable in the outputs even when a step is skipped or errors out.
109
146
  okf_ok: "unknown"
@@ -128,9 +165,10 @@ workflow:
128
165
  script: { ref: actions }
129
166
  subcommand: feature-paths
130
167
  outputs:
131
- - { name: feature, from: "$.feature" }
132
- - { name: feature_dir, from: "$.feature_dir" }
133
- - { name: spec_file, from: "$.spec_file" }
168
+ - { name: feature, from: "$.feature" }
169
+ - { name: feature_dir, from: "$.feature_dir" }
170
+ - { name: spec_file, from: "$.spec_file" }
171
+ - { name: backend_plan, from: "$.backend_plan" } # the ports + §4.3 wiring the composition pass reads
134
172
 
135
173
  - id: require_feature
136
174
  action: verify_context
@@ -256,13 +294,55 @@ workflow:
256
294
  - { name: test_log, from: "$.log_tail" }
257
295
  - { name: test_matched, from: "$.matched" }
258
296
  - { name: test_scoped, from: "$.scoped" }
297
+ # The lines that NAME the failure, per failing lane, with the command and exit
298
+ # code that produced them — what the fix step reads. A positional tail can be
299
+ # entirely green while the failure sits above it.
300
+ - { name: test_excerpt, from: "$.failing_excerpt" }
301
+ - { name: test_failing_lanes, from: "$.failing_lanes" }
302
+ # "No suite ran, and here is why" — no test script, no runner, no test project.
303
+ # Distinct from a red suite, and from a scoped filter that matched nothing.
304
+ - { name: test_no_setup, from: "$.no_setup" }
305
+ - { name: test_setup_reason, from: "$.setup_reason" }
259
306
  - id: green_break
260
307
  action: break
261
308
  condition: "{{test_ok}} == true"
309
+ # WAS A SUITE ACTUALLY RUN? Two ways it was not, and both are STRUCTURAL: there is no
310
+ # failing test to repair, and three fix attempts against one only spend model passes
311
+ # to re-discover the same emptiness.
312
+ # • `no_setup` — test-run read the runner's own words: no test script wired up, no
313
+ # runner installed, no test project. It carries the reason.
314
+ # • `matched=false` on a SCOPED run — a .NET slice filter that selected zero tests.
315
+ # Only meaningful when scoped, since `matched` reads a .NET-shaped summary line;
316
+ # a failing jest run says "Tests: 3 failed" and would otherwise read as empty.
317
+ # The engine has no compound conditions, so each AND is an ordered assignment —
318
+ # `not_green` carries "the run was not green" into both tests.
319
+ - id: flag_not_green
320
+ action: set
321
+ vars: { not_green: "true" }
322
+ - id: flag_no_setup
323
+ condition: "{{test_no_setup}} == true"
324
+ action: set
325
+ vars: { tests_missing: "{{not_green}}" }
326
+ - id: flag_zero_match
327
+ condition: "{{test_scoped}} == true"
328
+ action: set
329
+ vars: { zero_match: "true" }
330
+ - id: unflag_zero_match
331
+ condition: "{{test_matched}} == true"
332
+ action: set
333
+ vars: { zero_match: "false" }
334
+ - id: flag_tests_missing
335
+ condition: "{{zero_match}} == true"
336
+ action: set
337
+ vars: { tests_missing: "{{not_green}}" }
338
+ - id: missing_break
339
+ action: break
340
+ condition: "{{tests_missing}} == true"
262
341
  - id: ctx_fix
263
342
  action: build_context
264
343
  vars:
265
344
  test_log: "{{test_log}}"
345
+ test_excerpt: "{{test_excerpt}}"
266
346
  context:
267
347
  - { path: "{{spec_file}}" }
268
348
  - { path: "{{domain_model}}", optional: true }
@@ -272,29 +352,64 @@ workflow:
272
352
  tools: "Read,Write,Edit,Bash"
273
353
  permission_mode: acceptEdits
274
354
  task: |
275
- Tests failed. Make them pass by fixing the code (or a genuinely wrong
355
+ Tests failed (tool {{test_tool}}, failing lane(s) {{test_failing_lanes}}; each
356
+ excerpt below is labelled with the lane, its command and its exit code).
357
+ Make them pass by fixing the code (or a genuinely wrong
276
358
  test) in the files in context, keeping behaviour aligned with the spec's
277
359
  acceptance criteria (spec in context). Where the spec cites an invariant
278
360
  by id (BR-nnn), the bounded context's domain-model.md in context carries
279
361
  its text and the entity names it is stated over — read the rule there
280
- rather than inferring it from the id. Test log:
362
+ rather than inferring it from the id.
363
+ FIX WHAT THE LOG ACTUALLY SAYS. The failing lines are below; if they do not
364
+ show a real failure, say so and change nothing rather than inventing a repair.
365
+ Failing lines:
366
+ {{test_excerpt}}
367
+ Log tail (context, may be from a lane that passed):
281
368
  {{test_log}}
282
369
  outputs:
283
370
  - { name: fixed_files, from: "$.files" }
284
371
 
285
- - id: test_gate
372
+ # THE RED GATE — a suite that RAN and failed, after 3 fix attempts. It stops here, and
373
+ # deliberately: the passes below score the code against the model, the container and the
374
+ # referrers, and running them over code whose own tests fail reports findings nobody can
375
+ # act on until the tests are green. The structural case does NOT come here (see below).
376
+ - id: flag_tests_red
377
+ condition: "{{test_ok}} != true"
378
+ action: set
379
+ vars: { tests_red: "true" }
380
+ - id: unflag_tests_red
381
+ condition: "{{tests_missing}} == true"
382
+ action: set
383
+ vars: { tests_red: "false", slice_testable: "false", tests_present: "false" }
384
+ - id: red_gate
286
385
  action: verify_context
287
- require_true: test_ok
288
- on_false: stop
289
- on_false_message: >
386
+ require_value: tests_red
387
+ equals: "false"
388
+ on_mismatch: stop
389
+ on_mismatch_message: >
290
390
  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}}
391
+ matched={{test_matched}}). scoped=true means a per-feature name filter ran; scoped=false
392
+ means this stack has no per-test selector, so the lane's whole declared suite ran and
393
+ every failure in it counts here, including ones this feature did not cause.
394
+ Failing lines (lane(s) {{test_failing_lanes}}):
395
+ {{test_excerpt}}
396
+ Last log: {{test_log}}
397
+
398
+ # THE STRUCTURAL CASE — nothing to run. It WARNS and carries on, so the passes below
399
+ # still report what they find; the gate fails at the end, once they have. Before this,
400
+ # a slice with no tests stopped here and the conformance, composition, integrity and
401
+ # knowledge passes never executed on any run.
402
+ - id: warn_no_tests
403
+ action: verify_context
404
+ require_true: tests_present
405
+ on_false: continue
406
+ on_false_message: >
407
+ WARNING — this slice has NO TESTS TO RUN (tool={{test_tool}}, scoped={{test_scoped}},
408
+ matched={{test_matched}}). Reason: {{test_setup_reason}} — or, where that is blank,
409
+ the slice filter selected zero tests because no test name carries the aggregate's
410
+ name. Nothing failed and nothing passed. PASS 1 proves nothing on this run, and the integrity fence below
411
+ is skipped for the same reason — every other pass still runs, and this gate fails at
412
+ the end with what they found.
298
413
 
299
414
  # ═══════ PASS 2 — DOMAIN CONFORMANCE: code vs the gate overlay checklist (validation) ═══════
300
415
  # With tests green, scan the feature's CODE against the domain gate overlays (the
@@ -402,6 +517,99 @@ workflow:
402
517
  on_false: stop
403
518
  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
519
 
520
+ # ═══════ PASS 2b — COMPOSITION: is everything the slice injects actually BOUND? ═══════
521
+ # The one failure class both other passes are blind to. A dependency-injection graph is
522
+ # not compile-checked, so a port that is injected and never registered builds green, runs
523
+ # every unit test green (they construct their subject directly), and fails on the first
524
+ # request that needs it. The mock run shipped exactly that: six of seven handlers took
525
+ # ports the composition root never bound.
526
+ #
527
+ # It reads FILES, so the query declares tools — the composition root is shared
528
+ # infrastructure and is often not among the slice's own concept paths. Registration by
529
+ # assembly scan or convention counts; assuming one without finding it does not.
530
+ - id: lane_resolve
531
+ action: execute_script
532
+ on_error: continue
533
+ script: { ref: actions }
534
+ subcommand: lane-resolve
535
+ outputs:
536
+ - { name: be_declared, from: "$.backend_declared" }
537
+ - { name: be_lane_id, from: "$.backend_lane_id" }
538
+ - { name: be_source_path, from: "$.backend_source_path" }
539
+ - id: wiring_arm
540
+ condition: "{{be_declared}} == true"
541
+ action: set
542
+ vars: { wiring_ok: "false" }
543
+
544
+ - id: wiring_loop
545
+ condition: "{{be_declared}} == true"
546
+ action: loop
547
+ while: "{{wiring_ok}} != true"
548
+ max_iterations: 2
549
+ steps:
550
+ - id: ctx_wiring
551
+ action: build_context
552
+ context:
553
+ - { path: "{{backend_plan}}", optional: true } # §2.1 ports + §4.1 adapters + §4.3 wiring
554
+ context_from: all_paths
555
+ - id: check_wiring
556
+ action: agent_query
557
+ tools: "Read,Grep"
558
+ schema: { ok: true, unbound: [ { dependency: "<injected type>", injected_by: "<file:type>", checked: "<the registration file(s) read>" } ], notes: [ "<a binding worth a second look>" ] }
559
+ mock: { ok: true, unbound: [], notes: [] }
560
+ task: |
561
+ WOULD THIS SLICE RUN? Check the service lane's composition against its code.
562
+ The lane is `{{be_lane_id}}` and its source path is `{{be_source_path}}`; the
563
+ backend plan is in context, and its §2.1 Ports, §4.1 Persistence and §4.3 Wiring
564
+ say what has to be bound.
565
+
566
+ 1. Collect every dependency this slice's own types take by INJECTION — handler,
567
+ endpoint, adapter and service constructors, and the ports the plan names.
568
+ 2. Read the lane's composition root: wherever this stack registers services (a
569
+ Program/Startup file, a registration/module/configuration class, a container
570
+ module, an assembly scan). Open the files; do not infer from names.
571
+ 3. For each dependency, say whether the container binds it — an explicit
572
+ registration, an assembly scan or convention you have CONFIRMED covers that
573
+ type, or the framework itself providing it (a logger, a clock, an options
574
+ object, the HTTP context). A scan you assumed rather than found is not a
575
+ binding.
576
+
577
+ Every dependency with no binding is an `unbound` entry, naming where it is
578
+ injected and which registration file(s) you read. A binding that exists but whose
579
+ LIFETIME contradicts what it holds — a singleton capturing a scoped unit of work,
580
+ a scoped dependency in a singleton — goes in `notes`, which never affects ok.
581
+ Judge only this slice's types; another feature's unbound port is not this gate's.
582
+ ok=true only when every dependency this slice injects is bound.
583
+ outputs:
584
+ - { name: wiring_ok, from: "$.ok" }
585
+ - { name: unbound, from: "$.unbound" }
586
+ - { name: wiring_notes, from: "$.notes" }
587
+ - id: wiring_break
588
+ action: break
589
+ condition: "{{wiring_ok}} == true"
590
+ - id: fix_wiring
591
+ action: agent_generate
592
+ tools: "Read,Write,Edit,Bash"
593
+ permission_mode: acceptEdits
594
+ task: |
595
+ Bind these dependencies in the `{{be_lane_id}}` lane's composition root, under
596
+ `{{be_source_path}}`: {{unbound}}
597
+ Register each one the way this project already registers its neighbours — same
598
+ file, same helper, same lifetime convention — and bind the implementation the
599
+ plan's §4.1 / §4.3 names, never a new one you invent. Change no behaviour: this
600
+ is wiring, not design. If a dependency has NO implementation to bind because
601
+ nothing was ever written for it, do not stub one — leave it and say so, since
602
+ that is a generation gap /appweaver implement closes.
603
+ Then re-run the slice's tests (Bash) to confirm they stay green.
604
+ outputs:
605
+ - { name: wiring_fixes, from: "$.files" }
606
+
607
+ - id: wiring_gate
608
+ action: verify_context
609
+ require_true: wiring_ok
610
+ on_false: stop
611
+ 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}}"
612
+
405
613
  # ═══════ INTEGRITY — did this feature's change break any aggregate that USES it? ═══════
406
614
  # The slice is green + domain-conformant on its own. But a change to a feature can break
407
615
  # any OTHER aggregate that references it. Ask the OKF Cross-References index "who refers to
@@ -411,6 +619,12 @@ workflow:
411
619
  # broken referrer STOPS the gate for a human to resolve, it is not auto-fixed here.
412
620
  # Empty referrer set (a leaf feature, or the only aggregate in the repo) → the fence is just
413
621
  # the feature's own tests again → passes trivially, blocks nothing.
622
+ #
623
+ # THE FENCE TAKES THE BUILT HALF OF THE CLOSURE. Edges are read off the lane PLAN, so a
624
+ # referrer can be fully planned and hold no code: running its tests means running none,
625
+ # and this fence would report that emptiness as a result. An unbuilt referrer is still
626
+ # counted and named — it refers to this aggregate and has never been exercised against
627
+ # it, which is a different statement from passing.
414
628
  - id: xref_referrers
415
629
  action: execute_script
416
630
  on_error: continue
@@ -418,11 +632,18 @@ workflow:
418
632
  subcommand: list
419
633
  args: ["--feature-dir", "{{feature_dir}}"]
420
634
  outputs:
421
- - { name: referrer_csv, from: "$.referrer_closure_csv" }
422
- - { name: referrer_count, from: "$.referrer_closure_count" }
423
- - { name: referrer_names, from: "$.referrer_closure" }
635
+ - { name: referrer_csv, from: "$.built_closure_csv" }
636
+ - { name: referrer_count, from: "$.built_count" }
637
+ - { name: referrer_names, from: "$.built_closure" }
638
+ - { name: unbuilt_referrers, from: "$.unbuilt_closure_csv" }
639
+ - { name: unbuilt_count, from: "$.unbuilt_count" }
424
640
 
641
+ # Skipped when PASS 1 found nothing to run: this is the same test command over a wider
642
+ # scope, so with no tests it would report the same emptiness a second time and fail a
643
+ # fence over it. `integrity_gate_ok` is what blocks, and it stays true for a fence that
644
+ # never ran — `integrity_ok` keeps reporting what actually happened.
425
645
  - id: run_integrity
646
+ condition: "{{tests_missing}} == false"
426
647
  action: execute_script
427
648
  on_error: continue
428
649
  script: { ref: actions }
@@ -431,6 +652,12 @@ workflow:
431
652
  outputs:
432
653
  - { name: integrity_ok, from: "$.fence_green" }
433
654
  - { name: integrity_log, from: "$.log_tail" }
655
+ # Ordered assignment again: a fence that is not green blocks UNLESS it was skipped, and
656
+ # `tests_missing` carries exactly that (it reads "true" only when the fence did not run).
657
+ - id: integrity_verdict
658
+ condition: "{{integrity_ok}} != true"
659
+ action: set
660
+ vars: { integrity_gate_ok: "{{tests_missing}}" }
434
661
 
435
662
  # On a RED fence, author a durable triage report BEFORE stopping — a broken referrer
436
663
  # means someone must go fix ANOTHER slice, so this is a record, not scrollback (mirrors
@@ -450,7 +677,7 @@ workflow:
450
677
  context_from: all_paths
451
678
  - id: write_broken_referrers
452
679
  action: agent_author
453
- condition: "{{integrity_ok}} != true"
680
+ condition: "{{integrity_gate_ok}} == false"
454
681
  include_standards: false # a triage report, not generated code
455
682
  output_target: { type: file, path: "{{feature_dir}}/broken-referrers.md" }
456
683
  task: |
@@ -458,7 +685,10 @@ workflow:
458
685
  REFER to this feature whose tests went red when this feature's change was run
459
686
  against them. This is a triage record for a human — analyze-slice does NOT fix a
460
687
  referrer (that is the referrer's own slice's job). Referrer blast radius
461
- ({{referrer_count}}): {{referrer_names}}. Failing test log:
688
+ ({{referrer_count}}): {{referrer_names}}. Also referring to this aggregate but NOT
689
+ YET BUILT ({{unbuilt_count}}): {{unbuilt_referrers}} — planned, holding no code, so
690
+ the fence ran nothing for them; say plainly that they were not exercised rather than
691
+ leaving them out of the report. Failing test log:
462
692
  {{integrity_log}}
463
693
  Write markdown, in this order:
464
694
  - `# Broken Referrers — <feature>` and a one-line statement of what this file is:
@@ -488,7 +718,7 @@ workflow:
488
718
 
489
719
  - id: integrity_gate
490
720
  action: verify_context
491
- require_true: integrity_ok
721
+ require_true: integrity_gate_ok
492
722
  on_false: stop
493
723
  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
724
 
@@ -545,6 +775,26 @@ workflow:
545
775
  - { name: okf_ok, from: "$.ok" }
546
776
  - { name: okf_orphans, from: "$.orphan_count" }
547
777
 
778
+ # THE LAST GATE — a slice with no tests fails HERE, not at PASS 1, so everything the
779
+ # other passes found has already been reported and any fix they made is on disk. It sits
780
+ # before the after-hooks, because an extension wired to `after_analyze_slice` runs on a
781
+ # slice this gate has passed.
782
+ - id: untested_gate
783
+ action: verify_context
784
+ require_true: tests_present
785
+ on_false: stop
786
+ on_false_message: >
787
+ The slice was analysed but NOT tested — it has no tests to run (tool={{test_tool}},
788
+ scoped={{test_scoped}}, matched={{test_matched}}). Reason: {{test_setup_reason}}.
789
+ So PASS 1 and the integrity fence proved nothing. Everything above ran: domain
790
+ conformance={{conformance_ok}}, composition={{wiring_ok}}, OKF index={{okf_ok}}.
791
+ A lane whose test command is not wired up is fixed in that lane — add the script or
792
+ the runner its declared command expects, or correct the command in
793
+ .appweaver/stacks.json. Otherwise, generate the missing tests —
794
+ /appweaver implement writes a paired test per plan element, under the test path the
795
+ lane declares — or, if the tests exist, name them so each carries its aggregate's
796
+ name, which is what the slice filter selects on. Then re-run /appweaver analyze-slice.
797
+
548
798
  - id: resolve_post_hooks
549
799
  action: execute_script
550
800
  script: { ref: actions }
@@ -570,7 +820,15 @@ outputs:
570
820
  # Reported by the conformance pass and never gated — surfaced so a real cross-aggregate
571
821
  # leak is visible rather than only living in one query's answer.
572
822
  model_notes: "{{model_notes}}"
823
+ # The composition pass. `true` with no backend lane declared — there is no container.
824
+ # `wiring_notes` are lifetime observations, reported and never gated.
825
+ wiring_ok: "{{wiring_ok}}"
826
+ wiring_notes: "{{wiring_notes}}"
573
827
  test_tool: "{{test_tool}}"
828
+ # false when the slice had no tests to run — PASS 1 and the integrity fence proved
829
+ # nothing on this run, and the gate failed at the end saying so.
830
+ tests_present: "{{tests_present}}"
831
+ tests_matched: "{{test_matched}}"
574
832
  referrer_count: "{{referrer_count}}"
575
833
  referrer_blast_radius: "{{referrer_names}}"
576
834
  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