aw-spec-cli 0.1.15 → 0.1.16

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.
@@ -104,6 +104,23 @@ vars:
104
104
  design_pass: "0"
105
105
  design_spec_version: ""
106
106
  design_stale: "false"
107
+ # WHICH LANES story-plan designed and which it dropped. Absent on a story designed before
108
+ # the keys existed, which is why the default message below keeps both readings.
109
+ designed_lanes: ""
110
+ dropped_lanes: ""
111
+ lane_keys_present: "false"
112
+ fe_lane_dropped: "false"
113
+ no_design_msg: ""
114
+ # the analyze-design verdict — law 40's evidence. certified defaults FALSE so a probe that
115
+ # could not run fails CLOSED, and the message default carries a route for the same reason.
116
+ pass_plan_hash: ""
117
+ gate_status: ""
118
+ gate_certified: "false"
119
+ gate_aborted: "false"
120
+ gate_gap_refs: ""
121
+ gate_report: ""
122
+ gate_lane_sha: ""
123
+ gate_msg: "The analyze-design verdict could not be read, so this run cannot tell whether the feature plans were ever certified. Run /appweaver analyze-design, then re-run."
107
124
  service_design_doc: ""
108
125
  # the feature's current code, per lane (OKF)
109
126
  okf_concept: ""
@@ -142,6 +159,13 @@ workflow:
142
159
  on_empty: stop
143
160
  on_empty_message: "No active feature. Run /appweaver select-feature first (pick or create the aggregate, e.g. Order), then re-run implement-ui."
144
161
 
162
+ # 1b. THE LEDGER FENCE — the same one implement carries, for the same two readings:
163
+ # the staleness computation below and the acceptance pass at the end.
164
+ - id: spec_state_guard
165
+ action: include
166
+ module: .appweaver/orchestrator/modules/spec-state-gate.orch.yaml
167
+ with: { command: "/appweaver implement-ui" }
168
+
145
169
  # 2. WHICH STORY — the same resolution story-plan and implement use.
146
170
  - id: story_scan
147
171
  action: execute_script
@@ -173,6 +197,11 @@ workflow:
173
197
  - { name: design_pass, from: "$.fe_last_pass" }
174
198
  - { name: design_spec_version, from: "$.fe_doc_spec_version" }
175
199
  - { name: design_stale, from: "$.fe_doc_stale" }
200
+ - { name: pass_plan_hash, from: "$.fe_pass_plan_hash" }
201
+ - { name: designed_lanes, from: "$.designed_lanes_csv" }
202
+ - { name: dropped_lanes, from: "$.dropped_lanes_csv" }
203
+ - { name: lane_keys_present, from: "$.lane_keys_present" }
204
+ - { name: fe_lane_dropped, from: "$.fe_lane_dropped" }
176
205
  - { name: service_design_doc, from: "$.be_doc_path" }
177
206
  - id: require_open_backlog
178
207
  action: verify_context
@@ -212,6 +241,11 @@ workflow:
212
241
  - { name: design_pass, from: "$.fe_last_pass" }
213
242
  - { name: design_spec_version, from: "$.fe_doc_spec_version" }
214
243
  - { name: design_stale, from: "$.fe_doc_stale" }
244
+ - { name: pass_plan_hash, from: "$.fe_pass_plan_hash" }
245
+ - { name: designed_lanes, from: "$.designed_lanes_csv" }
246
+ - { name: dropped_lanes, from: "$.dropped_lanes_csv" }
247
+ - { name: lane_keys_present, from: "$.lane_keys_present" }
248
+ - { name: fe_lane_dropped, from: "$.fe_lane_dropped" }
215
249
  - { name: service_design_doc, from: "$.be_doc_path" }
216
250
  - id: require_story
217
251
  action: verify_context
@@ -230,11 +264,34 @@ workflow:
230
264
  # 3. THE STORY MUST HAVE A UI DESIGN. A story with a service design and no UI one is
231
265
  # not a failure — it is a slice with no user-facing surface, and the message says
232
266
  # so rather than implying something is broken.
