aw-spec-cli 0.1.27 → 0.1.28

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 (74) hide show
  1. package/assets/.appweaver/RULE-PARTITION.md +8 -9
  2. package/assets/.appweaver/STACK-PROFILES.md +74 -9
  3. package/assets/.appweaver/interpreter/run-skill.py +28 -7
  4. package/assets/.appweaver/orchestrator/modules/delta-plan.orch.yaml +17 -7
  5. package/assets/.appweaver/orchestrator/modules/domain-select.orch.yaml +1 -1
  6. package/assets/.appweaver/orchestrator/modules/foundation-pack.orch.yaml +44 -1
  7. package/assets/.appweaver/orchestrator/modules/foundation-spec.orch.yaml +172 -5
  8. package/assets/.appweaver/orchestrator/rev-templates/delta-design-template.skeleton.md +5 -4
  9. package/assets/.appweaver/orchestrator/rev-templates/design-template.skeleton.md +3 -1
  10. package/assets/.appweaver/orchestrator/rev-templates/domain-rule-bindings.skeleton.md +16 -8
  11. package/assets/.appweaver/orchestrator/rev-templates/tech-rule-bindings.skeleton.md +15 -18
  12. package/assets/.appweaver/orchestrator/rev-templates/ui-delta-design-template.skeleton.md +4 -1
  13. package/assets/.appweaver/orchestrator/rev-templates/ui-design-template.skeleton.md +6 -4
  14. package/assets/.appweaver/orchestrator/workflows/aw-spec-adopt.orch.yaml +7 -6
  15. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan-delta.orch.yaml +7 -1
  16. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan.orch.yaml +7 -1
  17. package/assets/.appweaver/orchestrator/workflows/aw-spec-deploy.orch.yaml +1 -1
  18. package/assets/.appweaver/orchestrator/workflows/aw-spec-implement.orch.yaml +224 -16
  19. package/assets/.appweaver/orchestrator/workflows/aw-spec-onboard.orch.yaml +20 -19
  20. package/assets/.appweaver/orchestrator/workflows/aw-spec-plan.orch.yaml +35 -11
  21. package/assets/.appweaver/orchestrator/workflows/aw-spec-select-stack.orch.yaml +1 -1
  22. package/assets/.appweaver/orchestrator/workflows/aw-spec-specify.orch.yaml +1 -1
  23. package/assets/.appweaver/scripts/bash/aw-spec-actions.sh +91 -7
  24. package/assets/.appweaver/scripts/powershell/aw-spec-actions.ps1 +70 -7
  25. package/assets/.appweaver/scripts/python/aw-foundation.py +88 -12
  26. package/assets/.appweaver/scripts/python/aw-stack.py +481 -10
  27. package/assets/.appweaver/stack-profiles/dotnet-webapi.json +2 -0
  28. package/assets/.appweaver/stack-profiles/java-spring.json +2 -0
  29. package/assets/.appweaver/stacks.json +5 -2
  30. package/assets/.appweaver/templates/delta-design-template.md +7 -5
  31. package/assets/.appweaver/templates/design-template.md +3 -0
  32. package/assets/.appweaver/templates/ui-delta-design-template.md +1 -1
  33. package/assets/.appweaver/templates/ui-design-template.md +6 -4
  34. package/assets/foundations/dotnet/ModularMonolith/ModularMonolith.lane.json +6 -0
  35. package/assets/foundations/dotnet/VerticalSlice/VerticalSlice.lane.json +6 -0
  36. package/assets/foundations/foundation.spec.template.txt +26 -1
  37. package/assets/foundations/java/Hexagonal/Hexagonal.lane.json +6 -0
  38. package/assets/foundations/java/Hexagonal/Hexagonal.pack.txt +7 -0
  39. package/assets/guardrails/project-specific/domain-rule-bindings.md +20 -8
  40. package/assets/guardrails/project-specific/tech-rule-bindings.md +25 -37
  41. package/assets/guardrails/technology/dotnet-webapi/api-versioning.md +1 -1
  42. package/assets/guardrails/technology/dotnet-webapi/auditing.md +1 -1
  43. package/assets/guardrails/technology/dotnet-webapi/authorization.md +1 -1
  44. package/assets/guardrails/technology/dotnet-webapi/clean-architecture.md +1 -1
  45. package/assets/guardrails/technology/dotnet-webapi/coding-standards.md +1 -1
  46. package/assets/guardrails/technology/dotnet-webapi/cqrs.md +1 -1
  47. package/assets/guardrails/technology/dotnet-webapi/data-classification.md +1 -1
  48. package/assets/guardrails/technology/dotnet-webapi/dependency-policy.md +1 -1
  49. package/assets/guardrails/technology/dotnet-webapi/eventing.md +1 -1
  50. package/assets/guardrails/technology/dotnet-webapi/folder-structure.md +1 -1
  51. package/assets/guardrails/technology/dotnet-webapi/framework-api-map.md +1 -1
  52. package/assets/guardrails/technology/dotnet-webapi/modular-monolith.md +1 -1
  53. package/assets/guardrails/technology/dotnet-webapi/persistence.md +1 -1
  54. package/assets/guardrails/technology/dotnet-webapi/testing.md +1 -1
  55. package/assets/guardrails/technology/dotnet-webapi/vertical-slices.md +1 -1
  56. package/assets/guardrails/technology/dotnet-webapi/zero-trust.md +1 -1
  57. package/assets/guardrails/technology/nextjs/accessibility-implementation.md +1 -1
  58. package/assets/guardrails/technology/nextjs/api-client.md +1 -1
  59. package/assets/guardrails/technology/nextjs/api-integration.dotnet-webapi.md +1 -1
  60. package/assets/guardrails/technology/nextjs/api-integration.java-spring.md +1 -1
  61. package/assets/guardrails/technology/nextjs/auth-session.md +1 -1
  62. package/assets/guardrails/technology/nextjs/client-security.md +1 -1
  63. package/assets/guardrails/technology/nextjs/component-architecture.md +1 -1
  64. package/assets/guardrails/technology/nextjs/component-conventions.md +1 -1
  65. package/assets/guardrails/technology/nextjs/design-system.md +1 -1
  66. package/assets/guardrails/technology/nextjs/error-handling.md +1 -1
  67. package/assets/guardrails/technology/nextjs/forms-and-validation.md +1 -1
  68. package/assets/guardrails/technology/nextjs/frontend-testing.md +1 -1
  69. package/assets/guardrails/technology/nextjs/i18n.md +1 -1
  70. package/assets/guardrails/technology/nextjs/observability.md +1 -1
  71. package/assets/guardrails/technology/nextjs/performance-budgets.md +1 -1
  72. package/assets/guardrails/technology/nextjs/routing-navigation.md +1 -1
  73. package/assets/guardrails/technology/nextjs/state-management.md +1 -1
  74. package/package.json +1 -1
