aw-spec-cli 0.1.21 → 0.1.23

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 (51) 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/orchestrator.py +47 -3
  4. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan.orch.yaml +41 -8
  5. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice-delta.orch.yaml +62 -2
  6. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice.orch.yaml +18 -5
  7. package/assets/.appweaver/orchestrator/workflows/aw-spec-constitution.orch.yaml +1 -1
  8. package/assets/.appweaver/orchestrator/workflows/aw-spec-dependency-tree.orch.yaml +4 -1
  9. package/assets/.appweaver/orchestrator/workflows/aw-spec-finalize-delta.orch.yaml +115 -27
  10. package/assets/.appweaver/orchestrator/workflows/aw-spec-foundation.orch.yaml +5 -1
  11. package/assets/.appweaver/orchestrator/workflows/aw-spec-implement.orch.yaml +225 -27
  12. package/assets/.appweaver/orchestrator/workflows/aw-spec-inventory.orch.yaml +4 -15
  13. package/assets/.appweaver/orchestrator/workflows/aw-spec-onboard.orch.yaml +1 -1
  14. package/assets/.appweaver/orchestrator/workflows/aw-spec-plan.orch.yaml +39 -0
  15. package/assets/.appweaver/orchestrator/workflows/aw-spec-recall-delta.orch.yaml +28 -7
  16. package/assets/.appweaver/orchestrator/workflows/aw-spec-select-domain.orch.yaml +3 -1
  17. package/assets/.appweaver/orchestrator/workflows/aw-spec-specify.orch.yaml +104 -1
  18. package/assets/.appweaver/orchestrator/workflows/aw-spec-ui-model.orch.yaml +159 -0
  19. package/assets/.appweaver/orchestrator/workflows/aw-spec-ui-prototype.orch.yaml +386 -0
  20. package/assets/.appweaver/orchestrator/workflows/aw-spec-workflow-model.orch.yaml +153 -0
  21. package/assets/.appweaver/scripts/bash/aw-spec-actions.sh +203 -212
  22. package/assets/.appweaver/scripts/bash/aw-xref.sh +4 -1
  23. package/assets/.appweaver/scripts/powershell/aw-spec-actions.ps1 +191 -224
  24. package/assets/.appweaver/scripts/powershell/aw-xref.ps1 +4 -1
  25. package/assets/.appweaver/scripts/python/aw-crstate.py +127 -2
  26. package/assets/.appweaver/scripts/python/aw-gaps.py +119 -0
  27. package/assets/.appweaver/scripts/python/aw-payload-parity.py +17 -1
  28. package/assets/.appweaver/scripts/python/aw-plan-coverage.py +84 -4
  29. package/assets/.appweaver/scripts/python/aw-plangate.py +216 -0
  30. package/assets/.appweaver/scripts/python/aw-principles.py +108 -0
  31. package/assets/.appweaver/scripts/python/aw-stack.py +6 -0
  32. package/assets/.appweaver/scripts/python/aw-xref.py +141 -4
  33. package/assets/.appweaver/stack-profiles/angular.json +26 -0
  34. package/assets/.appweaver/stack-profiles/blazor-wasm.json +38 -0
  35. package/assets/.appweaver/stack-profiles/dotnet-webapi.json +38 -0
  36. package/assets/.appweaver/stack-profiles/java-spring.json +19 -0
  37. package/assets/.appweaver/stack-profiles/nextjs.json +26 -0
  38. package/assets/.appweaver/stack-profiles/node-express.json +26 -0
  39. package/assets/.appweaver/stack-profiles/python-fastapi.json +15 -0
  40. package/assets/.appweaver/stack-profiles/react-vite.json +26 -0
  41. package/assets/.appweaver/stack-profiles/sveltekit.json +26 -0
  42. package/assets/.appweaver/stack-profiles/vue-vite.json +26 -0
  43. package/assets/.appweaver/templates/ui-model-template.md +67 -0
  44. package/assets/.appweaver/templates/workflow-model-template.md +62 -0
  45. package/assets/foundations/dotnet/ModularMonolith/ModularMonolith.pack.txt +3 -3
  46. package/assets/foundations/dotnet/VerticalSlice/VerticalSlice.pack.txt +5 -123
  47. package/assets/foundations/java/Hexagonal/Hexagonal.pack.txt +784 -0
  48. package/assets/foundations/java/Hexagonal/pack-foundation.sh +357 -0
  49. package/assets/foundations/java/Hexagonal/unpack-foundation.sh +399 -0
  50. package/assets/guardrails/technology/dotnet-webapi/framework-api-map.md +44 -0
  51. 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.
