aw-spec-cli 0.1.21 → 0.1.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/assets/.appweaver/STACK-PROFILES.md +87 -8
  2. package/assets/.appweaver/orchestrator/modules/delta-plan.orch.yaml +30 -0
  3. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan.orch.yaml +41 -8
  4. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice-delta.orch.yaml +62 -2
  5. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice.orch.yaml +18 -5
  6. package/assets/.appweaver/orchestrator/workflows/aw-spec-finalize-delta.orch.yaml +115 -27
  7. package/assets/.appweaver/orchestrator/workflows/aw-spec-implement.orch.yaml +225 -27
  8. package/assets/.appweaver/orchestrator/workflows/aw-spec-plan.orch.yaml +39 -0
  9. package/assets/.appweaver/orchestrator/workflows/aw-spec-recall-delta.orch.yaml +28 -7
  10. package/assets/.appweaver/orchestrator/workflows/aw-spec-specify.orch.yaml +100 -0
  11. package/assets/.appweaver/orchestrator/workflows/aw-spec-ui-model.orch.yaml +141 -0
  12. package/assets/.appweaver/orchestrator/workflows/aw-spec-ui-prototype.orch.yaml +386 -0
  13. package/assets/.appweaver/orchestrator/workflows/aw-spec-workflow-model.orch.yaml +135 -0
  14. package/assets/.appweaver/scripts/bash/aw-spec-actions.sh +190 -209
  15. package/assets/.appweaver/scripts/bash/aw-xref.sh +4 -1
  16. package/assets/.appweaver/scripts/powershell/aw-spec-actions.ps1 +182 -224
  17. package/assets/.appweaver/scripts/powershell/aw-xref.ps1 +4 -1
  18. package/assets/.appweaver/scripts/python/aw-crstate.py +127 -2
  19. package/assets/.appweaver/scripts/python/aw-gaps.py +119 -0
  20. package/assets/.appweaver/scripts/python/aw-payload-parity.py +17 -1
  21. package/assets/.appweaver/scripts/python/aw-plan-coverage.py +84 -4
  22. package/assets/.appweaver/scripts/python/aw-plangate.py +216 -0
  23. package/assets/.appweaver/scripts/python/aw-principles.py +108 -0
  24. package/assets/.appweaver/scripts/python/aw-stack.py +6 -0
  25. package/assets/.appweaver/scripts/python/aw-xref.py +141 -4
  26. package/assets/.appweaver/stack-profiles/angular.json +26 -0
  27. package/assets/.appweaver/stack-profiles/blazor-wasm.json +38 -0
  28. package/assets/.appweaver/stack-profiles/dotnet-webapi.json +38 -0
  29. package/assets/.appweaver/stack-profiles/java-spring.json +19 -0
  30. package/assets/.appweaver/stack-profiles/nextjs.json +26 -0
  31. package/assets/.appweaver/stack-profiles/node-express.json +26 -0
  32. package/assets/.appweaver/stack-profiles/python-fastapi.json +15 -0
  33. package/assets/.appweaver/stack-profiles/react-vite.json +26 -0
  34. package/assets/.appweaver/stack-profiles/sveltekit.json +26 -0
  35. package/assets/.appweaver/stack-profiles/vue-vite.json +26 -0
  36. package/assets/.appweaver/templates/ui-model-template.md +67 -0
  37. package/assets/.appweaver/templates/workflow-model-template.md +62 -0
  38. package/assets/guardrails/technology/dotnet-webapi/framework-api-map.md +44 -0
  39. package/package.json +1 -1
@@ -60,7 +60,7 @@
60
60
  # risk of "fixing" the change's new tests back toward pre-change behaviour.
61
61
  # 2. A red analyze-slice still lands on an OPEN change, so the remedy is "fix and
62
62
  # re-run" rather than a NEW change to repair the one already shipped.
