devrites 6.0.1 → 6.0.3

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 (156) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +15 -64
  3. package/SECURITY.md +7 -11
  4. package/bin/devrites.mjs +0 -8
  5. package/docs/adr/0034-workflow-artifact-byte-corrections-get-code-review.md +60 -0
  6. package/docs/adr/README.md +2 -1
  7. package/docs/architecture.md +2 -5
  8. package/docs/cli.md +4 -11
  9. package/docs/command-map.md +1 -2
  10. package/docs/engine/commands.md +6 -6
  11. package/docs/release.md +5 -6
  12. package/engine/help.go +12 -7
  13. package/engine/help_test.go +1 -0
  14. package/engine/internal/gate/readiness_binding.go +19 -0
  15. package/engine/internal/lib/cli_observe.go +16 -0
  16. package/engine/internal/lib/context.go +191 -19
  17. package/engine/internal/lib/context_next_test.go +186 -0
  18. package/engine/internal/lib/dispatch.go +146 -11
  19. package/engine/internal/lib/drift.go +10 -2
  20. package/engine/internal/lib/drift_test.go +29 -0
  21. package/engine/internal/lib/slicecheck.go +19 -0
  22. package/engine/internal/lib/slicecheck_test.go +41 -0
  23. package/engine/internal/lib/windows.go +109 -13
  24. package/engine/internal/lib/windows_test.go +85 -0
  25. package/engine/internal/release/release.go +3 -13
  26. package/engine/internal/release/release_test.go +6 -197
  27. package/engine/main.go +2 -2
  28. package/install.sh +1 -116
  29. package/pack/.claude/agents/devrites-slice-wright.md +3 -1
  30. package/pack/.claude/skills/devrites-debug-recovery/reference/hypotheses.md +6 -0
  31. package/pack/.claude/skills/devrites-lib/reference/parallel-dispatch.md +1 -0
  32. package/pack/.claude/skills/devrites-lib/reference/standards/README.md +1 -1
  33. package/pack/.claude/skills/devrites-lib/reference/standards/afk-hitl.md +2 -0
  34. package/pack/.claude/skills/devrites-lib/reference/standards/code-review.md +3 -0
  35. package/pack/.claude/skills/devrites-lib/reference/standards/glossary.md +1 -1
  36. package/pack/.claude/skills/devrites-lib/reference/standards/security.md +1 -0
  37. package/pack/.claude/skills/devrites-lib/reference/standards/windows.md +11 -3
  38. package/pack/.claude/skills/devrites-lib/reference/workspace-artifact-schema.md +5 -2
  39. package/pack/.claude/skills/rite-build/SKILL.md +1 -1
  40. package/pack/.claude/skills/rite-build/reference/one-slice-cycle.md +3 -2
  41. package/pack/.claude/skills/rite-build/reference/parallel-batch.md +5 -3
  42. package/pack/.claude/skills/rite-build/reference/phase-contract.md +5 -3
  43. package/pack/.claude/skills/rite-build/reference/spec-drift-guard.md +3 -1
  44. package/pack/.claude/skills/rite-build/reference/wright-dispatch.md +23 -9
  45. package/pack/.claude/skills/rite-define/SKILL.md +2 -1
  46. package/pack/.claude/skills/rite-plan/SKILL.md +2 -1
  47. package/pack/.claude/skills/rite-pressure-test/SKILL.md +6 -3
  48. package/pack/.claude/skills/rite-vet/SKILL.md +9 -1
  49. package/pack/.claude/skills/rite-vet/reference/review-axes.md +6 -2
  50. package/pack/generated/claude/agents/devrites-slice-wright.md +3 -1
  51. package/pack/generated/claude/skills/devrites-debug-recovery/reference/hypotheses.md +6 -0
  52. package/pack/generated/claude/skills/devrites-lib/reference/parallel-dispatch.md +1 -0
  53. package/pack/generated/claude/skills/devrites-lib/reference/standards/README.md +1 -1
  54. package/pack/generated/claude/skills/devrites-lib/reference/standards/afk-hitl.md +2 -0
  55. package/pack/generated/claude/skills/devrites-lib/reference/standards/code-review.md +3 -0
  56. package/pack/generated/claude/skills/devrites-lib/reference/standards/glossary.md +1 -1
  57. package/pack/generated/claude/skills/devrites-lib/reference/standards/security.md +1 -0
  58. package/pack/generated/claude/skills/devrites-lib/reference/standards/windows.md +11 -3
  59. package/pack/generated/claude/skills/devrites-lib/reference/workspace-artifact-schema.md +5 -2
  60. package/pack/generated/claude/skills/rite-build/SKILL.md +1 -1
  61. package/pack/generated/claude/skills/rite-build/reference/one-slice-cycle.md +3 -2
  62. package/pack/generated/claude/skills/rite-build/reference/parallel-batch.md +5 -3
  63. package/pack/generated/claude/skills/rite-build/reference/phase-contract.md +5 -3
  64. package/pack/generated/claude/skills/rite-build/reference/spec-drift-guard.md +3 -1
  65. package/pack/generated/claude/skills/rite-build/reference/wright-dispatch.md +23 -9
  66. package/pack/generated/claude/skills/rite-define/SKILL.md +2 -1
  67. package/pack/generated/claude/skills/rite-plan/SKILL.md +2 -1
  68. package/pack/generated/claude/skills/rite-pressure-test/SKILL.md +6 -3
  69. package/pack/generated/claude/skills/rite-vet/SKILL.md +9 -1
  70. package/pack/generated/claude/skills/rite-vet/reference/review-axes.md +6 -2
  71. package/pack/generated/codex/agents/devrites-slice-wright.toml +3 -1
  72. package/pack/generated/codex/skills/devrites-debug-recovery/reference/hypotheses.md +6 -0
  73. package/pack/generated/codex/skills/devrites-lib/reference/parallel-dispatch.md +1 -0
  74. package/pack/generated/codex/skills/devrites-lib/reference/standards/README.md +1 -1
  75. package/pack/generated/codex/skills/devrites-lib/reference/standards/afk-hitl.md +2 -0
  76. package/pack/generated/codex/skills/devrites-lib/reference/standards/code-review.md +3 -0
  77. package/pack/generated/codex/skills/devrites-lib/reference/standards/glossary.md +1 -1
  78. package/pack/generated/codex/skills/devrites-lib/reference/standards/security.md +1 -0
  79. package/pack/generated/codex/skills/devrites-lib/reference/standards/windows.md +11 -3
  80. package/pack/generated/codex/skills/devrites-lib/reference/workspace-artifact-schema.md +5 -2
  81. package/pack/generated/codex/skills/rite-build/SKILL.md +1 -1
  82. package/pack/generated/codex/skills/rite-build/reference/one-slice-cycle.md +3 -2
  83. package/pack/generated/codex/skills/rite-build/reference/parallel-batch.md +5 -3
  84. package/pack/generated/codex/skills/rite-build/reference/phase-contract.md +5 -3
  85. package/pack/generated/codex/skills/rite-build/reference/spec-drift-guard.md +3 -1
  86. package/pack/generated/codex/skills/rite-build/reference/wright-dispatch.md +23 -9
  87. package/pack/generated/codex/skills/rite-define/SKILL.md +2 -1
  88. package/pack/generated/codex/skills/rite-plan/SKILL.md +2 -1
  89. package/pack/generated/codex/skills/rite-pressure-test/SKILL.md +6 -3
  90. package/pack/generated/codex/skills/rite-vet/SKILL.md +9 -1
  91. package/pack/generated/codex/skills/rite-vet/reference/review-axes.md +6 -2
  92. package/pack/generated/devin/agents/devrites-slice-wright.md +3 -1
  93. package/pack/generated/devin/skills/devrites-debug-recovery/reference/hypotheses.md +6 -0
  94. package/pack/generated/devin/skills/devrites-lib/reference/parallel-dispatch.md +1 -0
  95. package/pack/generated/devin/skills/devrites-lib/reference/standards/README.md +1 -1
  96. package/pack/generated/devin/skills/devrites-lib/reference/standards/afk-hitl.md +2 -0
  97. package/pack/generated/devin/skills/devrites-lib/reference/standards/code-review.md +3 -0
  98. package/pack/generated/devin/skills/devrites-lib/reference/standards/glossary.md +1 -1
  99. package/pack/generated/devin/skills/devrites-lib/reference/standards/security.md +1 -0
  100. package/pack/generated/devin/skills/devrites-lib/reference/standards/windows.md +11 -3
  101. package/pack/generated/devin/skills/devrites-lib/reference/workspace-artifact-schema.md +5 -2
  102. package/pack/generated/devin/skills/rite-build/SKILL.md +1 -1
  103. package/pack/generated/devin/skills/rite-build/reference/one-slice-cycle.md +3 -2
  104. package/pack/generated/devin/skills/rite-build/reference/parallel-batch.md +5 -3
  105. package/pack/generated/devin/skills/rite-build/reference/phase-contract.md +5 -3
  106. package/pack/generated/devin/skills/rite-build/reference/spec-drift-guard.md +3 -1
  107. package/pack/generated/devin/skills/rite-build/reference/wright-dispatch.md +23 -9
  108. package/pack/generated/devin/skills/rite-define/SKILL.md +2 -1
  109. package/pack/generated/devin/skills/rite-plan/SKILL.md +2 -1
  110. package/pack/generated/devin/skills/rite-pressure-test/SKILL.md +6 -3
  111. package/pack/generated/devin/skills/rite-vet/SKILL.md +9 -1
  112. package/pack/generated/devin/skills/rite-vet/reference/review-axes.md +6 -2
  113. package/pack/generated/omp/agents/devrites-slice-wright.md +3 -1
  114. package/pack/generated/omp/skills/devrites-debug-recovery/reference/hypotheses.md +6 -0
  115. package/pack/generated/omp/skills/devrites-lib/reference/parallel-dispatch.md +1 -0
  116. package/pack/generated/omp/skills/devrites-lib/reference/standards/README.md +1 -1
  117. package/pack/generated/omp/skills/devrites-lib/reference/standards/afk-hitl.md +2 -0
  118. package/pack/generated/omp/skills/devrites-lib/reference/standards/code-review.md +3 -0
  119. package/pack/generated/omp/skills/devrites-lib/reference/standards/glossary.md +1 -1
  120. package/pack/generated/omp/skills/devrites-lib/reference/standards/security.md +1 -0
  121. package/pack/generated/omp/skills/devrites-lib/reference/standards/windows.md +11 -3
  122. package/pack/generated/omp/skills/devrites-lib/reference/workspace-artifact-schema.md +5 -2
  123. package/pack/generated/omp/skills/rite-build/SKILL.md +1 -1
  124. package/pack/generated/omp/skills/rite-build/reference/one-slice-cycle.md +3 -2
  125. package/pack/generated/omp/skills/rite-build/reference/parallel-batch.md +5 -3
  126. package/pack/generated/omp/skills/rite-build/reference/phase-contract.md +5 -3
  127. package/pack/generated/omp/skills/rite-build/reference/spec-drift-guard.md +3 -1
  128. package/pack/generated/omp/skills/rite-build/reference/wright-dispatch.md +23 -9
  129. package/pack/generated/omp/skills/rite-define/SKILL.md +2 -1
  130. package/pack/generated/omp/skills/rite-plan/SKILL.md +2 -1
  131. package/pack/generated/omp/skills/rite-pressure-test/SKILL.md +6 -3
  132. package/pack/generated/omp/skills/rite-vet/SKILL.md +9 -1
  133. package/pack/generated/omp/skills/rite-vet/reference/review-axes.md +6 -2
  134. package/pack/generated/pi/agents/devrites-slice-wright.md +3 -1
  135. package/pack/generated/pi/skills/devrites-debug-recovery/reference/hypotheses.md +6 -0
  136. package/pack/generated/pi/skills/devrites-lib/reference/parallel-dispatch.md +1 -0
  137. package/pack/generated/pi/skills/devrites-lib/reference/standards/README.md +1 -1
  138. package/pack/generated/pi/skills/devrites-lib/reference/standards/afk-hitl.md +2 -0
  139. package/pack/generated/pi/skills/devrites-lib/reference/standards/code-review.md +3 -0
  140. package/pack/generated/pi/skills/devrites-lib/reference/standards/glossary.md +1 -1
  141. package/pack/generated/pi/skills/devrites-lib/reference/standards/security.md +1 -0
  142. package/pack/generated/pi/skills/devrites-lib/reference/standards/windows.md +11 -3
  143. package/pack/generated/pi/skills/devrites-lib/reference/workspace-artifact-schema.md +5 -2
  144. package/pack/generated/pi/skills/rite-build/SKILL.md +1 -1
  145. package/pack/generated/pi/skills/rite-build/reference/one-slice-cycle.md +3 -2
  146. package/pack/generated/pi/skills/rite-build/reference/parallel-batch.md +5 -3
  147. package/pack/generated/pi/skills/rite-build/reference/phase-contract.md +5 -3
  148. package/pack/generated/pi/skills/rite-build/reference/spec-drift-guard.md +3 -1
  149. package/pack/generated/pi/skills/rite-build/reference/wright-dispatch.md +23 -9
  150. package/pack/generated/pi/skills/rite-define/SKILL.md +2 -1
  151. package/pack/generated/pi/skills/rite-plan/SKILL.md +2 -1
  152. package/pack/generated/pi/skills/rite-pressure-test/SKILL.md +6 -3
  153. package/pack/generated/pi/skills/rite-vet/SKILL.md +9 -1
  154. package/pack/generated/pi/skills/rite-vet/reference/review-axes.md +6 -2
  155. package/package.json +1 -1
  156. package/scripts/install-lib.sh +0 -16
