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.
- package/CHANGELOG.md +16 -0
- package/README.md +15 -64
- package/SECURITY.md +7 -11
- package/bin/devrites.mjs +0 -8
- package/docs/adr/0034-workflow-artifact-byte-corrections-get-code-review.md +60 -0
- package/docs/adr/README.md +2 -1
- package/docs/architecture.md +2 -5
- package/docs/cli.md +4 -11
- package/docs/command-map.md +1 -2
- package/docs/engine/commands.md +6 -6
- package/docs/release.md +5 -6
- package/engine/help.go +12 -7
- package/engine/help_test.go +1 -0
- package/engine/internal/gate/readiness_binding.go +19 -0
- package/engine/internal/lib/cli_observe.go +16 -0
- package/engine/internal/lib/context.go +191 -19
- package/engine/internal/lib/context_next_test.go +186 -0
- package/engine/internal/lib/dispatch.go +146 -11
- package/engine/internal/lib/drift.go +10 -2
- package/engine/internal/lib/drift_test.go +29 -0
- package/engine/internal/lib/slicecheck.go +19 -0
- package/engine/internal/lib/slicecheck_test.go +41 -0
- package/engine/internal/lib/windows.go +109 -13
- package/engine/internal/lib/windows_test.go +85 -0
- package/engine/internal/release/release.go +3 -13
- package/engine/internal/release/release_test.go +6 -197
- package/engine/main.go +2 -2
- package/install.sh +1 -116
- package/pack/.claude/agents/devrites-slice-wright.md +3 -1
- package/pack/.claude/skills/devrites-debug-recovery/reference/hypotheses.md +6 -0
- package/pack/.claude/skills/devrites-lib/reference/parallel-dispatch.md +1 -0
- package/pack/.claude/skills/devrites-lib/reference/standards/README.md +1 -1
- package/pack/.claude/skills/devrites-lib/reference/standards/afk-hitl.md +2 -0
- package/pack/.claude/skills/devrites-lib/reference/standards/code-review.md +3 -0
- package/pack/.claude/skills/devrites-lib/reference/standards/glossary.md +1 -1
- package/pack/.claude/skills/devrites-lib/reference/standards/security.md +1 -0
- package/pack/.claude/skills/devrites-lib/reference/standards/windows.md +11 -3
- package/pack/.claude/skills/devrites-lib/reference/workspace-artifact-schema.md +5 -2
- package/pack/.claude/skills/rite-build/SKILL.md +1 -1
- package/pack/.claude/skills/rite-build/reference/one-slice-cycle.md +3 -2
- package/pack/.claude/skills/rite-build/reference/parallel-batch.md +5 -3
- package/pack/.claude/skills/rite-build/reference/phase-contract.md +5 -3
- package/pack/.claude/skills/rite-build/reference/spec-drift-guard.md +3 -1
- package/pack/.claude/skills/rite-build/reference/wright-dispatch.md +23 -9
- package/pack/.claude/skills/rite-define/SKILL.md +2 -1
- package/pack/.claude/skills/rite-plan/SKILL.md +2 -1
- package/pack/.claude/skills/rite-pressure-test/SKILL.md +6 -3
- package/pack/.claude/skills/rite-vet/SKILL.md +9 -1
- package/pack/.claude/skills/rite-vet/reference/review-axes.md +6 -2
- package/pack/generated/claude/agents/devrites-slice-wright.md +3 -1
- package/pack/generated/claude/skills/devrites-debug-recovery/reference/hypotheses.md +6 -0
- package/pack/generated/claude/skills/devrites-lib/reference/parallel-dispatch.md +1 -0
- package/pack/generated/claude/skills/devrites-lib/reference/standards/README.md +1 -1
- package/pack/generated/claude/skills/devrites-lib/reference/standards/afk-hitl.md +2 -0
- package/pack/generated/claude/skills/devrites-lib/reference/standards/code-review.md +3 -0
- package/pack/generated/claude/skills/devrites-lib/reference/standards/glossary.md +1 -1
- package/pack/generated/claude/skills/devrites-lib/reference/standards/security.md +1 -0
- package/pack/generated/claude/skills/devrites-lib/reference/standards/windows.md +11 -3
- package/pack/generated/claude/skills/devrites-lib/reference/workspace-artifact-schema.md +5 -2
- package/pack/generated/claude/skills/rite-build/SKILL.md +1 -1
- package/pack/generated/claude/skills/rite-build/reference/one-slice-cycle.md +3 -2
- package/pack/generated/claude/skills/rite-build/reference/parallel-batch.md +5 -3
- package/pack/generated/claude/skills/rite-build/reference/phase-contract.md +5 -3
- package/pack/generated/claude/skills/rite-build/reference/spec-drift-guard.md +3 -1
- package/pack/generated/claude/skills/rite-build/reference/wright-dispatch.md +23 -9
- package/pack/generated/claude/skills/rite-define/SKILL.md +2 -1
- package/pack/generated/claude/skills/rite-plan/SKILL.md +2 -1
- package/pack/generated/claude/skills/rite-pressure-test/SKILL.md +6 -3
- package/pack/generated/claude/skills/rite-vet/SKILL.md +9 -1
- package/pack/generated/claude/skills/rite-vet/reference/review-axes.md +6 -2
- package/pack/generated/codex/agents/devrites-slice-wright.toml +3 -1
- package/pack/generated/codex/skills/devrites-debug-recovery/reference/hypotheses.md +6 -0
- package/pack/generated/codex/skills/devrites-lib/reference/parallel-dispatch.md +1 -0
- package/pack/generated/codex/skills/devrites-lib/reference/standards/README.md +1 -1
- package/pack/generated/codex/skills/devrites-lib/reference/standards/afk-hitl.md +2 -0
- package/pack/generated/codex/skills/devrites-lib/reference/standards/code-review.md +3 -0
- package/pack/generated/codex/skills/devrites-lib/reference/standards/glossary.md +1 -1
- package/pack/generated/codex/skills/devrites-lib/reference/standards/security.md +1 -0
- package/pack/generated/codex/skills/devrites-lib/reference/standards/windows.md +11 -3
- package/pack/generated/codex/skills/devrites-lib/reference/workspace-artifact-schema.md +5 -2
- package/pack/generated/codex/skills/rite-build/SKILL.md +1 -1
- package/pack/generated/codex/skills/rite-build/reference/one-slice-cycle.md +3 -2
- package/pack/generated/codex/skills/rite-build/reference/parallel-batch.md +5 -3
- package/pack/generated/codex/skills/rite-build/reference/phase-contract.md +5 -3
- package/pack/generated/codex/skills/rite-build/reference/spec-drift-guard.md +3 -1
- package/pack/generated/codex/skills/rite-build/reference/wright-dispatch.md +23 -9
- package/pack/generated/codex/skills/rite-define/SKILL.md +2 -1
- package/pack/generated/codex/skills/rite-plan/SKILL.md +2 -1
- package/pack/generated/codex/skills/rite-pressure-test/SKILL.md +6 -3
- package/pack/generated/codex/skills/rite-vet/SKILL.md +9 -1
- package/pack/generated/codex/skills/rite-vet/reference/review-axes.md +6 -2
- package/pack/generated/devin/agents/devrites-slice-wright.md +3 -1
- package/pack/generated/devin/skills/devrites-debug-recovery/reference/hypotheses.md +6 -0
- package/pack/generated/devin/skills/devrites-lib/reference/parallel-dispatch.md +1 -0
- package/pack/generated/devin/skills/devrites-lib/reference/standards/README.md +1 -1
- package/pack/generated/devin/skills/devrites-lib/reference/standards/afk-hitl.md +2 -0
- package/pack/generated/devin/skills/devrites-lib/reference/standards/code-review.md +3 -0
- package/pack/generated/devin/skills/devrites-lib/reference/standards/glossary.md +1 -1
- package/pack/generated/devin/skills/devrites-lib/reference/standards/security.md +1 -0
- package/pack/generated/devin/skills/devrites-lib/reference/standards/windows.md +11 -3
- package/pack/generated/devin/skills/devrites-lib/reference/workspace-artifact-schema.md +5 -2
- package/pack/generated/devin/skills/rite-build/SKILL.md +1 -1
- package/pack/generated/devin/skills/rite-build/reference/one-slice-cycle.md +3 -2
- package/pack/generated/devin/skills/rite-build/reference/parallel-batch.md +5 -3
- package/pack/generated/devin/skills/rite-build/reference/phase-contract.md +5 -3
- package/pack/generated/devin/skills/rite-build/reference/spec-drift-guard.md +3 -1
- package/pack/generated/devin/skills/rite-build/reference/wright-dispatch.md +23 -9
- package/pack/generated/devin/skills/rite-define/SKILL.md +2 -1
- package/pack/generated/devin/skills/rite-plan/SKILL.md +2 -1
- package/pack/generated/devin/skills/rite-pressure-test/SKILL.md +6 -3
- package/pack/generated/devin/skills/rite-vet/SKILL.md +9 -1
- package/pack/generated/devin/skills/rite-vet/reference/review-axes.md +6 -2
- package/pack/generated/omp/agents/devrites-slice-wright.md +3 -1
- package/pack/generated/omp/skills/devrites-debug-recovery/reference/hypotheses.md +6 -0
- package/pack/generated/omp/skills/devrites-lib/reference/parallel-dispatch.md +1 -0
- package/pack/generated/omp/skills/devrites-lib/reference/standards/README.md +1 -1
- package/pack/generated/omp/skills/devrites-lib/reference/standards/afk-hitl.md +2 -0
- package/pack/generated/omp/skills/devrites-lib/reference/standards/code-review.md +3 -0
- package/pack/generated/omp/skills/devrites-lib/reference/standards/glossary.md +1 -1
- package/pack/generated/omp/skills/devrites-lib/reference/standards/security.md +1 -0
- package/pack/generated/omp/skills/devrites-lib/reference/standards/windows.md +11 -3
- package/pack/generated/omp/skills/devrites-lib/reference/workspace-artifact-schema.md +5 -2
- package/pack/generated/omp/skills/rite-build/SKILL.md +1 -1
- package/pack/generated/omp/skills/rite-build/reference/one-slice-cycle.md +3 -2
- package/pack/generated/omp/skills/rite-build/reference/parallel-batch.md +5 -3
- package/pack/generated/omp/skills/rite-build/reference/phase-contract.md +5 -3
- package/pack/generated/omp/skills/rite-build/reference/spec-drift-guard.md +3 -1
- package/pack/generated/omp/skills/rite-build/reference/wright-dispatch.md +23 -9
- package/pack/generated/omp/skills/rite-define/SKILL.md +2 -1
- package/pack/generated/omp/skills/rite-plan/SKILL.md +2 -1
- package/pack/generated/omp/skills/rite-pressure-test/SKILL.md +6 -3
- package/pack/generated/omp/skills/rite-vet/SKILL.md +9 -1
- package/pack/generated/omp/skills/rite-vet/reference/review-axes.md +6 -2
- package/pack/generated/pi/agents/devrites-slice-wright.md +3 -1
- package/pack/generated/pi/skills/devrites-debug-recovery/reference/hypotheses.md +6 -0
- package/pack/generated/pi/skills/devrites-lib/reference/parallel-dispatch.md +1 -0
- package/pack/generated/pi/skills/devrites-lib/reference/standards/README.md +1 -1
- package/pack/generated/pi/skills/devrites-lib/reference/standards/afk-hitl.md +2 -0
- package/pack/generated/pi/skills/devrites-lib/reference/standards/code-review.md +3 -0
- package/pack/generated/pi/skills/devrites-lib/reference/standards/glossary.md +1 -1
- package/pack/generated/pi/skills/devrites-lib/reference/standards/security.md +1 -0
- package/pack/generated/pi/skills/devrites-lib/reference/standards/windows.md +11 -3
- package/pack/generated/pi/skills/devrites-lib/reference/workspace-artifact-schema.md +5 -2
- package/pack/generated/pi/skills/rite-build/SKILL.md +1 -1
- package/pack/generated/pi/skills/rite-build/reference/one-slice-cycle.md +3 -2
- package/pack/generated/pi/skills/rite-build/reference/parallel-batch.md +5 -3
- package/pack/generated/pi/skills/rite-build/reference/phase-contract.md +5 -3
- package/pack/generated/pi/skills/rite-build/reference/spec-drift-guard.md +3 -1
- package/pack/generated/pi/skills/rite-build/reference/wright-dispatch.md +23 -9
- package/pack/generated/pi/skills/rite-define/SKILL.md +2 -1
- package/pack/generated/pi/skills/rite-plan/SKILL.md +2 -1
- package/pack/generated/pi/skills/rite-pressure-test/SKILL.md +6 -3
- package/pack/generated/pi/skills/rite-vet/SKILL.md +9 -1
- package/pack/generated/pi/skills/rite-vet/reference/review-axes.md +6 -2
- package/package.json +1 -1
- 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
|
|
37
|
-
State
|
|
38
|
-
Reason
|
|
39
|
-
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
|
-
|
|
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,
|
|
16
|
-
// skipped
|
|
17
|
-
//
|
|
18
|
-
//
|
|
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.
|
|
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
|
|
38
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
+
}
|