267
+ # WHICH OF THE TWO READINGS IT IS, where the story says. analyze-design STOPS on a lane
268
+ # whose plan is behind; story-plan DROPS it and designs the other — so one command later
269
+ # "this slice has no user-facing surface" and "the UI lane was out of scope when this was
270
+ # designed" are the same absence on disk, and the message below could only offer both.
271
+ # story-plan now stamps `designed_lanes:` and `dropped_lanes:`, so the answer is recorded.
272
+ # ORDERED SETS, most specific last. The default is the two-reading message, which stays
273
+ # exactly right for a story designed BEFORE those keys existed: it has neither, and a
274
+ # missing key must never be read as "no user-facing surface".
275
+ - id: no_design_msg_default
276
+ action: set
277
+ vars:
278
+ no_design_msg: "Story {{story}} has no UI design — {{story_dir}}/story-frontend-plan.md does not exist, and the story records no designed/dropped lanes (it was designed before the kit stamped them). Either this slice has no user-facing surface (nothing to do here; run /appweaver implement for the service half), or the UI lane was out of scope when it was designed — a frontend lane that is undeclared, or a frontend-plan.md that is missing or behind the spec, drops that lane from a story-plan run and says so. Fix whichever it was and re-run /appweaver story-plan {{story}}."
279
+ - id: no_design_msg_no_surface
280
+ condition: "{{lane_keys_present}} == true"
281
+ action: set
282
+ vars:
283
+ no_design_msg: "Story {{story}} has no UI design and its UI lane was NOT dropped — the story records designed lanes [{{designed_lanes}}] and dropped lanes [{{dropped_lanes}}], so this slice genuinely has no user-facing surface. There is nothing for implement-ui to do here; run /appweaver implement for the service half. If it SHOULD have a UI surface, declare the frontend lane and re-run /appweaver story-plan {{story}}."
284
+ - id: no_design_msg_dropped
285
+ condition: "{{fe_lane_dropped}} == true"
286
+ action: set
287
+ vars:
288
+ no_design_msg: "Story {{story}} has no UI design because the UI LANE WAS DROPPED when it was designed — the story records dropped lanes [{{dropped_lanes}}]. This is not a slice without a user-facing surface; it is one whose UI half was never designed, because the frontend lane was undeclared or frontend-plan.md was missing or behind the spec. Fix that lane (/appweaver plan-ui, or declare it with /appweaver select-stack), then re-run /appweaver story-plan {{story}} and this command."
289
+
233
290
  - id: require_design
234
291
  action: verify_context
235
292
  require_true: design_exists
236
293
  on_false: stop
237
- on_false_message: "Story {{story}} has no UI design — {{story_dir}}/story-frontend-plan.md does not exist. Either this slice has no user-facing surface (nothing to do here; run /appweaver implement for the service half), or the UI lane was out of scope when it was designed — a frontend lane that is undeclared, or a frontend-plan.md that is missing or behind the spec, drops that lane from a story-plan run and says so. Fix whichever it was and re-run /appweaver story-plan {{story}}."
294
+ on_false_message: "{{no_design_msg}}"
238
295
 
239
296
  # 3a. Announced, never refused — the same contract implement keeps. design_stale is a
240
297
  # JSON boolean, so `condition:` does the deciding and the value gate is only the
@@ -253,6 +310,62 @@ workflow:
253
310
  on_mismatch: continue
254
311
  on_mismatch_message: "The spec has moved on ids THIS story carries: {{stale_csv}}. Whatever this run generates for them realises superseded wording. Consider /appweaver sync-stories to bring the backlog back in step, then /appweaver story-plan {{story}}, before building."
255
312
 
313
+ # ═══════ 3a-bis. LAW 40, THE LAST HOP — the plan this pass was designed against ═══════
314
+ # THREE HASHES, not two. story-plan proved the feature plan was certified in the shape it
315
+ # was in when this pass was designed, and story-plan-doc stamped that certified hash into
316
+ # the pass header. But a pass is written once and read forever: comparing only the CURRENT
317
+ # plan to the CURRENT verdict would let a pass designed against plan H1 still generate
318
+ # after the plan was edited to H2 and re-gated — which is precisely the case law 40 names,
319
+ # one level below where it was being checked.
320
+ # pass hash what this pass was designed against (the story pass header)
321
+ # verdict hash what analyze-design last certified (the gate verdict)
322
+ # A MISMATCH IS A STOP, at the same severity as never-gated: continuing ships code built
323
+ # to an architecture nobody approved. Per lane, and only for the lane this command builds.
324
+ - id: gate_verdict
325
+ action: execute_script
326
+ on_error: continue
327
+ script: { ref: actions }
328
+ subcommand: gate-read
329
+ args: ["--feature-dir", "{{feature_dir}}", "--gate", "analyze-design"]
330
+ outputs:
331
+ - { name: gate_status, from: "$.status" }
332
+ - { name: gate_certified, from: "$.certified" }
333
+ - { name: gate_aborted, from: "$.aborted" }
334
+ - { name: gate_gap_refs, from: "$.gap_refs_csv" }
335
+ - { name: gate_report, from: "$.report" }
336
+ - { name: gate_lane_sha, from: "$.frontend_sha256" }
337
+
338
+ # Three reasons, three routes; ordered sets, last match wins. Aborted is last because it
339
+ # also carries status "fail" — the red gate_result_open writes BEFORE any analysis — and
340
+ # sending someone to read gaps that were never found is the one wrong answer here.
341
+ - id: gate_msg_never
342
+ action: set
343
+ vars: { gate_msg: "The feature plans for this feature have never been certified: there is no analyze-design verdict beside them. Generation builds from a story pass that sits under an unapproved architecture. Run /appweaver analyze-design, then /appweaver story-plan {{story}}, then re-run /appweaver implement-ui." }
344
+ - id: gate_msg_red
345
+ condition: "{{gate_status}} == fail"
346
+ action: set
347
+ vars: { gate_msg: "The feature plans do NOT conform to the spec: analyze-design's last verdict is FAIL on {{gate_gap_refs}} (report: {{gate_report}}). Close those gaps before generating code beneath them, then re-run /appweaver implement-ui." }
348
+ - id: gate_msg_aborted
349
+ condition: "{{gate_aborted}} == true"
350
+ action: set
351
+ vars: { gate_msg: "The last analyze-design run STARTED and never reached a verdict — the red on disk is the one it writes before any analysis, so nothing was found and there are no gaps to read. The plans are UNCERTIFIED, not rejected. Run /appweaver analyze-design to completion, then re-run /appweaver implement-ui." }
352
+
353
+ - id: gate_certified_guard
354
+ action: verify_context
355
+ require_true: gate_certified
356
+ on_false: stop
357
+ on_false_message: "{{gate_msg}}"
358
+
359
+ # An EMPTY pass hash fails this, and that is correct: a pass written before the hash was
360
+ # stamped cannot say which plan shape it was designed against, so nothing can prove it was
361
+ # designed against the approved one. The route is the same either way — re-design.
362
+ - id: pass_plan_shape_guard
363
+ action: verify_context
364
+ require_value: pass_plan_hash
365
+ equals: "{{gate_lane_sha}}"
366
+ on_mismatch: stop
367
+ on_mismatch_message: "Design pass {{design_pass}} for story {{story}} was designed against a frontend plan that is NOT the one analyze-design currently certifies — the plan has been edited and re-gated since, or this pass predates plan-hash stamping and cannot say what it was designed against. Generating from it would build to an architecture nobody approved for this shape of the plan. Re-run /appweaver story-plan {{story}} to design against the currently certified plan, then re-run /appweaver implement-ui."
368
+
256
369
  # 4. THE FRONTEND LANE and its source path — the only subtree this phase may write