@@ -78,8 +78,8 @@ lane loads the technical base, the frontend lane loads the UI base. What they sh
78
78
  - **Domain rule files: NEVER.** Domain reaches plan solely as spec ids and the context's
79
79
  `domain-model.md`.
80
80
  - **Technical rule files: yes** — the architecture and cross-cutting rules each lane's plan
81
- mediates the spec against: `## /aw-spec-plan` for the service lane, `## /aw-spec-plan-ui`
82
- for the frontend lane.
81
+ mediates the spec against: `## /aw-spec-plan-backend` for the service lane,
82
+ `## /aw-spec-plan-frontend` for the frontend lane.
83
83
  - Constitution: the **technical** half in the service lane, the **UI** half in the frontend one.
84
84
 
85
85
  ### `implement` — *code generation, both lanes, both paths*
@@ -91,10 +91,9 @@ lane loads the technical base, the frontend lane loads the UI base. What they sh
91
91
  `domain-model.<CR-id>.delta.md` joins it, since the master is not merged until
92
92
  finalize-delta. The model is READ, never mined for work: what it holds and no plan element
93
93
  realises is a planning gap, not code to invent.
94
- - **Technical rule files: yes** — the code-generation and convention set, keyed per lane and,
95
- for the service lane, per path: `## /aw-spec-implement` generating a master plan,
96
- `## /aw-spec-implement-delta` generating a change's delta plan, `## /aw-spec-implement-ui`
97
- for the frontend lane either way.
94
+ - **Technical rule files: yes** — the code-generation and convention set, keyed per lane, the
95
+ same on the master and the delta path: `## /aw-spec-implement-backend` for the service lane,
96
+ `## /aw-spec-implement-frontend` for the frontend lane.
98
97
  - Constitution: the **technical** half in the service lane, the **UI** half in the frontend one.
99
98
 
100
99
  ### `build` — *compile + repair*
@@ -133,8 +132,8 @@ lane loads the technical base, the frontend lane loads the UI base. What they sh
133
132
  there is no map to read. The order is therefore inverted: the half is authored from the code,
134
133
  then the `frontend/` rule set is generated as its detail layer, then the lane's three headings
135
134
  are appended to the technical map — `## /aw-spec-ui-constitution` (what a LATER `constitution`
136
- run re-ratifies from, so it behaves like the greenfield case above), `## /aw-spec-plan-ui` and
137
- `## /aw-spec-implement-ui` (what the frontend lane of `plan` and `implement` load).
135
+ run re-ratifies from, so it behaves like the greenfield case above), `## /aw-spec-plan-frontend`
136
+ and `## /aw-spec-implement-frontend` (what the frontend lane of `plan` and `implement` load).
138
137
  - `onboard` **never writes** `.appweaver/stacks.json`. `lane-declare` is its sole writer and
139
138
  `select-stack` its only caller, so every lane is one a person confirmed.
140
139
  - The **document templates** it derives obey the same partition as the halves that govern them:
@@ -160,7 +159,7 @@ lane loads the technical base, the frontend lane loads the UI base. What they sh
160
159
  - Same partition, reversed: the reversed **spec** is authored with domain rules in context; the
161
160
  reversed **plan** is authored with the technical set only, so the plan stays domain-free.
162
161
  - Each lane's reversed plan is authored under its own lane's rules, as in `plan`: the backend
163
- plan under `## /aw-spec-adopt`, and the frontend plan under `## /aw-spec-plan-ui`, which
162
+ plan under `## /aw-spec-adopt-backend`, and the frontend plan under `## /aw-spec-plan-frontend`, which
164
163
  REPLACES the backend set before that step rather than stacking on it.
165
164
  - Technical files here are **reference, not authority — the CODE wins.**
166
165
 
@@ -22,7 +22,7 @@ and why nothing in the format assumes there are only two lanes.
22
22
 
23
23
  **Lanes get map headings, never a `--lane` flag.** Rule maps are keyed on
24
24
  `## /aw-spec-<name>`, so a lane's rule set is reached under its own heading —
25
- `## /aw-spec-implement-ui` for the frontend lane of `implement`, and so on. A flag would have
25
+ `## /aw-spec-implement-frontend` for the frontend lane of `implement`, and so on. A flag would have
26
26
  no heading to key on. Where the two lanes are folded into one command, as `plan` and
27
27
  `implement` are, the heading names the lane and the command dispatches on the declaration.
28
28
 
@@ -56,6 +56,9 @@ no heading to key on. Where the two lanes are folded into one command, as `plan`
56
56
  | `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 root folder. `select-stack` decides it from where the stack's build file sits — see §3.1 |
57
57
  | `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`, as seen from where the command runs (§4.2) |
58
58
  | `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) |
59
+ | `contract_run` | *(optional, backend)* the command that starts the service on its own so `implement` can export its API document (§4.5). Absent, the profile's is used. `foundation` sets it from a pack's `<pack>.lane.json` |
60
+ | `contract` | *(optional, backend)* the URL the running service serves its API document at (§4.5). Absent, the profile's is used |
61
+ | `client` | *(optional, frontend)* the command that generates the frontend's API client from that document; `{{contract}}` in it is the document's path. No shipped profile has one: `implement` asks the developer the first time and records the answer here (§4.5) |
59
62
 
