aw-spec-cli 0.1.21 → 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 (39) hide show
  1. package/assets/.appweaver/STACK-PROFILES.md +87 -8
  2. package/assets/.appweaver/orchestrator/modules/delta-plan.orch.yaml +30 -0
  3. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan.orch.yaml +41 -8
  4. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice-delta.orch.yaml +62 -2
  5. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice.orch.yaml +18 -5
  6. package/assets/.appweaver/orchestrator/workflows/aw-spec-finalize-delta.orch.yaml +115 -27
  7. package/assets/.appweaver/orchestrator/workflows/aw-spec-implement.orch.yaml +225 -27
  8. package/assets/.appweaver/orchestrator/workflows/aw-spec-plan.orch.yaml +39 -0
  9. package/assets/.appweaver/orchestrator/workflows/aw-spec-recall-delta.orch.yaml +28 -7
  10. package/assets/.appweaver/orchestrator/workflows/aw-spec-specify.orch.yaml +100 -0
  11. package/assets/.appweaver/orchestrator/workflows/aw-spec-ui-model.orch.yaml +141 -0
  12. package/assets/.appweaver/orchestrator/workflows/aw-spec-ui-prototype.orch.yaml +386 -0
  13. package/assets/.appweaver/orchestrator/workflows/aw-spec-workflow-model.orch.yaml +135 -0
  14. package/assets/.appweaver/scripts/bash/aw-spec-actions.sh +190 -209
  15. package/assets/.appweaver/scripts/bash/aw-xref.sh +4 -1
  16. package/assets/.appweaver/scripts/powershell/aw-spec-actions.ps1 +182 -224
  17. package/assets/.appweaver/scripts/powershell/aw-xref.ps1 +4 -1
  18. package/assets/.appweaver/scripts/python/aw-crstate.py +127 -2
  19. package/assets/.appweaver/scripts/python/aw-gaps.py +119 -0
  20. package/assets/.appweaver/scripts/python/aw-payload-parity.py +17 -1
  21. package/assets/.appweaver/scripts/python/aw-plan-coverage.py +84 -4
  22. package/assets/.appweaver/scripts/python/aw-plangate.py +216 -0
  23. package/assets/.appweaver/scripts/python/aw-principles.py +108 -0
  24. package/assets/.appweaver/scripts/python/aw-stack.py +6 -0
  25. package/assets/.appweaver/scripts/python/aw-xref.py +141 -4
  26. package/assets/.appweaver/stack-profiles/angular.json +26 -0
  27. package/assets/.appweaver/stack-profiles/blazor-wasm.json +38 -0
  28. package/assets/.appweaver/stack-profiles/dotnet-webapi.json +38 -0
  29. package/assets/.appweaver/stack-profiles/java-spring.json +19 -0
  30. package/assets/.appweaver/stack-profiles/nextjs.json +26 -0
  31. package/assets/.appweaver/stack-profiles/node-express.json +26 -0
  32. package/assets/.appweaver/stack-profiles/python-fastapi.json +15 -0
  33. package/assets/.appweaver/stack-profiles/react-vite.json +26 -0
  34. package/assets/.appweaver/stack-profiles/sveltekit.json +26 -0
  35. package/assets/.appweaver/stack-profiles/vue-vite.json +26 -0
  36. package/assets/.appweaver/templates/ui-model-template.md +67 -0
  37. package/assets/.appweaver/templates/workflow-model-template.md +62 -0
  38. package/assets/guardrails/technology/dotnet-webapi/framework-api-map.md +44 -0
  39. package/package.json +1 -1
@@ -51,15 +51,17 @@ no heading to key on. Where the two lanes are folded into one command, as `plan`
51
51
  | `stack` | *(optional)* what the code was found to be, when `profile` is `detected` — e.g. `asp.net-webforms`. The honest record for a legacy lane |
52
52
  | `origin` | *(optional)* `reverse-engineered` on a lane `onboard` derived from an existing codebase. Absent means a person declared it, which is the greenfield shape and the historical one |
53
53
  | `root` | the lane's **source path** — the subtree it owns (see §3). `--source-path` and `--root` are two spellings of this one field |
54
+ | `test_root` | *(optional)* the lane's **test path**, written by hand only when the profile's answer does not fit this layout. Absent, `lane-resolve` / `lane-verify` work it out from the profile at read time (§4.3), so no lane has to be re-declared for it |
54
55
  | `run_from` | *(optional)* **where the commands run**: `repo` (the default, and the shape of every lane written before this key existed) at the repository root, or `root` inside the lane's own source path. See §3.1 |
