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.
@@ -12,7 +12,8 @@
12
12
  # extract). See .appweaver/DESIGN-FORMAT.md + .appweaver/templates/design-template.md.
13
13
  # (`plan` re-uses the name of a long-retired phase whose plan.md had zero consumers.)
14
14
  #
15
- # WHERE IT SITS. specify/sync-spec move the spec; the developer then runs plan, which
15
+ # WHERE IT SITS. specify/sync-spec (⌛ sync-spec is not built yet; the hand route is
16
+ # .appweaver/SPEC-EDIT-PROTOCOL.md) move the spec; the developer then runs plan, which
16
17
  # conforms this feature's design to what moved. Story designs (/appweaver story-plan)
17
18
  # READ this document for the ids they carry and NEVER write back — nothing merges into
18
19
  # it, so it keeps exactly one writer and cannot be silently rewritten. It is the
@@ -198,19 +199,10 @@ workflow:
198
199
  # conformance theatre: an edit that skipped the Change History has no version, so
199
200
  # the derivation stamp this run writes would claim a baseline the ledger cannot
200
201
  # name. spec-check's only ok:false is exactly that case.
201
- - id: spec_guard_read
202
- action: execute_script
203
- script: { ref: actions }
204
- subcommand: spec-check
205
- args: ["--feature-dir", "{{feature_dir}}"]
206
- outputs:
207
- - { name: spec_ok, from: "$.ok" }
208
- - { name: spec_state, from: "$.state" }
209
202
  - id: spec_guard
210
- action: verify_context
211
- require_true: spec_ok
212
- on_false: stop
213
- 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 — a plan derived from an unledgered spec stamps a baseline the ledger cannot name."
203
+ action: include
204
+ module: .appweaver/orchestrator/modules/spec-state-gate.orch.yaml
205
+ with: { command: "/appweaver plan" }
214
206
 
215
207
  # 4. Scaffold backend-plan.md (idempotent). --lane backend resolves the pair
216
208
  # design-template → backend-plan.md, and MIGRATES a legacy design.md by renaming
@@ -284,7 +276,7 @@ workflow:
284
276
  action: verify_context
285
277
  require_true: plan_stale
286
278
  on_false: stop
287
- on_false_message: "backend-plan.md is in step with spec {{current_version}} — nothing to update."
279
+ on_false_message: "backend-plan.md is in step with spec {{current_version}} — nothing to update. If the PLAN is wrong and the spec is right, hand-edit backend-plan.md and re-run /appweaver plan 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."
288
280
 
289
281
  # 5b. THE CODE. The plan is an architectural confirmation, so it must know what the
290
282
  # feature already looks like on disk: concept-paths resolves the feature's OKF
@@ -549,6 +541,13 @@ workflow:
549
541
  - AN ID THE SPEC NO LONGER DEFINES LOSES ITS REALISATION: remove the elements
550
542
  that existed only to serve it (and their §6 rows). An id whose wording moved
551
543
  is re-realised from the spec's CURRENT text — the spec is the wording.
544
+ - AN ID THE SPEC HAS NEWLY DEFINED GAINS ONE. An id in the work list that NO
545
+ existing section realises is an ADDITION, not an error and not out of scope:
546
+ give it a home — the §1/§2/§3 elements that realise it, its §4.2 or §5 rows
547
+ where it needs them — and a §6 traceability row like any other. The scope rule
548
+ above says which sections may be touched; it does not say a new requirement may
549
+ not be designed, and reading it that way would let every added requirement
550
+ select nothing to rework and quietly land unrealised.
552
551
  - THE SPEC IS THE ONLY DOMAIN INPUT, same as at bootstrap: realise ids, invent
553
552
  nothing; an unavoidable new concept is a §7 DA-n.
554
553
  - THE CODE IS THE WHAT-EXISTS AUTHORITY, and it is a DIFFERENT authority from the
@@ -663,6 +662,40 @@ workflow:
663
662
  - { name: if_unknown_csv, from: "$.unknown_ids_csv" }
664
663
  - { name: if_badsup_csv, from: "$.bad_supersedes_csv" }
665
664
 