257
370
  # into, resolved before the project gate that searches it.
258
371
  - id: lane_gate
@@ -353,9 +466,12 @@ workflow:
353
466
  create and what to modify. Create ONLY the files it names as new; modify the
354
467
  named existing files IN PLACE. Every other file belongs to work already done —
355
468
  do not regenerate, reformat or "tidy" it.
356
- - The story delivers {{exercises_csv}} and must satisfy {{constraints_csv}}, of
357
- which the UX- constraints are yours to realise. Anything outside those ids is
358
- another story's work, even if you can see it is missing.
469
+ - The story delivers {{exercises_csv}} and must satisfy {{constraints_csv}} — the
470
+ complete set of bounds the spec declares for this aggregate, stamped into the
471
+ story's frontmatter by the kit rather than transcribed by anyone. ALL of them
472
+ apply here: the UX- are yours to realise, and the CN-/DC-/IC- still bind what this
473
+ surface may show, send and store. Anything outside those ids is another story's
474
+ work, even if you can see it is missing.
359
475
  - The code that already exists for this feature, per lane, is below. The UI files
360
476
  ({{ui_source_file_count}} on disk) are yours to read before you edit; the
361
477
  service list is there so you can see what the contract is actually backed by.
@@ -444,9 +560,16 @@ workflow:
444
560
  action: execute_script
445
561
  script: { ref: xref }
446
562
  subcommand: concept-upsert
447
- args: ["--feature-dir", "{{feature_dir}}", "--source-files", "{{files}}", "--merge", "--lane", "frontend", "--origin", "greenfield"]
563
+ # --ids + --slice record WHICH REQUIREMENT this run's files realise, as Requirement
564
+ # Anchors rows in the concept. Both halves were already here and thrown away: the ids
565
+ # the story exercises, and the files just generated. Only `adopt` writes a
566
+ # source-map.md, so without this every id on a greenfield feature came back unanchored
567
+ # at analyze-slice and the trace pass had to read the whole tree to find out what
568
+ # implements what. Reconcile-on-write, keyed by slice + lane.
569
+ args: ["--feature-dir", "{{feature_dir}}", "--source-files", "{{files}}", "--merge", "--lane", "frontend", "--origin", "greenfield", "--ids", "{{exercises_csv}}", "--slice", "{{story}}"]
448
570
  outputs:
449
- - { name: okf_concept, from: "$.concept" }
571
+ - { name: okf_concept, from: "$.concept" }
572
+ - { name: okf_anchors, from: "$.anchors_written" }
450
573
 
451
574
  # ═══════════════ ACCEPTANCE — the ONE place the spec is read ═════════════════
452
575
  # READ-ONLY and REPORT-ONLY: it never edits code, never edits the pass, and never
@@ -512,7 +635,7 @@ workflow:
512
635
  Nothing was changed to fix this: the code follows UI design pass {{design_pass}}, so a gap
513
636
  here means either the design was wrong or the spec has moved. Route by which it is —
514
637
  the design is wrong: /appweaver story-plan {{story}} (a new pass supersedes this one);
515
- the spec is wrong: /appweaver sync-spec, then sync-stories, then story-plan.
638
+ the spec is wrong: /appweaver sync-spec (⌛ not yet built — until it is, fold the change back by hand under .appweaver/SPEC-EDIT-PROTOCOL.md), then sync-stories, then story-plan.
516
639
  Run /appweaver build either way — a design that is right and code that does not compile
517
640
  is a third thing, and build is where that one surfaces.
518
641
 
@@ -537,4 +660,4 @@ outputs:
537
660
  okf_concept: "{{okf_concept}} (this run's files MERGED into ui_source_files; the service lane's source_files is untouched)"
538
661
  acceptance: "ok={{accept_ok}} · gaps: {{accept_gaps}}"
539
662
  stale_ids: "{{stale_csv}}"
