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.
- package/assets/.appweaver/ORCH-DIALECT.md +8 -3
- package/assets/.appweaver/orchestrator/modules/delta-spec.orch.yaml +22 -1
- package/assets/.appweaver/orchestrator/orchestrator.py +65 -3
- package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan-delta.orch.yaml +6 -6
- package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan.orch.yaml +107 -48
- package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice-delta.orch.yaml +17 -9
- package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice.orch.yaml +265 -20
- package/assets/.appweaver/orchestrator/workflows/aw-spec-constitution.orch.yaml +4 -4
- package/assets/.appweaver/orchestrator/workflows/aw-spec-finalize-delta.orch.yaml +89 -15
- package/assets/.appweaver/orchestrator/workflows/aw-spec-foundation.orch.yaml +140 -36
- package/assets/.appweaver/orchestrator/workflows/aw-spec-implement.orch.yaml +354 -52
- package/assets/.appweaver/orchestrator/workflows/aw-spec-onboard.orch.yaml +3 -3
- package/assets/.appweaver/orchestrator/workflows/aw-spec-select-stack.orch.yaml +47 -23
- package/assets/.appweaver/orchestrator/workflows/aw-spec-specify.orch.yaml +1 -1
- package/assets/.appweaver/scripts/bash/aw-spec-actions.sh +651 -76
- package/assets/.appweaver/scripts/powershell/aw-spec-actions.ps1 +600 -59
- package/assets/.appweaver/scripts/python/aw-crstate.py +194 -0
- package/assets/.appweaver/scripts/python/aw-foundation.py +151 -0
- package/assets/.appweaver/scripts/python/aw-payload-parity.py +3 -0
- package/assets/.appweaver/scripts/python/aw-plan-coverage.py +423 -0
- package/assets/.appweaver/scripts/python/aw-stack.py +251 -28
- package/assets/.appweaver/scripts/python/aw-testlog.py +148 -0
- package/assets/.appweaver/stack-profiles/dotnet-webapi.json +1 -0
- package/assets/.appweaver/stack-profiles/java-spring.json +1 -0
- package/assets/.appweaver/stack-profiles/node-express.json +1 -0
- package/assets/.appweaver/stack-profiles/python-fastapi.json +1 -0
- package/assets/.appweaver/templates/constitution-domain-template.md +17 -2
- package/assets/guardrails/projects/dotnet-default-project/domain-rule-bindings.md +10 -6
- package/assets/guardrails/projects/dotnet-default-project/tech-rule-bindings.md +4 -0
- package/assets/guardrails/technology/dotnet-webapi/auditing.md +3 -1
- package/assets/guardrails/technology/dotnet-webapi/coding-standards.md +5 -3
- package/assets/guardrails/technology/dotnet-webapi/cqrs.md +5 -1
- package/assets/guardrails/technology/dotnet-webapi/domain-driven-design.md +13 -5
- package/assets/guardrails/technology/dotnet-webapi/examples/cqrs.md +2 -0
- package/assets/guardrails/technology/dotnet-webapi/examples/domain-driven-design.md +6 -2
- package/assets/guardrails/technology/dotnet-webapi/framework-api-map.md +70 -0
- package/assets/guardrails/technology/dotnet-webapi/technology-constitution.md +3 -0
- package/assets/guardrails/technology/dotnet-webapi/testing.md +10 -0
- package/assets/guardrails/technology/nextjs/ui-constitution.md +8 -1
- package/package.json +1 -1
- 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.
|
|
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,
|
|
132
|
-
- { name: feature_dir,
|
|
133
|
-
- { name: 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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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}}).
|
|
292
|
-
per-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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: "{{
|
|
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:
|
|
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.
|
|
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
|
|
565
|
-
principles, domain folder additions, regulatory posture &
|
|
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).
|
|
44
|
-
#
|
|
45
|
-
# and `implement` reads it through plan-gate-check before any
|
|
46
|
-
# master run whose plans are no longer the ones the gate
|
|
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
|
|
100
|
-
# reflect, so a sibling sharing the same base_version compares EQUAL and would merge
|
|
101
|
-
# masters that had already shifted. The version alone is not a proxy for "the masters
|
|
102
|
-
# moved";
|
|
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.
|
|
106
|
-
#
|
|
107
|
-
#
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|