devrites 6.0.2 → 6.0.4
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 +1 -1
- package/docs/adr/0034-workflow-artifact-byte-corrections-get-code-review.md +60 -0
- package/docs/adr/README.md +2 -1
- package/docs/engine/commands.md +8 -8
- 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 +197 -19
- package/engine/internal/lib/context_next_test.go +238 -0
- package/engine/internal/lib/dispatch.go +189 -15
- package/engine/internal/lib/drift.go +10 -2
- package/engine/internal/lib/drift_test.go +29 -0
- package/engine/internal/lib/metrics.go +74 -1
- package/engine/internal/lib/observe_budgets_test.go +32 -0
- package/engine/internal/lib/observe_summary.go +8 -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/main.go +3 -3
- package/pack/.claude/agents/devrites-slice-wright.md +7 -2
- 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/afk-discipline.md +3 -1
- package/pack/.claude/skills/rite-build/reference/one-slice-cycle.md +3 -2
- package/pack/.claude/skills/rite-build/reference/output.md +1 -1
- package/pack/.claude/skills/rite-build/reference/parallel-batch.md +5 -3
- package/pack/.claude/skills/rite-build/reference/phase-contract.md +7 -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 +7 -2
- 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/afk-discipline.md +3 -1
- package/pack/generated/claude/skills/rite-build/reference/one-slice-cycle.md +3 -2
- package/pack/generated/claude/skills/rite-build/reference/output.md +1 -1
- package/pack/generated/claude/skills/rite-build/reference/parallel-batch.md +5 -3
- package/pack/generated/claude/skills/rite-build/reference/phase-contract.md +7 -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 +7 -2
- 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/afk-discipline.md +3 -1
- package/pack/generated/codex/skills/rite-build/reference/one-slice-cycle.md +3 -2
- package/pack/generated/codex/skills/rite-build/reference/output.md +1 -1
- package/pack/generated/codex/skills/rite-build/reference/parallel-batch.md +5 -3
- package/pack/generated/codex/skills/rite-build/reference/phase-contract.md +7 -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 +7 -2
- 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/afk-discipline.md +3 -1
- package/pack/generated/devin/skills/rite-build/reference/one-slice-cycle.md +3 -2
- package/pack/generated/devin/skills/rite-build/reference/output.md +1 -1
- package/pack/generated/devin/skills/rite-build/reference/parallel-batch.md +5 -3
- package/pack/generated/devin/skills/rite-build/reference/phase-contract.md +7 -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 +7 -2
- 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/afk-discipline.md +3 -1
- package/pack/generated/omp/skills/rite-build/reference/one-slice-cycle.md +3 -2
- package/pack/generated/omp/skills/rite-build/reference/output.md +1 -1
- package/pack/generated/omp/skills/rite-build/reference/parallel-batch.md +5 -3
- package/pack/generated/omp/skills/rite-build/reference/phase-contract.md +7 -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 +7 -2
- 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/afk-discipline.md +3 -1
- package/pack/generated/pi/skills/rite-build/reference/one-slice-cycle.md +3 -2
- package/pack/generated/pi/skills/rite-build/reference/output.md +1 -1
- package/pack/generated/pi/skills/rite-build/reference/parallel-batch.md +5 -3
- package/pack/generated/pi/skills/rite-build/reference/phase-contract.md +7 -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
|
@@ -7,6 +7,7 @@ import (
|
|
|
7
7
|
"io"
|
|
8
8
|
"os"
|
|
9
9
|
"path/filepath"
|
|
10
|
+
"slices"
|
|
10
11
|
"sort"
|
|
11
12
|
"strconv"
|
|
12
13
|
"strings"
|
|
@@ -23,7 +24,7 @@ const MetricsFile = "metrics.jsonl"
|
|
|
23
24
|
const metricsUsage = `usage: devrites-engine metrics <record|summary> ...
|
|
24
25
|
|
|
25
26
|
metrics record <slug> --phase <p> --event <e> [--role r] [--bytes n] [--note s]
|
|
26
|
-
metrics summary [slug]
|
|
27
|
+
metrics summary [slug] [--by-slice]
|
|
27
28
|
`
|
|
28
29
|
|
|
29
30
|
type metricEvent struct {
|
|
@@ -150,6 +151,8 @@ func recordMetric(root, slug, phase, event, role string, bytes int64) {
|
|
|
150
151
|
}
|
|
151
152
|
|
|
152
153
|
func metricsSummary(root string, args []string, stdout, stderr io.Writer) int {
|
|
154
|
+
bySlice := slices.Contains(args, "--by-slice")
|
|
155
|
+
args = slices.DeleteFunc(slices.Clone(args), func(a string) bool { return a == "--by-slice" })
|
|
153
156
|
slug, code, err := ActiveSlug(root, args)
|
|
154
157
|
if err != nil {
|
|
155
158
|
fmt.Fprintf(stderr, "metrics: %v\n", err)
|
|
@@ -163,6 +166,9 @@ func metricsSummary(root string, args []string, stdout, stderr io.Writer) int {
|
|
|
163
166
|
fmt.Fprintf(stderr, "metrics: %v\n", err)
|
|
164
167
|
return 2
|
|
165
168
|
}
|
|
169
|
+
if bySlice {
|
|
170
|
+
return sliceCostSummary(root, slug, stdout, stderr)
|
|
171
|
+
}
|
|
166
172
|
f, err := os.Open(filepath.Join(dir, MetricsFile)) // #nosec G304 -- dir is a validated workspace and filename is fixed
|
|
167
173
|
if err != nil {
|
|
168
174
|
fmt.Fprintf(stdout, "metrics: no events recorded for %s\n", slug)
|
|
@@ -258,3 +264,70 @@ func metricsSummary(root string, args []string, stdout, stderr io.Writer) int {
|
|
|
258
264
|
fmt.Fprintln(stdout)
|
|
259
265
|
return 0
|
|
260
266
|
}
|
|
267
|
+
|
|
268
|
+
// sliceCostSummary reports, per slice named on dispatch waves, the build wright
|
|
269
|
+
// rounds, slice-bound Vet rechecks, and agent-minutes (role start to return; a
|
|
270
|
+
// wave naming several slices splits its minutes evenly). Abandoned waves are
|
|
271
|
+
// excluded, and waves recorded before returns were timestamped count rounds only.
|
|
272
|
+
func sliceCostSummary(root, slug string, stdout, stderr io.Writer) int {
|
|
273
|
+
f, _, err := loadDispatch(root, slug)
|
|
274
|
+
if err != nil {
|
|
275
|
+
fmt.Fprintf(stderr, "metrics: %v\n", err)
|
|
276
|
+
return 2
|
|
277
|
+
}
|
|
278
|
+
type cost struct {
|
|
279
|
+
wright, vet int
|
|
280
|
+
minutes float64
|
|
281
|
+
timed bool
|
|
282
|
+
}
|
|
283
|
+
per := map[string]*cost{}
|
|
284
|
+
for _, w := range f.Waves {
|
|
285
|
+
kind := roundKind(w)
|
|
286
|
+
if kind == "" || w.State == "abandoned" {
|
|
287
|
+
continue
|
|
288
|
+
}
|
|
289
|
+
minutes, timed := 0.0, false
|
|
290
|
+
for _, r := range w.Roles {
|
|
291
|
+
start, startErr := time.Parse(time.RFC3339, r.StartedAt)
|
|
292
|
+
end, endErr := time.Parse(time.RFC3339, r.ReturnedAt)
|
|
293
|
+
if startErr == nil && endErr == nil && !end.Before(start) {
|
|
294
|
+
minutes += end.Sub(start).Minutes()
|
|
295
|
+
timed = true
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
for _, id := range w.Slices {
|
|
299
|
+
c := per[id]
|
|
300
|
+
if c == nil {
|
|
301
|
+
c = &cost{}
|
|
302
|
+
per[id] = c
|
|
303
|
+
}
|
|
304
|
+
if kind == "wright" {
|
|
305
|
+
c.wright++
|
|
306
|
+
} else {
|
|
307
|
+
c.vet++
|
|
308
|
+
}
|
|
309
|
+
if timed {
|
|
310
|
+
c.minutes += minutes / float64(len(w.Slices))
|
|
311
|
+
c.timed = true
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if len(per) == 0 {
|
|
316
|
+
fmt.Fprintf(stdout, "metrics: no slice-tagged dispatch waves for %s\n", slug)
|
|
317
|
+
return 0
|
|
318
|
+
}
|
|
319
|
+
ids := make([]string, 0, len(per))
|
|
320
|
+
for id := range per {
|
|
321
|
+
ids = append(ids, id)
|
|
322
|
+
}
|
|
323
|
+
sort.Strings(ids)
|
|
324
|
+
for _, id := range ids {
|
|
325
|
+
c := per[id]
|
|
326
|
+
minutes := "n/a"
|
|
327
|
+
if c.timed {
|
|
328
|
+
minutes = strconv.FormatFloat(c.minutes, 'f', 0, 64)
|
|
329
|
+
}
|
|
330
|
+
fmt.Fprintf(stdout, "metrics: %s wright_rounds=%d vet_rechecks=%d agent_minutes=%s\n", id, c.wright, c.vet, minutes)
|
|
331
|
+
}
|
|
332
|
+
return 0
|
|
333
|
+
}
|
|
@@ -394,3 +394,35 @@ func TestObserveArtifactBudgetsOversizedArtifactIsOverWithoutFullRead(t *testing
|
|
|
394
394
|
t.Fatalf("allocated %d bytes for a %d byte artifact; the read must be bounded", allocated, size)
|
|
395
395
|
}
|
|
396
396
|
}
|
|
397
|
+
|
|
398
|
+
func TestUnreturnedWavesWarnAndSurfaceInOrient(t *testing.T) {
|
|
399
|
+
root, _ := writeBudgetWorkspace(t, map[string]string{
|
|
400
|
+
"state.md": "| phase | build |\n| schema | 4 |\n",
|
|
401
|
+
"tasks.md": budgetTasks,
|
|
402
|
+
})
|
|
403
|
+
open := func(wave string) string {
|
|
404
|
+
stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{}
|
|
405
|
+
args := []string{"feature", "open", "--phase", "build", "--wave", wave, "--role", "slice-wright", "--slice", "SLICE-001"}
|
|
406
|
+
if code := RunDispatch(root, args, stdout, stderr); code != 0 {
|
|
407
|
+
t.Fatalf("open %s: code=%d\n%s%s", wave, code, stdout, stderr)
|
|
408
|
+
}
|
|
409
|
+
return stdout.String()
|
|
410
|
+
}
|
|
411
|
+
if out := open("r1"); strings.Contains(out, "WARNING") {
|
|
412
|
+
t.Fatalf("first wave must not warn:\n%s", out)
|
|
413
|
+
}
|
|
414
|
+
if out := open("r2"); !strings.Contains(out, "WARNING: SLICE-001 earlier wave(s) r1 never returned") {
|
|
415
|
+
t.Fatalf("unreturned earlier wave not warned:\n%s", out)
|
|
416
|
+
}
|
|
417
|
+
summary, err := ObserveSummaryFor(root, "feature")
|
|
418
|
+
if err != nil {
|
|
419
|
+
t.Fatal(err)
|
|
420
|
+
}
|
|
421
|
+
if len(summary.InflightWaves) != 2 {
|
|
422
|
+
t.Fatalf("inflight waves=%+v", summary.InflightWaves)
|
|
423
|
+
}
|
|
424
|
+
first := summary.InflightWaves[0]
|
|
425
|
+
if first.Wave != "r1" || first.State != "open" || len(first.Pending) != 1 || first.Pending[0] != "slice-wright" || first.Slices[0] != "SLICE-001" {
|
|
426
|
+
t.Fatalf("inflight wave=%+v", first)
|
|
427
|
+
}
|
|
428
|
+
}
|
|
@@ -51,6 +51,7 @@ type ObserveSummary struct {
|
|
|
51
51
|
ArtifactBudgets []ArtifactBudget `json:"artifact_budgets,omitempty"`
|
|
52
52
|
BulkFiles []BulkFile `json:"bulk_files,omitempty"`
|
|
53
53
|
Sequence *ObserveSequenceCursor `json:"sequence,omitempty"`
|
|
54
|
+
InflightWaves []InflightWave `json:"inflight_waves,omitempty"`
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
// ObserveSummaryFor builds a summary for one feature slug.
|
|
@@ -91,6 +92,13 @@ func ObserveSummaryFor(root, slug string) (ObserveSummary, error) {
|
|
|
91
92
|
OK: len(graph.Problems) == 0,
|
|
92
93
|
}
|
|
93
94
|
}
|
|
95
|
+
// Open or sealed waves are agents whose results the root never recorded;
|
|
96
|
+
// a resume reconciles them before starting new work.
|
|
97
|
+
if waves, _, err := loadDispatch(root, slug); err == nil {
|
|
98
|
+
summary.InflightWaves = inflightWaves(waves)
|
|
99
|
+
} else {
|
|
100
|
+
summary.Unreadable = append(summary.Unreadable, "dispatch.json")
|
|
101
|
+
}
|
|
94
102
|
if report.SequenceParent != "" || report.SequencePosition != "" || report.SequenceWorkspacesRemaining != "" || report.SequenceRole != "" {
|
|
95
103
|
summary.Sequence = &ObserveSequenceCursor{
|
|
96
104
|
Parent: report.SequenceParent,
|
|
@@ -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
|
+
}
|
package/engine/main.go
CHANGED
|
@@ -48,11 +48,11 @@ Usage:
|
|
|
48
48
|
devrites-engine orient <slug> Alias for observe summary
|
|
49
49
|
devrites-engine next [slug] Print the minimal remaining lifecycle path with advisory skips
|
|
50
50
|
devrites-engine handoff [slug] Emit the deterministic resume record (cursor, blocking gates, ledger, dead ends)
|
|
51
|
-
devrites-engine context [slug] (--phase <p> | --skill <name>) [--role <r>] [--trigger a,b]
|
|
51
|
+
devrites-engine context [slug] (--phase <p> | --skill <name>) [--role <r>] [--slice <id>] [--trigger a,b]
|
|
52
52
|
Emit one deduplicated read-set bundle for a phase, skill, or dispatch role
|
|
53
53
|
devrites-engine metrics record <slug> --phase <p> --event <e> [--role r] [--bytes n] [--note s]
|
|
54
|
-
devrites-engine metrics summary [slug]
|
|
55
|
-
devrites-engine dispatch <slug> <sub> Launch-wave barrier for parallel dispatch: open/start/seal/return/status/abandon
|
|
54
|
+
devrites-engine metrics summary [slug] [--by-slice] Summarize the metrics ledger, or per-slice rounds and agent-minutes
|
|
55
|
+
devrites-engine dispatch <slug> <sub> Launch-wave barrier for parallel dispatch: open/start/seal/return/status/abandon (build wright waves: --slice, round cap)
|
|
56
56
|
devrites-engine claim <add|release|list|check> Advisory session-scoped file claims for same-tree concurrent sessions (.devrites/claims.jsonl)
|
|
57
57
|
devrites-engine note <subcommand> <slug> Anchored workspace notes bound to verbatim code quotes; drift regrades them (notes.md)
|
|
58
58
|
devrites-engine observe slice <slug> <SLICE-ID> Print one SLICE-### section of tasks.md
|
|
@@ -120,7 +120,10 @@ unclear boundary is underspecified: return `Escalation` without proceeding.
|
|
|
120
120
|
new fingerprint. At the limit return the gate and repro. `Escalation` is only for product/irreversible choices
|
|
121
121
|
or human-only access; technical failure is a blocker.
|
|
122
122
|
Before handoff, inspect your cumulative slice diff against the contract and
|
|
123
|
-
scenario matrix, including correction-affected adjacent variants
|
|
123
|
+
scenario matrix, including correction-affected adjacent variants, then against
|
|
124
|
+
the reviewers' own checks: [code review § What to check](../skills/devrites-lib/reference/standards/code-review.md#what-to-check-tests-first)
|
|
125
|
+
and test analysis (each acceptance criterion has a test that fails without the
|
|
126
|
+
change; no assertion-free, tautological, or mock-asserting test). Fix supported
|
|
124
127
|
in-scope gaps together, rerun affected proof, and report remaining gaps. Self-check
|
|
125
128
|
never replaces independent review; green tests do not prove all possible cases.
|
|
126
129
|
5. **TRANSFER when isolated.** After green writer-safe proof, verify the diff again,
|
|
@@ -138,7 +141,9 @@ Stay inside the exact project-relative paths listed in the dispatch task.
|
|
|
138
141
|
return an `Escalation`** when:
|
|
139
142
|
- the slice is **underspecified**, the **plan looks wrong**, or requirements, code,
|
|
140
143
|
and tests conflict;
|
|
141
|
-
- the slice needs a **new dependency** or a **second design system
|
|
144
|
+
- the slice needs a **new dependency** or a **second design system**, or a planned
|
|
145
|
+
package fails to install or resolve (report the failure; never retry under a
|
|
146
|
+
similar or alternative package name);
|
|
142
147
|
- the work requires an unapproved product/security-policy choice, public API break,
|
|
143
148
|
external-service contract change, destructive live migration, or filesystem
|
|
144
149
|
destruction outside the workspace. A path allowlist alone does not approve those
|
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
Generate **3-5 ranked hypotheses** *before* testing any of them.
|
|
4
4
|
Single-hypothesis generation anchors on the first plausible idea.
|
|
5
5
|
|
|
6
|
+
**Check past root causes first.** Search `.devrites/archive/*/evidence.md` with
|
|
7
|
+
native file search for the failure's error text, symbol, and file names. A hit
|
|
8
|
+
whose recorded correct hypothesis matches enters the ranked list with its
|
|
9
|
+
recurrence noted; it is still tested, never assumed. No archive or no hit →
|
|
10
|
+
skip silently.
|
|
11
|
+
|
|
6
12
|
Each hypothesis must be **falsifiable**: state the prediction it makes.
|
|
7
13
|
|
|
8
14
|
> Format: *"If <X> is the cause, then <changing Y> will make the failure
|
|
@@ -36,6 +36,7 @@ If the candidate diff has no matching surfaces, record
|
|
|
36
36
|
2. Build each reviewer's read-set once with
|
|
37
37
|
`devrites-engine context <slug> --phase <p> --role <role> --trigger <set>` and
|
|
38
38
|
hand the bundle path as the packet's source list instead of a file roster.
|
|
39
|
+
Build roles add `--slice <SLICE-ID>` (slice-scoped, slice-named bundle).
|
|
39
40
|
Evaluate the skill's trigger conditions and pass each that fires — a role's
|
|
40
41
|
Trigger column maps to manifest names: `security-auditor` → `security`,
|
|
41
42
|
`frontend-reviewer` → `ui`/`frontend`, `performance-reviewer` → `performance`.
|
|
@@ -59,7 +59,7 @@ topic's owner.
|
|
|
59
59
|
| `architecture-health.md` | An audit needs a whole-codebase structural score from a code index — coupling, cohesion, cycles, god files, orphans, depth. |
|
|
60
60
|
| `assumption-checkpoints.md` | Scope generalizes an existing boundary: a second case, a newly optional field, or a constant turned parameter. |
|
|
61
61
|
| `debug-recovery.md` | Waiting on async readiness (server start, job completion, browser signal) needs a bounded condition-based poll. |
|
|
62
|
-
| `windows.md` | A deferral marker (TODO/FIXME/skipped test/stub) is introduced or waived — the `check windows` ledger and grammar. |
|
|
62
|
+
| `windows.md` | A deferral or bar-lowering marker (TODO/FIXME/skipped test/stub/suppression/removed test) is introduced or waived — the `check windows` ledger and grammar. |
|
|
63
63
|
| `glossary.md` | A DevRites term is unclear; resolve it once here instead of inferring. |
|
|
64
64
|
|
|
65
65
|
The table is a load trigger, not an exemption. [`core.md` § Rule summary](core.md#rule-summary-load-the-full-file-when-in-scope)
|
|
@@ -357,6 +357,8 @@ access, safety and irreversible-action boundaries independently stop work.
|
|
|
357
357
|
renaming/splitting an unchanged cause does not. Suggestion/Nit/FYI cannot extend
|
|
358
358
|
recovery. Initial discovery and expected test-first RED are not corrections;
|
|
359
359
|
total rounds, re-batches and distinct-finding counts never exhaust recovery.
|
|
360
|
+
Every 5 Build wright rounds on one slice, the next first needs a reslice review
|
|
361
|
+
([`wright-dispatch.md`](../../../rite-build/reference/wright-dispatch.md#run)).
|
|
360
362
|
- **Rechecks are scoped; late findings never reopen the round.** *Late* applies only
|
|
361
363
|
from a phase's **second** review pass (Vet 1b, Build's recheck after the first repair,
|
|
362
364
|
Review step 7); an initial pass has no late findings and every supported finding is
|
|
@@ -30,6 +30,9 @@ If it does not, do not merge it.
|
|
|
30
30
|
no justification), stripped assertions, deleted or skipped tests, and new
|
|
31
31
|
unimplemented stubs or `throw`-only placeholders. Each names what it lowers and
|
|
32
32
|
needs a recorded reason in the diff or `decisions.md`; unexplained is a finding.
|
|
33
|
+
`devrites-engine check windows` flags the mechanical subset (suppressions,
|
|
34
|
+
skip/focus, stubs, empty catch, deleted tests, stripped assertions) against
|
|
35
|
+
`windows.md` waivers; loosened thresholds remain this sweep's judgment.
|
|
33
36
|
3. **Correctness:** logic, edge cases, error paths, race conditions, wrong assumptions. For branching or boundary changes, run the [`edge-case trace`](edge-case-trace.md): relevant probe classes, fixed-set siblings, real wiring, negative intent, and deletion contracts with an evidence disposition.
|
|
34
37
|
4. **Readability:** names, function size, control flow, intent obvious without the author.
|
|
35
38
|
5. **Architecture:** right seam, coupling/cohesion, fits existing patterns, no premature
|
|
@@ -35,6 +35,6 @@
|
|
|
35
35
|
| Standards | The rule files in this directory; `core.md` is always-on, the rest load on trigger (`README.md` table). |
|
|
36
36
|
| Transfer commit | The isolated wright's local unpushed `WIP(<slug>):` transport commit; never the control-branch checkpoint. |
|
|
37
37
|
| Trigger | A named conditional read-set in a `loads:` manifest (e.g. `tdd`, `applicability`, `ui`); engine fails closed on unknown names. |
|
|
38
|
-
| Windows ledger | `windows.md`: one waiver row per deferral marker the change introduces; `check windows` fails on unwaived hits. [`windows.md`](windows.md). |
|
|
38
|
+
| Windows ledger | `windows.md`: one waiver row per deferral or bar-lowering marker the change introduces; `check windows` fails on unwaived hits. [`windows.md`](windows.md). |
|
|
39
39
|
| Workflow Artifact | A Vet-ready, root-authored executable file under `.devrites/work/<slug>/`; [`workflow-artifacts.md`](workflow-artifacts.md). |
|
|
40
40
|
| Wright | `devrites-slice-wright`: the sole product source/test writer under a path-bounded contract. |
|
|
@@ -96,6 +96,7 @@ On any security-relevant error: deny, roll back; never default to allow or half-
|
|
|
96
96
|
- Install reproducibly from a committed lockfile (`npm ci` / frozen); never resolving installs in CI. Hand-editing lockfiles bypasses review.
|
|
97
97
|
- Distrust install scripts (`postinstall` runs arbitrary code) — review before adding; prefer `--ignore-scripts`.
|
|
98
98
|
- Typosquats are a delivery vector: confirm exact name/publisher, not install success.
|
|
99
|
+
After a failed install, never try a similarly named package; report the failure.
|
|
99
100
|
- A CI/release change names the SLSA build provenance level it claims and where the build runs; a claimed level with no verified provenance attestation is `cannot_verify`, not a pass.
|
|
100
101
|
|
|
101
102
|
## Trust boundary (three tiers)
|