63
- # The downstream fence below still guards the MERGE (a change that breaks a dependent
63
+ # The regression fence below still guards the MERGE (a change that breaks a dependent or a co-owner
64
64
  # must not reach the master docs); close-delta guards nothing — it is the bookkeeping.
65
65
  #
66
66
  # Prereqs: run /appweaver implement (answering it with THIS change), then build, then
@@ -89,7 +89,7 @@
89
89
  # is one close-delta could never archive, so it must not reach the masters.
90
90
  #
91
91
  # THE MERGE FENCE — THREE REFUSALS, read off disk in one cr-merge-check call and placed
92
- # before the downstream fence, because they are free while that one runs a test suite:
92
+ # before the regression fence, because they are free while that one runs a test suite:
93
93
  #
94
94
  # REBASE. The delta carries the master version it was authored against (`**Base version**:`,
95
95
  # stamped by cr-setup). If the master has moved, the delta's MODIFIED items name a BEFORE
@@ -137,8 +137,8 @@
137
137
  # =============================================================================
138
138
 
139
139
  name: aw-spec-finalize-delta
140
- description: Merge ONE chosen open change's delta-spec into the feature's spec.md and its model delta into the bounded context's domain-model.md — one aggregate section at a time, only the aggregates the delta names — in a single transaction, and append the spec's Change History row. ASKS which change (a bare CR id as the argument skips the prompt) and activates it, rather than taking whichever was last opened. Fenced by three hard refusals read off disk before anything is written — a moved base_version (rebase first), another change merged but not yet closed, and an open LOWER-wave predecessor — then by a SCOPED downstream fence over the transitive blast radius (fence_green), which writes a triage report and asks Abort/Override on red. Snapshots both masters to changes/<id>/pre-merge/ immediately before the rewrite. The lane plans are NOT merged — backend-plan.md and frontend-plan.md are derived from these two documents and are re-derived by /appweaver plan main. Leaves the change OPEN — analyze-slice runs next against the merged masters, and close-delta archives the change and bumps the version.
141
- version: 2.4.0
140
+ description: Merge ONE chosen open change's delta-spec into the feature's spec.md and its model delta into the bounded context's domain-model.md — one aggregate section at a time, only the aggregates the delta names — in a single transaction, and append the spec's Change History row. ASKS which change (a bare CR id as the argument skips the prompt) and activates it, rather than taking whichever was last opened. Fenced by three hard refusals read off disk before anything is written — a moved base_version (rebase first), another change merged but not yet closed, and an open LOWER-wave predecessor — then by a SCOPED regression fence over the whole blast radius (fence_green) — the transitive REFERRER closure and the features that CO-OWN the aggregates this delta touches, which hold no edge to it but whose code is in the same files — writing a triage report and asking Abort/Override on red. Snapshots both masters to changes/<id>/pre-merge/ immediately before the rewrite. The lane plans are NOT merged — backend-plan.md and frontend-plan.md are derived from these two documents and are re-derived by /appweaver plan main. Leaves the change OPEN — analyze-slice runs next against the merged masters, and close-delta archives the change and bumps the version.
141
+ version: 2.5.0
142
142
  agent_agnostic: true
143
143
  dialect: aw-spec.orchestrator/v1
144
144
 
@@ -165,8 +165,16 @@ vars:
165
165
  has_changes: "false" # from cr-list — the DIRECTORY's answer to "is anything open", not the ledger's
166
166
  version: "" # the CURRENT version — unchanged by this stage
167
167
  next_version: "" # what close-delta will bump to; the number the Change History rows carry
168
- downstream_count: "0" # transitive + placeholder-filtered dependents (blast radius); drives the gate
168
+ downstream_count: "0" # transitive + placeholder-filtered dependents THAT HAVE CODE; drives the gate
169
169
  downstream_csv: "" # the blast-radius names, fed to the scoped fence's --also and shown to the human