60
63
  ### Three properties matter more than the schema
61
64
 
@@ -217,6 +220,11 @@ workflow.**
217
220
  "host_patterns": [], // optional — where the runnable project sits, for
218
221
  // `{{host}}` in a command — see §4.2
219
222
  "migrate": "", // optional — schema migration, if the stack has one
223
+ "contract_run": "", // optional, backend — start the service to export
224
+ // its API document — see §4.5
225
+ "contract": "", // optional, backend — the URL it serves it at
226
+ "client": "", // optional, frontend — generate the API client
227
+ // from `{{contract}}` — see §4.5
220
228
  "prototype_run": "npm run dev --prefix prototype", // frontend only — fills the prototype block
221
229
  "reserved_names": ["Event", "Request", "Response"], // optional — names this stack's AMBIENT
222
230
  // scope already defines; see §4.4
@@ -434,18 +442,66 @@ A layout the table gets wrong is fixed by writing `test_root` on the lane in `st
434
442
  | `go-chi` | `backend/` | *(inside the lane root folder)* | `internal/` | — |
435
443
  | every frontend | `frontend/` | *(inside the lane root folder)* | `web/` | — |
436
444
 
445
+ ### 4.5 `contract_run`, `contract` and `client` — the API contract between the lanes
446
+
447
+ The frontend lane binds to the service through the service's published API document, and
448
+ the document that counts is the one the **built code** serves. `implement` therefore runs
449
+ three steps between the two lanes, each of which warns rather than stops:
450
+
451
+ 1. **Export** (`contract-export`) — start the service with the backend lane's
452
+ `contract_run`, fetch its `contract` URL, write the document to
453
+ `<service lane root folder>/contracts/openapi.json`, and stop the service with
454
+ everything it started. A service already answering at the URL is used as it is, and
455
+ the run says it may be older than the code.
456
+ 2. **Client** (`contract-client`) — run the frontend lane's `client` command, with
457
+ `{{contract}}` standing for that document as seen from where the command runs.
458
+ 3. **Check** (`contract-check`) — every operation id the frontend plan cites must be an
459
+ `operationId` in the document.
460
+
461
+ Each key is read from the lane first and from its profile second, so a lane declared
462
+ before the keys existed still answers them. Where the values come from:
463
+
464
+ | Key | Profile default | Set on the lane by |
465
+ |---|---|---|
466
+ | `contract_run` | `dotnet-webapi`: `dotnet run --project {{host}} -- --environment Development --urls http://localhost:5000`<br>`java-spring`: `./mvnw spring-boot:run -Dspring-boot.run.arguments=--server.port=8080` | `foundation`: on the pack route from the pack's `<pack>.lane.json` — the shipped .NET packs start their API project alone (the AppHost needs Docker), the Java `Hexagonal` pack its one Maven module; on the spec route from the spec's `# contract-run:` header line, or the operator's answer when it has none |
467
+ | `contract` | `dotnet-webapi`: `http://localhost:5000/swagger/v1/swagger.json`<br>`java-spring`: `http://localhost:8080/v3/api-docs` (springdoc's default path) | `foundation`, the same way — the `# contract:` header line on the spec route |
468
+ | `client` | none — no generator is assumed | `foundation` on the spec route, from the `# client:` header line or the operator's answer; else `implement`, which asks the first time and records the answer |
469
+
470
+ On the spec route an empty answer keeps the lane's current value, so a lane nobody set
471
+ anything on stays on the profile's default. `foundations/foundation.spec.template.txt`
472
+ shows the three header lines; `<App>` in them is the app name given at `foundation`.
473
+
474
+ `--environment Development` matters for the .NET packs: outside Development they redirect
475
+ to HTTPS, and the export fetches over http. Their API projects also read
476
+ `ConnectionStrings:SqliteConnection` when started without the AppHost, and the packs'
477
+ configuration carries none, so their commands pass one. A pack's `<pack>.lane.json` sits
478
+ beside its `.pack.txt`, names the pack's sentinel the way its files do, and is renamed with
479
+ them:
480
+
481
+ ```json
482
+ {"format": "aw-spec-pack-lane/v1",
483
+ "contract_run": "dotnet run --project src/AppWeaver.VerticalSlice.Service -- --environment Development --urls http://localhost:5000 --ConnectionStrings:SqliteConnection \"Data Source=App_Data/app.db\"",
484
+ "contract": "http://localhost:5000/swagger/v1/swagger.json"}
485
+ ```
486
+
437
487
  ---
438
488
 
439
489
  ## 5. Who may write this file
440
490
 
441
- **`lane-declare` writes it. Nothing else does, and it has exactly ONE caller** — so the
442
- rule is a grep, not a convention:
491
+ **`lane-declare` writes whole lanes, and it has exactly ONE caller** — so the rule is a
492
+ grep, not a convention:
443
493
 
444
494
  ```sh
445
495
  grep -rl "lane-declare" .appweaver/orchestrator/workflows/ # must print exactly one file
446
496
  # aw-spec-select-stack.orch.yaml — a person picks
447
497
  ```
448
498
 
499
+ **`lane-set` changes three keys on a lane that is already there, and nothing else** —
500
+ `contract_run`, `contract` and `client` (§4.5). Its callers are `foundation`, recording
501
+ what a pack knows about starting its service, and `implement`, recording the client
502
+ command a developer types. They cannot use `lane-declare`, which rebuilds the whole lane
503
+ and would reset every command on it to the profile's default.
504
+
449
505
  `onboard` used to be a second caller. Before `select-stack` was the first command of the
450
506
  brownfield sequence a repo could reach `onboard` undeclared, so `onboard` ran its own
451
507
  detection pass, recorded what it found marked `origin: reverse-engineered`, and was gated
@@ -545,7 +601,11 @@ implementation and the twins cannot drift.
545
601
  |---|---|
546
602
  | `stack-list [--kind k] [--preselect text] [--include-none] [--suggest]` | enumerate the installed profiles; returns `select_options[]`, and `preselect` when the text names one of *this kind's* ids. `--suggest` also runs detection, so the menu **leads with** the stack this repo's files point at and `root_options[]` leads with the directory the evidence sits in |
547
603
  | `stack-detect [--kind k] [--exclude-root p]…` | match the repo's OWN files against the installed profiles — the brownfield candidate list, since an existing repo already answered the question. Writes nothing. `--exclude-root` keeps a service profile from relocating onto a subtree another lane owns |
548
- | `lane-declare --profile <id\|label\|none\|detected> [--kind k] [--id lane] [--source-path\|--root p] [--stack label] [--origin declared\|reverse-engineered] [--build/--test/--run/--migrate c] [--run-from repo\|root] [--no-prototype] [--require-existing-root]` | upsert a lane — **the sole writer**. `detected` declares a stack the kit ships no profile for (needs `--stack` and a lane root folder), which is how a legacy WebForms, JSP or ERB lane gets recorded at all |
604
+ | `lane-declare --profile <id\|label\|none\|detected> [--kind k] [--id lane] [--source-path\|--root p] [--stack label] [--origin declared\|reverse-engineered] [--build/--test/--run/--migrate c] [--run-from repo\|root] [--no-prototype] [--require-existing-root]` | upsert a WHOLE lane — its one caller is `select-stack` (§5). `detected` declares a stack the kit ships no profile for (needs `--stack` and a lane root folder), which is how a legacy WebForms, JSP or ERB lane gets recorded at all |
605
+ | `lane-set (--id lane\|--kind k) [--set KEY=VALUE]… [--unset KEY]…` | change only `contract_run`, `contract` or `client` on a lane already declared, leaving the rest of it alone. An empty VALUE changes nothing; `--unset` puts the key back on the profile's value. Called by `foundation` (a pack's lane settings) and `implement` (the client command a developer supplies) — re-declaring would reset every other command |
606
+ | `contract-export [--timeout s]` | start the service lane with `contract_run`, fetch `contract`, save it to `<service root>/contracts/openapi.json`, stop the service and everything it started. A service already answering at the URL is used as it is, and said so (§4.5) |
607
+ | `contract-client` | `run-phase --phase client` for the frontend lane — its `client` command, `{{contract}}` expanded |
608
+ | `contract-check --plan p [--doc d]` | the operation ids a frontend plan cites (its §5 `Operation id` column, or a delta plan's `Contract Binding` rows) against the document's `operationId`s — by script, no model |
549
609
  | `lane-resolve [--kind k] [--id lane]` | read; exit 0 always. `exists` reports the file, `declared` reports the lanes — the shipped-but-unfilled seed is `exists:true, declared:false`. Returns the matched lane **and** both kinds flattened — `backend_source_path`, `frontend_root_exists`, `backend_build` … — so a workflow binds one variable instead of calling twice |
550
610
  | `lane-verify [--require backend\|frontend]… [--allow-missing-root] [--command phase]` | **the gate**: is the declaration usable for the phase about to run. Never writes, and **always exits 0** — the calling workflow's `verify_context` owns the stop, so the reason arrives as text |
551
611
  | `lane-probe [--kind k] [--hint text]` | read; the **file** signals for "does this lane exist" — a declared lane, and a stack named in free prose. Retained for callers that want a soft hint; the authoritative answer is the declaration, read through `lane-resolve` or `lane-verify` |
@@ -610,15 +670,20 @@ they are content the kit cannot invent:
610
670
  | `{{backend_ecosystem}}` / `{{frontend_ecosystem}}` | that lane's `ecosystem` |
611
671
 
612
672
  ```md
613
- ## /aw-spec-plan
673
+ ## /aw-spec-plan-backend
614
674
  - guardrails/technology/{{stack}}/persistence.md
615
- - guardrails/technology/{{frontend_stack}}/ui-constitution.md
675
+
676
+ ## /aw-spec-plan-frontend
677
+ - guardrails/technology/{{frontend_stack}}/component-architecture.md
678
+ - guardrails/technology/{{frontend_stack}}/api-integration.{{backend_stack}}.md
616
679
  ```
617
680
 
618
681
  One map, read from a Spring repo, loads `java-spring/persistence.md`; read from a .NET repo it
619
- loads `dotnet-webapi/persistence.md`. A placeholder for a lane that is not declared **skips**
620
- that bullet — a frontend rule in a backend-only repo is not an error — and a resolved path with
621
- no file on disk is reported under `missing` rather than silently dropped.
682
+ loads `dotnet-webapi/persistence.md`. Each heading holds one lane's rules, and a workflow loads
683
+ a lane's heading only when that lane runs. A placeholder for a lane that is not declared
684
+ **skips** that bullet — in a frontend-only repo the `api-integration.{{backend_stack}}.md`
685
+ pairing bullet is skipped, not an error — and a resolved path with no file on disk is reported
686
+ under `missing` rather than silently dropped.
622
687
 
623
688
  **A bullet that spells the stack out is still used**, because a shared corpus may be referenced
624
689
  on purpose. But when that directory is not one the repo declares, `resolve-tech` returns
@@ -62,13 +62,18 @@ def _indent(line):
62
62
 
63
63
 
64
64
  def _strip_comment(s):
65
- """Strip a trailing ' #...' comment that is outside quotes/flow."""
65
+ """Strip a trailing ' #...' comment that is outside quotes/flow. Inside double
66
+ quotes a backslash escapes the next character, so \\" does not end the string."""
66
67
  out, i, depth = [], 0, 0
67
68
  quote = None
68
69
  while i < len(s):
69
70
  c = s[i]
70
71
  if quote:
71
72
  out.append(c)
73
+ if quote == '"' and c == "\\" and i + 1 < len(s):
74
+ out.append(s[i + 1])
75
+ i += 2
76
+ continue
72
77
  if c == quote:
73
78
  quote = None
74
79
  elif c in ("'", '"'):
@@ -93,6 +98,9 @@ def _scalar(tok):
93
98
  if tok == "" or tok == "~" or tok.lower() == "null":
94
99
  return None
95
100
  if len(tok) >= 2 and tok[0] == tok[-1] and tok[0] in "'\"":
101
+ if tok[0] == '"':
102
+ # A double-quoted value reads \" as " and \\ as \, as YAML does.
103
+ return re.sub(r'\\(["\\])', r"\1", tok[1:-1])
96
104
  return tok[1:-1]
97
105
  low = tok.lower()
98
106
  if low == "true":
@@ -140,7 +148,9 @@ def _parse_flow(s):
140
148
  while pos[0] < len(s):
141
149
  ch = s[pos[0]]
142
150
  if quote:
143
- if ch == quote:
151
+ if quote == '"' and ch == "\\":
152
+ pos[0] += 1 # the escaped character never closes the quote
153
+ elif ch == quote:
144
154
  quote = None
145
155
  elif ch in "'\"":
146
156
  quote = ch
@@ -328,12 +338,23 @@ def _parse_block_scalar(lines, parent_indent, folded, chomp):
328
338
  trailing += 1
329
339
 
330
340
  if folded:
331
- text = ""
332
- for i, ln in enumerate(block):
333
- if i == 0:
334
- text = ln
341
+ # YAML folding: one line break between two lines becomes a space, a line break
342
+ # followed by k empty lines becomes k newlines, and the break before or after a
343
+ # more-indented line is kept as it is.
344
+ parts, prev, empties = [], None, 0
345
+ for ln in block:
346
+ if ln == "":
347
+ empties += 1
348
+ continue
349
+ if prev is None:
350
+ parts.append("\n" * empties)
351
+ elif prev[:1] not in " \t" and ln[:1] not in " \t":
352
+ parts.append("\n" * empties if empties else " ")
335
353
  else:
336
- text += ("\n" if (block[i - 1] == "" or ln == "") else " ") + ln
354
+ parts.append("\n" * (empties + 1))
355
+ parts.append(ln)
356
+ prev, empties = ln, 0
357
+ text = "".join(parts)
337
358
  else:
338
359
  text = "\n".join(block)
339
360
 
@@ -130,6 +130,9 @@ 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: .appweaver/memory/constitution.technical.md, optional: true }
134
+ # the lane's ratified technology base, the same one
135
+ # the master plan is authored under — no domain.
133
136
  - { path: "{{cr_dir}}/delta-slice-analyze-report.md", optional: true }
