copy gate: scan internal/host/movesoffline.go; its RetiredPath Reason is a diagnostic
The park report's Reason for a path left in place is the OS error of the rename, not copy (Paul's rule, 2026-10-16). One narrowly scoped entry in movesDiagnosticWrites leaves that write (retire, RetiredPath.Reason) out of the field's writes; the Phase 1 Reason reads in move.go keep their own rows.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2 files changed,
+41
−1
approved_moves_copy_scan_test.go+10 −1approved_moves_copy_test.go+31 −0
diff --git a/approved_moves_copy_scan_test.go b/approved_moves_copy_scan_test.go| index 1de9a25b0..27019c88b 100644 |
| --- a/approved_moves_copy_scan_test.go |
| +++ b/approved_moves_copy_scan_test.go |
| @@ -373,6 +373,9 @@ type movesUniverse struct { |
| paramSinks map[string]bool // functions whose parameter reaches a sink |
| files []string |
| steps int |
| + // diagSkipped counts, per movesDiagnosticWrites entry, the writes the |
| + // universe left out of the field's writes. |
| + diagSkipped map[string]int |
| } |
| func (u *movesUniverse) at(p string) string { |
| @@ -386,7 +389,7 @@ func newUniverse(t *testing.T, root string, files []string) *movesUniverse { |
| t.Helper() |
| u := &movesUniverse{t: t, root: root, fieldWrites: map[string][]*fieldWrite{}, fieldTypes: map[string]map[string]bool{}, |
| structFields: map[string][]string{}, fieldTypeOf: map[string]map[string]ast.Expr{}, results: map[string]*fieldResult{}, fieldTexts: map[string]string{}, used: map[string]bool{}, |
| - paramSinks: map[string]bool{}, files: files} |
| + paramSinks: map[string]bool{}, files: files, diagSkipped: map[string]int{}} |
| parsed := map[string]*ast.File{} |
| for _, f := range files { |
| af, err := parser.ParseFile(movesFset, u.at(f), nil, 0) |
| @@ -468,6 +471,12 @@ func newUniverse(t *testing.T, root string, files []string) *movesUniverse { |
| for i, el := range n.Elts { |
| if kv, ok := el.(*ast.KeyValueExpr); ok { |
| if k, ok := kv.Key.(*ast.Ident); ok { |
| + if fd != nil { |
| + if key := fd.Name.Name + "|" + ctype + "." + k.Name; movesDiagnosticWrites[f][key] != "" { |
| + u.diagSkipped[f+"|"+key]++ |
| + continue |
| + } |
| + } |
| u.fieldWrites[k.Name] = append(u.fieldWrites[k.Name], &fieldWrite{file: f, fd: fd, expr: kv.Value, ctype: ctype}) |
| } |
| } else if i < len(order) { |
diff --git a/approved_moves_copy_test.go b/approved_moves_copy_test.go| index e9ac74371..1622610d5 100644 |
| --- a/approved_moves_copy_test.go |
| +++ b/approved_moves_copy_test.go |
| @@ -1261,6 +1261,7 @@ var movesScanFiles = []string{ |
| "internal/host/movesparks.go", |
| "internal/host/movespaging.go", |
| "internal/host/movespass.go", |
| + "internal/host/movesoffline.go", |
| "internal/host/movesphase1.go", |
| "internal/host/movespolicy.go", |
| "internal/host/movesprobe.go", |
| @@ -1524,6 +1525,20 @@ var movesSinkAllow = map[string]map[string]string{ |
| }, |
| } |
| +// movesDiagnosticWrites are field writes the scan leaves out of a field's |
| +// writes because what they write is an OS diagnostic for the operator's |
| +// eye, not copy. Paul's rule (2026-10-16): internal error fragments and |
| +// diagnostics stay out of copy review. The key is "<function>|<struct>.<field>" |
| +// and the value says why. An entry leaves out only the composite-literal |
| +// writes of that field in that function of that file, so the field's other |
| +// writers (the Phase 1 Reason reads in move.go among them) are judged as before. |
| +// TestMovesDiagnosticWritesAreReal holds each entry to a write it leaves out. |
| +var movesDiagnosticWrites = map[string]map[string]string{ |
| + "internal/host/movesoffline.go": { |
| + "retire|RetiredPath.Reason": "the parked report's reason for a path left in place is the OS error of the rename (leftCannot or leftNotYet plus err.Error()), a box diagnostic that `carlos move offline status` shows as MV-O58's <reason>; it is not copy", |
| + }, |
| +} |
| + |
| // movesServerText names, per file, the type that carries text the console |
| // wrote (fix round 2 of Plan 6 Task 1a, ruling 1). It is made in one function |
| // that reads an HTTP response body, and said through one sink; the gate |
| @@ -2471,3 +2486,19 @@ func TestMovesServerTextRefusesForgedValues(t *testing.T) { |
| } |
| } |
| } |
| + |
| +// Every movesDiagnosticWrites entry leaves out a write that exists, and the |
| +// reason it gives is a sentence. |
| +func TestMovesDiagnosticWritesAreReal(t *testing.T) { |
| + u := newUniverse(t, ".", movesScanFiles) |
| + for f, keys := range movesDiagnosticWrites { |
| + for key, why := range keys { |
| + if len(why) < 40 { |
| + t.Errorf("%s %s: the reason is too short to be one", f, key) |
| + } |
| + if u.diagSkipped[f+"|"+key] == 0 { |
| + t.Errorf("%s %s leaves out no write: drop the entry", f, key) |
| + } |
| + } |
| + } |
| +} |