170
+ unbuilt_count: "0" # dependants that are planned and not implemented: real, with nothing to re-test
171
+ unbuilt_csv: ""
172
+ reached_count: "0" # the whole closure — what this change reaches, built or not
173
+ co_owner_count: "0" # features that CO-OWN the aggregates this delta touches
174
+ co_owner_csv: "" # their names, fed to the same fence as a second --also
175
+ co_owner_aggs: "" # the aggregates the co-owner search was scoped to
176
+ co_owner_source: "" # `delta` (the change's own header) or `feature-owned` (the fallback)
177
+ blast_any: "" # truthy when EITHER half of the blast radius is non-empty
170
178
  fence_ok: "true" # default green so the gate is a no-op when nothing downstream
171
179
  gate_decision: "clean" # audit: 'clean' unless a RED fence was deliberately overridden
172
180
  # The change's own `## Depends on` vs its wave — the fence that keeps the number and the
@@ -186,12 +194,13 @@ vars:
186
194
  delta_model_exists: "false" # false ⇒ a behaviour-only change: the spec merge is the whole merge
187
195
  model_orphan: "false" # a model delta whose context has no master to merge it into
188
196
  model_master_ok: "true" # lowered by model_orphan_raise; the gate reads only this
189
- # THE MERGE FENCE. All three flags default DOWN and are raised only by the good state, so
197
+ # THE MERGE FENCE. All four flags default DOWN and are raised only by the good state, so
190
198
  # a cr-merge-check that errored or a field that was renamed leaves the merge refused.
191
199
  # Fail-closed is the only safe default in front of an in-place edit of the masters.
192
200
  rebase_ok: "false"
193
201
  merged_open_ok: "false"
194
202
  no_blockers: "false"
203
+ wave_ordering_ok: "false"
195
204
  rebase_needed: "true" # the raw readings, defaulted to the blocking value for the
196
205
  has_merged_open: "true" # same reason — an unanswered question is not a clean answer
197
206
  has_blockers: "true"
@@ -200,7 +209,11 @@ vars:
200
209
  blocker_ids: ""
201
210
  blocker_count: "0"
202
211
  merged_open_ids: ""
203
- merge_wave: "1"
212
+ wave_problem: "" # which changes could not be ordered, and why that ends the check
213
+ unordered_ids: ""
214
+ unordered_count: "0"
215
+ merge_wave: "" # EMPTY, not 1: an unreadable wave is not the floor, and the
216
+ # gate below refuses rather than letting it pass as one
204
217
  snapshot_dir: "" # changes/<id>/pre-merge/ — the before-image recall-delta restores
205
218
  snapshot_count: "0"
206
219
  # THE OKF OVERVIEW'S SUBJECT. The Overview step selects a `## Aggregate:` section BY
@@ -416,6 +429,10 @@ workflow:
416
429
  - { name: has_merged_open, from: "$.has_merged_open" }
417
430
  - { name: merged_open_ids, from: "$.merged_open_ids" }
418
431
  - { name: merge_wave, from: "$.wave" }
432
+ - { name: wave_ordering_ok, from: "$.wave_ordering_ok" }
433
+ - { name: wave_problem, from: "$.wave_problem" }
434
+ - { name: unordered_ids, from: "$.unordered_ids" }
435
+ - { name: unordered_count, from: "$.unordered_count" }
419
436
 
420
437
  - id: rebase_ok_raise
421
438
  condition: "{{rebase_needed}} == false"
@@ -435,7 +452,21 @@ workflow:
435
452
  action: verify_context
436
453
  require_true: merged_open_ok
437
454
  on_false: stop