665
+ # 7c. THE TWO INTEGRITY SIGNALS log-append ALREADY RETURNS. Both were bound and neither
666
+ # was said out loud — the idiom every other integrity check in these workflows uses
667
+ # is a verify_context that continues, and these had none.
668
+ # unknown_ids_csv an entry's `serves:` names an id the spec does not define,
669
+ # so the BE→FE contract anchor points at nothing.
670
+ # bad_supersedes_csv an entry supersedes one the log does not carry, so what it
671
+ # claims to replace is not there — and readers take the LATEST
672
+ # entry per interface, which is now ambiguous.
673
+ # Announce-only: the entries are appended by the time this is visible, ids are
674
+ # permanent, and an append-only log is corrected by a later entry, never by a rewrite.
675
+ - id: if_unknown_notice
676
+ condition: "{{if_unknown_csv}} != ''"
677
+ action: verify_context
678
+ require_value: if_unknown_csv
679
+ equals: ""
680
+ on_mismatch: continue
681
+ on_mismatch_message: >
682
+ interfaces.md now carries entries whose `serves:` names ids the spec does not define:
683
+ {{if_unknown_csv}}. That is a broken trace anchor in the BE→FE contract — the frontend
684
+ binds to these entries by id, and the requirement they claim to serve is not in
685
+ {{spec_file}}. Either the id is a typo, or the requirement was never specified: fix it
686
+ with a superseding entry (never by editing the log), or take it through /appweaver specify.
687
+ - id: if_badsup_notice
688
+ condition: "{{if_badsup_csv}} != ''"
689
+ action: verify_context
690
+ require_value: if_badsup_csv
691
+ equals: ""
692
+ on_mismatch: continue
693
+ on_mismatch_message: >
694
+ An entry appended to interfaces.md supersedes an id the log does not carry:
695
+ {{if_badsup_csv}}. Nothing was replaced, so the interface it meant to retire is still
696
+ the latest entry for its surface and both now read as current. Append a corrected
697
+ superseding entry naming the real id.
698
+
666
699
  # 8. Post-execution hooks.
667
700
  - id: resolve_post_hooks
668
701
  action: execute_script
@@ -685,4 +718,4 @@ outputs:
685
718
  interfaces_published: "{{if_ids_csv}}"
686
719
  interfaces_error: "{{if_error}}"
687
720
  source_files_read: "{{source_count}}"
688
- next: "Gate this write: /appweaver analyze-design (spec ↔ plan conformance) — after EVERY bootstrap and EVERY update. Then the backlog and the stories: /appweaver sync-stories, then /appweaver story-plan <id>, which READS this plan and refuses to design against one the spec has moved past. {{fe_next}} READ THE FIELDS ABOVE FIRST: interfaces_published are the IF- ids this run appended to interfaces.md as status:proposed — cite them in the plan's API section on the next update, and never edit the log by hand; interfaces_error non-empty means the extraction was refused and .aw-if-new.tmp.md is still beside the plan for a hand-run of log-append, so THIS RUN PUBLISHED NOTHING; source_files_read 0 means no code was found for this feature, so the plan was written greenfield. NOTE while the pipeline is mid-rename: migrated_legacy_design=true means design.md is now backend-plan.md, and stages not yet rebuilt for the rename (implement, the delta chain) still look for design.md — rebuild or point them at backend-plan.md before relying on them."
721
+ next: "Gate this write: /appweaver analyze-design (spec ↔ plan conformance) — after EVERY bootstrap and EVERY update. Then the backlog and the stories: /appweaver sync-stories, then /appweaver story-plan <id>, which READS this plan and refuses to design against one the spec has moved past. {{fe_next}} READ THE FIELDS ABOVE FIRST: interfaces_published are the IF- ids this run appended to interfaces.md as status:proposed — cite them in the plan's API section on the next update, and never edit the log by hand; interfaces_error non-empty means the extraction was refused and .aw-if-new.tmp.md is still beside the plan for a hand-run of log-append, so THIS RUN PUBLISHED NOTHING; source_files_read 0 means no code was found for this feature, so the plan was written greenfield."
@@ -10,7 +10,8 @@
10
10
  # it wrote. It does not touch stories/. The spec is the CONTRACT; a story is an
11
11
  # execution SLICE over it, and the two directions between them each have their own
12
12
  # workflow — /appweaver sync-stories refreshes the backlog after the spec moves,
13
- # /appweaver update-spec folds a hand-edited story back into the contract. Keeping
13
+ # /appweaver sync-spec (⌛ not yet built — .appweaver/SPEC-EDIT-PROTOCOL.md is the hand
14
+ # route meanwhile) folds a hand-edited story back into the contract. Keeping
14
15
  # specify story-blind is what stops the spec->story rule having two implementations.
15
16
  #
16
17
  # Auto-detects first-vs-subsequent from spec.md:
@@ -392,6 +393,12 @@ workflow:
392
393
  # nothing. Fed to the author as a preference, then checked against the diff below —
393
394
  # the preference is a nudge, the check after the fact is the detector.
394
395
  # Empty until the backlog exists, which degrades to "no preference, no ask".
396
+ # CARRIED, NOT COVERED. `covered_csv` is the DELIVERY relation — the FR-/BR- ids open
397
+ # stories are delivering — and a story also CARRIES the bounds it is built to satisfy.
398
+ # Amending or removing a CN-/DC-/IC-/UX- moves ground somebody is standing on exactly
399
+ # as amending an FR- does, and reading the narrower set here meant it raised no ask at
400
+ # all and printed "nobody has open work" while stories were being built against it.
401
+ # `carried_csv` is exercises: ∪ the `constraints:` frontmatter story-stamp lifts.
395
402
  - id: ext_story_context