540
- next: "/appweaver build — it compiles every declared lane, so a break in either surfaces there. Then /appweaver analyze-slice. The story stays OPEN until /appweaver close-story ratifies its ADRs, flips its interface entries live and archives the passes."
663
+ next: "/appweaver build — it compiles every declared lane, so a break in either surfaces there. Then /appweaver analyze-slice. The story stays OPEN: /appweaver close-story — which would ratify its ADRs, flip its interface entries live and archive the passes — is ⌛ NOT BUILT YET, so there is no transition out of in-flight and no last gate before merge."
@@ -94,6 +94,12 @@ vars:
94
94
  story_title: ""
95
95
  exercises_csv: ""
96
96
  constraints_csv: ""
97
+ # UNASKED IS NOT UNMATCHED. detect-project answers true whenever no --ecosystem was
98
+ # given, and the gate below only reads it on the repo-scope fallback — so the default
99
+ # has to be permissive or a probe that could not run would refuse a scaffolded lane.
100
+ ecosystem_match: "true"
101
+ project_scope: ""
102
+ project_markers: ""
97
103
  current_version: ""
98
104
  stale_csv: ""
99
105
  stale_note: ""
@@ -106,6 +112,16 @@ vars:
106
112
  design_pass: "0"
107
113
  design_spec_version: ""
108
114
  design_stale: "false"
115
+ # the analyze-design verdict — law 40's evidence. certified defaults FALSE so a probe that
116
+ # could not run fails CLOSED, and the message default carries a route for the same reason.
117
+ pass_plan_hash: ""
118
+ gate_status: ""
119
+ gate_certified: "false"
120
+ gate_aborted: "false"
121
+ gate_gap_refs: ""
122
+ gate_report: ""
123
+ gate_lane_sha: ""
124
+ gate_msg: "The analyze-design verdict could not be read, so this run cannot tell whether the feature plans were ever certified. Run /appweaver analyze-design, then re-run."
109
125
  # the feature's current code, per lane (OKF)
110
126
  okf_concept: ""
111
127
  coverage_digest: ""
@@ -149,6 +165,15 @@ workflow:
149
165
  on_empty: stop
150
166
  on_empty_message: "No active feature. Run /appweaver select-feature first (pick or create the aggregate, e.g. Order), then re-run implement."
151
167
 
168
+ # 1b. THE LEDGER FENCE. Generation is design-only by contract, but this run reads the
169
+ # spec twice all the same: stale_csv below is computed against the ledger, and the
170
+ # acceptance pass at the end reads spec.md as the wording the code is judged
171
+ # against. Both are worthless if the version does not name the contents.
172
+ - id: spec_state_guard
173
+ action: include
174
+ module: .appweaver/orchestrator/modules/spec-state-gate.orch.yaml
175
+ with: { command: "/appweaver implement" }
176
+
152
177
  # 2. WHICH STORY. Same resolution story-plan uses: an explicit id in the input wins,
153
178
  # anything else (nothing named, an unknown id, several) becomes a pick from the open
154
179
  # backlog. One scan returns every fact the gates below read.
@@ -182,6 +207,7 @@ workflow:
182
207
  - { name: design_pass, from: "$.be_last_pass" }
183
208
  - { name: design_spec_version, from: "$.be_doc_spec_version" }
184
209
  - { name: design_stale, from: "$.be_doc_stale" }
210
+ - { name: pass_plan_hash, from: "$.be_pass_plan_hash" }
185
211
  - id: require_open_backlog
186
212
  action: verify_context
187
213
  require_true: any_open
@@ -220,6 +246,7 @@ workflow:
220
246
  - { name: design_pass, from: "$.be_last_pass" }
221
247
  - { name: design_spec_version, from: "$.be_doc_spec_version" }
222
248
  - { name: design_stale, from: "$.be_doc_stale" }
249
+ - { name: pass_plan_hash, from: "$.be_pass_plan_hash" }
223
250
  - id: require_story
224
251
  action: verify_context
225
252
  require_true: resolved
@@ -264,6 +291,62 @@ workflow:
264
291
  on_mismatch: continue
265
292
  on_mismatch_message: "The spec has moved on ids THIS story carries: {{stale_csv}}. Whatever this run generates for them realises superseded wording. Consider /appweaver sync-stories to bring the backlog back in step, then /appweaver story-plan {{story}}, before building."
266
293
 