134
137
  # WHAT THE LAST GATE FOUND, when this change
135
138
  # has already been through analyze-slice-delta.
@@ -191,7 +194,9 @@ workflow:
191
194
  lane" with every addition or change to this feature's published surface, as field
192
195
  shapes. That table is the only part of this document the frontend delta plan may
193
196
  bind against; a contract the UI needs and this document never states becomes a
194
- client-side workaround. "None" when the surface is unchanged.
197
+ client-side workaround. "None" when the surface is unchanged. Each row carries the
198
+ endpoint's operation id: a changed endpoint keeps the one the master plan gives
199
+ it, and a new endpoint gets one unique across the service.
195
200
  - Stay technology-agnostic (no code) and keep the master's form: behaviour as
196
201
  signatures + pre/post/invariant/raises, contracts as field shapes.
197
202
  - TWO CASES — decide from the delta-spec in context:
@@ -208,11 +213,12 @@ workflow:
208
213
 
209
214
  # ══════════ THE LANE SWAP — the frontend's rules replace the backend's ═══════
210
215
  # The delta twin of the swap `plan` performs before its own master frontend sections.
211
- # Sticky standards only accumulate, and the caller loaded BOTH lanes' rule sets before
212
- # including this module, so without this the service delta plan is authored with the UI
213
- # rules in its envelope and the experience delta plan with the service rules. `replace`
214
- # resets to the workflow's declared seed and loads `## /aw-spec-plan-ui`, so the backend
215
- # delta above kept the service set and everything below this line is the frontend's.
216
+ # The caller loads only the service lane's rules (`## /aw-spec-plan-backend`, when that
217
+ # lane is declared) before including this module, and sticky standards only accumulate,
218
+ # so without this the experience delta plan would be authored with the service rules in
219
+ # its envelope. `replace` resets to the workflow's declared seed and loads
220
+ # `## /aw-spec-plan-frontend`, so the backend delta above is authored under the service
221
+ # set and everything below this line under the frontend's.
216
222
  #