396
403
  condition: "{{needs_bootstrap}} == false"
397
404
  action: execute_script
@@ -401,18 +408,21 @@ workflow:
401
408
  args: ["--feature-dir", "{{feature_dir}}"]
402
409
  outputs:
403
410
  - { name: covered_csv, from: "$.covered_csv" }
411
+ - { name: carried_csv, from: "$.carried_csv" }
412
+ - { name: backlog_scanned, from: "$.ok" }
404
413
 
405
414
  # 6b-bis. WORD THE PREFERENCE FOR THE CASE AT HAND. The rule below is an argument for
406
415
  # preferring an ADDITION over an amendment, and it only makes sense while somebody is
407
- # actually building. Interpolating covered_csv straight into it left a spec with no
416
+ # actually building. Interpolating the carried set straight into it left a spec with no
408
417
  # backlog reading an eight-line case for adding requirements under a blank list — the
409
418
  # logic was vacuous ("one of those" names nothing) but the rhetoric was not, and the
410
419
  # first extend of a spec nobody has stories against is exactly where amending in place
411
- # is the honest thing to do. Two ordered sets rather than a compound condition, the
412
- # same idiom as readdit_verdict_na/_real: the no-backlog wording is the default, and
413
- # the preference replaces it only when there is open work to protect.
414
- # Safe on the bootstrap path too — ext_story_context is skipped there, so covered_csv
415
- # is empty and the second step cannot fire.
420
+ # is the honest thing to do. THREE ORDERED SETS rather than a compound condition, the
421
+ # same idiom as readdit_verdict_na/_real: the no-backlog wording is the default, the
422
+ # preference replaces it where there is open work to protect, and the scan-failed
423
+ # wording is last so it wins over both.
424
+ # Safe on the bootstrap path too — ext_story_context is skipped there, so nothing is
425
+ # bound and only the default can fire.
416
426
  - id: ext_covered_none
417
427
  action: set
418
428
  vars:
@@ -424,11 +434,13 @@ workflow:
424
434
  without inventing a companion requirement to avoid touching anything.
425
435
 
426
436
  - id: ext_covered_some
427
- condition: "{{covered_csv}} != ''"
437
+ condition: "{{carried_csv}} != ''"
428
438
  action: set
429
439
  vars:
430
440
  covered_note: >
431
- THESE REQUIREMENTS ARE BEING BUILT RIGHT NOW: {{covered_csv}}. Where the change can
441
+ THESE REQUIREMENTS AND BOUNDS ARE BEING BUILT AGAINST RIGHT NOW: {{carried_csv}}.
442
+ (FR-/BR- are what open stories DELIVER; CN-/DC-/IC-/UX- are what they are built to
443
+ MEET — moving either moves ground someone is standing on.) Where the change can
432
444
  be expressed EITHER as an amendment to one of those or as a new requirement
433
445
  alongside it, prefer the new requirement — an amendment moves ground someone is
434
446
  standing on, and a requirement that gains a companion leaves their work correct as
@@ -438,6 +450,21 @@ workflow:
438
450
  Requirements NOT in that list have no open work against them: amend those in place
439
451
  wherever the change genuinely belongs to them.
440
452
 
453
+ # THE SCAN ITSELF CAN FAIL, and ext_story_context carries `on_error: continue` because a
454
+ # spec must stay editable when its backlog cannot be read. What it must not do is answer
455
+ # as though the scan had run and found nothing: nothing binds, ext_covered_some cannot
456
+ # fire, and the unconditional default above then tells the author NOBODY HAS OPEN WORK —
457
+ # the strongest claim available, on no evidence at all.
458
+ - id: ext_covered_unknown
459
+ condition: "{{backlog_scanned}} != true"
460
+ action: set
461
+ vars:
462
+ covered_note: >
463
+ THE BACKLOG COULD NOT BE READ THIS RUN, so this run does not know who is building
464
+ what. Treat every requirement as possibly under way: prefer adding a requirement to
465
+ amending one, and where you must amend, say in the Change History row exactly what
466
+ moved so the next sync can find every story that carries it.
467
+
441
468
  - id: ext_ctx
442
469
  condition: "{{needs_bootstrap}} == false"
443
470
  action: build_context
@@ -755,6 +782,26 @@ workflow:
755
782
  - { name: disturbing_csv, from: "$.disturbing_csv" }
756
783
  - { name: reissued_csv, from: "$.reissued_csv" }
757
784
  - { name: change_count, from: "$.change_count" }