294
+ # ═══════ 3a-bis. LAW 40, THE LAST HOP — the plan this pass was designed against ═══════
295
+ # THREE HASHES, not two. story-plan proved the feature plan was certified in the shape it
296
+ # was in when this pass was designed, and story-plan-doc stamped that certified hash into
297
+ # the pass header. But a pass is written once and read forever: comparing only the CURRENT
298
+ # plan to the CURRENT verdict would let a pass designed against plan H1 still generate
299
+ # after the plan was edited to H2 and re-gated — which is precisely the case law 40 names,
300
+ # one level below where it was being checked.
301
+ # pass hash what this pass was designed against (the story pass header)
302
+ # verdict hash what analyze-design last certified (the gate verdict)
303
+ # A MISMATCH IS A STOP, at the same severity as never-gated: continuing ships code built
304
+ # to an architecture nobody approved. Per lane, and only for the lane this command builds.
305
+ - id: gate_verdict
306
+ action: execute_script
307
+ on_error: continue
308
+ script: { ref: actions }
309
+ subcommand: gate-read
310
+ args: ["--feature-dir", "{{feature_dir}}", "--gate", "analyze-design"]
311
+ outputs:
312
+ - { name: gate_status, from: "$.status" }
313
+ - { name: gate_certified, from: "$.certified" }
314
+ - { name: gate_aborted, from: "$.aborted" }
315
+ - { name: gate_gap_refs, from: "$.gap_refs_csv" }
316
+ - { name: gate_report, from: "$.report" }
317
+ - { name: gate_lane_sha, from: "$.backend_sha256" }
318
+
319
+ # Three reasons, three routes; ordered sets, last match wins. Aborted is last because it
320
+ # also carries status "fail" — the red gate_result_open writes BEFORE any analysis — and
321
+ # sending someone to read gaps that were never found is the one wrong answer here.
322
+ - id: gate_msg_never
323
+ action: set
324
+ vars: { gate_msg: "The feature plans for this feature have never been certified: there is no analyze-design verdict beside them. Generation builds from a story pass that sits under an unapproved architecture. Run /appweaver analyze-design, then /appweaver story-plan {{story}}, then re-run /appweaver implement." }
325
+ - id: gate_msg_red
326
+ condition: "{{gate_status}} == fail"
327
+ action: set
328
+ vars: { gate_msg: "The feature plans do NOT conform to the spec: analyze-design's last verdict is FAIL on {{gate_gap_refs}} (report: {{gate_report}}). Close those gaps before generating code beneath them, then re-run /appweaver implement." }
329
+ - id: gate_msg_aborted
330
+ condition: "{{gate_aborted}} == true"
331
+ action: set
332
+ vars: { gate_msg: "The last analyze-design run STARTED and never reached a verdict — the red on disk is the one it writes before any analysis, so nothing was found and there are no gaps to read. The plans are UNCERTIFIED, not rejected. Run /appweaver analyze-design to completion, then re-run /appweaver implement." }
333
+
334
+ - id: gate_certified_guard
335
+ action: verify_context
336
+ require_true: gate_certified
337
+ on_false: stop
338
+ on_false_message: "{{gate_msg}}"
339
+
340
+ # An EMPTY pass hash fails this, and that is correct: a pass written before the hash was
341
+ # stamped cannot say which plan shape it was designed against, so nothing can prove it was
342
+ # designed against the approved one. The route is the same either way — re-design.
343
+ - id: pass_plan_shape_guard
344
+ action: verify_context
345
+ require_value: pass_plan_hash
346
+ equals: "{{gate_lane_sha}}"
347
+ on_mismatch: stop
348
+ on_mismatch_message: "Design pass {{design_pass}} for story {{story}} was designed against a backend plan that is NOT the one analyze-design currently certifies — the plan has been edited and re-gated since, or this pass predates plan-hash stamping and cannot say what it was designed against. Generating from it would build to an architecture nobody approved for this shape of the plan. Re-run /appweaver story-plan {{story}} to design against the currently certified plan, then re-run /appweaver implement."
349
+
267
350
  # 4. THE SERVICE LANE and its source path. This phase writes code, and the lane's root
268
351
  # is the only subtree it may write into — so this runs before the project gate,
269
352
  # which searches that subtree.
@@ -277,21 +360,46 @@ workflow:
277
360
  # searches the LANE'S OWN subtree for an ecosystem manifest, falling back to the
278
361
  # whole repo for the layout that keeps its manifest beside the lane root rather than
279
362
  # inside it. How the project got there is not asked.
363
+ # THE FALLBACK IS DELIBERATE AND IT NEEDED A DISCRIMINATOR. Searching the whole repo
364
+ # when the lane's own subtree has no manifest is right — a .NET .sln legitimately sits
365
+ # ABOVE the src/ it builds — but it answered with whatever manifest it found first, so
366
+ # in a two-lane repo where only the frontend is scaffolded, the SERVICE lane's gate
367
+ # passed on the frontend's package.json. Which is exactly the failure implement-ui's
368
+ # `project_scope == lane` rule was written to prevent, one lane over.
369
+ # `--ecosystem` is the fix, and NOT implement-ui's rule: this passes the value
370
+ # lane-gate has already bound from stacks.json (that include runs above), so the repo
371
+ # -scope hit is accepted only when the manifest is one this lane's ecosystem knows.
372
+ # Nothing here re-derives it — a second guess is a second thing to keep in step.
280
373
  - id: detect_foundation
281
374
  action: execute_script
282
375
  script: { ref: actions }
283
376
  subcommand: detect-project
284
- args: ["--root", "{{lane_source_path}}"]
377
+ args: ["--root", "{{lane_source_path}}", "--ecosystem", "{{lane_ecosystem}}"]
285
378
  outputs:
286
379
  - { name: foundation_present, from: "$.scaffolded" }
287
380
  - { name: project_markers, from: "$.markers" }
288
381
  - { name: project_scope, from: "$.scope" }
382
+ - { name: ecosystem_match, from: "$.ecosystem_match" }
289
383
  - id: foundation_gate
290
384
  action: verify_context
291
385
  require_true: foundation_present
292
386
  on_false: stop
