From fe48963b9060b6eb6fa18d10a6ace8ec1e75e63a Mon Sep 17 00:00:00 2001 From: hatayama Date: Thu, 15 Oct 2026 15:49:57 +0900 Subject: [PATCH 1/3] refactor: move compile-check's summary wording into its own file compile_check.go was at 488 lines against the 500 SLOC limit, and the next change grows the summary wording. Move the functions that build the one-line Message unchanged, so the wording fix lands in a file of its own. --- .../internal/dispatcher/compile_check.go | 63 ----------------- .../dispatcher/compile_check_message.go | 68 +++++++++++++++++++ 2 files changed, 68 insertions(+), 63 deletions(-) create mode 100644 cli/dispatcher/internal/dispatcher/compile_check_message.go diff --git a/cli/dispatcher/internal/dispatcher/compile_check.go b/cli/dispatcher/internal/dispatcher/compile_check.go index ee88cd659..42cf724ab 100644 --- a/cli/dispatcher/internal/dispatcher/compile_check.go +++ b/cli/dispatcher/internal/dispatcher/compile_check.go @@ -3,7 +3,6 @@ package dispatcher import ( "context" "encoding/json" - "fmt" "io" "path/filepath" "strconv" @@ -306,68 +305,6 @@ func newCompileCheckIssue(assembly string, diagnostic compilecheck.Diagnostic) c } } -// compileCheckMessage states what the run did in one line. -// Why the blocked assemblies are added rather than folded in: without them a run that reports one -// error and hundreds of assemblies it never got to reads as a run that lost them. -// Why the fast-checked ones get a reminder: the check skipped rules only a full compile runs, and -// the caller has to know to run --full before finishing. -func compileCheckMessage(response compileCheckResponse) string { - message := compileCheckRunSummary(response) - if len(response.BlockedAssemblies) > 0 { - message += fmt.Sprintf( - " %d assemblies were not compiled because an assembly they reference has errors.", - len(response.BlockedAssemblies)) - } - if len(response.FastCheckedAssemblies) > 0 { - message += fmt.Sprintf( - " %d assemblies were checked on their changed files only; run with --full before finishing to cover assembly-wide rules.", - len(response.FastCheckedAssemblies)) - } - - return message -} - -// compileCheckRunSummary states what the run compiled, reused, and fast-checked. -// Why only the non-zero counts are named: a run that reused nothing reads exactly as it did before -// reuse existed, and a run that compiled nothing should not open by saying so. -func compileCheckRunSummary(response compileCheckResponse) string { - counts := []struct { - verb string - count int - }{ - {"compiled", len(response.CompiledAssemblies)}, - {"reused", len(response.ReusedAssemblies)}, - {"fast-checked", len(response.FastCheckedAssemblies)}, - } - parts := []string{} - for _, entry := range counts { - if entry.count == 0 { - continue - } - if len(parts) == 0 { - parts = append(parts, fmt.Sprintf("%s%s %d assemblies", - strings.ToUpper(entry.verb[:1]), entry.verb[1:], entry.count)) - continue - } - parts = append(parts, fmt.Sprintf("%s %d", entry.verb, entry.count)) - } - if len(parts) == 0 { - return compileCheckNoChangeMessage - } - - return fmt.Sprintf("%s with %d errors and %d warnings.", - joinCompileCheckSummaryParts(parts), response.ErrorCount, response.WarningCount) -} - -// joinCompileCheckSummaryParts joins the summary parts as "A and b" or "A, b and c". -func joinCompileCheckSummaryParts(parts []string) string { - if len(parts) == 1 { - return parts[0] - } - - return strings.Join(parts[:len(parts)-1], ", ") + " and " + parts[len(parts)-1] -} - // printCompileCheckHelp documents the command and the build it replays. func printCompileCheckHelp(stdout io.Writer) { clicore.WriteLine(stdout, "Usage:") diff --git a/cli/dispatcher/internal/dispatcher/compile_check_message.go b/cli/dispatcher/internal/dispatcher/compile_check_message.go new file mode 100644 index 000000000..ee28544b0 --- /dev/null +++ b/cli/dispatcher/internal/dispatcher/compile_check_message.go @@ -0,0 +1,68 @@ +package dispatcher + +import ( + "fmt" + "strings" +) + +// compileCheckMessage states what the run did in one line. +// Why the blocked assemblies are added rather than folded in: without them a run that reports one +// error and hundreds of assemblies it never got to reads as a run that lost them. +// Why the fast-checked ones get a reminder: the check skipped rules only a full compile runs, and +// the caller has to know to run --full before finishing. +func compileCheckMessage(response compileCheckResponse) string { + message := compileCheckRunSummary(response) + if len(response.BlockedAssemblies) > 0 { + message += fmt.Sprintf( + " %d assemblies were not compiled because an assembly they reference has errors.", + len(response.BlockedAssemblies)) + } + if len(response.FastCheckedAssemblies) > 0 { + message += fmt.Sprintf( + " %d assemblies were checked on their changed files only; run with --full before finishing to cover assembly-wide rules.", + len(response.FastCheckedAssemblies)) + } + + return message +} + +// compileCheckRunSummary states what the run compiled, reused, and fast-checked. +// Why only the non-zero counts are named: a run that reused nothing reads exactly as it did before +// reuse existed, and a run that compiled nothing should not open by saying so. +func compileCheckRunSummary(response compileCheckResponse) string { + counts := []struct { + verb string + count int + }{ + {"compiled", len(response.CompiledAssemblies)}, + {"reused", len(response.ReusedAssemblies)}, + {"fast-checked", len(response.FastCheckedAssemblies)}, + } + parts := []string{} + for _, entry := range counts { + if entry.count == 0 { + continue + } + if len(parts) == 0 { + parts = append(parts, fmt.Sprintf("%s%s %d assemblies", + strings.ToUpper(entry.verb[:1]), entry.verb[1:], entry.count)) + continue + } + parts = append(parts, fmt.Sprintf("%s %d", entry.verb, entry.count)) + } + if len(parts) == 0 { + return compileCheckNoChangeMessage + } + + return fmt.Sprintf("%s with %d errors and %d warnings.", + joinCompileCheckSummaryParts(parts), response.ErrorCount, response.WarningCount) +} + +// joinCompileCheckSummaryParts joins the summary parts as "A and b" or "A, b and c". +func joinCompileCheckSummaryParts(parts []string) string { + if len(parts) == 1 { + return parts[0] + } + + return strings.Join(parts[:len(parts)-1], ", ") + " and " + parts[len(parts)-1] +} From 1519a5f8775cfbd99504470e34ba73514dc50049 Mon Sep 17 00:00:00 2001 From: hatayama Date: Thu, 15 Oct 2026 15:51:21 +0900 Subject: [PATCH 2/3] fix: compile-check's summary says 1 assembly and 1 error The one-line Message always used the plural, so a run read as "Compiled 1 assemblies ... with 1 errors" and "1 assemblies were checked on their changed files only". Every count followed by a noun now takes the singular for exactly one and the plural otherwise, including zero ("0 errors"). --- .../compile_check_fast_path_test.go | 6 +- .../dispatcher/compile_check_message.go | 36 ++++-- .../dispatcher/compile_check_message_test.go | 107 ++++++++++++++++++ .../internal/dispatcher/compile_check_test.go | 8 +- 4 files changed, 140 insertions(+), 17 deletions(-) create mode 100644 cli/dispatcher/internal/dispatcher/compile_check_message_test.go diff --git a/cli/dispatcher/internal/dispatcher/compile_check_fast_path_test.go b/cli/dispatcher/internal/dispatcher/compile_check_fast_path_test.go index c68e0f09c..9250c051b 100644 --- a/cli/dispatcher/internal/dispatcher/compile_check_fast_path_test.go +++ b/cli/dispatcher/internal/dispatcher/compile_check_fast_path_test.go @@ -84,8 +84,8 @@ func TestResponseListsFallbackReasons(t *testing.T) { func TestMessageMentionsFastCheckedAssemblies(t *testing.T) { response := buildCompileCheckResponse(fastCheckedResult(), "/projects/sample") - want := "Compiled 1 assemblies, reused 1 and fast-checked 1 with 0 errors and 0 warnings." + - " 1 assemblies were checked on their changed files only; run with --full before finishing to cover assembly-wide rules." + want := "Compiled 1 assembly, reused 1 and fast-checked 1 with 0 errors and 0 warnings." + + " 1 assembly was checked on its changed files only; run with --full before finishing to cover assembly-wide rules." if response.Message != want { t.Errorf("message = %q, want %q", response.Message, want) } @@ -102,7 +102,7 @@ func TestFastCheckOnlyRunWithErrorDoesNotSayNoAssembliesChanged(t *testing.T) { }}, }, "/projects/sample") - if strings.Contains(response.Message, compileCheckNoChangeMessage) || !strings.HasPrefix(response.Message, "Fast-checked 1 assemblies with 1 errors and 0 warnings.") { + if strings.Contains(response.Message, compileCheckNoChangeMessage) || !strings.HasPrefix(response.Message, "Fast-checked 1 assembly with 1 error and 0 warnings.") { t.Errorf("unexpected message %q", response.Message) } if response.ErrorCount != 1 || response.Success { diff --git a/cli/dispatcher/internal/dispatcher/compile_check_message.go b/cli/dispatcher/internal/dispatcher/compile_check_message.go index ee28544b0..29906468f 100644 --- a/cli/dispatcher/internal/dispatcher/compile_check_message.go +++ b/cli/dispatcher/internal/dispatcher/compile_check_message.go @@ -13,14 +13,17 @@ import ( func compileCheckMessage(response compileCheckResponse) string { message := compileCheckRunSummary(response) if len(response.BlockedAssemblies) > 0 { - message += fmt.Sprintf( - " %d assemblies were not compiled because an assembly they reference has errors.", - len(response.BlockedAssemblies)) + count := len(response.BlockedAssemblies) + message += fmt.Sprintf(compileCheckPlural(count, + " %d assembly was not compiled because an assembly it references has errors.", + " %d assemblies were not compiled because an assembly they reference has errors."), count) } if len(response.FastCheckedAssemblies) > 0 { - message += fmt.Sprintf( - " %d assemblies were checked on their changed files only; run with --full before finishing to cover assembly-wide rules.", - len(response.FastCheckedAssemblies)) + count := len(response.FastCheckedAssemblies) + message += fmt.Sprintf(compileCheckPlural(count, + " %d assembly was checked on its changed files only;", + " %d assemblies were checked on their changed files only;"), count) + + " run with --full before finishing to cover assembly-wide rules." } return message @@ -44,8 +47,9 @@ func compileCheckRunSummary(response compileCheckResponse) string { continue } if len(parts) == 0 { - parts = append(parts, fmt.Sprintf("%s%s %d assemblies", - strings.ToUpper(entry.verb[:1]), entry.verb[1:], entry.count)) + parts = append(parts, fmt.Sprintf("%s%s %d %s", + strings.ToUpper(entry.verb[:1]), entry.verb[1:], entry.count, + compileCheckPlural(entry.count, "assembly", "assemblies"))) continue } parts = append(parts, fmt.Sprintf("%s %d", entry.verb, entry.count)) @@ -54,8 +58,20 @@ func compileCheckRunSummary(response compileCheckResponse) string { return compileCheckNoChangeMessage } - return fmt.Sprintf("%s with %d errors and %d warnings.", - joinCompileCheckSummaryParts(parts), response.ErrorCount, response.WarningCount) + return fmt.Sprintf("%s with %d %s and %d %s.", + joinCompileCheckSummaryParts(parts), + response.ErrorCount, compileCheckPlural(response.ErrorCount, "error", "errors"), + response.WarningCount, compileCheckPlural(response.WarningCount, "warning", "warnings")) +} + +// compileCheckPlural picks the wording for count, which is singular only for exactly one; zero +// takes the plural ("0 errors"). +func compileCheckPlural(count int, singular string, plural string) string { + if count == 1 { + return singular + } + + return plural } // joinCompileCheckSummaryParts joins the summary parts as "A and b" or "A, b and c". diff --git a/cli/dispatcher/internal/dispatcher/compile_check_message_test.go b/cli/dispatcher/internal/dispatcher/compile_check_message_test.go new file mode 100644 index 000000000..3569cfca0 --- /dev/null +++ b/cli/dispatcher/internal/dispatcher/compile_check_message_test.go @@ -0,0 +1,107 @@ +package dispatcher + +import ( + "strconv" + "testing" +) + +const compileCheckFastCheckedReminder = "run with --full before finishing to cover assembly-wide rules." + +// compileCheckMessageNames returns count distinct assembly names, since the summary counts names. +func compileCheckMessageNames(count int) []string { + names := []string{} + for index := range count { + names = append(names, "Assembly"+strconv.Itoa(index)) + } + + return names +} + +// Verifies that every count the summary puts before a noun uses the singular for exactly one and +// the plural for zero and for more than one. +func TestCompileCheckMessageUsesTheSingularOnlyForACountOfOne(t *testing.T) { + cases := []struct { + name string + response compileCheckResponse + want string + }{ + { + name: "no compiled assembly leaves the first noun to the next count", + response: compileCheckResponse{ReusedAssemblies: compileCheckMessageNames(2)}, + want: "Reused 2 assemblies with 0 errors and 0 warnings.", + }, + { + name: "one compiled assembly", + response: compileCheckResponse{CompiledAssemblies: compileCheckMessageNames(1)}, + want: "Compiled 1 assembly with 0 errors and 0 warnings.", + }, + { + name: "two compiled assemblies", + response: compileCheckResponse{CompiledAssemblies: compileCheckMessageNames(2)}, + want: "Compiled 2 assemblies with 0 errors and 0 warnings.", + }, + { + name: "one error", + response: compileCheckResponse{CompiledAssemblies: compileCheckMessageNames(2), ErrorCount: 1}, + want: "Compiled 2 assemblies with 1 error and 0 warnings.", + }, + { + name: "two errors", + response: compileCheckResponse{CompiledAssemblies: compileCheckMessageNames(2), ErrorCount: 2}, + want: "Compiled 2 assemblies with 2 errors and 0 warnings.", + }, + { + name: "one warning", + response: compileCheckResponse{CompiledAssemblies: compileCheckMessageNames(2), WarningCount: 1}, + want: "Compiled 2 assemblies with 0 errors and 1 warning.", + }, + { + name: "two warnings", + response: compileCheckResponse{CompiledAssemblies: compileCheckMessageNames(2), WarningCount: 2}, + want: "Compiled 2 assemblies with 0 errors and 2 warnings.", + }, + { + name: "one blocked assembly", + response: compileCheckResponse{ + CompiledAssemblies: compileCheckMessageNames(2), + BlockedAssemblies: compileCheckMessageNames(1), + }, + want: "Compiled 2 assemblies with 0 errors and 0 warnings." + + " 1 assembly was not compiled because an assembly it references has errors.", + }, + { + name: "two blocked assemblies", + response: compileCheckResponse{ + CompiledAssemblies: compileCheckMessageNames(2), + BlockedAssemblies: compileCheckMessageNames(2), + }, + want: "Compiled 2 assemblies with 0 errors and 0 warnings." + + " 2 assemblies were not compiled because an assembly they reference has errors.", + }, + { + name: "one fast-checked assembly", + response: compileCheckResponse{ + CompiledAssemblies: compileCheckMessageNames(2), + FastCheckedAssemblies: compileCheckMessageNames(1), + }, + want: "Compiled 2 assemblies and fast-checked 1 with 0 errors and 0 warnings." + + " 1 assembly was checked on its changed files only; " + compileCheckFastCheckedReminder, + }, + { + name: "two fast-checked assemblies", + response: compileCheckResponse{ + CompiledAssemblies: compileCheckMessageNames(2), + FastCheckedAssemblies: compileCheckMessageNames(2), + }, + want: "Compiled 2 assemblies and fast-checked 2 with 0 errors and 0 warnings." + + " 2 assemblies were checked on their changed files only; " + compileCheckFastCheckedReminder, + }, + } + for _, testCase := range cases { + t.Run(testCase.name, func(t *testing.T) { + if got := compileCheckMessage(testCase.response); got != testCase.want { + t.Fatalf("unexpected summary\n got: %q\nwant: %q", got, testCase.want) + } + }) + } +} diff --git a/cli/dispatcher/internal/dispatcher/compile_check_test.go b/cli/dispatcher/internal/dispatcher/compile_check_test.go index 118cfb275..c844f39b8 100644 --- a/cli/dispatcher/internal/dispatcher/compile_check_test.go +++ b/cli/dispatcher/internal/dispatcher/compile_check_test.go @@ -225,7 +225,7 @@ func TestBuildCompileCheckResponseCountsWarningsOnASuccessfulRun(t *testing.T) { if !response.Success || response.WarningCount != 1 { t.Fatalf("expected a clean run with one warning, got %+v", response) } - if response.Message != "Compiled 1 assemblies with 0 errors and 1 warnings." { + if response.Message != "Compiled 1 assembly with 0 errors and 1 warning." { t.Fatalf("the summary should report the warning count, got %q", response.Message) } } @@ -285,7 +285,7 @@ func TestBuildCompileCheckResponseSummarizesCompiledAndReusedTogether(t *testing }, }, "/projects/sample") - if response.Message != "Compiled 1 assemblies and reused 2 with 0 errors and 0 warnings." { + if response.Message != "Compiled 1 assembly and reused 2 with 0 errors and 0 warnings." { t.Fatalf("unexpected summary, got %q", response.Message) } } @@ -312,7 +312,7 @@ func TestBuildCompileCheckResponseKeepsTheCompiledOnlySummary(t *testing.T) { Units: []compilecheck.UnitResult{{Assembly: "A", Succeeded: true}}, }, "/projects/sample") - if response.Message != "Compiled 1 assemblies with 0 errors and 0 warnings." { + if response.Message != "Compiled 1 assembly with 0 errors and 0 warnings." { t.Fatalf("unexpected summary, got %q", response.Message) } } @@ -378,7 +378,7 @@ func TestBuildCompileCheckResponseSaysWhyBlockedAssembliesWereNotCompiled(t *tes Blocked: []string{"B", "C"}, }, "/projects/sample") - want := "Compiled 1 assemblies with 1 errors and 0 warnings." + + want := "Compiled 1 assembly with 1 error and 0 warnings." + " 2 assemblies were not compiled because an assembly they reference has errors." if response.Message != want { t.Fatalf("unexpected summary, got %q", response.Message) From 360f838fdaf641af0f9179d9f384d00de980b41c Mon Sep 17 00:00:00 2001 From: hatayama Date: Thu, 15 Oct 2026 15:52:48 +0900 Subject: [PATCH 3/3] docs: compile-check skill says which full compiles drop the baseline and how a failure prints The skill said only a --full run with errors drops an assembly's baseline. A run that fell back to a full compile and ended in errors drops it too, and a run that compiled the assembly only because a reference changed records none, so an agent could not tell whether its next run would be a full compile again. It also said only that ErrorCount above zero exits 1. A failure of the command itself exits 1 as well, with no payload on stdout and the error envelope, carrying its ErrorCode, on stderr. --- .../Editor/CliOnlyTools~/CompileCheck/Skill/SKILL.md | 11 +++++++---- docs/compile-check.md | 8 +++++--- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/Packages/src/Editor/CliOnlyTools~/CompileCheck/Skill/SKILL.md b/Packages/src/Editor/CliOnlyTools~/CompileCheck/Skill/SKILL.md index 5652dd074..8f3df62b0 100644 --- a/Packages/src/Editor/CliOnlyTools~/CompileCheck/Skill/SKILL.md +++ b/Packages/src/Editor/CliOnlyTools~/CompileCheck/Skill/SKILL.md @@ -58,9 +58,10 @@ declaration is checked on the changed files only, in a few seconds. Adding or re directive counts as a declaration change. Such a check skips a few assembly-wide rules, so run once with `--full` before finishing. When the project treats warnings as errors, a rule listed in `UncheckedCodes`, such as CS0414, can fail that `--full` run only. The first body edit after a Unity -build, after a `--full` that had errors, after a run that skipped the assembly, or after -`--all` in an assembly that references another of the project is compiled in full and records what -the next check compares against; `FastPathFallbacks` says why. +build, after a full compile that had errors (a `--full` run or a run that fell back to one), after +a run that compiled it only because an assembly it references changed, after a run that skipped the +assembly, or after `--all` in an assembly that references another of the project is compiled in +full and records what the next check compares against; `FastPathFallbacks` says why. ## Parameters @@ -92,7 +93,9 @@ A JSON payload: - `ProjectRoot`: resolved project root - `Message`: one-line summary -The process exits 1 when `ErrorCount` is greater than zero. +The process exits 1 when `ErrorCount` is greater than zero. A failure of the command itself, such +as `COMPILE_CHECK_UNITY_BUILD_REQUIRED`, also exits 1 but prints no payload: the error, with its +`ErrorCode`, goes to stderr. Only one compile-check runs per project at a time. A second one waits for the first, up to five minutes, then fails with `COMPILE_CHECK_PROJECT_BUSY`: wait for the other run to finish and run diff --git a/docs/compile-check.md b/docs/compile-check.md index 49bcc733d..ac9d11d99 100644 --- a/docs/compile-check.md +++ b/docs/compile-check.md @@ -199,8 +199,9 @@ So the first body edit is compiled in full more often than it looks: references has errors keeps them: the next run checks them against the inputs and output hashes they were recorded with before using them, so a reference that comes back the same leaves the assembly reused or checked on its changed files, and one that comes back changed compiles it. -- `--full` while the assembly still has errors drops its baseline, so the run after the fix is - compiled in full. Running `--full` once the errors are fixed keeps it. +- A full compile while the assembly still has errors, whether a `--full` run or a run that fell + back to one, drops its baseline, so the run after the fix is compiled in full. Running `--full` + once the errors are fixed keeps it. What the check does not guarantee, so run `--full` before finishing: @@ -258,7 +259,8 @@ check of the same changed files instead of checking them again), and `FastPathFa `Reason` of each assembly the check was tried on and compiled in full. `FastPathBaselineNotes` gives the `Assembly` and `Reason` of each successful full compile that left no baseline. The process exits 1 when -`ErrorCount` is greater than zero. +`ErrorCount` is greater than zero. A failure of the command itself also exits 1 but writes nothing to +stdout: stderr gets `{"Success": false, "Error": {...}}`, whose `ErrorCode` names the failure. With the `ULOOP_DEBUG` environment variable set, each run also writes two entries to the CLI VibeLog of the project, `cli_compile_check_start` and `cli_compile_check_complete`, with which