785
+ - { name: diff_warnings, from: "$.warnings_note" }
786
+
787
+ # 10a-bis. IDS THE LEDGER CANNOT KEY TO A DEFINITION. The diff keys an id to the line that
788
+ # DEFINES it and hashes the block under it; three authoring forms fall outside that,
789
+ # and each one is silent — the requirement moves, change_count stays 0, no row is
790
+ # appended, and the stamp below re-baselines the new body as clean. A standing
791
+ # property of the document rather than a verdict on this run, so it is reported every
792
+ # time until the requirement is rewritten in the shape the template uses.
793
+ - id: diff_warning_notice
794
+ condition: "{{diff_warnings}} != ''"
795
+ action: verify_context
796
+ require_value: diff_warnings
797
+ equals: ""
798
+ on_mismatch: continue
799
+ on_mismatch_message: >
800
+ THE LEDGER CANNOT SEE PART OF THIS SPEC. {{spec_file}} states requirements in a form
801
+ the version diff cannot key to a definition, so an edit to one of them raises no
802
+ Change History row and no story citing it is ever told: {{diff_warnings}}. Rewrite them
803
+ in the template's shape — `- **FR-001**: …`, defined ABOVE the Change History table,
804
+ with any continuation on the lines directly under the id and no blank line between.
758
805
 
759
806
  # 10b. DID THIS MOVE GROUND SOMEONE IS STANDING ON? The diff is fact, so this is the
760
807
  # detector the pre-authoring preference lacked: of the requirements this run
@@ -1065,6 +1112,23 @@ workflow:
1065
1112
  args: ["--feature-dir", "{{feature_dir}}", "--ids", "{{disturbing_csv}}"]
1066
1113
  outputs:
1067
1114
  - { name: disturbed_stories, from: "$.story_csv" }
1115
+ - { name: final_refs_ok, from: "$.ok" }
1116
+
1117
+ # 10h-bis. THE SAME HONESTY THE PRE-AUTHORING SCAN NOW HAS. This step carries
1118
+ # `on_error: continue` so a failed re-scan cannot strand a written document, and on
1119
+ # failure disturbed_stories binds to nothing — which the post-restore report (law 45)
1120
+ # then reads as "nobody was disturbed", the one claim it must not make on no evidence.
1121
+ - id: final_refs_unknown_notice
1122
+ condition: "{{final_refs_ok}} != true"
1123
+ action: verify_context
1124
+ require_value: final_refs_ok
1125
+ equals: "true"
1126
+ on_mismatch: continue
1127
+ on_mismatch_message: >
1128
+ The final backlog re-scan did not complete, so the disturbed-stories list below is
1129
+ NOT a list of nobody — it is unknown. The requirements this run moved are
1130
+ {{disturbing_csv}}; run /appweaver sync-stories to find and reconcile the stories
1131
+ carrying them.
1068
1132
 
1069
1133
  # 10i. AN ID THE LEDGER HAS ALREADY SPENT. Ids are permanent: re-issuing a retired one
1070
1134
  # silently re-points stories, designs and code that have already shipped, which is
@@ -1097,6 +1161,29 @@ workflow:
1097
1161
  outputs:
1098
1162
  - { name: ledger_version, from: "$.version" }
1099
1163
 
1164
+ # 11a-bis. A PROSE-ONLY EDIT RE-BASELINES WITH NO ROW, AND SAYS SO. ledger-append is
1165
+ # conditional on change_count, because law 5 requires a version row to NAME at least
1166
+ # one id and an edit that touched no requirement has none to give. The stamp below is
1167
+ # not conditional, so this run rewrites the baseline and the document's own history
1168
+ # records nothing — which is right, and was silent. Aggregate Boundary, Experience
1169
+ # Scope and the rest of the load-bearing prose are read by every later phase and have
1170
+ # no staleness signal of their own; the least this can do is say that the baseline
1171
+ # moved and nothing recorded why.
1172
+ - id: prose_only_notice
1173
+ condition: "{{change_count}} == 0"
1174
+ action: verify_context
1175
+ require_value: needs_bootstrap
1176
+ equals: "true"
1177
+ on_mismatch: continue
1178
+ on_mismatch_message: >
1179
+ This run changed {{spec_file}} but moved no requirement: the diff found no id added,
1180
+ modified or removed, so no Change History row was appended — a row must name at least
1181
+ one id, and there is none to name. The document is still RE-BASELINED below, so the
1182
+ next run reads it as clean. Nothing else is: if the edit touched load-bearing prose
1183
+ (Aggregate Boundary, Experience Scope, Scope & Assumptions), no story is stale, no
1184
+ sync will notice, and the only record is this message and your commit. Say what moved
1185
+ in the commit message.
1186
+
1100
1187
  # ═══════════════ STORY COVERAGE — both paths, before the baseline ══════════
1101
1188
  # 11b. Rewrite the spec's AW:COVERAGE block: every FR-/BR- in the document and the
1102
1189
  # stories delivering it, read out of the stories/ frontmatter. The relation is
@@ -1105,7 +1192,7 @@ workflow:
1105
1192
  #
1106
1193
  # Deliberately OUTSIDE the ledger. The hash guards the CONTRACT, and coverage is a
1107
1194
  # projection of the backlog — spec-check, spec-diff and ledger-append all cut this
1108
- # block out before they look. That is what lets user-story and sync-stories write
1195
+ # block out before they look. That is what lets add-story and sync-stories write
1109
1196
  # it too without a story creation reading as an unledgered spec edit, or a story
1110
1197
  # raised against FR-003 reading as FR-003 having changed.
1111
1198
  #
@@ -120,10 +120,31 @@ vars:
120
120
  fe_plan: ""
121
121
  spec_ok: "false"
122
122
  spec_state: ""
123
+ # the analyze-design verdict — law 40's evidence. certified defaults FALSE so a probe
124
+ # that could not run fails CLOSED, and the message default carries a route for the same
125
+ # reason: a refusal with no route is a wedged feature.
126
+ gate_status: ""
127
+ gate_certified: "false"
128
+ gate_aborted: "false"
129
+ gate_gap_refs: ""
130
+ gate_report: ""
131
+ gate_be_sha: ""
132
+ gate_fe_sha: ""
133
+ gate_spec_version: ""
134
+ 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 story-plan."
135
+ # what has to land first (read here, gated nowhere — see deps_notice)
136
+ story_wave: ""
137
+ depends_on_csv: ""
138
+ open_deps_csv: ""
139
+ unresolved_deps_csv: ""
140
+ deps_clear: "true"
141
+ deps_note: ""
123
142
  # per-lane feature-plan freshness (the one-way coupling: a story pass READS a plan)
124
143
  be_plan_state: ""
125
144
  be_plan_derived: ""
126
145
  be_plan_changed: ""
146
+ be_plan_sha: ""
147
+ fe_plan_sha: ""
127
148
  be_plan_note: ""
128
149
  be_drop_msg: ""
129
150
  be_dropped: "false"
@@ -246,19 +267,10 @@ workflow:
246
267
  # 3. The spec's ledger tripwire: no design pass from an unledgered spec — every
247
268
  # pass records the spec version it designed against, and an unledgered edit has
248
269
  # no version to record.
249
- - id: spec_guard_read
250
- action: execute_script
251
- script: { ref: actions }
252
- subcommand: spec-check
253
- args: ["--feature-dir", "{{feature_dir}}"]
254
- outputs:
255
- - { name: spec_ok, from: "$.ok" }
256
- - { name: spec_state, from: "$.state" }
257
270
  - id: spec_guard
258
- action: verify_context
259
- require_true: spec_ok
260
- on_false: stop
261
- on_false_message: "spec.md has moved outside the ledger (state: {{spec_state}}). Record what changed in a Change History row (fold it back through the spec workflow), re-run spec-stamp to re-baseline, then re-run story-plan — a design pass derived from an unledgered spec records a baseline the ledger cannot name."
271
+ action: include
272
+ module: .appweaver/orchestrator/modules/spec-state-gate.orch.yaml
273
+ with: { command: "/appweaver story-plan" }
262
274
 
263
275
  # 4. THE FEATURE-PLAN GATES, one per declared lane. A story design is a SLICE of
264
276
  # the feature's architecture, so it READS that lane's plan and NEVER writes
@@ -286,6 +298,10 @@ workflow:
286
298
  - { name: be_plan_state, from: "$.state" }
287
299
  - { name: be_plan_derived, from: "$.derived_version" }
288
300
  - { name: be_plan_changed, from: "$.changed_csv" }
301
+ # THE PLAN'S SHAPE, not just whether it moved. The certification check below compares
302
+ # this against the hash analyze-design recorded in its verdict — which is the only way
303
+ # to tell "approved" from "approved, then edited".
304
+ - { name: be_plan_sha, from: "$.body_sha256" }
289
305
  - id: fe_plan_read
290
306
  condition: "{{fe_in_scope}} == true"
291
307
  action: execute_script
@@ -297,6 +313,7 @@ workflow:
297
313
  - { name: fe_plan_state, from: "$.state" }
298
314
  - { name: fe_plan_derived, from: "$.derived_version" }
299
315
  - { name: fe_plan_changed, from: "$.changed_csv" }
316
+ - { name: fe_plan_sha, from: "$.body_sha256" }
300
317
 
301
318
  # 4a. SERVICE lane verdict. Only `clean` and `edited` keep the lane; every other
302
319
  # state (including the empty string a crashed probe leaves) drops it.
@@ -494,6 +511,14 @@ workflow:
494
511
  - { name: fe_next_pass, from: "$.fe_next_pass" }
495
512
  - { name: fe_pass_tmp, from: "$.fe_pass_tmp" }
496
513
  - { name: if_tmp, from: "$.if_tmp" }
514
+ - { name: story_wave, from: "$.story_wave" }
515
+ - { name: depends_on_csv, from: "$.depends_on_csv" }
516
+ - { name: open_deps_csv, from: "$.open_deps_csv" }
517
+ - { name: unresolved_deps_csv, from: "$.unresolved_deps_csv" }
518
+ - { name: deps_clear, from: "$.deps_clear" }
519
+ - { name: deps_note, from: "$.deps_note" }
520
+ - { name: be_pass_plan_hash, from: "$.be_pass_plan_hash" }
521
+ - { name: fe_pass_plan_hash, from: "$.fe_pass_plan_hash" }
497
522
  - id: require_story
498
523
  action: verify_context
499
524
  require_true: resolved
@@ -511,6 +536,25 @@ workflow:
511
536
  # 5a. A dropped lane is announced HERE — after the story is known, so the notice
512
537
  # names the story it applies to, and before any write, so the author can stop
513
538
  # and fix the plan rather than discovering the gap after a pass has landed.
539
+ # 5.0 WHAT HAS TO LAND FIRST. sync-stories computes depends_on: and wave: and stamps both
540
+ # into every story, and until now nothing downstream read either — the ordering was
541
+ # recorded and never enforced, in the one place with a story in hand to enforce it on.
542
+ # A NOTICE, NOT A REFUSAL: a dependency is about CODE ordering, and designing against
543
+ # work that has not landed is legitimate — it is BUILDING against it that starts to
544
+ # cost something. Two misses are kept apart: a dependency still OPEN in this feature
545
+ # is work in progress, and one with no story file here is UNRESOLVED, which is the
546
+ # ordinary shape of a cross-feature edge and not at all the same as satisfied.
547
+ # Deliberately NOT "an open story in a lower wave": a story becomes startable when its
548
+ # OWN dependencies land, not when the whole previous wave finishes, so warning on the
549
+ # wave would serialise a level that exists precisely to be unordered. The wave is
550
+ # surfaced and gated by nothing.
551
+ - id: deps_notice
552
+ condition: "{{deps_clear}} != true"
553
+ action: verify_context
554
+ require_true: deps_clear
555
+ on_false: continue
556
+ on_false_message: "{{deps_note}}"
557
+
514
558
  - id: be_drop_notice
515
559
  condition: "{{be_dropped}} == true"
516
560
  action: verify_context
@@ -526,6 +570,120 @@ workflow:
526
570
  on_mismatch: continue
527
571
  on_mismatch_message: "{{fe_drop_msg}}"
528
572
 
573
+ # ═══════ 5a-bis. LAW 40 — approved, changed, never re-approved must not reach code ═══════
574
+ # analyze-design certifies the feature plans and writes a machine-readable verdict beside
575
+ # its report. Nothing in the kit read one back, so "was this plan approved, and was it
576
+ # approved in the shape it is in NOW" was a question only a person opening the JSON could
577
+ # answer — and this is the phase that turns a feature plan into a slice design that
578
+ # implement builds from without ever re-asking it.
579
+ #
580
+ # HERE, AND NOT AT be_plan_read. Those probes feed the drop-on-behind decision above;
581
+ # gating there would change WHICH lanes get dropped. This runs after the keep/drop set is
582
+ # settled and before anything is authored, so it can supersede be_keep_edited — that step
583
+ # keeps an edited plan and tells the author it "is authoritative here", which is exactly
584
+ # the state law 40 forbids when nobody certified the edit.
585
+ - id: gate_verdict
586
+ action: execute_script
587
+ on_error: continue
588
+ script: { ref: actions }
589
+ subcommand: gate-read
590
+ args: ["--feature-dir", "{{feature_dir}}", "--gate", "analyze-design"]
591
+ outputs:
592
+ - { name: gate_status, from: "$.status" }
593
+ - { name: gate_certified, from: "$.certified" }
594
+ - { name: gate_aborted, from: "$.aborted" }
595
+ - { name: gate_gap_refs, from: "$.gap_refs_csv" }
596
+ - { name: gate_report, from: "$.report" }
597
+ - { name: gate_be_sha, from: "$.backend_sha256" }
598
+ - { name: gate_fe_sha, from: "$.frontend_sha256" }
599
+ - { name: gate_spec_version, from: "$.spec_version" }
600
+
601
+ # THREE REASONS, THREE ROUTES, and telling them apart is the whole value. Ordered sets,
602
+ # last match wins: never ran → gated red → started and aborted. Aborted is last because it
603
+ # also carries status "fail" (gate_result_open writes a red BEFORE any analysis, so an
604
+ # abandoned run never leaves a stale green) and sending someone to read gaps that were
605
+ # never found is the one wrong answer here.
606
+ - id: gate_msg_never
607
+ action: set
608
+ vars:
609
+ gate_msg: "The feature plans for this feature have never been certified: there is no analyze-design verdict beside them. A story design turns the feature plan into the slice implement builds, so designing against an ungated plan puts an unapproved architecture into code through two documents that never re-ask the question. Run /appweaver analyze-design, then re-run story-plan {{story}}."
610
+ - id: gate_msg_red
611
+ condition: "{{gate_status}} == fail"
612
+ action: set
613
+ vars:
614
+ gate_msg: "The feature plans do NOT conform to the spec: analyze-design's last verdict is FAIL on {{gate_gap_refs}}. The full report is at {{gate_report}}. Close those gaps first — a wrong PLAN is repaired by re-running /appweaver analyze-design and accepting the repair, or by hand-editing it and re-running /appweaver plan (or plan-ui) with accept_manual: true; a wrong SPEC goes through the spec route. Then re-run story-plan {{story}}."
615
+ - id: gate_msg_aborted
616
+ condition: "{{gate_aborted}} == true"
617
+ action: set
618
+ vars:
619
+ 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 go and read. The plans are UNCERTIFIED, not rejected. Run /appweaver analyze-design to completion, then re-run story-plan {{story}}."
620
+
621
+ - id: gate_certified_guard
622
+ condition: "{{have_lane}} == true"
623
+ action: verify_context
624
+ require_true: gate_certified
625
+ on_false: stop
626
+ on_false_message: "{{gate_msg}}"
627
+
628
+ # PER KEPT LANE, and only per kept lane. A lane that is undeclared or was dropped above
629
+ # has no certified hash and must not fail the run for lacking one.
630
+ # An EMPTY recorded hash fails the comparison, and that is correct: a verdict that does
631
+ # not say which shape it certified cannot prove this is that shape.
632
+ - id: be_certified_shape
633
+ condition: "{{be_in_scope}} == true"
634
+ action: verify_context
635
+ require_value: be_plan_sha
636
+ equals: "{{gate_be_sha}}"
637
+ on_mismatch: stop
638
+ on_mismatch_message: "backend-plan.md is not the document analyze-design certified — it has been edited since (or the verdict predates hash recording and cannot say what it approved). Designing this slice against it would carry an unapproved architecture into code, which is exactly what law 40 forbids. Re-run /appweaver analyze-design to certify the plan as it now stands, then re-run story-plan {{story}}."
639
+ - id: fe_certified_shape
640
+ condition: "{{fe_in_scope}} == true"
641
+ action: verify_context
642
+ require_value: fe_plan_sha
643
+ equals: "{{gate_fe_sha}}"
644
+ on_mismatch: stop
645
+ on_mismatch_message: "frontend-plan.md is not the document analyze-design certified — it has been edited since (or the verdict predates hash recording and cannot say what it approved). Re-run /appweaver analyze-design to certify the plan as it now stands, then re-run story-plan {{story}}."
646
+
647
+ # ═══════ 5a-ter. WHICH LANES THIS STORY WAS DESIGNED FOR, AND WHICH WERE DROPPED ═══════
648
+ # Two fields, because one of them cannot say the difference. analyze-design STOPS on a
649
+ # lane whose plan is behind; this workflow DROPS it and designs the other — so one command
650
+ # later "this slice has no user-facing surface" and "the UI lane was out of scope when it
651
+ # was designed" are the same state on disk, and a UX--bearing requirement can ship
652
+ # backend-only with a green story-scoped gate.
653
+ # story-stamp is the frontmatter writer, and every one of its fields is guarded by its own
654
+ # flag, so these calls touch nothing else in the story. One per lane per outcome, because
655
+ # the keys UNION: a lane that has ever been designed keeps saying so.
656
+ # NOT story-plan-doc — that writes the PASS, and a dropped lane has no pass, which is the
657
+ # entire point. NOT sync-stories — stories are not designed there.
658
+ - id: stamp_lane_be_designed
659
+ condition: "{{be_in_scope}} == true"
660
+ action: execute_script
661
+ on_error: continue
662
+ script: { ref: actions }
663
+ subcommand: story-stamp
664
+ args: ["--feature-dir", "{{feature_dir}}", "--story", "{{story}}", "--designed-lanes", "backend"]
665
+ - id: stamp_lane_be_dropped
666
+ condition: "{{be_dropped}} == true"
667
+ action: execute_script
668
+ on_error: continue
669
+ script: { ref: actions }
670
+ subcommand: story-stamp
671
+ args: ["--feature-dir", "{{feature_dir}}", "--story", "{{story}}", "--dropped-lanes", "backend"]
672
+ - id: stamp_lane_fe_designed
673
+ condition: "{{fe_in_scope}} == true"
674
+ action: execute_script
675
+ on_error: continue
676
+ script: { ref: actions }
677
+ subcommand: story-stamp
678
+ args: ["--feature-dir", "{{feature_dir}}", "--story", "{{story}}", "--designed-lanes", "frontend"]
679
+ - id: stamp_lane_fe_dropped
680
+ condition: "{{fe_dropped}} == true"
681
+ action: execute_script
682
+ on_error: continue
683
+ script: { ref: actions }
684
+ subcommand: story-stamp
685
+ args: ["--feature-dir", "{{feature_dir}}", "--story", "{{story}}", "--dropped-lanes", "frontend"]
686
+
529
687
  # 5b. A pre-split story-plan.md beside the story is REPORTED, never read and never
530
688
  # written. Silently leaving it there would leave a design document nobody
531
689
  # consumes sitting beside two that are read — and the next person to open the
@@ -706,7 +864,12 @@ workflow:
706
864
  action: execute_script
707
865
  script: { ref: actions }
708
866
  subcommand: story-plan-doc
709
- args: ["--feature-dir", "{{feature_dir}}", "--story", "{{story}}", "--lane", "backend", "--pass-file", "{{be_pass_tmp}}", "--spec-version", "{{current_version}}"]
867
+ # --plan-hash stamps the CERTIFIED feature-plan hash into the pass header — the one
868
+ # analyze-design recorded, which the guard above has just proved is the plan on disk.
869
+ # A pass is written once and read forever, so without it implement could only ask
870
+ # "is the plan certified NOW", and a pass designed against plan H1 would still
871
+ # generate after the plan was edited to H2 and re-gated — the exact case law 40 names.
872
+ args: ["--feature-dir", "{{feature_dir}}", "--story", "{{story}}", "--lane", "backend", "--pass-file", "{{be_pass_tmp}}", "--spec-version", "{{current_version}}", "--plan-hash", "{{gate_be_sha}}"]
710
873
  outputs:
711
874
  - { name: be_pass_n, from: "$.pass" }
712
875
  - { name: be_created, from: "$.created" }
@@ -778,6 +941,40 @@ workflow:
778
941
  - { name: if_unknown_csv, from: "$.unknown_ids_csv" }
779
942
  - { name: if_badsup_csv, from: "$.bad_supersedes_csv" }
780
943
 
944
+ # 7c. THE TWO INTEGRITY SIGNALS log-append ALREADY RETURNS. Both were bound and neither
945
+ # was said out loud — the idiom every other integrity check in these workflows uses
946
+ # is a verify_context that continues, and these had none.
947
+ # unknown_ids_csv an entry's `serves:` names an id the spec does not define,
948
+ # so the BE→FE contract anchor points at nothing.
949
+ # bad_supersedes_csv an entry supersedes one the log does not carry, so what it
950
+ # claims to replace is not there — and readers take the LATEST
951
+ # entry per interface, which is now ambiguous.
952
+ # Announce-only: the entries are appended by the time this is visible, ids are
953
+ # permanent, and an append-only log is corrected by a later entry, never by a rewrite.
954
+ - id: if_unknown_notice
955
+ condition: "{{if_unknown_csv}} != ''"
956
+ action: verify_context
957
+ require_value: if_unknown_csv
958
+ equals: ""
959
+ on_mismatch: continue
960
+ on_mismatch_message: >
961
+ interfaces.md now carries entries whose `serves:` names ids the spec does not define:
962
+ {{if_unknown_csv}}. That is a broken trace anchor in the BE→FE contract — the frontend
963
+ binds to these entries by id, and the requirement they claim to serve is not in
964
+ {{spec_file}}. Either the id is a typo, or the requirement was never specified: fix it
965
+ with a superseding entry (never by editing the log), or take it through /appweaver specify.
966
+ - id: if_badsup_notice
967
+ condition: "{{if_badsup_csv}} != ''"
968
+ action: verify_context
969
+ require_value: if_badsup_csv
970
+ equals: ""
971
+ on_mismatch: continue
972
+ on_mismatch_message: >
973
+ An entry appended to interfaces.md supersedes an id the log does not carry:
974
+ {{if_badsup_csv}}. Nothing was replaced, so the interface it meant to retire is still
975
+ the latest entry for its surface and both now read as current. Append a corrected
976
+ superseding entry naming the real id.
977
+
781
978
  # ══════════════════════ THE UI DESIGN (payload-only + Read) ══════════════════
782
979
  # The UI lane's rules load here, AFTER the service pass has been written, so a
783
980
  # service author is never handed component and styling rules. The pass reads the
@@ -913,7 +1110,12 @@ workflow:
913
1110
  action: execute_script
914
1111
  script: { ref: actions }
915
1112
  subcommand: story-plan-doc
916
- args: ["--feature-dir", "{{feature_dir}}", "--story", "{{story}}", "--lane", "frontend", "--pass-file", "{{fe_pass_tmp}}", "--spec-version", "{{current_version}}"]
1113
+ # --plan-hash stamps the CERTIFIED feature-plan hash into the pass header — the one
1114
+ # analyze-design recorded, which the guard above has just proved is the plan on disk.
1115
+ # A pass is written once and read forever, so without it implement could only ask
1116
+ # "is the plan certified NOW", and a pass designed against plan H1 would still
1117
+ # generate after the plan was edited to H2 and re-gated — the exact case law 40 names.
1118
+ args: ["--feature-dir", "{{feature_dir}}", "--story", "{{story}}", "--lane", "frontend", "--pass-file", "{{fe_pass_tmp}}", "--spec-version", "{{current_version}}", "--plan-hash", "{{gate_fe_sha}}"]
917
1119
  outputs:
918
1120
  - { name: fe_pass_n, from: "$.pass" }
919
1121
  - { name: fe_created, from: "$.created" }