293
387
  on_false_message: "No project found under the {{lane_id}} lane's source path `{{lane_source_path}}`, nor anywhere else in this repository. Implement GENERATES this story's code into an existing project tree, so one has to be scaffolded first: run /appweaver foundation <FoundationName> <AppName> where a foundation bundle ships for this stack, or create the project with the stack's own tooling under that path. Any ecosystem manifest marks it — .sln/.csproj, pom.xml, build.gradle(.kts), package.json, go.mod, Cargo.toml or pyproject.toml."
294
388
 
389
+ # 5b. THE GATE PASSED ON SOMEBODY ELSE'S PROJECT. A repo-scope hit whose manifests are
390
+ # none of this lane's ecosystem means the search fell back past the lane root and
391
+ # found the OTHER lane's project. STOPS rather than warns: the next thing this run
392
+ # does is generate {{lane_language}} code, and it has just been told there is nowhere
393
+ # of that kind to put it — carrying on writes a service tree inside a frontend, or
394
+ # into the repo root. Scoped to the fallback, so the deliberate manifest-above-the-lane
395
+ # layout (a .sln over src/) still passes: that hit IS this lane's ecosystem.
396
+ - id: foundation_ecosystem_gate
397
+ condition: "{{project_scope}} == repo"
398
+ action: verify_context
399
+ require_true: ecosystem_match
400
+ on_false: stop
401
+ on_false_message: "The {{lane_id}} lane's source path `{{lane_source_path}}` holds no project, and the repo-wide fallback found only manifests that are not {{lane_ecosystem}}: {{project_markers}}. That is the OTHER lane's project — this gate passing on it would send {{lane_language}} code into a tree built for something else. Scaffold this lane: /appweaver foundation <FoundationName> <AppName> where a bundle ships for {{lane_profile}}, or create the project with the stack's own tooling under `{{lane_source_path}}`. (A manifest deliberately kept ABOVE the lane root — a .sln over src/ — is accepted, because it is this lane's ecosystem.)"
402
+
295
403
  # 6. WHAT ALREADY EXISTS, from the feature's OKF concept, split by lane. This is what
296
404
  # makes the run incremental: the pass names the files it changes, and this names the
297
405
  # files that are there to change. Advisory — the first story of a greenfield feature
@@ -363,8 +471,12 @@ workflow:
363
471
  to modify. Create ONLY the files it names as new; modify the named existing
364
472
  files IN PLACE. Every other file in this feature belongs to work that is
365
473
  already done — do not regenerate, reformat or "tidy" it.
366
- - The story delivers {{exercises_csv}} and must satisfy {{constraints_csv}}.
367
- Anything outside those ids is another story's work, even if you can see it is
474
+ - The story delivers {{exercises_csv}} and must satisfy {{constraints_csv}} — the
475
+ complete set of bounds the spec declares for this aggregate, stamped into the
476
+ story's frontmatter by the kit rather than transcribed by anyone. Satisfy the
477
+ CN-, DC- and IC- among them. IGNORE the UX-: those are what the UI lane realises,
478
+ and a service-side obligation is stated as a CN- by design. Anything outside those
479
+ ids is another story's work, even if you can see it is
368
480
  missing. If the slice genuinely cannot be built without touching shared
369
481
  structure, make the smallest change that works and SAY SO in your summary.
370
482
  - The code that already exists for this feature, per lane, is below. The service
@@ -445,9 +557,16 @@ workflow:
445
557
  action: execute_script
446
558
  script: { ref: xref }
447
559
  subcommand: concept-upsert
448
- args: ["--feature-dir", "{{feature_dir}}", "--source-files", "{{files}}", "--merge", "--lane", "backend", "--origin", "greenfield"]
560
+ # --ids + --slice record WHICH REQUIREMENT this run's files realise, as Requirement
561
+ # Anchors rows in the concept. Both halves were already here and thrown away: the ids
562
+ # the story exercises, and the files just generated. Only `adopt` writes a
563
+ # source-map.md, so without this every id on a greenfield feature came back unanchored
564
+ # at analyze-slice and the trace pass had to read the whole tree to find out what
565
+ # implements what. Reconcile-on-write, keyed by slice + lane.
566
+ args: ["--feature-dir", "{{feature_dir}}", "--source-files", "{{files}}", "--merge", "--lane", "backend", "--origin", "greenfield", "--ids", "{{exercises_csv}}", "--slice", "{{story}}"]
449
567
  outputs:
450
- - { name: okf_concept, from: "$.concept" }
568
+ - { name: okf_concept, from: "$.concept" }
569
+ - { name: okf_anchors, from: "$.anchors_written" }
451
570
 
452
571
  # 8a. Cross-slice edges are a FEATURE property, not a story's: which sibling
453
572
  # aggregates this one references is stated by the feature plan's domain model,
@@ -568,7 +687,7 @@ workflow:
568
687
  Nothing was changed to fix this: the code follows design pass {{design_pass}}, so a gap
569
688
  here means either the design was wrong or the spec has moved. Route by which it is —
570
689
  the design is wrong: /appweaver story-plan {{story}} (a new pass supersedes this one);
571
- the spec is wrong: /appweaver sync-spec, then sync-stories, then story-plan.
690
+ the spec is wrong: /appweaver sync-spec (⌛ not yet built — until it is, fold the change back by hand under .appweaver/SPEC-EDIT-PROTOCOL.md), then sync-stories, then story-plan.
572
691
  Run /appweaver build either way — a design that is right and code that does not compile
573
692
  is a third thing, and build is where that one surfaces.
574
693
 
@@ -593,4 +712,4 @@ outputs:
593
712
  okf_concept: "{{okf_concept}} (this run's files MERGED into source_files; {{xref_edge_count}} cross-slice edge(s) recorded into sibling aggregates)"
594
713
  acceptance: "ok={{accept_ok}} · gaps: {{accept_gaps}}"
595
714
  stale_ids: "{{stale_csv}}"
596
- next: "/appweaver build — it compiles every declared lane. Then /appweaver analyze-slice (spec ↔ code, through the design), and /appweaver implement-ui if this story has a UI design. The story stays OPEN until /appweaver close-story ratifies its ADRs, flips its interface entries live and archives the passes."
715
+ next: "/appweaver build — it compiles every declared lane. Then /appweaver analyze-slice (spec ↔ code, through the design), and /appweaver implement-ui if this story has a UI design. The story stays OPEN: /appweaver close-story — which would ratify its ADRs, flip its interface entries live and archive the passes — is ⌛ NOT BUILT YET, so there is no transition out of in-flight and no last gate before merge."
@@ -30,15 +30,17 @@
30
30
  # stamped at the same moment never contends with it. Hand edits are refused unless accept_manual:true
31
31
  # folds them in. The model writes NO version anywhere.
32
32
  #
33
- # WHERE IT SITS. specify/sync-spec move the spec; the developer then runs plan (the
33
+ # WHERE IT SITS. specify/sync-spec (⌛ sync-spec is not built yet; the hand route is
34
+ # .appweaver/SPEC-EDIT-PROTOCOL.md) move the spec; the developer then runs plan (the
34
35
  # service lane) and plan-ui, which conform each lane's design to what moved. FE story
35
36
  # designs READ this document for the ids they carry and NEVER write back — nothing
36
37
  # merges into it, so it keeps exactly one writer. It is the FEATURE-LEVEL
37
38
  # ARCHITECTURAL CONFIRMATION for the experience, with the concrete component-level
38
39
  # work left to the per-story passes.
39
40
  #
40
- # IT READS THE CODE. concept-paths resolves the feature's OKF concept to the files
41
- # realising its UI today and the authoring steps declare `tools: Read`, so a plan over
41
+ # IT READS THE CODE — ITS OWN LANE'S. concept-coverage resolves the feature's OKF concept
42
+ # to the `ui_source_files` realising its UI today (the service lane's `source_files` is a
43
+ # different key and a different tree) and the authoring steps declare `tools: Read`, so a plan over
42
44
  # an existing frontend ADJUSTS to the composition, naming and state handling actually
43
45
  # there. The code is the what-exists authority; the spec stays the only authority on
44
46
  # required behaviour. source_count 0 = greenfield, and the phase never blocks on it.
@@ -90,7 +92,7 @@ standards:
90
92
  - { path: .appweaver/ai-rules.md } # kit-resident: the universal operating rules, every flow
91
93
 
92
94
  vars:
93
- source_csv: "" # concept-paths — the files realising this feature's UI today
95
+ source_csv: "" # concept-coverage — the UI files realising this feature today
94
96
  source_count: "0" # 0 ⇒ greenfield: no frontend code for this feature yet
95
97
  okf_concept: "" # the feature's OKF concept doc
96
98
  ui_plan_mode: "bootstrap_gf" # resolved below → bootstrap_gf | bootstrap_bf | update
@@ -167,19 +169,10 @@ workflow:
167
169
 
168
170
  # 1b. The spec's own ledger tripwire — a plan derived from an unledgered spec
169
171
  # stamps a baseline the ledger cannot name (same fence as plan's).
170
- - id: spec_guard_read
171
- action: execute_script
172
- script: { ref: actions }
173
- subcommand: spec-check
174
- args: ["--feature-dir", "{{feature_dir}}"]
175
- outputs:
176
- - { name: spec_ok, from: "$.ok" }
177
- - { name: spec_state, from: "$.state" }
178
172
  - id: spec_guard
179
- action: verify_context
180
- require_true: spec_ok
181
- on_false: stop
182
- on_false_message: "spec.md has moved outside the ledger (state: {{spec_state}}). Record what changed in a Change History row (or fold it back through /appweaver sync-spec), re-run spec-stamp to re-baseline, then re-run plan-ui."
173
+ action: include
174
+ module: .appweaver/orchestrator/modules/spec-state-gate.orch.yaml
175
+ with: { command: "/appweaver plan-ui" }
183
176
 
184
177
  # 2. Scaffold frontend-plan.md (idempotent). --lane frontend resolves the pair
185
178
  # ui-design-template → frontend-plan.md, and MIGRATES a legacy design.ui.md by
@@ -245,24 +238,28 @@ workflow:
245
238
  action: verify_context
246
239
  require_true: plan_stale
247
240
  on_false: stop
