[object Object]

← back to Cli Printing Press

fix(scorer): resolve binary at build/stage/bin/ before cliDir (#1150) (#1412)

2abbfec04826d9763877490df1612d433f2debce · 2026-05-16 03:32:14 -0500 · kberger111

* fix(scorer): resolve binary at build/stage/bin/ before cliDir (#1150)

The Sample Output Probe was looking for the printed CLI's binary at
<cliDir>/<name>(.exe), but the generator's --validate "build runnable
binary" gate emits the binary at <cliDir>/build/stage/bin/<name>(.exe).
On every printed CLI in the catalog the probe missed and reported
"is not executable" — a scorer-calibration false alarm, not a real
defect.

Expand liveCheckBinaryCandidatesForGOOS to try the canonical staged
path first, then fall back to the legacy cliDir layout. This subsumes
the original .exe-suffix scope of #1150 (already partially fixed via
platform.ExecutablePathForGOOS) and addresses piercekiltoff's wider
finding from the servicetitan-crm retro: it's the directory, not just
the suffix.

Resolution order:
  1. <cliDir>/build/stage/bin/<name>     canonical Unix
  2. <cliDir>/build/stage/bin/<name>.exe canonical Windows
  3. <cliDir>/<name>                     legacy fallback
  4. <cliDir>/<name>.exe                 legacy Windows fallback

Adds TestLiveCheckBinaryCandidatesPreferBuildStageBin to lock the
order in.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* test(scorer): strengthen candidate ordering + add build/stage/bin integration test

Per Greptile review on #1412:

1. Ordering test now asserts all four cross-product positions:
   stagedUnix < legacyUnix, stagedUnix < legacyWin,
   stagedWin  < legacyUnix, stagedWin  < legacyWin.
   Previously only stagedUnix < legacyUnix was checked, leaving
   a silent window for reorder regressions on the Windows paths.

2. TestLiveCheck_FindsBinaryInBuildStageBin exercises the full
   RunLiveCheck probe with the stub binary placed at
   build/stage/bin/<name> — the canonical post-v4.5.1 layout.
   Skipped on Windows (shell-script stubs not supported there,
   matching the existing integration-test convention).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Ken Berger <kberger@megadatahs.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>

Files touched

Diff

commit 2abbfec04826d9763877490df1612d433f2debce
Author: kberger111 <ksberger@gmail.com>
Date:   Sat May 16 03:32:14 2026 -0500

    fix(scorer): resolve binary at build/stage/bin/ before cliDir (#1150) (#1412)
    
    * fix(scorer): resolve binary at build/stage/bin/ before cliDir (#1150)
    
    The Sample Output Probe was looking for the printed CLI's binary at
    <cliDir>/<name>(.exe), but the generator's --validate "build runnable
    binary" gate emits the binary at <cliDir>/build/stage/bin/<name>(.exe).
    On every printed CLI in the catalog the probe missed and reported
    "is not executable" — a scorer-calibration false alarm, not a real
    defect.
    
    Expand liveCheckBinaryCandidatesForGOOS to try the canonical staged
    path first, then fall back to the legacy cliDir layout. This subsumes
    the original .exe-suffix scope of #1150 (already partially fixed via
    platform.ExecutablePathForGOOS) and addresses piercekiltoff's wider
    finding from the servicetitan-crm retro: it's the directory, not just
    the suffix.
    
    Resolution order:
      1. <cliDir>/build/stage/bin/<name>     canonical Unix
      2. <cliDir>/build/stage/bin/<name>.exe canonical Windows
      3. <cliDir>/<name>                     legacy fallback
      4. <cliDir>/<name>.exe                 legacy Windows fallback
    
    Adds TestLiveCheckBinaryCandidatesPreferBuildStageBin to lock the
    order in.
    
    Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
    
    * test(scorer): strengthen candidate ordering + add build/stage/bin integration test
    
    Per Greptile review on #1412:
    
    1. Ordering test now asserts all four cross-product positions:
       stagedUnix < legacyUnix, stagedUnix < legacyWin,
       stagedWin  < legacyUnix, stagedWin  < legacyWin.
       Previously only stagedUnix < legacyUnix was checked, leaving
       a silent window for reorder regressions on the Windows paths.
    
    2. TestLiveCheck_FindsBinaryInBuildStageBin exercises the full
       RunLiveCheck probe with the stub binary placed at
       build/stage/bin/<name> — the canonical post-v4.5.1 layout.
       Skipped on Windows (shell-script stubs not supported there,
       matching the existing integration-test convention).
    
    Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
    
    ---------
    
    Co-authored-by: Ken Berger <kberger@megadatahs.com>
    Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
    Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
---
 internal/pipeline/live_check.go      | 12 ++++++++-
 internal/pipeline/live_check_test.go | 49 ++++++++++++++++++++++++++++++++++++
 2 files changed, 60 insertions(+), 1 deletion(-)

diff --git a/internal/pipeline/live_check.go b/internal/pipeline/live_check.go
index 62300394..b26007b0 100644
--- a/internal/pipeline/live_check.go
+++ b/internal/pipeline/live_check.go
@@ -276,13 +276,23 @@ func liveCheckBinaryCandidatesForGOOS(cliDir, name, goos string) []string {
 		base := filepath.Base(cliDir)
 		names = []string{base + "-pp-cli", base}
 	}
-	candidates := make([]string, 0, len(names)*2)
+	// Resolution order (per issue #1150):
+	//   1. <cliDir>/build/stage/bin/<name>           canonical Unix
+	//   2. <cliDir>/build/stage/bin/<name>.exe       canonical Windows
+	//   3. <cliDir>/<name>                           legacy fallback
+	//   4. <cliDir>/<name>.exe                       legacy Windows fallback
+	// The generator's --validate "build runnable binary" gate emits the
+	// binary under build/stage/bin/; older layouts left it at cliDir.
+	stagedDir := filepath.Join(cliDir, "build", "stage", "bin")
+	candidates := make([]string, 0, len(names)*4)
 	seen := map[string]struct{}{}
 	for _, candidate := range names {
 		if candidate == "" {
 			continue
 		}
 		for _, path := range []string{
+			filepath.Join(stagedDir, candidate),
+			platform.ExecutablePathForGOOS(filepath.Join(stagedDir, candidate), goos),
 			filepath.Join(cliDir, candidate),
 			platform.ExecutablePathForGOOS(filepath.Join(cliDir, candidate), goos),
 		} {
diff --git a/internal/pipeline/live_check_test.go b/internal/pipeline/live_check_test.go
index 081dc2c4..85169a8a 100644
--- a/internal/pipeline/live_check_test.go
+++ b/internal/pipeline/live_check_test.go
@@ -508,6 +508,55 @@ func TestLiveCheck_BinaryAutoDerivation(t *testing.T) {
 	require.Contains(t, result.Features[0].Example, "stub x matched")
 }
 
+func TestLiveCheckBinaryCandidatesPreferBuildStageBin(t *testing.T) {
+	t.Parallel()
+
+	dir := filepath.Join("tmp", "sample-cli")
+	stagedUnix := filepath.Join(dir, "build", "stage", "bin", "sample-cli-pp-cli")
+	stagedWin := platform.ExecutablePathForGOOS(filepath.Join(dir, "build", "stage", "bin", "sample-cli-pp-cli"), "windows")
+	legacyUnix := filepath.Join(dir, "sample-cli-pp-cli")
+
+	cands := liveCheckBinaryCandidatesForGOOS(dir, "", "windows")
+	assert.Contains(t, cands, stagedUnix)
+	assert.Contains(t, cands, stagedWin)
+	assert.Contains(t, cands, legacyUnix)
+
+	// Canonical staged path must come before the legacy cliDir fallback.
+	stagedIdx := -1
+	legacyIdx := -1
+	for i, c := range cands {
+		if c == stagedUnix && stagedIdx == -1 {
+			stagedIdx = i
+		}
+		if c == legacyUnix && legacyIdx == -1 {
+			legacyIdx = i
+		}
+	}
+	assert.True(t, stagedIdx >= 0 && legacyIdx >= 0 && stagedIdx < legacyIdx,
+		"staged build/stage/bin path must be tried before cliDir legacy path, got order %v", cands)
+}
+
+func TestLiveCheck_FindsBinaryInBuildStageBin(t *testing.T) {
+	// Verify that RunLiveCheck finds and executes a binary placed at
+	// <cliDir>/build/stage/bin/<name> — the canonical layout written by the
+	// generator's --validate "build runnable binary" gate (post-v4.5.1).
+	if runtime.GOOS == "windows" {
+		t.Skip("shell-script stub not supported on Windows")
+	}
+	dir := t.TempDir()
+	stagedBinDir := filepath.Join(dir, "build", "stage", "bin")
+	require.NoError(t, os.MkdirAll(stagedBinDir, 0o755))
+	stub := filepath.Join(stagedBinDir, "stub")
+	require.NoError(t, os.WriteFile(stub, []byte("#!/bin/sh\necho '{\"data\":[{\"id\":\"1\"}]}'\n"), 0o755))
+	writeTestResearchJSON(t, dir, []NovelFeature{
+		{Name: "List items", Command: "items list", Example: "stub items list --json"},
+	})
+	result := RunLiveCheck(LiveCheckOptions{CLIDir: dir, BinaryName: "stub", Timeout: 5 * time.Second})
+	require.False(t, result.Unable, "check was Unable: %s", result.Reason)
+	require.Equal(t, 1, result.Checked())
+	assert.Equal(t, 1, result.Passed, "expected binary at build/stage/bin/ to be found and run")
+}
+
 func TestLiveCheckBinaryCandidatesIncludeHostExecutableName(t *testing.T) {
 	t.Parallel()
 

← 793d5b27 fix(skills): surface novel-feature hand-code scope at Phase  ·  back to Cli Printing Press  ·  fix(skills): drop cli-skills mirror regen from publish flow f64e9913 →