55
56
  | `build` / `test` / `run` | project-supplied command strings the engine shells out to. These are the matched **profile's** defaults and may not fit a legacy layout — reviewing them is what `select-stack` is for. `{{root}}` in a command resolves to this lane's `root` (§4.2) |
56
57
  | `migrate` | *(optional)* the lane's schema-migration command, recorded only when the profile declares one. Absent, `run-migrate` falls back to `dotnet ef` and otherwise reports that nothing ran (§4.2) |
57
58
 
58
59
  ### Three properties matter more than the schema
59
60
 
60
- 1. **`root` is ownership, and it is the source path.** `implement`'s service lane writes only
61
- under a backend lane's root and its frontend lane only under a frontend lane's,
62
- `build`/`test`/`run` act on it, and `onboard` reverse-engineers out of it.
61
+ 1. **`root` is ownership, and it is the source path.** `implement`'s service lane writes its
62
+ code only under a backend lane's root and its frontend lane only under a frontend lane's,
63
+ `build`/`test`/`run` act on it, and `onboard` reverse-engineers out of it. The lane's
64
+ **test path** (§4.3) is owned the same way, and is where `implement` writes the lane's tests.
63
65
  2. **The commands are project-supplied strings.** The engine stays stdlib-only and
64
66
  stack-ignorant. This is how `build` already avoids knowing what a compiler is, and it
65
67
  is the only thing that makes "any frontend stack" tractable.
@@ -151,12 +153,17 @@ Declaring **`(none)`** for a lane is a real answer and the way a project says it
151
153
  ## 3. The ownership rule
152
154
 
153
155
  **Lane roots own their subtree and must be disjoint.** Two generators must never be able
154
- to write the same file.
156
+ to write the same file. A lane owns its test path too, so the rule covers both: no source or
157
+ test path of one lane may overlap a source or test path of another.
155
158
 
156
- `lane-declare` refuses a declaration whose root contains, or is contained by, another
159
+ `lane-declare` refuses a declaration whose paths contain, or are contained by, another
157
160
  lane's — compared as `/`-terminated paths, so `src/` is clear of `src-gen/` but collides
158
161
  with `src/api/`. The refusal is a non-zero exit and **`stacks.json` is left untouched**.
159
162
 
163
+ The one write `implement` may make outside a lane's two paths is registering a test project
164
+ it had to create, in the file the lane's test command discovers tests through — the solution
165
+ file for .NET. `analyze-slice-delta`'s placement check accepts that edit and nothing else.
166
+
160
167
  This is checked at declaration time on purpose. The alternative — discovering it when two
161
168
  implement phases fight over a file — is a much worse place to find out.
162
169
 
@@ -176,12 +183,15 @@ workflow.**
176
183
  "language": "TypeScript", // shown in the menu, parenthesised
177
184
  "ecosystem": "node", // which stack-partitioned corpus this stack draws from
178
185
  "root": "web/", // the lane root this profile defaults to
186
+ "test_root": "", // optional — where tests go when not inside root — see §4.3
179
187
  "run_from": "root", // where the commands run — see §3.1
180
188
  "build": "npm run build",
181
189
  "test": "npm test",
182
190
  "run": "npm run dev",
183
191
  "migrate": "", // optional — schema migration, if the stack has one
184
192
  "prototype_run": "npm run dev --prefix prototype", // frontend only — fills the prototype block
193
+ "reserved_names": ["Event", "Request", "Response"], // optional — names this stack's AMBIENT
194
+ // scope already defines; see §4.4
185
195
  "detect": ["web/package.json", "web/vite.config.*"] // ANY-of markers — how stack-detect recognises this stack
186
196
  }