438
- on_false_message: "finalize-delta REFUSED — {{merged_open_ids}} has already been MERGED into a master but not yet CLOSED, so the masters carry it while the version still reads {{current_version}}. That change may belong to ANOTHER feature in this context: domain-model.md is shared, so a sibling's merge moves a master this delta was authored against, and it is counted here when it touched an aggregate this delta also names. Change {{active_change}} was authored against those pre-merge masters and the version comparison cannot see the difference. Finish the merged change first: /appweaver analyze-slice then /appweaver close-delta {{merged_open_ids}}, which bumps the version — then rebase this one against the new number and re-run /appweaver finalize-delta {{active_change}}."
455
+ on_false_message: "finalize-delta REFUSED — a change has already been MERGED into a master but not yet CLOSED, so the masters carry it while the version still reads {{current_version}}: {{merged_open_ids}}. The feature named beside it is where it lives — domain-model.md is shared, so a SIBLING feature's merge moves a master this delta was authored against, and it is counted here when it touched an aggregate this delta also names. Change {{active_change}} was authored against those pre-merge masters and the version comparison cannot see the difference. Finish the merged change first, under ITS OWN feature: select that feature (/appweaver select-feature), then /appweaver analyze-slice and /appweaver close-delta on it, which bumps the version — then rebase this one against the new number and re-run /appweaver finalize-delta {{active_change}}."
456
+
457
+ # WAVE ORDERING IS A PRECONDITION OF THE FENCE BELOW, not a finding of it. The predecessor
458
+ # test is `their wave < mine`, and a wave that cannot be read used to answer that question
459
+ # as 1 — the floor — so an unstamped delta posed as a predecessor to everything above it,
460
+ # and an unstamped SUBJECT compared against nothing at all and sailed through. Neither
461
+ # showed up as a refusal, because both look exactly like "no blockers found".
462
+ # It is narrow by construction: nothing sits below the floor, so a change AT wave 1 is
463
+ # never held up by a neighbour nobody stamped. Above it, the fence says so instead of
464
+ # guessing.
465
+ - id: require_wave_ordering
466
+ action: verify_context
467
+ require_true: wave_ordering_ok
468
+ on_false: stop
469
+ on_false_message: "finalize-delta REFUSED — the wave fence cannot order change {{active_change}}, so it cannot tell you whether a predecessor is still open: {{wave_problem}}. A wave is stamped by a script and read by both merge gates; `**Wave**: [N]` is the unfilled template line, which means the stamp never ran on that document. Stamp each one — `.appweaver/scripts/bash/aw-spec-actions.sh cr-wave-derive --cr-id <CR-id> --stamp` derives the wave from that change's own `## Depends on` section and writes it — then re-run /appweaver finalize-delta {{active_change}}. A change in another feature is stamped under that feature (/appweaver select-feature first)."
439
470
 
440
471
  - id: no_blockers_raise
441
472
  condition: "{{has_blockers}} == false"
@@ -445,7 +476,7 @@ workflow:
445
476
  action: verify_context
446
477
  require_true: no_blockers
447
478
  on_false: stop
448
- on_false_message: "finalize-delta REFUSED — change {{active_change}} is at wave {{merge_wave}} and {{blocker_count}} open change(s) sit at a LOWER wave: {{blocker_ids}}. A lower wave is committed to landing FIRST, and this delta was planned with those in its context as the base it builds on — merging it now folds in behaviour that assumes a predecessor the masters do not yet carry. Merge and close the lower-wave change(s) first (/appweaver finalize-delta <CR-id> → analyze-slice → close-delta), then rebase and re-run this one. Same-wave changes are independent and are not counted here; if this delta does NOT in fact build on them, correct its `## Depends on` section (naming only the deltas it truly builds on) and re-run /appweaver specify's wave derivation — `.appweaver/scripts/bash/aw-spec-actions.sh cr-wave-derive --cr-id {{active_change}} --stamp` applies it directly."
479
+ on_false_message: "finalize-delta REFUSED — change {{active_change}} is at wave {{merge_wave}} and {{blocker_count}} open change(s) sit at a LOWER wave. A lower wave is committed to landing FIRST, and this delta was planned with those in its context as the base it builds on — merging it now folds in behaviour that assumes a predecessor the masters do not yet carry. THE LOWER-WAVE CHANGES: {{blocker_ids}}. This scan is context-wide, so one of them may belong to a feature you have never opened — and finalize-delta resolves a change under the ACTIVE feature. Select the feature named beside a change (/appweaver select-feature) before running /appweaver finalize-delta <CR-id> → analyze-slice → close-delta on it; then rebase and re-run this one. Same-wave changes are independent and are not counted here; if this delta does NOT in fact build on them, correct its `## Depends on` section (naming only the deltas it truly builds on) and re-run /appweaver specify's wave derivation — `.appweaver/scripts/bash/aw-spec-actions.sh cr-wave-derive --cr-id {{active_change}} --stamp` applies it directly."
449
480
 
450
481
  # THE DECLARED DEPENDENCIES AND THE WAVE MUST AGREE. The fence above orders changes by
451
482
  # their wave NUMBER; this one checks that number against what the change's own documents
@@ -472,15 +503,21 @@ workflow:
472
503
  # its OKF Cross-References, TRANSITIVELY (C→B→A all count) and placeholder-filtered
473
504
  # (the "_(none observed)_" row is NOT a dependent) = the real blast radius. If any
474
505
  # slice does, this change could ripple into it, so run a SCOPED regression fence over
475
- # this feature + its downstream closure (--scope slice --also <closure>) BEFORE the
506
+ # this feature + its blast radius (--scope slice --also <referrers> --also <co-owners>) BEFORE the
476
507
  # doc merge, so a block leaves the repo untouched. The fence binds fence_GREEN (ran
477
508
  # AND matched >=1), not raw ok, so an empty / zero-match run is RED, never a vacuous
478
509
  # pass. If RED we do NOT hard-stop blindly: we WRITE a triage report, then surface the
479
- # failing log + dependent slices and ASK the developer — Abort (a real downstream
510
+ # failing log + affected slices and ASK the developer — Abort (a real downstream
480
511
  # break: fix it first) or, having reviewed the log (pre-existing / flaky / unrelated),
481
512
  # deliberately Override and close anyway. Non-interactive / CI runs take the FIRST
482
513
  # option (Abort), so automation never silently overrides. Skipped when nothing depends
483
- # on this slice (count 0) or the fence is green.
514
+ # on this slice and nothing co-owns its aggregates, or the fence is green.
515
+ # THE FENCE RUNS TESTS, so it takes the BUILT half of the closure. Edges are read off
516
+ # the lane PLAN, not off code, so a dependant can be fully planned and hold none:
517
+ # selecting it would hand the fence a slice with no tests, and the fence counts a
518
+ # zero-match run as RED. An unbuilt dependant is still reported — it is a real
519
+ # dependant with nothing to re-test yet, which is a different thing from being
520
+ # unaffected, and it is what /appweaver implement on that feature will settle.
484
521
  - id: xref_impact
485
522
  action: execute_script
486
523
  on_error: continue
@@ -488,16 +525,49 @@ workflow:
488
525
  subcommand: list
489
526
  args: ["--feature-dir", "{{feature_dir}}"]
490
527
  outputs:
491
- - { name: downstream_count, from: "$.referrer_closure_count" } # transitive + placeholder-filtered
492
- - { name: downstream_csv, from: "$.referrer_closure_csv" } # blast-radius names → --also + display
528
+ - { name: downstream_count, from: "$.built_count" } # transitive, placeholder-filtered, HAS CODE
529
+ - { name: downstream_csv, from: "$.built_closure_csv" } # blast-radius names → --also + display
530
+ - { name: unbuilt_count, from: "$.unbuilt_count" } # dependants planned but never implemented
531
+ - { name: unbuilt_csv, from: "$.unbuilt_closure_csv" }
532
+ - { name: reached_count, from: "$.referrer_closure_count" } # the whole closure, for the report
533
+
534
+ # THE OTHER HALF OF THE BLAST RADIUS. A referrer reaches this aggregate through an edge;
535
+ # a CO-OWNER has no edge to it because it is not a user of the aggregate — it is one of
536
+ # the capabilities the aggregate is made of, and its code is in the very files this merge
537
+ # rewrites. `Encounter.cs` is extended by every feature claiming Encounter, so a change to
538
+ # it can break a co-owner while every referrer still passes. Scoped to the aggregates THIS
539
+ # delta names, so a feature owning two aggregates does not drag the second one's co-owners
540
+ # through a fence they cannot have been affected by.
541
+ - id: xref_co_owners
542
+ action: execute_script
543
+ on_error: continue
544
+ script: { ref: xref }
545
+ subcommand: co-owners
546
+ args: ["--cr", "{{active_change}}", "--feature-dir", "{{feature_dir}}"]
547
+ outputs:
548
+ - { name: co_owner_count, from: "$.count" }
549
+ - { name: co_owner_csv, from: "$.features_csv" }
550
+ - { name: co_owner_aggs, from: "$.aggregates_csv" }
551
+ - { name: co_owner_source, from: "$.source" }
552
+
553
+ # The fence runs when EITHER half is non-empty. Two ordered assignments rather than one
554
+ # condition, because the dialect has no compound conditions.
555
+ - id: blast_from_referrers
556
+ condition: "{{downstream_count}} != 0"
557
+ action: set
558
+ vars: { blast_any: "true" }
559
+ - id: blast_from_co_owners
560
+ condition: "{{co_owner_count}} != 0"
561
+ action: set
562
+ vars: { blast_any: "true" }
493
563
 
494
564
  - id: run_downstream_fence
495
- condition: "{{downstream_count}} != 0"
565
+ condition: "{{blast_any}}"
496
566
  action: execute_script
497
567
  on_error: continue
498
568
  script: { ref: actions }
499
569
  subcommand: test-run
500
- args: ["--feature-dir", "{{feature_dir}}", "--scope", "slice", "--also", "{{downstream_csv}}"]
570
+ args: ["--feature-dir", "{{feature_dir}}", "--scope", "slice", "--also", "{{downstream_csv}}", "--also", "{{co_owner_csv}}"]
501
571
  outputs:
502
572
  - { name: fence_ok, from: "$.fence_green" }
503
573
  - { name: fence_tool, from: "$.tool" }
@@ -511,11 +581,14 @@ workflow:
511
581
  # only when nothing depends on this slice (no fence ran, nothing to report).
512
582
  - id: ctx_broken_downstream
513
583
  action: build_context
514
- condition: "{{downstream_count}} != 0"
584
+ condition: "{{blast_any}}"
515
585
  vars:
516
586
  active_change: "{{active_change}}"
517
587
  downstream_count: "{{downstream_count}}"
518
588
  downstream_csv: "{{downstream_csv}}"
589
+ co_owner_count: "{{co_owner_count}}"
590
+ co_owner_csv: "{{co_owner_csv}}"
591
+ co_owner_aggs: "{{co_owner_aggs}}"
519
592
  fence_ok: "{{fence_ok}}"
520
593
  fence_log: "{{fence_log}}"
521
594
  context:
@@ -526,14 +599,27 @@ workflow:
526
599
  - { path: "{{delta_backend_plan}}", optional: true }
527
600
  - id: write_broken_downstream
528
601
  action: agent_author
529
- condition: "{{downstream_count}} != 0"
602
+ condition: "{{blast_any}}"
530
603
  include_standards: false # a triage report, not generated code
531
604
  output_target: { type: file, path: "{{feature_dir}}/changes/{{active_change}}/broken-downstream.md" }
532
605
  task: |
533
606
  Author the DOWNSTREAM report for change {{active_change}}: the result of re-running this
534
- change against the aggregates that DEPEND ON this feature. finalize-delta does NOT fix a
535
- downstream slice (that is the downstream slice's own job). Downstream blast radius
536
- ({{downstream_count}}): {{downstream_csv}}. The fence result was fence_green={{fence_ok}}
607
+ change against everything it could have broken. finalize-delta does NOT fix a
608
+ downstream slice (that is the downstream slice's own job).
609
+ THE BLAST RADIUS HAS TWO HALVES and they fail for different reasons — say which is which
610
+ wherever the report names a slice:
611
+ - DEPENDENTS ({{downstream_count}}): {{downstream_csv}} — aggregates that USE this one
612
+ through a Cross-Reference edge. A failure here is a broken CONTRACT between two slices.
613
+ - NOT YET BUILT ({{unbuilt_count}}): {{unbuilt_csv}} — aggregates that also depend on this
614
+ one and hold no code yet, so the fence had nothing to run for them. Edges come from the
615
+ lane PLAN, so these are real dependants whose contract with this aggregate has never
616
+ been exercised. Say so plainly: they were NOT tested, which is not the same as passing,
617
+ and /appweaver implement on those features is what settles them.
618
+ - CO-OWNERS ({{co_owner_count}}): {{co_owner_csv}} — features that share the aggregate(s)
619
+ this change touches ({{co_owner_aggs}}). They hold no edge to it: their code is IN the
620
+ files this merge rewrites, so a failure here is this change breaking a sibling
621
+ capability of the same aggregate, not a contract between two of them.
622
+ Either list may be empty. The fence result was fence_green={{fence_ok}}
537
623
  (true = every downstream test passed; false = at least one failed). Test log:
538
624
  {{fence_log}}
539
625
  Always write the file (it is regenerated on every finalize-delta run so a green close never
@@ -541,8 +627,9 @@ workflow:
541
627
 
542
628
  IF fence_green is TRUE (all passed): write a SHORT all-clear — the heading
543
629
  `# Broken Downstream — change {{active_change}}`, then a few lines stating that the
544
- {{downstream_count}} slice(s) that use this feature ({{downstream_csv}}) were re-run against
545
- this change and all passed, so nothing downstream broke. No per-slice sections; invent nothing.
630
+ {{downstream_count}} dependent slice(s) ({{downstream_csv}}) and {{co_owner_count}} co-owner
631
+ feature(s) ({{co_owner_csv}}) were re-run against this change and all passed, so nothing it
632
+ could reach broke. No per-slice sections; invent nothing.
546
633
 
547
634
  IF fence_green is FALSE (something failed): write the triage record —
548
635
  - `# Broken Downstream — change {{active_change}}` and a one-line statement: aggregates that
@@ -550,10 +637,11 @@ workflow:
550
637
  finalize-delta's downstream gate. Note it is the record a human reads while deciding Abort vs
551
638
  Override, and if the change is Overridden it is archived with the change as the record of
552
639
  what was knowingly closed over.
553
- - A SUMMARY line: total downstream slices in the blast radius, and how many have a failing test.
554
- - One `##` section per DOWNSTREAM slice that has a failing test — attribute a failure to a
555
- slice by matching the failing test names in the log to the slice's name. Under it, on their
556
- own labelled lines: the downstream aggregate, the failing test(s) named in the log, what
640
+ - A SUMMARY line: total slices in the blast radius (dependents + co-owners), and how many have a failing test.
641
+ - One `##` section per slice with a failing test, from EITHER half — attribute a failure to a
642
+ slice by matching the failing test names in the log to the slice's name, and label the
643
+ section `dependent` or `co-owner`. Under it, on their
644
+ own labelled lines: the aggregate or feature, the failing test(s) named in the log, what
557
645
  broke (as far as the log shows), and a RESOLUTION line — either this change broke the
558
646
  downstream slice's expected contract (fix it in the downstream slice's OWN CR, or re-baseline
559
647
  its golden-master) or the failure is pre-existing / flaky / unrelated (a candidate for a