217
223
  # Conditioned on the same flag as the lane it precedes: a change with no frontend half
218
224
  # authors nothing here and must not have its envelope swapped out from under the
@@ -221,7 +227,7 @@ workflow:
221
227
  condition: "{{fe_existing}} == true"
222
228
  action: include
223
229
  module: .appweaver/orchestrator/modules/tech-select.orch.yaml
224
- with: { command: aw-spec-plan-ui, replace: "true" }
230
+ with: { command: aw-spec-plan-frontend, replace: "true" }
225
231
 
226
232
  # ═════════════════════ FRONTEND LANE — the experience delta plan ═════════════
227
233
  - id: ctx_frontend_delta
@@ -292,6 +298,10 @@ workflow:
292
298
  and introduce no server-side behaviour or persistence assumption. What the
293
299
  experience needs and the surface does not offer goes under "Contract deltas
294
300
  needed", raised back to the service lane — never worked around in the client.
301
+ - A BINDING ROW'S ELEMENT LEADS WITH ITS OPERATION ID — `OperationId` · `VERB
302
+ /route` — copied exactly from the service delta's table or, for an endpoint the
303
+ change does not touch, the master backend plan. Implement looks the endpoint up by
304
+ it in the service's exported API document.
295
305
  - UX- IS THIS DOCUMENT'S TO REALISE. The service plan excludes the spec's Experience
296
306
  Standards by contract, so a UX- item in the delta-spec that reaches neither plan
297
307
  is a dropped requirement.
@@ -62,7 +62,7 @@ workflow:
62
62
  require_value: dg_gate
63
63
  equals: "continue"
64
64
  on_mismatch: stop
65
- on_mismatch_message: "Exited — run /appweaver select-domain, or set \"domain\" in .appweaver/domain.json to a folder under guardrails/ that holds domain-rule-bindings.md (project-specific by default), then re-run."
65
+ on_mismatch_message: "Exited — run /appweaver select-domain, or set 'domain' in .appweaver/domain.json to a folder under guardrails/ that holds domain-rule-bindings.md (project-specific by default), then re-run."
66
66
 
67
67
  # 3. Selection box — only when a rule map with candidates exists.
68
68
  - id: select_mode
@@ -6,6 +6,9 @@
6
6
  # collisions, unpack into the lane root folder exactly as packed with the sentinel renamed,
7
7
  # optionally add the reference sample on top, and check that no sentinel is left behind.
8
8
  #
9
+ # Its LANE SETTINGS (<pack name>.lane.json beside the pack), when it has any, are recorded
10
+ # on the lane once it is unpacked.
11
+ #
9
12
  # Every file in a pack lands as packed, replacing a file already there — except the ones
10
13
  # its RULE FILE (<pack name>.rules.json beside the pack) says to merge. The model is used
11
14
  # ONLY for those merges: an `agent_author` (read-only generation, the ENGINE writes the
@@ -63,6 +66,13 @@ workflow:
63
66
  fp_clean: ""
64
67
  fp_residue: ""
65
68
  fp_residue_note: "not checked"
69
+ fp_lane_found: "false"
70
+ fp_lane_file: ""
71
+ fp_lane_contract_run: ""
72
+ fp_lane_contract: ""
73
+ fp_lane_client: ""
74
+ fp_lane_recorded: ""
75
+ fp_lane_note: ""
66
76
 
67
77
  # 1. Locate the bundle + its unpacker and read the (bounded) pack header: the sentinel,
68
78
  # the file count, and whether a Reference pack ships beside it.
@@ -187,6 +197,39 @@ workflow:
187
197
  message_var: fl_status
188
198
  on_false_message: "NOT scaffolded — the {{fl_name}} unpacker failed and wrote {{fp_written}} file(s). Its output ends: {{fp_unpack_out}}"
189
199
 
200
+ # 5b. The pack's LANE SETTINGS (<pack name>.lane.json beside it): what only the pack
201
+ # knows about the project it scaffolds — chiefly how its service is started on its
202
+ # own so implement can export the API document. Recorded on the lane with lane-set;
203
+ # a key the file leaves empty is not touched, and a pack with no such file leaves
204
+ # the lane on its stack profile's values.
205
+ - id: fp_lane_settings
206
+ action: execute_script
207
+ on_error: continue
208
+ script: { ref: actions }
209
+ subcommand: foundation-pack-lane
210
+ args: ["--name", "{{fl_name}}", "--app", "{{app}}"]
211
+ outputs:
212
+ - { name: fp_lane_found, from: "$.found" }
213
+ - { name: fp_lane_file, from: "$.file" }
214
+ - { name: fp_lane_contract_run, from: "$.contract_run" }
215
+ - { name: fp_lane_contract, from: "$.contract" }
216
+ - { name: fp_lane_client, from: "$.client" }
217
+
218
+ - id: fp_lane_record
219
+ action: execute_script
220
+ condition: "{{fp_lane_found}} == true"
221
+ on_error: continue
222
+ script: { ref: actions }
223
+ subcommand: lane-set
224
+ args: ["--id", "{{fl_lane_id}}", "--set", "contract_run={{fp_lane_contract_run}}", "--set", "contract={{fp_lane_contract}}", "--set", "client={{fp_lane_client}}"]
225
+ outputs:
226
+ - { name: fp_lane_recorded, from: "$.ok" }
227
+
228
+ - id: fp_lane_noted
229
+ action: set
230
+ condition: "{{fp_lane_recorded}} == true"
231
+ vars: { fp_lane_note: "; the pack's API-export settings ({{fp_lane_file}}) recorded on the {{fl_lane_id}} lane" }
232
+
190
233
  # 6. The merges the rule file asks for: the file that was there (put aside) and the
191
234
  # pack's version (now on disk) become one, as the rule's instruction says.
192
235
  - id: fp_merge
@@ -346,4 +389,4 @@ workflow:
346
389
  action: set
347
390
  vars:
348
391
  fl_files: "{{fp_written}}"
349
- fl_status: "scaffolded from the {{fl_name}} pack — {{fp_written}} file(s) into {{fl_source_path}}{{fp_tests_note}}{{fp_rules_note}}{{fp_ref_note}}; {{fp_residue_note}}"
392
+ fl_status: "scaffolded from the {{fl_name}} pack — {{fp_written}} file(s) into {{fl_source_path}}{{fp_tests_note}}{{fp_rules_note}}{{fp_ref_note}}{{fp_lane_note}}; {{fp_residue_note}}"
@@ -22,7 +22,8 @@
22
22
  # gates on), and its own build command can be run on it.
23
23
  #
24
24
  # The spec actually used is copied to .appweaver/foundation/<lane>.spec.md — the record of
25
- # what the project was built from.
25
+ # what the project was built from. The API-contract settings its header gives — or the
26
+ # operator types when it gives none — are recorded on the lane once the project is built.
26
27
  #
27
28
  # Every refusal RETURNS rather than stops, with the reason in fl_status, so the run
28
29
  # carries on with the next lane.
@@ -68,6 +69,30 @@ workflow:
68
69
  fs_build_ack: ""
69
70
  fs_build_ok: ""
70
71
  fs_build_note: "build not run — /appweaver build compiles it and repairs compile errors"
72
+ fs_hdr_contract_run: ""
73
+ fs_hdr_contract: ""
74
+ fs_hdr_client: ""
75
+ fs_settings_text: "none"
76
+ fs_placeholder: ""
77
+ fs_is_backend: "false"
78
+ fs_is_frontend: "false"
79
+ fs_need_run: "false"
80
+ fs_need_url: "false"
81
+ fs_need_client: "false"
82
+ fs_ans_contract_run: ""
83
+ fs_ans_contract: ""
84
+ fs_ans_client: ""
85
+ fs_set_run: ""
86
+ fs_set_url: ""
87
+ fs_set_client: ""
88
+ fs_cur_contract_run: ""
89
+ fs_cur_contract: ""
90
+ fs_cur_client: ""
91
+ fs_cur_run_shown: "none — the export is skipped until one is set"
92
+ fs_cur_url_shown: "none — the export is skipped until one is set"
93
+ fs_cur_client_shown: "none — implement asks for one before it generates the frontend"
94
+ fs_lane_written: ""
95
+ fs_lane_note: ""
71
96
 
72
97
  # 1. A lane that already had files reaches this module only when the operator agreed, in
73
98
  # foundation-lane, to overwrite them. The agent is told so, with what that means for
@@ -120,26 +145,36 @@ workflow:
120
145
  condition: "{{fl_route}} == spec"
121
146
  script: { ref: actions }
122
147
  subcommand: foundation-spec-capture
123
- args: ["--lane", "{{fl_lane_id}}", "--kind", "{{fl_kind}}", "--file", "{{fl_spec_file}}"]
148
+ args: ["--lane", "{{fl_lane_id}}", "--kind", "{{fl_kind}}", "--file", "{{fl_spec_file}}", "--app", "{{app}}"]
124
149
  outputs:
125
150
  - { name: fs_ok, from: "$.ok" }
126
151
  - { name: fs_spec_path, from: "$.spec_path" }
127
152
  - { name: fs_source, from: "$.source" }
128
153
  - { name: fs_title, from: "$.title" }
129
154
  - { name: fs_reason, from: "$.reason" }
155
+ - { name: fs_hdr_contract_run, from: "$.contract_run" }
156
+ - { name: fs_hdr_contract, from: "$.contract" }
157
+ - { name: fs_hdr_client, from: "$.client" }
158
+ - { name: fs_settings_text, from: "$.settings_text" }
159
+ - { name: fs_placeholder, from: "$.placeholder" }
130
160
 
131
161
  - id: fs_capture_manual
132
162
  action: execute_script
133
163
  condition: "{{fl_route}} == manual"
134
164
  script: { ref: actions }
135
165
  subcommand: foundation-spec-capture
136
- args: ["--lane", "{{fl_lane_id}}", "--kind", "{{fl_kind}}", "--input", "{{fs_spec_input}}"]
166
+ args: ["--lane", "{{fl_lane_id}}", "--kind", "{{fl_kind}}", "--input", "{{fs_spec_input}}", "--app", "{{app}}"]
137
167
  outputs:
138
168
  - { name: fs_ok, from: "$.ok" }
139
169
  - { name: fs_spec_path, from: "$.spec_path" }
140
170
  - { name: fs_source, from: "$.source" }
141
171
  - { name: fs_title, from: "$.title" }
142
172
  - { name: fs_reason, from: "$.reason" }
173
+ - { name: fs_hdr_contract_run, from: "$.contract_run" }
174
+ - { name: fs_hdr_contract, from: "$.contract" }
175
+ - { name: fs_hdr_client, from: "$.client" }
176
+ - { name: fs_settings_text, from: "$.settings_text" }
177
+ - { name: fs_placeholder, from: "$.placeholder" }
143
178
 
144
179
  - id: fs_capture_gate
145
180
  action: verify_context
@@ -165,7 +200,7 @@ workflow:
165
200
  - id: fs_rules_why_project
166
201
  action: set
167
202
  condition: "{{tech_domain}} == "
168
- vars: { fs_rules_why: "no project is named in .appweaver/domain.json — run /appweaver select-domain, or set \"domain\" to a folder under guardrails/ (project-specific by default)" }
203
+ vars: { fs_rules_why: "no project is named in .appweaver/domain.json — run /appweaver select-domain, or set 'domain' to a folder under guardrails/ (project-specific by default)" }
169
204
 
170
205
  - id: fs_rules_why_heading
171
206
  action: set
@@ -278,6 +313,19 @@ workflow:
278
313
  /appweaver implement from the plans, so add none the spec does not ask for.
279
314
  - The project must build with `{{fl_build}}` run in {{fl_workdir}}. Restore or
280
315
  install what that needs; run the build once if you can and fix what it reports.
316
+ - THE API CONTRACT SETTINGS the spec's header gives: {{fs_settings_text}}. They are
317
+ recorded on the lane, and /appweaver implement uses them before it generates the
318
+ frontend, so the project you build has to honour them exactly:
319
+ · a START COMMAND (backend) — the API starts with exactly that command, run in
320
+ {{fl_workdir}}, ON ITS OWN: no orchestration host, and nothing it needs that
321
+ is not on this machine. Name the project so the path the command names exists.
322
+ · a DOCUMENT URL (backend) — the running API serves its OpenAPI document at
323
+ exactly that URL, over http, on the port it names.
324
+ · a CLIENT COMMAND (frontend) — that command can run in {{fl_workdir}}: install
325
+ its tool as a development dependency. Do not run it; the service's document
326
+ does not exist yet.
327
+ A backend built without them still serves an OpenAPI document; say at what URL,
328
+ and how the API starts on its own, in your summary.
281
329
  - Do not commit, push, or change git configuration.
282
330
 
283
331
  List every file you created or changed in `files` (paths relative to the repository
@@ -373,9 +421,128 @@ workflow:
373
421
  condition: "{{fs_build_ok}} == false"
374
422
  vars: { fs_build_note: "build FAILED — run /appweaver build, which repairs compile errors: {{fs_build_tail}}" }
375
423
 
424
+ # 8. THE API CONTRACT SETTINGS — what implement uses between the lanes to export the
425
+ # service's OpenAPI document and generate the frontend's client from it
426
+ # (STACK-PROFILES.md §4.5). A backend lane needs its start command and document URL,
427
+ # a frontend lane its client command. Each comes from the spec's header
428
+ # (`# contract-run:`, `# contract:`, `# client:`, read at capture); one the header
429
+ # does not give is asked for, and an empty answer keeps the lane's current value —
430
+ # the stack profile's, unless something set it already. What there is, is recorded
431
+ # on the lane with lane-set, as the pack route records a pack's .lane.json.
432
+ - id: fs_kind_backend
433
+ action: set
434
+ condition: "{{fl_kind}} == backend"
435
+ vars: { fs_is_backend: "true" }
436
+ - id: fs_kind_frontend
437
+ action: set
438
+ condition: "{{fl_kind}} == frontend"
439
+ vars: { fs_is_frontend: "true" }
440
+ - id: fs_need_run_arm
441
+ action: set
442
+ condition: "{{fs_hdr_contract_run}} == "
443
+ vars: { fs_need_run: "{{fs_is_backend}}" }
444
+ - id: fs_need_url_arm
445
+ action: set
446
+ condition: "{{fs_hdr_contract}} == "
447
+ vars: { fs_need_url: "{{fs_is_backend}}" }
448
+ - id: fs_need_client_arm
449
+ action: set
450
+ condition: "{{fs_hdr_client}} == "
451
+ vars: { fs_need_client: "{{fs_is_frontend}}" }
452
+
453
+ # The lane's current values, shown as what an empty answer keeps.
454
+ - id: fs_current
455
+ action: execute_script
456
+ on_error: continue
457
+ script: { ref: actions }
458
+ subcommand: lane-resolve
459
+ args: ["--id", "{{fl_lane_id}}"]
460
+ outputs:
461
+ - { name: fs_cur_contract_run, from: "$.backend_contract_run" }
462
+ - { name: fs_cur_contract, from: "$.backend_contract" }
463
+ - { name: fs_cur_client, from: "$.frontend_client" }
464
+ - id: fs_cur_run_known
465
+ action: set
466
+ condition: "{{fs_cur_contract_run}} != "
467
+ vars: { fs_cur_run_shown: "{{fs_cur_contract_run}}" }
468
+ - id: fs_cur_url_known
469
+ action: set
470
+ condition: "{{fs_cur_contract}} != "
471
+ vars: { fs_cur_url_shown: "{{fs_cur_contract}}" }
472
+ - id: fs_cur_client_known
473
+ action: set
474
+ condition: "{{fs_cur_client}} != "
475
+ vars: { fs_cur_client_shown: "{{fs_cur_client}}" }
476
+
477
+ - id: fs_ask_contract_run
478
+ action: ask
479
+ condition: "{{fs_need_run}} == true"
480
+ prompt: |
481
+ How does the API in lane `{{fl_lane_id}}` start ON ITS OWN? /appweaver implement starts
482
+ it with this command to fetch its OpenAPI document before it generates the frontend.
483
+ The spec's header gives no `# contract-run:`.
484
+ Type the command — it runs in {{fl_workdir}} — with no orchestration host and nothing
485
+ the API cannot reach on this machine.
486
+ Leave it empty to keep the current one: {{fs_cur_run_shown}}
487
+ answer_var: fs_ans_contract_run
488
+ - id: fs_ask_contract
489
+ action: ask
490
+ condition: "{{fs_need_url}} == true"
491
+ prompt: |
492
+ At what URL does that running API serve its OpenAPI document? The spec's header gives
493
+ no `# contract:`.
494
+ Leave it empty to keep the current one: {{fs_cur_url_shown}}
495
+ answer_var: fs_ans_contract
496
+ - id: fs_ask_client
497
+ action: ask
498
+ condition: "{{fs_need_client}} == true"
499
+ prompt: |
500
+ Which command generates this frontend's API client from the service's OpenAPI
501
+ document? The spec's header gives no `# client:`.
502
+ It runs in {{fl_workdir}}, and {{fs_placeholder}} in it stands for the document's path —
503
+ for example: npx openapi-typescript {{fs_placeholder}} -o lib/api/schema.d.ts
504
+ Leave it empty to keep the current one: {{fs_cur_client_shown}}
505
+ answer_var: fs_ans_client
506
+
507
+ # What to record: the header's value for this lane's kind, or the answer where it gave
508
+ # none. An empty value is left out by lane-set, so the lane keeps what it has.
509
+ - id: fs_set_backend
510
+ action: set
511
+ condition: "{{fs_is_backend}} == true"
512
+ vars: { fs_set_run: "{{fs_hdr_contract_run}}", fs_set_url: "{{fs_hdr_contract}}" }
513
+ - id: fs_set_run_answer
514
+ action: set
515
+ condition: "{{fs_need_run}} == true"
516
+ vars: { fs_set_run: "{{fs_ans_contract_run}}" }
517
+ - id: fs_set_url_answer
518
+ action: set
519
+ condition: "{{fs_need_url}} == true"
520
+ vars: { fs_set_url: "{{fs_ans_contract}}" }
521
+ - id: fs_set_frontend
522
+ action: set
523
+ condition: "{{fs_is_frontend}} == true"
524
+ vars: { fs_set_client: "{{fs_hdr_client}}" }
525
+ - id: fs_set_client_answer
526
+ action: set
527
+ condition: "{{fs_need_client}} == true"
528
+ vars: { fs_set_client: "{{fs_ans_client}}" }
529
+
530
+ - id: fs_lane_record
531
+ action: execute_script
532
+ on_error: continue
533
+ script: { ref: actions }
534
+ subcommand: lane-set
535
+ args: ["--id", "{{fl_lane_id}}", "--set", "contract_run={{fs_set_run}}", "--set", "contract={{fs_set_url}}", "--set", "client={{fs_set_client}}"]
536
+ outputs:
537
+ - { name: fs_lane_written, from: "$.written" }
538
+ - id: fs_lane_noted
539
+ action: set
540
+ condition: "{{fs_lane_written}} == true"
541
+ vars: { fs_lane_note: "; API-contract settings recorded on the {{fl_lane_id}} lane" }
542
+
376
543
  - id: fs_done
377
544
  action: set
378
545
  vars:
379
546
  fl_spec_path: "{{fs_spec_path}}"
380
547
  fl_files: "{{fs_files}}"
381
- fl_status: "built from the spec in {{fs_spec_path}} (from {{fs_source}}) into {{fl_source_path}} — {{fs_summary}}; {{fs_build_note}}"
548
+ fl_status: "built from the spec in {{fs_spec_path}} (from {{fs_source}}) into {{fl_source_path}} — {{fs_summary}}; {{fs_build_note}}{{fs_lane_note}}"