@@ -6,6 +6,8 @@ import (
6
6
  "io"
7
7
  "os"
8
8
  "path/filepath"
9
+ "regexp"
10
+ "slices"
9
11
  "sort"
10
12
  "strings"
11
13
  "time"
@@ -19,24 +21,38 @@ import (
19
21
  // fan-out cannot silently degrade to serial (open -> every role started ->
20
22
  // seal -> returns -> complete). start/return also auto-record metrics events.
21
23
  //
22
- // devrites-engine dispatch <slug> <sub> [--phase p] [--wave w] [--role r] [--handle h] [--reason s]
24
+ // devrites-engine dispatch <slug> <sub> [--phase p] [--wave w] [--role r] [--handle h] [--reason s] [--slice SLICE-###] [--reslice-review <DRIFT-ID>]
23
25
 
24
- const dispatchUsage = `usage: devrites-engine dispatch <slug> <open|start|seal|return|status|abandon> [--phase p] [--wave w] [--role r] [--handle h] [--reason s]
26
+ const dispatchUsage = `usage: devrites-engine dispatch <slug> <open|start|seal|return|status|abandon> [--phase p] [--wave w] [--role r] [--handle h] [--reason s] [--slice SLICE-###] [--reslice-review <DRIFT-ID>]
25
27
 
26
28
  Wave barrier for parallel agent dispatch: seal fails until every declared
27
29
  role has a distinct handle; return fails before seal; status exits nonzero
28
30
  while a wave is open, sealed, or abandoned.
31
+
32
+ A build wave with a wright role must name its slice(s) with --slice; a vet
33
+ wave may (slice-bound recheck). Every 5 rounds of one kind on one slice, the
34
+ next open is blocked until --reslice-review names a drift.md
35
+ "## <DRIFT-ID>" heading for that slice.
29
36
  `
30
37
 
38
+ // wrightRoundsPerReview is how many build wright rounds (or slice-bound Vet
39
+ // rechecks) one slice may take before (and after each) reslice review.
40
+ // Recovery itself stays unbounded; the cap only forces the split question
41
+ // onto the record.
42
+ const wrightRoundsPerReview = 5
43
+
31
44
  type dispatchFile struct {
32
45
  Waves map[string]*dispatchWave `json:"waves"`
33
46
  }
34
47
 
35
48
  type dispatchWave struct {
36
- Phase string `json:"phase"`
37
- State string `json:"state"` // open, sealed, abandoned, complete
38
- Reason string `json:"reason,omitempty"`
39
- Roles map[string]*dispatchRole `json:"roles"`
49
+ Phase string `json:"phase"`
50
+ State string `json:"state"` // open, sealed, abandoned, complete
51
+ Reason string `json:"reason,omitempty"`
52
+ Roles map[string]*dispatchRole `json:"roles"`
53
+ Slices []string `json:"slices,omitempty"`
54
+ ResliceReview string `json:"reslice_review,omitempty"`
55
+ ResliceReviewed []string `json:"reslice_reviewed,omitempty"`
40
56
  }
41
57
 
42
58
  type dispatchRole struct {
@@ -89,8 +105,8 @@ func nowStamp() string { return time.Now().UTC().Format(time.RFC3339) }
89
105
 
90
106
  // RunDispatch implements the dispatch wave state machine.
91
107
  func RunDispatch(root string, args []string, stdout, stderr io.Writer) int {
92
- var slug, sub, phase, wave, role, handle, reason string
93
- var roles []string
108
+ var slug, sub, phase, wave, role, handle, reason, review string
109
+ var roles, sliceIDs []string
94
110
  var positional []string
95
111
  for i := 0; i < len(args); i++ {
96
112
  value := argAt(args, i+1)
@@ -106,6 +122,10 @@ func RunDispatch(root string, args []string, stdout, stderr io.Writer) int {
106
122
  handle = value
107
123
  case "--reason":
108
124
  reason = value
125
+ case "--slice":
126
+ sliceIDs = append(sliceIDs, value)
127
+ case "--reslice-review":
128
+ review = value
109
129
  default:
110
130
  if strings.HasPrefix(args[i], "-") {
111
131
  fmt.Fprintf(stderr, "dispatch: unknown flag %q\n", args[i])
@@ -129,7 +149,7 @@ func RunDispatch(root string, args []string, stdout, stderr io.Writer) int {
129
149
  // an update and strand the wave short of complete.
130
150
  code := 0
131
151
  if err := state.WithFeatureLock(root, slug, func() error {
132
- code = dispatchLocked(root, slug, sub, phase, wave, role, handle, reason, roles, stdout, stderr)
152
+ code = dispatchLocked(root, slug, sub, phase, wave, role, handle, reason, roles, sliceIDs, review, stdout, stderr)
133
153
  return nil
134
154
  }); err != nil {
135
155
  fmt.Fprintf(stderr, "dispatch: %v\n", err)
@@ -138,7 +158,7 @@ func RunDispatch(root string, args []string, stdout, stderr io.Writer) int {
138
158
  return code
139
159
  }
140
160
 
141
- func dispatchLocked(root, slug, sub, phase, wave, role, handle, reason string, roles []string, stdout, stderr io.Writer) int {
161
+ func dispatchLocked(root, slug, sub, phase, wave, role, handle, reason string, roles, sliceIDs []string, review string, stdout, stderr io.Writer) int {
142
162
  f, path, err := loadDispatch(root, slug)
143
163
  if err != nil {
144
164
  fmt.Fprintf(stderr, "dispatch: %v\n", err)
@@ -166,18 +186,25 @@ func dispatchLocked(root, slug, sub, phase, wave, role, handle, reason string, r
166
186
  if w := getWave(); w != nil && w.State != "abandoned" {
167
187
  return fail("wave %q already exists (state %s)", wave, w.State)
168
188
  }
169
- w := &dispatchWave{Phase: phase, State: "open", Roles: map[string]*dispatchRole{}}
189
+ w := &dispatchWave{Phase: phase, State: "open", Roles: map[string]*dispatchRole{}, Slices: sliceIDs}
170
190
  for _, r := range roles {
171
191
  if w.Roles[r] != nil {
172
192
  return fail("duplicate role %q in wave", r)
173
193
  }
174
194
  w.Roles[r] = &dispatchRole{}
175
195
  }
196
+ rounds, code := admitWrightRounds(root, slug, f, w, review, stderr)
197
+ if code != 0 {
198
+ return code
199
+ }
176
200
  f.Waves[wave] = w
177
201
  if err := saveDispatch(path, f); err != nil {
178
202
  return fail("%v", err)
179
203
  }
180
204
  fmt.Fprintf(stdout, "dispatch: wave %s open, %d roles declared\n", wave, len(roles))
205
+ for _, line := range rounds {
206
+ fmt.Fprintln(stdout, line)
207
+ }
181
208
  return 0
182
209
 
183
210
  case "start":
@@ -322,6 +349,114 @@ func dispatchLocked(root, slug, sub, phase, wave, role, handle, reason string, r
322
349
  return 2
323
350
  }
324
351
 
352
+ // roundKind names the capped loop a wave belongs to: build wright rounds or
353
+ // slice-bound Vet rechecks. Other waves are uncapped.
354
+ func roundKind(w *dispatchWave) string {
355
+ switch {
356
+ case isBuildWrightWave(w):
357
+ return "wright"
358
+ case strings.EqualFold(w.Phase, "vet") && len(w.Slices) > 0:
359
+ return "vet"
360
+ }
361
+ return ""
362
+ }
363
+
364
+ func isBuildWrightWave(w *dispatchWave) bool {
365
+ if w.Phase != "build" {
366
+ return false
367
+ }
368
+ for r := range w.Roles {
369
+ if strings.Contains(r, "wright") {
370
+ return true
371
+ }
372
+ }
373
+ return false
374
+ }
375
+
376
+ // admitWrightRounds enforces the per-slice round cap for a new build wright
377
+ // or slice-bound Vet wave and returns the round lines to print. A slice may
378
+ // take wrightRoundsPerReview rounds of each kind per reslice review (the first
379
+ // window is free); beyond that the open needs --reslice-review naming an
380
+ // unused drift.md heading for every slice over the cap.
381
+ func admitWrightRounds(root, slug string, f *dispatchFile, w *dispatchWave, review string, stderr io.Writer) ([]string, int) {
382
+ kind := roundKind(w)
383
+ if kind == "" {
384
+ if review != "" || (len(w.Slices) > 0 && !strings.EqualFold(w.Phase, "vet")) {
385
+ fmt.Fprintln(stderr, "dispatch: --slice/--reslice-review apply only to build wright waves and slice-bound vet waves")
386
+ return nil, 2
387
+ }
388
+ return nil, 0
389
+ }
390
+ if len(w.Slices) == 0 {
391
+ fmt.Fprintln(stderr, "dispatch: a build wright wave needs --slice SLICE-### for each slice it builds")
392
+ return nil, 2
393
+ }
394
+ for _, id := range w.Slices {
395
+ if !state.ValidSliceID(id) {
396
+ fmt.Fprintf(stderr, "dispatch: %q is not a SLICE-### id\n", id)
397
+ return nil, 2
398
+ }
399
+ }
400
+ var lines, overCap []string
401
+ for _, id := range w.Slices {
402
+ prior, reviews := 0, 0
403
+ for _, other := range f.Waves {
404
+ if other.State == "abandoned" || roundKind(other) != kind {
405
+ continue
406
+ }
407
+ if slices.Contains(other.Slices, id) {
408
+ prior++
409
+ }
410
+ if slices.Contains(other.ResliceReviewed, id) {
411
+ reviews++
412
+ }
413
+ }
414
+ round := prior + 1
415
+ if round > wrightRoundsPerReview*(1+reviews) {
416
+ overCap = append(overCap, id)
417
+ }
418
+ lines = append(lines, fmt.Sprintf("dispatch: %s %s round %d (reslice review every %d)", id, kind, round, wrightRoundsPerReview))
419
+ }
420
+ if len(overCap) == 0 {
421
+ if review != "" {
422
+ fmt.Fprintf(stderr, "dispatch: --reslice-review given but no slice has reached its %d-round cap\n", wrightRoundsPerReview)
423
+ return nil, 2
424
+ }
425
+ return lines, 0
426
+ }
427
+ if review == "" {
428
+ fmt.Fprintf(stderr, "dispatch: BLOCKED: %s reached %d %s rounds; run a reslice review (acceptance-preserving split or simpler proof seam, or a drift.md entry recording why neither applies) and reopen with --reslice-review <DRIFT-ID>\n",
429
+ strings.Join(overCap, ","), wrightRoundsPerReview, kind)
430
+ return nil, 3
431
+ }
432
+ for _, other := range f.Waves {
433
+ if other.State != "abandoned" && other.ResliceReview == review && roundKind(other) == kind {
434
+ fmt.Fprintf(stderr, "dispatch: BLOCKED: reslice review %s already admitted another round window\n", review)
435
+ return nil, 3
436
+ }
437
+ }
438
+ drift, err := readWorkspaceArtifact(root, slug, "drift.md")
439
+ if err != nil {
440
+ fmt.Fprintf(stderr, "dispatch: BLOCKED: reslice review %s: %v\n", review, err)
441
+ return nil, 3
442
+ }
443
+ heading := driftHeading(string(drift), review)
444
+ for _, id := range overCap {
445
+ if !regexp.MustCompile(`\b` + regexp.QuoteMeta(id) + `\b`).MatchString(heading) {
446
+ fmt.Fprintf(stderr, "dispatch: BLOCKED: drift.md has no \"## %s\" heading naming %s\n", review, id)
447
+ return nil, 3
448
+ }
449
+ }
450
+ w.ResliceReview, w.ResliceReviewed = review, overCap
451
+ return lines, 0
452
+ }
453
+
454
+ // driftHeading returns the drift.md level-2 heading line for id, or "".
455
+ func driftHeading(drift, id string) string {
456
+ re := regexp.MustCompile(`(?m)^## ` + regexp.QuoteMeta(id) + `\b.*$`)
457
+ return re.FindString(drift)
458
+ }
459
+
325
460
  func waveState(w *dispatchWave) string {
326
461
  if w == nil {
327
462
  return "absent"
@@ -48,7 +48,7 @@ type driftBaseline struct {
48
48
  // driftDiff is one input whose state differs from the baseline.
49
49
  type driftDiff struct {
50
50
  Path string `json:"path"`
51
- Kind string `json:"kind"` // changed | missing | added
51
+ Kind string `json:"kind"` // changed | missing | added | reviewed-identity-only
52
52
  }
53
53
 
54
54
  // RunCheckDrift implements `devrites-engine check drift`.
@@ -130,6 +130,10 @@ func compareDrift(workspace, baselinePath string, digests []gate.ReadinessInputD
130
130
  if baseline.Binding != binding && len(diffs) == 0 {
131
131
  fmt.Fprintln(stdout, "DRIFT: aggregate binding changed without a recorded input diff; re-record the baseline")
132
132
  }
133
+ if len(diffs) == 1 && diffs[0].Kind == "reviewed-identity-only" {
134
+ fmt.Fprintln(stdout, "check drift: reviewed-identity-only — only the Workflow Artifact reviewed identity moved; /rite-vet may use its bytes-only recheck")
135
+ return 0
136
+ }
133
137
  fmt.Fprintf(stdout, "check drift: %d input(s) differ — a change is legal only as a dated delta through the owning rite; otherwise restore the input or rerun /rite-vet and --record\n", len(diffs))
134
138
  return 0
135
139
  }
@@ -189,7 +193,11 @@ func diffInputs(base, current []gate.ReadinessInputDigest) []driftDiff {
189
193
  }
190
194
  diffs = append(diffs, driftDiff{Path: b.Path, Kind: kind})
191
195
  case b.Present && b.SHA256 != c.SHA256:
192
- diffs = append(diffs, driftDiff{Path: b.Path, Kind: "changed"})
196
+ kind := "changed"
197
+ if b.Normalized != "" && b.Normalized == c.Normalized {
198
+ kind = "reviewed-identity-only"
199
+ }
200
+ diffs = append(diffs, driftDiff{Path: b.Path, Kind: kind})
193
201
  }
194
202
  }
195
203
  sort.Slice(diffs, func(i, j int) bool { return diffs[i].Path < diffs[j].Path })
@@ -104,3 +104,32 @@ func TestDriftCorruptBaselineBlocks(t *testing.T) {
104
104
  t.Fatalf("code=%d out=%s", code, out)
105
105
  }
106
106
  }
107
+
108
+ func TestDriftClassifiesReviewedIdentityOnlyChange(t *testing.T) {
109
+ root, featureDir := driftWorkspace(t)
110
+ if code, out := checkDrift(t, root, "feat", "--record"); code != 0 {
111
+ t.Fatalf("record code=%d out=%s", code, out)
112
+ }
113
+ plan := filepath.Join(featureDir, "test-plan.md")
114
+ line := "Workflow Artifact reviewed identity: " + strings.Repeat("ab", 32) + "\n"
115
+ writeFile(t, plan, "# test-plan.md\n\ncontent\n"+line)
116
+ code, out := checkDrift(t, root, "feat")
117
+ if code != 0 || !strings.Contains(out, "test-plan.md reviewed-identity-only") || !strings.Contains(out, "bytes-only recheck") {
118
+ t.Fatalf("identity-only change not classified: code=%d\n%s", code, out)
119
+ }
120
+ // Any other edit, including a malformed identity line, is ordinary drift.
121
+ for _, body := range []string{
122
+ "# test-plan.md\n\nchanged\n" + line,
123
+ "# test-plan.md\n\ncontent\nWorkflow Artifact reviewed identity: not-a-digest\n",
124
+ } {
125
+ writeFile(t, plan, body)
126
+ if _, out := checkDrift(t, root, "feat"); !strings.Contains(out, "test-plan.md changed") || strings.Contains(out, "bytes-only") {
127
+ t.Fatalf("non-identity edit hidden:\n%s", out)
128
+ }
129
+ }
130
+ writeFile(t, plan, "# test-plan.md\n\ncontent\n"+line)
131
+ writeFile(t, filepath.Join(featureDir, "plan.md"), "# plan.md\n\nrewritten\n")
132
+ if _, out := checkDrift(t, root, "feat"); strings.Contains(out, "bytes-only") {
133
+ t.Fatalf("a second changed input must void the bytes-only route:\n%s", out)
134
+ }
135
+ }
@@ -29,6 +29,22 @@ var (
29
29
  windowsDrivePath = regexp.MustCompile(`^[A-Za-z]:[\\/]`)
30
30
  )
31
31
 
32
+ // sliceFileAdvisory is the Writer allowlist size at which a slice is flagged
33
+ // for an acceptance-preserving split. Advisory only: it never changes an exit
34
+ // code. Measured on real plans, slices at or above it averaged ~10 wright
35
+ // rounds versus ~3.5 below it.
36
+ const sliceFileAdvisory = 12
37
+
38
+ // sliceSizeAdvisory returns the Writer allowlist size and whether the slice is
39
+ // oversized without a recorded `Size rationale:`.
40
+ func sliceSizeAdvisory(block string) (int, bool) {
41
+ fields := sliceFields(block)
42
+ raw, _ := fieldValue(fields, "writer allowlist")
43
+ n := len(parsePathList(raw))
44
+ rationale, _ := fieldValue(fields, "size rationale")
45
+ return n, n >= sliceFileAdvisory && rationale == ""
46
+ }
47
+
32
48
  // sliceFields extracts the single-line `Field: value` pairs of a slice block.
33
49
  func sliceFields(block string) map[string]string {
34
50
  fields := map[string]string{}
@@ -169,6 +185,9 @@ func RunCheckSlice(root string, args []string, stdout, stderr io.Writer) int {
169
185
  problems = append(problems, "Tests/proof field missing or empty — no evidence plan for the slice")
170
186
  }
171
187
 
188
+ if n, over := sliceSizeAdvisory(block); over {
189
+ fmt.Fprintf(stdout, "check slice: ADVISORY size: %s has %d Writer allowlist files (>= %d); consider an acceptance-preserving split\n", sliceID, n, sliceFileAdvisory)
190
+ }
172
191
  if len(problems) > 0 {
173
192
  fmt.Fprintf(stdout, "check slice: %s BLOCKED\n", sliceID)
174
193
  for _, p := range problems {
@@ -127,3 +127,44 @@ func TestCheckSliceTouchedComparesNormalizedPaths(t *testing.T) {
127
127
  t.Fatalf("code=%d want 0\n%s%s", code, stdout, stderr)
128
128
  }
129
129
  }
130
+
131
+ func bigSliceTasks(rationale string) string {
132
+ var paths []string
133
+ for i := 1; i <= sliceFileAdvisory; i++ {
134
+ paths = append(paths, "`src/f"+strings.Repeat("x", i)+".go`")
135
+ }
136
+ return "# Tasks\n\n## SLICE-001 Big\nGoal: do it.\nSatisfies: AC-001\ndepends_on: []\nWriter allowlist: " +
137
+ strings.Join(paths, ", ") + "\nTests/proof: go test ./...\n" + rationale +
138
+ "\n## SLICE-002 Small\nGoal: tiny.\nSatisfies: AC-001\ndepends_on: [SLICE-001]\nWriter allowlist: `src/a.go`\nTests/proof: go test ./...\n"
139
+ }
140
+
141
+ func TestSliceSizeAdvisoryNeverBlocks(t *testing.T) {
142
+ root := sliceWorkspace(t, bigSliceTasks(""), "## Acceptance criteria\n- AC-001 works\n")
143
+ stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{}
144
+ if code := RunCheckSlice(root, []string{"feat", "SLICE-001"}, stdout, stderr); code != 0 {
145
+ t.Fatalf("advisory must not block: code=%d\n%s%s", code, stdout, stderr)
146
+ }
147
+ if !strings.Contains(stdout.String(), "ADVISORY size: SLICE-001 has 12 Writer allowlist files") {
148
+ t.Fatalf("advisory missing:\n%s", stdout)
149
+ }
150
+ stdout.Reset()
151
+ if code := RunTaskGraphCheck(root, "feat", stdout, stderr); code != 0 {
152
+ t.Fatalf("task-graph code=%d\n%s%s", code, stdout, stderr)
153
+ }
154
+ out := stdout.String()
155
+ if !strings.Contains(out, "task-graph: ADVISORY size: SLICE-001 has 12") || strings.Contains(out, "SLICE-002 has") {
156
+ t.Fatalf("task-graph advisory wrong:\n%s", out)
157
+ }
158
+ stdout.Reset()
159
+ if code := RunMetrics(root, []string{"summary", "feat"}, stdout, stderr); code != 0 || !strings.Contains(stdout.String(), "size-advisory=1") {
160
+ t.Fatalf("size-advisory metric not recorded: code=%d\n%s", code, stdout)
161
+ }
162
+ }
163
+
164
+ func TestSliceSizeRationaleSilencesAdvisory(t *testing.T) {
165
+ root := sliceWorkspace(t, bigSliceTasks("Size rationale: one generated golden set moves together\n"), "## Acceptance criteria\n- AC-001 works\n")
166
+ stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{}
167
+ if code := RunTaskGraphCheck(root, "feat", stdout, stderr); code != 0 || strings.Contains(stdout.String(), "ADVISORY") {
168
+ t.Fatalf("rationale must silence advisory: code=%d\n%s", code, stdout)
169
+ }
170
+ }
@@ -4,6 +4,7 @@ import (
4
4
  "fmt"
5
5
  "io"
6
6
  "os"
7
+ "path"
7
8
  "path/filepath"
8
9
  "regexp"
9
10
  "strconv"
@@ -12,17 +13,21 @@ import (
12
13
  "github.com/devrites/devrites/internal/devritespaths"
13
14
  )
14
15
 
15
- // windows.go — the broken-windows check. Review prose flags a new TODO, stub, or
16
- // skipped test as a finding; this gate makes the register countable instead: any
17
- // deferral marker the change *introduces* must carry a recorded waiver row in the
18
- // workspace's windows.md, or the check fails. A silent deferral is the bug.
16
+ // windows.go — the broken-windows check. Review prose flags a new TODO, stub,
17
+ // skipped or focused test, checker suppression, swallowed error, deleted test
18
+ // file, or stripped assertion as a finding; this gate makes the register
19
+ // countable instead: any such marker the change *introduces* must carry a
20
+ // recorded waiver row in the workspace's windows.md, or the check fails. A
21
+ // silent deferral or lowered bar is the bug.
19
22
  //
20
23
  // devrites-engine check windows <slug> [--worktree|--staged|--base <ref>] [--cwd <dir>]
21
24
  //
22
25
  // 0 no unwaived markers 2 usage error 3 unwaived markers or blocked
23
26
  //
24
27
  // Only added lines count: a marker already on the baseline is pre-existing debt,
25
- // never attributable to this change. Each windows.md row waives exactly one hit —
28
+ // never attributable to this change. Removals count once per file: a deleted
29
+ // test file (`deleted-test`) or a net loss of assertion lines in a test file
30
+ // that remains (`removed-assertion`). Each windows.md row waives exactly one hit —
26
31
  // one blanket row cannot cover a file's five new TODOs.
27
32
  //
28
33
  // windows.md row grammar (one waiver per line):
@@ -34,8 +39,10 @@ import (
34
39
 
35
40
  const windowsUsage = `usage: devrites-engine check windows <slug> [--worktree|--staged|--base <ref>] [--cwd <dir>]
36
41
 
37
- Reports deferral markers introduced by the change (added lines only) and requires
38
- each to be waived by a row in .devrites/work/<slug>/windows.md:
42
+ Reports deferral and bar-lowering markers introduced by the change (added lines:
43
+ TODO/stub/skip/focus idioms, checker suppressions, empty-catch; removals:
44
+ deleted-test, removed-assertion) and requires each to be waived by a row in
45
+ .devrites/work/<slug>/windows.md:
39
46
  - <path> | <marker> | <reason>
40
47
  Each row waives one hit at that path with that marker token.
41
48
  Exit 0 clean or fully waived, 3 unwaived markers or blocked.`
@@ -55,8 +62,22 @@ var deferralIdioms = []string{
55
62
  "panic(\"TODO", "panic(\"unimplemented", "panic(\"not implemented",
56
63
  "throw new Error(\"unimplemented", "throw new Error(\"not implemented",
57
64
  "TODO: implement", "not yet implemented",
65
+ // Focused tests and checker suppressions lower the bar the same way.
66
+ "it.only(", "describe.only(", "test.only(",
67
+ "@ts-ignore", "@ts-expect-error", "@ts-nocheck", "eslint-disable",
68
+ "biome-ignore", "deno-lint-ignore", "istanbul ignore", "c8 ignore",
69
+ "noqa", "type: ignore", "pylint: disable", "pragma: no cover",
70
+ "nolint", "#nosec", "nosemgrep", "NOSONAR", "gitleaks:allow",
71
+ "rubocop:disable", "@SuppressWarnings", "#[allow(", "shellcheck disable",
72
+ "Stryker disable",
58
73
  }
59
74
 
75
+ // emptyCatchRe catches a one-line swallowed error: `catch (e) {}`, `except: pass`.
76
+ var emptyCatchRe = regexp.MustCompile(`\bcatch\s*(?:\([^)]*\))?\s*\{\s*\}|\bexcept\b[^:\n]*:\s*pass\b`)
77
+
78
+ // assertionRe matches a line carrying a test assertion in common frameworks.
79
+ var assertionRe = regexp.MustCompile(`(?i)\bassert|XCTAssert|\bexpect\(|\bt\.(?:error|fatal)|\brequire\.|\.should\b`)
80
+
60
81
  type windowHit struct {
61
82
  Path string
62
83
  Line int
@@ -64,6 +85,14 @@ type windowHit struct {
64
85
  Text string
65
86
  }
66
87
 
88
+ // location is path:line, or the bare path for a file-level hit (Line 0).
89
+ func (h windowHit) location() string {
90
+ if h.Line == 0 {
91
+ return h.Path
92
+ }
93
+ return fmt.Sprintf("%s:%d", h.Path, h.Line)
94
+ }
95
+
67
96
  type windowWaiver struct {
68
97
  Path string
69
98
  Marker string
@@ -92,7 +121,73 @@ func windowsMarkers(line string) []string {
92
121
  out = append(out, m[1])
93
122
  }
94
123
  }
95
- return append(out, idioms...)
124
+ out = append(out, idioms...)
125
+ if emptyCatchRe.MatchString(line) {
126
+ out = append(out, "empty-catch")
127
+ }
128
+ return out
129
+ }
130
+
131
+ // isTestPath reports whether a project-relative path names a test source file.
132
+ func isTestPath(p string) bool {
133
+ base := path.Base(p)
134
+ for _, suffix := range []string{"_test.go", "_test.py", "_spec.rb", "Test.java", "Tests.java", "Test.kt", "Test.cs", "Tests.cs"} {
135
+ if strings.HasSuffix(base, suffix) {
136
+ return true
137
+ }
138
+ }
139
+ return strings.Contains(base, ".test.") || strings.Contains(base, ".spec.") ||
140
+ strings.HasPrefix(base, "test_") && strings.HasSuffix(base, ".py") ||
141
+ strings.Contains("/"+p, "/__tests__/") ||
142
+ strings.Contains("/"+p, "/tests/") && strings.HasSuffix(base, ".rs")
143
+ }
144
+
145
+ // removedTestHits reports bar-lowering removals in a `git diff -U0` stream: a
146
+ // deleted test file, or a test file that still exists but lost assertion lines
147
+ // on net (moving an assertion within the file is not a loss). Renames are not
148
+ // deletions when git rename detection pairs them.
149
+ func removedTestHits(diff string) []windowHit {
150
+ var hits []windowHit
151
+ net := map[string]int{}
152
+ var order []string
153
+ var oldPath, file string
154
+ inHeader, deleted := false, false
155
+ for _, line := range strings.Split(diff, "\n") {
156
+ switch {
157
+ case strings.HasPrefix(line, "diff --git "):
158
+ inHeader, deleted, file = true, false, ""
159
+ case inHeader && strings.HasPrefix(line, "--- "):
160
+ oldPath = strings.TrimPrefix(strings.Trim(strings.TrimSpace(line[4:]), `"`), "a/")
161
+ case inHeader && strings.HasPrefix(line, "+++ "):
162
+ newPath := strings.Trim(strings.TrimSpace(line[4:]), `"`)
163
+ if newPath == "/dev/null" {
164
+ deleted = true
165
+ if isTestPath(oldPath) {
166
+ hits = append(hits, windowHit{Path: oldPath, Marker: "deleted-test", Text: "test file deleted"})
167
+ }
168
+ continue
169
+ }
170
+ if file = strings.TrimPrefix(newPath, "b/"); !isTestPath(file) {
171
+ file = ""
172
+ }
173
+ case strings.HasPrefix(line, "@@"):
174
+ inHeader = false
175
+ case inHeader || deleted || file == "":
176
+ case strings.HasPrefix(line, "-") && assertionRe.MatchString(line[1:]):
177
+ if _, seen := net[file]; !seen {
178
+ order = append(order, file)
179
+ }
180
+ net[file]++
181
+ case strings.HasPrefix(line, "+") && assertionRe.MatchString(line[1:]):
182
+ net[file]--
183
+ }
184
+ }
185
+ for _, f := range order {
186
+ if n := net[f]; n > 0 {
187
+ hits = append(hits, windowHit{Path: f, Marker: "removed-assertion", Text: fmt.Sprintf("net -%d assertion line(s)", n)})
188
+ }
189
+ }
190
+ return hits
96
191
  }
97
192
 
98
193
  // addedLinesFromDiff parses a `git diff -U0` stream into added (path, line,
@@ -180,17 +275,18 @@ func windowsHits(opts diffScopeOpts) ([]windowHit, error) {
180
275
  var diffArgs []string
181
276
  switch opts.mode {
182
277
  case "staged":
183
- diffArgs = []string{"diff", "--cached", "-U0", "--"}
278
+ diffArgs = []string{"diff", "--cached", "-M", "-U0", "--"}
184
279
  case "base":
185
- diffArgs = []string{"diff", opts.base, "-U0", "--"}
280
+ diffArgs = []string{"diff", opts.base, "-M", "-U0", "--"}
186
281
  default:
187
- diffArgs = []string{"diff", "HEAD", "-U0", "--"}
282
+ diffArgs = []string{"diff", "HEAD", "-M", "-U0", "--"}
188
283
  }
189
284
  diff, err := runGitCommand(dir, nil, diffArgs...)
190
285
  var diffPaths map[string]bool
191
286
  switch {
192
287
  case err == nil:
193
288
  hits, diffPaths = addedLinesFromDiff(string(diff))
289
+ hits = append(hits, removedTestHits(string(diff))...)
194
290
  case opts.mode == "worktree" && isUnbornHEAD(dir):
195
291
  // No commits yet: every changed file is wholly new.
196
292
  diffPaths = map[string]bool{}
@@ -334,11 +430,11 @@ func RunCheckWindows(root string, args []string, stdout, stderr io.Writer) int {
334
430
  if waivers[w] > 0 {
335
431
  waivers[w]--
336
432
  waived++
337
- fmt.Fprintf(stdout, "windows: waived: %s:%d %s\n", h.Path, h.Line, h.Marker)
433
+ fmt.Fprintf(stdout, "windows: waived: %s %s\n", h.location(), h.Marker)
338
434
  continue
339
435
  }
340
436
  unwaived++
341
- fmt.Fprintf(stdout, "windows: unwaived: %s:%d %s — %s\n", h.Path, h.Line, h.Marker, h.Text)
437
+ fmt.Fprintf(stdout, "windows: unwaived: %s %s — %s\n", h.location(), h.Marker, h.Text)
342
438
  }
343
439
  switch {
344
440
  case len(hits) == 0:
@@ -57,6 +57,12 @@ func TestWindowsMarkersTokensAndIdioms(t *testing.T) {
57
57
  {"describe.skip(\"x\", fn)", []string{"describe.skip("}},
58
58
  {"raise NotImplementedError", []string{"NotImplementedError"}},
59
59
  {"panic(\"TODO wire cache\")", []string{"panic(\"TODO"}},
60
+ {"// @ts-ignore", []string{"@ts-ignore"}},
61
+ {"x = f() # noqa: E501", []string{"noqa"}},
62
+ {"it.only(\"focus\", fn)", []string{"it.only("}},
63
+ {"try { f() } catch (e) {}", []string{"empty-catch"}},
64
+ {"except ValueError: pass", []string{"empty-catch"}},
65
+ {"} catch (e) { log(e) }", nil},
60
66
  {"plain line", nil},
61
67
  }
62
68
  for _, tc := range cases {
@@ -333,3 +339,82 @@ func TestCheckWindowsFailsWhenStatusScanFails(t *testing.T) {
333
339
  t.Fatalf("status failure must be reported on stderr: err=%q", errBuf.String())
334
340
  }
335
341
  }
342
+
343
+ func TestRemovedTestHits(t *testing.T) {
344
+ diff := "diff --git a/old_test.go b/old_test.go\ndeleted file mode 100644\n--- a/old_test.go\n+++ /dev/null\n@@ -1,2 +0,0 @@\n-package a\n-\tassert(true)\n" +
345
+ "diff --git a/a_test.go b/a_test.go\n--- a/a_test.go\n+++ b/a_test.go\n@@ -3,2 +3,0 @@\n-\tif got != 1 { t.Fatalf(\"got %d\", got) }\n-\trequire.NoError(t, err)\n" +
346
+ "diff --git a/moved.test.ts b/moved.test.ts\n--- a/moved.test.ts\n+++ b/moved.test.ts\n@@ -2 +2,0 @@\n- expect(x).toBe(1)\n@@ -9,0 +9 @@\n+ expect(x).toBe(1)\n" +
347
+ "diff --git a/lib.go b/lib.go\n--- a/lib.go\n+++ b/lib.go\n@@ -1 +0,0 @@\n-\tassert(ok)\n" +
348
+ "diff --git a/gone.go b/gone.go\ndeleted file mode 100644\n--- a/gone.go\n+++ /dev/null\n@@ -1 +0,0 @@\n-package a\n"
349
+ hits := removedTestHits(diff)
350
+ if len(hits) != 2 {
351
+ t.Fatalf("hits=%+v", hits)
352
+ }
353
+ if hits[0].Path != "old_test.go" || hits[0].Marker != "deleted-test" {
354
+ t.Fatalf("deleted test hit=%+v", hits[0])
355
+ }
356
+ if hits[1].Path != "a_test.go" || hits[1].Marker != "removed-assertion" || !strings.Contains(hits[1].Text, "net -2") {
357
+ t.Fatalf("removed assertion hit=%+v", hits[1])
358
+ }
359
+ }
360
+
361
+ func TestIsTestPath(t *testing.T) {
362
+ for p, want := range map[string]bool{
363
+ "pkg/a_test.go": true, "src/a.test.ts": true, "src/a.spec.js": true,
364
+ "tests/test_api.py": true, "spec/user_spec.rb": true, "src/__tests__/a.js": true,
365
+ "FooTest.java": true, "tests/integration.rs": true,
366
+ "pkg/a.go": false, "src/latest.ts": false, "tests/fixture.json": false, "contest.py": false,
367
+ } {
368
+ if got := isTestPath(p); got != want {
369
+ t.Fatalf("isTestPath(%q)=%v want %v", p, got, want)
370
+ }
371
+ }
372
+ }
373
+
374
+ func TestCheckWindowsBlocksLoweredBarUntilWaived(t *testing.T) {
375
+ project := t.TempDir()
376
+ initWindowsRepo(t, project)
377
+ root := windowsWorkspace(t, project, "feat")
378
+ testutil.WriteFile(t, filepath.Join(project, "a_test.go"), "package a\n\nfunc TestA(t *testing.T) {\n\tif A() != 1 {\n\t\tt.Fatal(\"bad\")\n\t}\n}\n")
379
+ testutil.WriteFile(t, filepath.Join(project, "b_test.go"), "package a\n")
380
+ runSecretScanGit(t, project, "add", "--", "a_test.go", "b_test.go")
381
+ runSecretScanGit(t, project, "commit", "-qm", "baseline")
382
+ testutil.WriteFile(t, filepath.Join(project, "a_test.go"), "package a\n\nfunc TestA(t *testing.T) {\n\t_ = A()\n}\n")
383
+ if err := os.Remove(filepath.Join(project, "b_test.go")); err != nil {
384
+ t.Fatal(err)
385
+ }
386
+
387
+ var out, errBuf bytes.Buffer
388
+ if code := RunCheckWindows(root, []string{"feat"}, &out, &errBuf); code != 3 {
389
+ t.Fatalf("code=%d out=%q err=%q", code, out.String(), errBuf.String())
390
+ }
391
+ for _, want := range []string{"unwaived: a_test.go removed-assertion", "unwaived: b_test.go deleted-test"} {
392
+ if !strings.Contains(out.String(), want) {
393
+ t.Fatalf("missing %q in out=%q", want, out.String())
394
+ }
395
+ }
396
+
397
+ testutil.WriteFile(t, filepath.Join(root, "work", "feat", "windows.md"),
398
+ "- a_test.go | removed-assertion | assertion moved to TestB in SLICE-002\n- b_test.go | deleted-test | feature removed per spec AC-3\n")
399
+ out.Reset()
400
+ if code := RunCheckWindows(root, []string{"feat"}, &out, &errBuf); code != 0 {
401
+ t.Fatalf("waived: code=%d out=%q", code, out.String())
402
+ }
403
+ }
404
+
405
+ func TestCheckWindowsRenamedTestIsNotDeleted(t *testing.T) {
406
+ project := t.TempDir()
407
+ initWindowsRepo(t, project)
408
+ root := windowsWorkspace(t, project, "feat")
409
+ runSecretScanGit(t, project, "config", "diff.renames", "false")
410
+ body := "package a\n\nfunc TestA(t *testing.T) {\n\tif A() != 1 {\n\t\tt.Fatal(\"bad\")\n\t}\n}\n"
411
+ testutil.WriteFile(t, filepath.Join(project, "a_test.go"), body)
412
+ runSecretScanGit(t, project, "add", "--", "a_test.go")
413
+ runSecretScanGit(t, project, "commit", "-qm", "baseline")
414
+ runSecretScanGit(t, project, "mv", "a_test.go", "b_test.go")
415
+
416
+ var out, errBuf bytes.Buffer
417
+ if code := RunCheckWindows(root, []string{"feat"}, &out, &errBuf); code != 0 {
418
+ t.Fatalf("rename flagged: code=%d out=%q err=%q", code, out.String(), errBuf.String())
419
+ }
420
+ }