187
197
  ```
@@ -206,10 +216,23 @@ trees that are physically per-ecosystem:
206
216
  compiling project and a component runbook edits a host project's build files — both are
207
217
  executable artifacts in one toolchain, so offering a `dotnet` bundle to a Spring repo does not
208
218
  produce a scaffold, it produces a second foreign project beside the real one. `foundation-list`
209
- therefore narrows to the declared lanes' ecosystems, and reports `all_foundations` alongside so
210
- the gate can say *what exists and why none of it applies*. A repo that has declared nothing yet
219
+ therefore narrows by ecosystem, and reports `all_foundations` alongside so the gate can say
220
+ *what exists and why none of it applies*. `--ecosystem` narrows to the ONE lane being
221
+ scaffolded, which is what `foundation` passes: a repo declaring a .NET service and a Node UI
222
+ must not be offered its .NET bundle while the UI lane is the target. Without that argument the
223
+ filter falls back to every declared lane's ecosystem, and a repo that has declared nothing yet
211
224
  is not filtered — there is no answer to filter on.
212
225
 
226
+ **A scaffold lands in a lane.** `foundation` unpacks a bundle into the SOURCE PATH of the lane
227
+ it is scaffolding — never the repository root, and there is no output-directory argument. One
228
+ bundle fills one lane, so a fullstack repo runs `foundation` once per lane. This is what makes
229
+ every phase's "write only into your lane" rule true of the scaffold as well as of generated
230
+ code: `implement`, `build`, `test` and `run` all resolve the same path the bundle unpacked into.
231
+ A pack carrying its own test tree has it lifted onto the lane's **test path** (§4.3) afterwards,
232
+ because for most backend stacks that path is a sibling of the source path rather than a
233
+ directory inside it. Lanes must therefore be declared before `foundation` runs, which is why
234
+ `select-stack` is the first command of both sequences.
235
+
213
236
  The technical RULE corpora at `guardrails/technology/<stack>/` are keyed by **profile id**, not
214
237
  ecosystem, because Spring and Quarkus are both `java` and do not share a persistence rule. They
215
238
  are selected through the project's `tech-rule-bindings.md` rather than filtered here — see §7.
@@ -274,6 +297,61 @@ about a backend, and the two errors are not symmetric. A false positive authors
274
297
  the project never asked for; a false negative only produces a warning that names the fix. Pick
275
298
  ids accordingly — a profile whose id is a common English word will be matched too eagerly.
276
299
 
300
+ ### 4.4 `reserved_names` — names the stack already defines
301
+
302
+ A stack's ambient scope defines types nobody imported: C#'s implicit usings put `System`,
303
+ `System.Threading.Tasks`, `Microsoft.AspNetCore.Http` and `Microsoft.AspNetCore.Routing` in
304
+ every file of a `Microsoft.NET.Sdk.Web` project; a browser bundle has `Event`, `Request` and
305
+ `Response`; `java.lang` is always imported; `typing` is star-imported by convention.
306
+
307
+ A domain noun that matches one of those is **not a naming mistake** — `Route`, `Task`,
308
+ `Order` and `Activity` are ordinary domain vocabulary, the plan carries the ubiquitous
309
+ language, and the generator spells it verbatim. But written bare it is ambiguous, and in C#
310
+ it does not compile (CS0104). The conflict is invisible in the file: the framework name
311
+ arrives through an implicit import and appears in no line of it.
312
+
313
+ `reserved_names` lists the ones that are BOTH ambient on this stack AND plausible domain
314
+ vocabulary — curated, not an exhaustive dump of the standard library. `lane-resolve` and
315
+ `lane-verify` report it as `<kind>_reserved_names` / `<kind>_reserved_csv`; `implement`
316
+ passes it to `plan-coverage --reserved`, which intersects it with the plan's own element
317
+ names and hands the generating lane the actual collisions before it writes anything. The
318
+ per-stack resolution convention is in that stack's rule corpus — for `dotnet-webapi`, in
319
+ [[framework-api-map]]: qualify with the enclosing namespace segment (`Domain.Route`), never
320
+ rename the domain type.
321
+
322
+ Absent, nothing is reported and generation is unchanged.
323
+
324
+ ### 4.3 `test_root` — where a lane's tests go
325
+
326
+ Most backend stacks keep their tests **beside** the source, not inside it: .NET's `tests/` next
327
+ to `src/`, Maven's `src/test/java/` next to `src/main/java/`, pytest's `tests/` next to `app/`.
328
+ A lane that owned only its source path could not hold its own tests, so each lane also owns a
329
+ **test path**, and `implement` writes the lane's paired tests there.
330
+
331
+ A profile's `test_root` is the test path that pairs with its default `root`. `lane-resolve` and
332
+ `lane-verify` report it per kind as `backend_test_path` / `frontend_test_path`, worked out at
333
+ read time:
334
+
335
+ | The lane | Its test path |
336
+ |---|---|
337
+ | declares `test_root` itself | that value |
338
+ | sits at the profile's default root (`src/`) | the profile's `test_root` (`tests/`) |
339
+ | sits at a path ending in the default root (`backend/src/`) | the profile's `test_root` under the same prefix (`backend/tests/`) |
340
+ | anything else, or the profile declares no `test_root` | the source path itself |
341
+
342
+ The last row is right for every shipped frontend (tests under `web/tests/`), for Go's
343
+ colocated `_test.go` files, and for a Maven lane rooted at the directory holding its
344
+ `pom.xml`, where `src/test/java/` is already inside the lane. A layout the table gets wrong is
345
+ fixed by writing `test_root` on the lane in `stacks.json`.
346
+
347
+ | Profile | `root` | `test_root` |
348
+ |---|---|---|
349
+ | `dotnet-webapi` | `src/` | `tests/` |
350
+ | `java-spring` | `src/main/java/` | `src/test/java/` |
351
+ | `python-fastapi` | `app/` | `tests/` |
352
+ | `node-express` | `src/` | `tests/` |
353
+ | `go-chi` and every frontend | — | *(none — inside the source path)* |
354
+
277
355
  ---
278
356
 
279
357
  ## 5. Who may write this file
@@ -398,7 +476,8 @@ set**, so a caller binds outputs without knowing which branch ran.
398
476
  | `test-run` | every declared lane's own `test`, via `run-phase` — **except** `--scope slice` on a repo where a .NET toolchain is present, which keeps the VSTest `--filter` path | the historic ladder |
399
477
  | `run-migrate` | a lane's declared `migrate` (§4.2) when there is one; otherwise `dotnet ef` searched under the **service lane's root** rather than an assumed `src/` | `dotnet ef` under `src/` |
400
478
  | `find-host-project` | searches the service lane's root, and reports the lane's `ecosystem` | searches `src/` |
401
- | `foundation-list`, `list-components` | narrowed to the declared lanes' `ecosystem` (§4.1) | unfiltered / `dotnet` |
479
+ | `foundation-list`, `list-components` | narrowed to the `ecosystem` of the lane being scaffolded, else of every declared lane (§4.1) | unfiltered / `dotnet` |
480
+ | `foundation-unpack` | unpacks into the lane's **source path**, and lifts a packed test tree onto its **test path** (§4.1, §4.3) | cannot run — `foundation` stops until a lane is declared |
402
481
  | `resolve-tech` | expands stack placeholders and flags a corpus mismatch (§7a) | no placeholders resolve; nothing is flagged |
403
482
 
404
483
  **Why `--scope slice` is the one exception.** VSTest's `--filter FullyQualifiedName~` is the
@@ -130,6 +130,14 @@ workflow:
130
130
  # the context's CANONICAL model — the aggregate root,
131
131
  # its entities and its invariants live here, not in the
132
132
  # spec and not in this plan.
133
+ - { path: "{{cr_dir}}/delta-slice-analyze-report.md", optional: true }
134
+ # WHAT THE LAST GATE FOUND, when this change
135
+ # has already been through analyze-slice-delta.
136
+ # A `plan` finding there says THIS document is
137
+ # what was wrong, and re-deriving it is how that
138
+ # finding is closed — the alternative is hand-
139
+ # editing a generated plan, which every
140
+ # authoring rule forbids. Absent on a first pass.
133
141
  - { path: .appweaver/DESIGN-FORMAT.md }
134
142
  - id: author_backend_delta
135
143
  condition: "{{be_existing}} == true"
@@ -148,6 +156,13 @@ workflow:
148
156
  IS, and a delta that ignores the difference plans an edit to something that is not
149
157
  there. Where the two disagree, plan against the code and record the divergence as
150
158
  a WA-n.
159
+ - THE LAST GATE'S REPORT, when one is in context (delta-slice-analyze-report.md).
160
+ It records what analyze-slice-delta found the last time this change was gated. A
161
+ finding tagged `fix: plan` says THIS document was what was wrong — re-derive the
162
+ rows it names, against the code it names. EVIDENCE, never a requirement source: the
163
+ delta-spec stays the only authority on what the change must do, and a finding
164
+ tagged `implement` or `specify` is closed elsewhere and is not this pass's work.
165
+ Absent on a first pass, which is the normal case.
151
166
  - Classify every change to the plan as ADDED / MODIFIED / REMOVED against the
152
167
  master, per the master's OWN section structure (whatever sections it actually has
153
168
  — do not assume a fixed layering), plus the cross-section impact, the regression
@@ -231,6 +246,14 @@ workflow:
231
246
  # frontend-only project has no service lane at all.
232
247
  - { path: "{{domain_model}}", optional: true }
233
248
  - { path: .appweaver/memory/constitution.ui.md, optional: true }
249
+ - { path: "{{cr_dir}}/delta-slice-analyze-report.md", optional: true }
250
+ # WHAT THE LAST GATE FOUND, when this change
251
+ # has already been through analyze-slice-delta.
252
+ # A `plan` finding there says THIS document is
253
+ # what was wrong, and re-deriving it is how that
254
+ # finding is closed — the alternative is hand-
255
+ # editing a generated plan, which every
256
+ # authoring rule forbids. Absent on a first pass.
234
257
  - { path: .appweaver/DESIGN-FORMAT.md }
235
258
  - id: author_frontend_delta
236
259
  condition: "{{fe_existing}} == true"
@@ -247,6 +270,13 @@ workflow:
247
270
  feature: {{source_files}}. Read the ones this change touches — a delta that plans
248
271
  an edit to a component that is not there, or that duplicates one that is, costs a
249
272
  whole implementation pass to discover.
273
+ - THE LAST GATE'S REPORT, when one is in context (delta-slice-analyze-report.md).
274
+ It records what analyze-slice-delta found the last time this change was gated. A
275
+ finding tagged `fix: plan` says THIS document was what was wrong — re-derive the
276
+ rows it names, against the code it names. EVIDENCE, never a requirement source: the
277
+ delta-spec stays the only authority on what the change must do, and a finding
278
+ tagged `implement` or `specify` is closed elsewhere and is not this pass's work.
279
+ Absent on a first pass, which is the normal case.
250
280
  - Classify every change ADDED / MODIFIED / REMOVED against the master, per the
251
281
  master's OWN sections. A shared component or a shared state container is MODIFIED
252
282
  in place, never copied into the change.
@@ -105,6 +105,7 @@ vars:
105
105
  may_repair: "false" # the ask, the repair loop and the conformance stop are MASTER-only
106
106
  gate_ok: "false" # raised by a run with no plan gap left OR by report mode, which cannot repair
107
107
  stamp_receipt: "false" # master mode ⇒ leave the receipt implement reads, whatever the verdict
108
+ repairs_left: "true" # lowered on the loop's final pass, which checks and never rewrites
108
109
  # THE GAP SPLIT, from plan-gaps after each analyze pass. Defaulted to the refusing
109
110
  # answer, so a classification that never arrived neither ends the loop nor passes.
110
111
  plan_ok: "false" # no gap left that a plan rewrite closes
@@ -383,13 +384,23 @@ workflow:
383
384
 
384
385
  # 5. Conformance + REPAIR loop (spec is the anchor): check → split the gaps → REPORT +
385
386
  # ASK → Repair on approval → re-check; "Report and Skip" writes plan-analyze-report.md
386
- # and stops. Bounded (3×). The loop runs while a PLAN gap is open: once only routed
387
- # gaps remain there is nothing a repair can change. The repair is PER LANE, so a
388
- # frontend drift never rewrites a conformant backend plan.
387
+ # and stops. The loop runs while a PLAN gap is open: once only routed gaps remain
388
+ # there is nothing a repair can change. The repair is PER LANE, so a frontend drift
389
+ # never rewrites a conformant backend plan.
390
+ #
391
+ # THREE REPAIRS, FOUR PASSES — the last pass checks and never rewrites. With the two
392
+ # equal, the third repair was the last thing to run: no pass ever saw its result, so a
393
+ # run that converged on it still reported `gaps` and told the operator it had not
394
+ # converged. Worse, plan_ok / gate_verdict / plan_gap_count then described the plans as
395
+ # they were BEFORE that repair, while plan-gate-stamp below hashed the files as they
396
+ # are AFTER it — a receipt pairing post-repair bytes with a pre-repair verdict, which
397
+ # plan-gate-check would later read as fresh. The extra pass costs one analyze in the
398
+ # case that used to end ambiguously, and it makes the last thing to touch the plans
399
+ # always earlier than the last thing to judge them, on every exit path.
389
400
  - id: analyze_loop
390
401
  action: loop
391
402
  while: "{{plan_ok}} != true"
392
- max_iterations: 3
403
+ max_iterations: 4
393
404
  steps:
394
405
  - id: analyze
395
406
  action: agent_query
@@ -546,7 +557,19 @@ workflow:
546
557
  action: break
547
558
  condition: "{{plan_ok}} == true"
548
559
 
560
+ # THE LAST PASS CONFIRMS THE LAST REPAIR AND DOES NOT ADD ONE. Raised for every pass
561
+ # and lowered on the final one, so the ask below and both lane rewrites are skipped
562
+ # there while the analyze above still runs and still sets the verdict.
563
+ - id: repairs_left_yes
564
+ action: set
565
+ vars: { repairs_left: "true" }
566
+ - id: repairs_left_no
567
+ condition: "{{iteration}} == 3"
568
+ action: set
569
+ vars: { repairs_left: "false" }
570
+
549
571
  - id: ask_repair
572
+ condition: "{{repairs_left}} == true"
550
573
  action: ask
551
574
  prompt: |
552
575
  analyze-plan found spec↔plan gaps (spec.md is the anchor; domain-model.md is
@@ -582,9 +605,11 @@ workflow:
582
605
  condition: "{{repair_choice}} == Report and Skip"
583
606
 
584
607
  # WHICH DOCUMENT GETS REWRITTEN. A repair runs only where the analyze pass reported
585
- # drift in that lane AND that lane is present — two ordered assignments per lane,
586
- # cleared first so a lane that was repaired on the previous iteration and is clean
587
- # on this one is not rewritten again.
608
+ # drift in that lane AND that lane is present AND this pass still carries a repair —
609
+ # three ordered assignments per lane, because the dialect takes no compound
610
+ # condition. Cleared first so a lane that was repaired on the previous iteration and
611
+ # is clean on this one is not rewritten again, and cleared last on the final pass,
612
+ # which exists to check the previous repair rather than to add another.
588
613
  - id: be_repair_clear
589
614
  action: set
590
615
  vars: { be_repair: "false" }
@@ -592,6 +617,10 @@ workflow:
592
617
  condition: "{{backend_repair}} == true"
593
618
  action: set
594
619
  vars: { be_repair: "{{be_present}}" }
620
+ - id: be_repair_last_pass
621
+ condition: "{{repairs_left}} == false"
622
+ action: set
623
+ vars: { be_repair: "false" }
595
624
  - id: fe_repair_clear
596
625
  action: set
597
626
  vars: { fe_repair: "false" }
@@ -599,6 +628,10 @@ workflow:
599
628
  condition: "{{frontend_repair}} == true"
600
629
  action: set
601
630
  vars: { fe_repair: "{{fe_present}}" }
631
+ - id: fe_repair_last_pass
632
+ condition: "{{repairs_left}} == false"
633
+ action: set
634
+ vars: { fe_repair: "false" }
602
635
 
603
636
  - id: repair_backend
604
637
  condition: "{{be_repair}} == true"
@@ -718,7 +751,7 @@ workflow:
718
751
  action: verify_context
719
752
  require_true: gate_ok
720
753
  on_false: stop
721
- on_false_message: "The lane plans do not conform to the spec: {{plan_gap_count}} gap(s) a plan rewrite closes are still open (all gaps: {{analyze_gaps}}) — repair was skipped (see plan-analyze-report.md), or it did not converge in 3 passes. The receipt records verdict `gaps`, so /appweaver implement will refuse until this gate is re-run clean. A `contract` gap is closed in the lane its row names. Gaps routed to `specify` ({{routed_csv}}) are not what stopped this run: they need /appweaver specify, which authors the spec and the model. Fix out of band, or re-run and choose Repair."
754
+ on_false_message: "The lane plans do not conform to the spec: {{plan_gap_count}} gap(s) a plan rewrite closes are still open (all gaps: {{analyze_gaps}}). Either you chose `Report and Skip`, in which case plan-analyze-report.md beside the plans lists them — or repair ran up to three times and the fourth pass, which checks and does not rewrite, still found these. Every gap above was read off the plans as they stand on disk now. The receipt records verdict `gaps`, so /appweaver implement will refuse until this gate is re-run clean. A `contract` gap is closed in the lane its row names. Gaps routed to `specify` ({{routed_csv}}) are not what stopped this run: they need /appweaver specify, which authors the spec and the model. Fix out of band, or re-run and choose Repair."
722
755
 
723
756
  # (No change-close here by design — this stage never archives anything in either mode,
724
757
  # and cr-archive belongs to close-delta, which runs after finalize-delta's merge.)
@@ -70,7 +70,7 @@
70
70
  # =============================================================================
71
71
 
72
72
  name: aw-spec-analyze-slice-delta
73
- version: 1.1.0
73
+ version: 1.2.0
74
74
  description: "The delta-spec↔delta-code gate — verify that the open change DOES WHAT IT WAS ASKED TO DO, before it reaches the master documents. delta-spec.md is the anchor, exactly as it is for analyze-plan-delta. Asks which open change it is gating, runs the slice's tests (the first execution on the change path, since build only compiles), then evaluates the code and its paired tests against the change's REQUIREMENTS — every ADDED or MODIFIED delta-spec item realised in code, every one covered by a test that cites its id, every invariant the model delta adds actually enforced — with the delta plan in context as the map from requirement to code and cross-checked in its own right, so an element the plan promised and nobody built is caught too. The dynamic twin of analyze-plan-delta and the delta twin of analyze-slice, which cannot see a missing implementation because a behaviour that was never built leaves no failing test. Verify-only — it reports each finding with the command that closes it and stops; it generates no code and repairs nothing."
75
75
  agent_agnostic: true
76
76
  dialect: aw-spec.orchestrator/v1
@@ -130,6 +130,18 @@ vars:
130
130
  test_matched: "false"
131
131
  # Verdict.
132
132
  requirements_ok: "false"
133
+ # Findings split by the command that closes them (gap-route). Defaults read as
134
+ # "nothing found", so a run where the router could not execute still prints.
135
+ gap_summary: "no findings"
136
+ gap_total: "0"
137
+ fix_code: "none"
138
+ fix_code_n: "0"
139
+ fix_plan: "none"
140
+ fix_plan_n: "0"
141
+ fix_spec: "none"
142
+ fix_spec_n: "0"
143
+ fix_review: "none"
144
+ fix_review_n: "0"
133
145
  gate_ok: "false"
134
146
 
135
147
  workflow:
@@ -547,6 +559,30 @@ workflow:
547
559
  action: set
548
560
  vars: { requirements_ok: "false" }
549
561
 
562
+ # WHO CAN CLOSE WHAT. Both lanes' findings carry a `fix` tag already; this turns the tag
563
+ # into routing. It matters most for a `plan` finding: the delta plan is what is wrong, and
564
+ # `implement` generates FROM that plan, so sending it there reproduces the defect — which
565
+ # is what left the mock run's ChiefComplaintField gap closable only by hand-editing a
566
+ # generated plan. Deterministic, and it never drops a finding: one with no recognised
567
+ # `fix` is routed to `review` and counted, rather than folded into the largest bucket.
568
+ - id: route_gaps
569
+ action: execute_script
570
+ on_error: continue
571
+ script: { ref: actions }
572
+ subcommand: gap-route
573
+ args: ["--cr", "{{cr_id}}", "--gaps", "{{be_gaps}}", "--gaps", "{{fe_gaps}}"]
574
+ outputs:
575
+ - { name: gap_summary, from: "$.summary" }
576
+ - { name: gap_total, from: "$.total" }
577
+ - { name: fix_code, from: "$.implement_text" }
578
+ - { name: fix_code_n, from: "$.implement_count" }
579
+ - { name: fix_plan, from: "$.plan_text" }
580
+ - { name: fix_plan_n, from: "$.plan_count" }
581
+ - { name: fix_spec, from: "$.specify_text" }
582
+ - { name: fix_spec_n, from: "$.specify_count" }
583
+ - { name: fix_review, from: "$.review_text" }
584
+ - { name: fix_review_n, from: "$.review_count" }
585
+
550
586
  # gate_ok = the tests ran green AND every gated lane's requirements are met.
551
587
  - id: gate_flag
552
588
  condition: "{{test_ok}} == true"
@@ -616,7 +652,31 @@ workflow:
616
652
  action: verify_context
617
653
  require_true: gate_ok
618
654
  on_false: stop
619
- on_false_message: "Change {{cr_id}} does NOT yet meet its delta spec. Tests green={{test_ok}} (tool `{{test_tool}}`); service lane gated={{be_gated}} ok={{be_ok}}; experience lane gated={{fe_gated}} ok={{fe_ok}}. Findings — backend: {{be_gaps}} · frontend: {{fe_gaps}}. The full report, with the command that closes each finding, is at {{cr_dir}}/delta-slice-analyze-report.md. DO NOT run finalize-delta yet: merging now would write requirements into spec.md and domain-model.md that the code does not satisfy, and the masters would describe an intention rather than the system. Fix the findings (a `requirement` or `plan` finding is usually /appweaver implement {{cr_id}}), re-run build, then re-run this gate."
655
+ on_false_message: >
656
+ Change {{cr_id}} does NOT yet meet its delta spec. Tests green={{test_ok}} (tool
657
+ `{{test_tool}}`); service lane gated={{be_gated}} ok={{be_ok}}; experience lane
658
+ gated={{fe_gated}} ok={{fe_ok}}. {{gap_summary}}
659
+
660
+ EACH FINDING IS CLOSED BY A DIFFERENT COMMAND — they are not interchangeable, and a
661
+ plan defect sent to implement comes back unchanged, because implement generates FROM
662
+ the plan.
663
+
664
+ SPEC / MODEL ({{fix_spec_n}}): {{fix_spec}}
665
+
666
+ DELTA PLAN ({{fix_plan_n}}): {{fix_plan}}
667
+
668
+ CODE ({{fix_code_n}}): {{fix_code}}
669
+
670
+ UNROUTED ({{fix_review_n}}): {{fix_review}}
671
+
672
+ Work them in that order — the spec before the plan that realises it, the plan before
673
+ the code that maps to it. Re-running /appweaver plan {{cr_id}} re-derives the delta
674
+ plans with this report in context, so a plan finding is fixed by re-deriving rather
675
+ than by hand-editing a generated document. The full report is at
676
+ {{cr_dir}}/delta-slice-analyze-report.md. DO NOT run finalize-delta yet: merging now
677
+ would write requirements into spec.md and domain-model.md that the code does not
678
+ satisfy, and the masters would describe an intention rather than the system. Then
679
+ re-run build and re-run this gate.
620
680
 
621
681
  # 6. Post-execution hooks.
622
682
  - id: resolve_post_hooks
@@ -136,9 +136,11 @@ vars:
136
136
  # Integrity pass (referrer blast radius) defaults — safe values if xref/test-run is
137
137
  # skipped or errors, so the gate reads a real verdict rather than an empty var.
138
138
  integrity_ok: "false"
139
- referrer_csv: ""
139
+ referrer_csv: "" # referrers THAT HAVE CODE — what the fence can actually run
140
140
  referrer_count: "0"
141
141
  referrer_names: ""
142
+ unbuilt_referrers: "" # referrers that are planned and not implemented: nothing to run
143
+ unbuilt_count: "0"
142
144
  # Pass-3 (OKF) reporting defaults — the knowledge pass is advisory, so these stay
143
145
  # readable in the outputs even when a step is skipped or errors out.
144
146
  okf_ok: "unknown"
@@ -617,6 +619,12 @@ workflow:
617
619
  # broken referrer STOPS the gate for a human to resolve, it is not auto-fixed here.
618
620
  # Empty referrer set (a leaf feature, or the only aggregate in the repo) → the fence is just
619
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.
620
628
  - id: xref_referrers
621
629
  action: execute_script
622
630
  on_error: continue
@@ -624,9 +632,11 @@ workflow:
624
632
  subcommand: list
625
633
  args: ["--feature-dir", "{{feature_dir}}"]
626
634
  outputs:
627
- - { name: referrer_csv, from: "$.referrer_closure_csv" }
628
- - { name: referrer_count, from: "$.referrer_closure_count" }
629
- - { 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" }
630
640
 
631
641
  # Skipped when PASS 1 found nothing to run: this is the same test command over a wider
632
642
  # scope, so with no tests it would report the same emptiness a second time and fail a
@@ -675,7 +685,10 @@ workflow:
675
685
  REFER to this feature whose tests went red when this feature's change was run
676
686
  against them. This is a triage record for a human — analyze-slice does NOT fix a
677
687
  referrer (that is the referrer's own slice's job). Referrer blast radius
678
- ({{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:
679
692
  {{integrity_log}}
680
693
  Write markdown, in this order:
681
694
  - `# Broken Referrers — <feature>` and a one-line statement of what this file is: