# fix(inspect): set whole_change granularity when posting a verdict to the trail

`6c85bc9`→[main](/content/gh/entireio/cli/commits/main/index.html)·

dipree·1mo ago·2 files·+43 added/-10 removed

Posting the review verdict to a trail failed with 'comments.0.location.granularity: Invalid option' (400): the finding input omitted Location, so an empty granularity was sent. A consolidated verdict isn't tied to a file/line, so set granularity=whole_change (matching the trail finding add path's default). Extracted reviewTrailFindingInput so it's unit tested for granularity + body, and pinned the regression.

## Sessions

d4bac437f615View transcript

[?\Checkout the hand off doc that I just added.Pi·Opus 4.8·2 steps](/content/gh/entireio/cli/session/019eca64-8c2c-7b00-90c6-3aa49738c497#timeline-d4bac437f615/index.html)

## Changes

2

- cmd/entire/cli

- Mreview_bridge.go+17/-10

- Mreview_bridge_test.go+26

```
41 unmodified lines

42
43
44
45
46
45
46
47
49
50
51
52
48
49
50
51
52
53
58
59
60
61
54
55
56
9 unmodified lines

66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86

41 unmodified lines

// destination. It lives in the cli package because the data API client and
// auth flow do.
func postReviewToTrail(ctx context.Context, out io.Writer, profileName, verdict string) error {
	verdict = strings.TrimSpace(verdict)
	if verdict == "" {
	if strings.TrimSpace(verdict) == "" {
		return errors.New("no review output to post")
	}
	body := verdict
	if p := strings.TrimSpace(profileName); p != "" {
		body = fmt.Sprintf("Review verdict (profile: %s)\n\n%s", p, verdict)
	}
	input := reviewTrailFindingInput(profileName, verdict)
	return runAuthenticatedDataAPI(ctx, out, false, func(ctx context.Context, client *api.Client) error {
		target, err := resolveTrailReviewTarget(ctx, client, "")
		if err != nil {
			return err
		}
		input := api.TrailReviewCommentInput{
			ClientID: generateTrailReviewClientID(),
			Body:     stringPtr(body),
		}
		if _, err := createTrailReviewFinding(ctx, client, target.Trail.ID, input); err != nil {
			return err
		}
9 unmodified lines

})
}

// reviewTrailFindingInput builds the trail finding payload for a review verdict.
// The verdict spans the whole change, so it uses "whole_change" granularity: the
// API requires a valid granularity and rejects a zero/empty value with a 400.
func reviewTrailFindingInput(profileName, verdict string) api.TrailReviewCommentInput {
	body := strings.TrimSpace(verdict)
	if p := strings.TrimSpace(profileName); p != "" {
		body = fmt.Sprintf("Review verdict (profile: %s)\n\n%s", p, body)
	}
	return api.TrailReviewCommentInput{
		ClientID: generateTrailReviewClientID(),
		Body:     stringPtr(body),
		Location: api.TrailReviewLocationCreateRequest{Granularity: "whole_change"},
	}
}

// trailWebURL builds the browser URL for a trail, matching the server's
// `<base>/<forge>/<owner>/<repo>/trails/<number>/<branch>` layout (the web UI
// shares the API origin). Returns "" when the target lacks the parts needed for
```

Mcmd/entire/cli/review_bridge.go+17/-10

```
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

package cli

import (
	"strings"
	"testing"

"github.com/entireio/cli/cmd/entire/cli/api"
)

func TestReviewTrailFindingInput(t *testing.T) {
	// Regression: a review verdict is not tied to a file/line, so the finding
	// must use whole_change granularity. An empty granularity is rejected by
	// the API with a 400.
	in := reviewTrailFindingInput("general", "  the verdict  ")
	if in.Location.Granularity != "whole_change" {
		t.Errorf("granularity = %q, want whole_change", in.Location.Granularity)
	}
	if in.ClientID == "" {
				}
	}

// No profile: the body is exactly the trimmed verdict.
	bare := reviewTrailFindingInput("", "  bare verdict  ")
	if bare.Body == nil || *bare.Body != "bare verdict" {
				}
	}
	if bare.Location.Granularity != "whole_change" {
		t.Errorf("granularity = %q, want whole_change", bare.Location.Granularity)
	}
}

func TestTrailWebURL(t *testing.T) {
	t.Setenv(api.BaseURLEnvVar, "https://entire.io")