@@ -101,6 +101,7 @@ Execution strategies (--strategy, overridable per step via `strategy:`):
101
101
  suspend/resume.
102
102
  """
103
103
  import argparse
104
+ import glob
104
105
  import importlib.util
105
106
  import json
106
107
  import os
@@ -1491,7 +1492,7 @@ def _module_script_refs(mpath, base_dir, seen):
1491
1492
  return out
1492
1493
 
1493
1494
 
1494
- def validate_workflow(wf, base_dir):
1495
+ def validate_workflow(wf, base_dir, path=""):
1495
1496
  errors, warnings = [], []
1496
1497
  if not isinstance(wf, dict):
1497
1498
  return (["top-level YAML is not a mapping"], [])
@@ -1621,9 +1622,51 @@ def validate_workflow(wf, base_dir):
1621
1622
  for name in sorted(used):
1622
1623
  if name.split(".")[0] not in produced:
1623
1624
  warnings.append(f"variable {{{{{name}}}}} referenced but never produced")
1625
+ errors_n, warnings_n = _validate_next(wf, base_dir, path)
1626
+ return errors + errors_n, warnings + warnings_n
1627
+
1628
+
1629
+ # outputs.next is the ONLY channel a finished run has for telling the operator what
1630
+ # to run next: the engine returns wf["outputs"] and the launching agent relays it.
1631
+ # Both faults it guards are silent — a next: indented out of the outputs map is
1632
+ # parsed and dropped, and a retired command name still reads as a valid sentence —
1633
+ # so the run ends looking successful and the agent invents a successor to fill the
1634
+ # gap. Warnings, not errors: a workflow may legitimately name a command that is
1635
+ # designed but not yet built, and pre-flight refuses to start on errors alone.
1636
+ def _validate_next(wf, base_dir, path=""):
1637
+ errors, warnings = [], []
1638
+ outputs = wf.get("outputs") or {}
1639
+ known = {os.path.basename(p)[len("aw-spec-"):-len(".orch.yaml")]
1640
+ for p in glob.glob(os.path.join(base_dir, "aw-spec-*.orch.yaml"))}
1641
+ # A module has no successor to name: it is included INTO a caller, and the caller's
1642
+ # own next covers the run. Only a launchable aw-spec-<name> workflow is asked for one.
1643
+ launchable = os.path.basename(path).startswith("aw-spec-") if path else bool(known)
1644
+ if launchable and "next" not in outputs:
1645
+ where = " (it is at the document's top level, OUTSIDE outputs:, so the " \
1646
+ "engine parses it and then drops it)" if "next" in wf else ""
1647
+ warnings.append(f"outputs: declares no 'next' — the run ends with no "
1648
+ f"suggested successor{where}")
1649
+ if not known: # validating a module, or a workflow read from elsewhere
1650
+ return errors, warnings
1651
+ for text in _output_strings(outputs):
1652
+ for cmd in re.findall(r"/appweaver\s+([a-z][a-z0-9-]*)", text):
1653
+ if cmd not in known:
1654
+ warnings.append(f"outputs names '/appweaver {cmd}', which resolves "
1655
+ f"to no aw-spec-{cmd}.orch.yaml")
1624
1656
  return errors, warnings
1625
1657
 
1626
1658
 
1659
+ def _output_strings(v):
1660
+ if isinstance(v, str):
1661
+ yield v
1662
+ elif isinstance(v, list):
1663
+ for x in v:
1664
+ yield from _output_strings(x)
1665
+ elif isinstance(v, dict):
1666
+ for x in v.values():
1667
+ yield from _output_strings(x)
1668
+
1669
+
1627
1670
  def _print_suspended(pending, opts):
1628
1671
  """Checkpoint transport: hand the pending request to the INVOKING agent."""
1629
1672
  if opts.json:
@@ -1710,7 +1753,7 @@ def main(argv=None):
1710
1753
  if opts.json else f"ERROR: parse: {e}")
1711
1754
  return 1
1712
1755
  errors, warnings = validate_workflow(
1713
- wf, os.path.dirname(os.path.abspath(opts.workflow)))
1756
+ wf, os.path.dirname(os.path.abspath(opts.workflow)), opts.workflow)
1714
1757
  if opts.json:
1715
1758
  print(json.dumps({"status": "valid" if not errors else "invalid",
1716
1759
  "errors": errors, "warnings": warnings}, indent=2))
@@ -1732,7 +1775,8 @@ def main(argv=None):
1732
1775
  try:
1733
1776
  with open(opts.workflow, encoding="utf-8") as fh:
1734
1777
  wf = load_yaml(fh.read())
1735
- errors, _ = validate_workflow(wf, os.path.dirname(os.path.abspath(opts.workflow)))
1778
+ errors, _ = validate_workflow(
1779
+ wf, os.path.dirname(os.path.abspath(opts.workflow)), opts.workflow)
1736
1780
  except Exception as e:
1737
1781
  wf, errors = None, [f"parse: {e}"]
1738
1782
  if errors:
@@ -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:
@@ -724,4 +724,4 @@ outputs:
724
724
  ui_validation_ok: "{{ui_ok}}"
725
725
  ui_validation_issues: "{{ui_issues}}"
726
726
  note: "Split constitution authored, one half per LANE the project declared in .appweaver/stacks.json. service_lane / ui_lane report which lanes were found there, and the declaration is the ONLY signal read: a half is a lane's standing governance, so a lane declared `(none)` in select-stack gets no half AND no file. constitution.technical.md is the service lane's base (standing input to the SERVICE lane of plan + implement); constitution.ui.md is the UI lane's UX-* base (standing input to their FRONTEND lane); constitution.domain.md is the domain addenda (standing input to specify + a gate checklist), and it is always authored because domain is not a lane — it addends whichever halves exist. All are composed into constitution.md, which now tolerates an absent technical half: a UI-only project's domain half opens the roman-numeral sequence at I instead of continuing one, and numbering_checked reports false because that check has no subject rather than because it silently passed. Mapping ## /aw-spec-ui-constitution supplies the rules the UI half ratifies but no longer creates the lane on its own — a mapped rule set with no declared lane produces a warning naming both fixes. ui_lane false with ui_included true means a lane was retired after its half was authored: the file is left alone and the log says so, because a ratified governance file is never deleted automatically. If a validation_ok is false, the matching validation_issues lists what still needs a manual touch. See each half's Sync Impact Report for its version bump. Suggested commit: 'docs: amend split constitution'."
727
- next: "Loaded per concern — /appweaver specify loads constitution.domain.md; the SERVICE lane of plan + implement loads constitution.technical.md; their FRONTEND lane loads constitution.ui.md. Re-run /appweaver constitution to amend, including to pick up a lane declared or dropped after this run — re-run /appweaver select-stack first to change which lanes exist."
727
+ next: "/appweaver select-feature → specify. select-feature picks the feature to build and every stage from specify onward reads that active feature and stops without it; on a brownfield workspace pick the bounded context first with /appweaver select-bounded-context. From specify the greenfield sequence runs plan → analyze-plan → implement → build → analyze-slice → run → deploy. THE HALVES ARE LOADED PER CONCERN, not all at once — /appweaver specify loads constitution.domain.md; the SERVICE lane of plan + implement loads constitution.technical.md; their FRONTEND lane loads constitution.ui.md. Re-run /appweaver constitution to amend, including to pick up a lane declared or dropped after this run — re-run /appweaver select-stack first to change which lanes exist."
@@ -66,4 +66,7 @@ outputs:
66
66
  tree_file: "{{tree_file}}"
67
67
  sequence_file: "{{sequence_file}}"
68
68
  note: "Migration Hierarchy Tree generated in {{tree_file}} and {{sequence_file}}."
69
- next: "Human Gate H0.1: Review specs/migration-tree.md and proceed to /appweaver mr per slice."
69
+ # Migration is a MODE of the ordinary brownfield loop, not a pipeline of its own:
70
+ # the tree only fixes the ORDER the slices are taken in, and each slice then runs
71
+ # adopt → (characterize on touch) → the CR delta loop like any other.
72
+ next: "Human Gate H0.1: Review {{tree_file}} — it orders {{aggregate_count}} aggregate(s) into {{wave_count}} migration wave(s), and that order is the only thing it decides. Take the slices in it ONE AT A TIME through the ordinary brownfield loop: /appweaver adopt <Feature> reverse-engineers the slice into spec.md + backend-plan.md + source-map.md, then every change to it goes through the CR delta loop (/appweaver specify → plan → analyze-plan-delta → implement → build → analyze-slice-delta → finalize-delta → analyze-slice → close-delta). If the slice has untested legacy behaviour with blast radius, run /appweaver characterize first to fence it. /appweaver parity is the cutover gate — run it per slice before the seam is routed away from the legacy path."