248
- on_false_message: "frontend-plan.md is in step with spec {{current_version}} — nothing to update."
241
+ on_false_message: "frontend-plan.md is in step with spec {{current_version}} — nothing to update. If the PLAN is wrong and the spec is right, hand-edit frontend-plan.md and re-run /appweaver plan-ui with accept_manual: true — that is what tells this command to keep the edit's intent and reconcile it rather than treat it as drift; without the flag the manual fence stops the run after the edit is already on disk."
249
242
 
250
243
  # 5. Pre-execution hooks.
251
- # THE CODE. The UI plan is an architectural confirmation too, so it must know what
252
- # this feature's experience already looks like on disk: concept-paths resolves the
253
- # feature's OKF concept to the files realising it, and the authoring steps declare
254
- # `tools: Read`. A PATH LIST, not inlined content (E60). on_error: continue — a
244
+ # THE CODE — THIS LANE'S. The UI plan is an architectural confirmation too, so it must
245
+ # know what this feature's experience already looks like on disk. concept-COVERAGE, not
246
+ # concept-paths: a feature has ONE OKF concept holding both lanes' file lists under
247
+ # different keys, and concept-paths returns the SERVICE lane's `source_files`. Reading it
248
+ # here planned the frontend against the backend's tree — a feature with a built frontend
249
+ # and no backend was planned as greenfield, and one with both handed the UI author the
250
+ # service lane's files to adjust to. concept-coverage splits the two and this binds
251
+ # `ui_source_files`. A PATH LIST, not inlined content (E60). on_error: continue — a
255
252
  # greenfield feature has no concept doc and must never be blocked by one;
256
253
  # source_count 0 is the greenfield signal the tasks read.
257
254
  - id: concept_paths
258
255
  action: execute_script
259
256
  on_error: continue
260
257
  script: { ref: xref }
261
- subcommand: concept-paths
258
+ subcommand: concept-coverage
262
259
  args: ["--feature-dir", "{{feature_dir}}"]
263
260
  outputs:
264
- - { name: source_csv, from: "$.csv" }
265
- - { name: source_count, from: "$.count" }
261
+ - { name: source_csv, from: "$.frontend_csv" }
262
+ - { name: source_count, from: "$.frontend_count" }
266
263
  - { name: okf_concept, from: "$.concept" }
267
264
 
268
265
  - id: resolve_pre_hooks
@@ -515,6 +512,12 @@ workflow:
515
512
  moved is re-realised from the spec's CURRENT text — the spec is the wording.
516
513
  A UX- is THIS document's to realise: a moved UX- with no home here after the
517
514
  update is a DROPPED requirement.
515
+ - AN ID THE SPEC HAS NEWLY DEFINED GAINS ONE. An id in the work list that NO
516
+ existing section realises is an ADDITION: give it a home in the sections that
517
+ realise it and a traceability row like any other. The scope rule above says
518
+ which sections may be touched; it does not say a new requirement may not be
519
+ designed, and reading it that way would let every added UX- or FR- with a
520
+ user-facing surface select nothing to rework and land unrealised.
518
521
  - THE CODE IS THE WHAT-EXISTS AUTHORITY, and it is a DIFFERENT authority from the
519
522
  spec. {{source_count}} file(s) realise this feature's experience today; re-read the
520
523
  ones the moved ids touch (code state is read FRESH every run, never assumed) and ADJUST this plan to the composition,
@@ -594,4 +597,4 @@ outputs:
594
597
  lane_root: "{{lane_root}}"
595
598
  derived_from_spec: "{{current_version}}"
596
599
  conformed_ids: "{{changed_csv}}"
597
- next: "frontend-plan.md written for the {{lane_id}} lane at {{ui_plan_doc}} (mode: {{ui_plan_mode}}). Gate this write: /appweaver analyze-design — one command over BOTH lanes, which is what proves no UX- fell between them; it reads §7 Traceability as the two-way spine. CHECK §5 CONTRACT BINDING: it binds to interfaces.md, which /appweaver plan publishes — if that log was absent this run, §5 was bound from the spec's IC- ids alone and a DA-n says so, which means running plan first and re-running this command is what turns an assumption into the real contract. Contract deltas needed goes back to the service lane, never into the client. source_files_read {{source_count}} is how many existing UI files this plan was written against; 0 means greenfield. Then /appweaver implement-ui. NOTE while the pipeline is mid-rename: migrated_legacy_design=true means design.ui.md is now frontend-plan.md, and stages not yet rebuilt for the rename (implement-ui, the delta chain) still look for design.ui.md — rebuild or point them at frontend-plan.md before relying on them."
600
+ next: "frontend-plan.md written for the {{lane_id}} lane at {{ui_plan_doc}} (mode: {{ui_plan_mode}}). Gate this write: /appweaver analyze-design — one command over BOTH lanes, which is what proves no UX- fell between them; it reads §7 Traceability as the two-way spine. CHECK §5 CONTRACT BINDING: it binds to interfaces.md, which /appweaver plan publishes — if that log was absent this run, §5 was bound from the spec's IC- ids alone and a DA-n says so, which means running plan first and re-running this command is what turns an assumption into the real contract. Contract deltas needed goes back to the service lane, never into the client. source_files_read {{source_count}} is how many existing UI files this plan was written against; 0 means greenfield. Then /appweaver implement-ui."