From e088d4a6a5c5b626cc446f135cb7ad81d3263214 Mon Sep 17 00:00:00 2001
From: jettwang
Date: Fri, 24 Jul 2026 14:58:04 +0800
Subject: [PATCH 1/4] feat(scorekit): settings-pinned binary path
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Add Settings → scorekit binary: an absolute-path pin that beats PATH and
well-known prefixes (SCOREBENCH_SCOREKIT still overrides), so machines
with several scorekit versions can choose one without env-var gymnastics.
The startup handshake reports which discovery channel won, the tested
range widens to >=0.3.0, <0.5.0, and the pin is re-seeded on every
settings save. Docs and acceptance matrix updated.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
---
README.md | 4 +-
docs-site/src/getting-started.md | 4 +-
docs-site/src/troubleshooting.md | 2 +-
docs/roadmap.md | 4 +-
src-tauri/src/lib.rs | 21 ++-
src-tauri/src/scorekit.rs | 166 +++++++++++++++++++---
src-tauri/src/settings.rs | 39 +++++
src-tauri/tests/fixtures/doctor.json | 2 +-
src-tauri/tests/fixtures/forest.meta.json | 63 ++++++++
src/lib/api.ts | 4 +
src/lib/components/SettingsModal.svelte | 37 +++++
src/lib/i18n.svelte.ts | 9 ++
12 files changed, 325 insertions(+), 30 deletions(-)
diff --git a/README.md b/README.md
index 44cfba1..b51da2f 100644
--- a/README.md
+++ b/README.md
@@ -81,7 +81,7 @@ Install scorebench from [Releases](https://github.com/talkincode/scorebench/rele
- Linux: use the `.deb` or `.AppImage` artifact.
- Windows: use the `.msi` or `-setup.exe` artifact.
-Install ScoreKit 0.3.x separately (for example with Homebrew `brew install talkincode/tap/scorekit`, or from [ScoreKit Releases](https://github.com/talkincode/scorekit/releases)), then confirm:
+Install ScoreKit 0.3.x–0.4.x separately (for example with Homebrew `brew install talkincode/tap/scorekit`, or from [ScoreKit Releases](https://github.com/talkincode/scorekit/releases)), then confirm:
```bash
brew trust --tap talkincode/tap # when installing ScoreKit via Homebrew tap
@@ -89,7 +89,7 @@ scorekit --version
scorekit doctor
```
-If scorebench starts but cannot find ScoreKit, set `SCOREBENCH_SCOREKIT` to the absolute path of the ScoreKit executable and restart the app.
+If scorebench starts but cannot find ScoreKit, pin the executable's absolute path in Settings → scorekit binary (also the way to choose between several installed versions), or set `SCOREBENCH_SCOREKIT` and restart the app.
## Development (Contributors)
diff --git a/docs-site/src/getting-started.md b/docs-site/src/getting-started.md
index ea50cf2..7dcd68c 100644
--- a/docs-site/src/getting-started.md
+++ b/docs-site/src/getting-started.md
@@ -4,7 +4,7 @@ This chapter takes the shortest path through the complete loop: install the depe
## 1. Install scorebench and ScoreKit
-scorebench needs the ScoreKit CLI at runtime. The desktop app does not bundle the CLI. This version is tested with ScoreKit 0.3.x.
+scorebench needs the ScoreKit CLI at runtime. The desktop app does not bundle the CLI. This version is tested with ScoreKit 0.3.x–0.4.x.
### macOS (Homebrew, recommended)
@@ -50,7 +50,7 @@ scorekit doctor
`scorekit doctor` should confirm FFmpeg, at least one renderer, and the default sound source status.
-> If scorebench cannot locate the command, set `SCOREBENCH_SCOREKIT` to the absolute path of the ScoreKit executable. GUI applications on macOS often receive a shorter `PATH` than terminal shells, so a command can work in Terminal and still be invisible to the app.
+> If scorebench cannot locate the command, pin the ScoreKit executable's absolute path in Settings → scorekit binary (or set `SCOREBENCH_SCOREKIT`). GUI applications on macOS often receive a shorter `PATH` than terminal shells, so a command can work in Terminal and still be invisible to the app.
## 2. Create a project directory
diff --git a/docs-site/src/troubleshooting.md b/docs-site/src/troubleshooting.md
index a59dc31..4cf7457 100644
--- a/docs-site/src/troubleshooting.md
+++ b/docs-site/src/troubleshooting.md
@@ -12,7 +12,7 @@ scorekit --version
scorekit doctor
```
-If the CLI works in a terminal but not in the desktop application, set `SCOREBENCH_SCOREKIT` to the executable's absolute path and restart scorebench. Confirm that the current user can execute the file.
+If the CLI works in a terminal but not in the desktop application, pin the executable's absolute path in Settings → scorekit binary. The same field selects one copy when several ScoreKit versions are installed; the version block underneath shows which path and discovery channel are active. `SCOREBENCH_SCOREKIT` still works and overrides the settings pin, but requires restarting scorebench. Confirm that the current user can execute the file.
## Homebrew says `untrusted tap`
diff --git a/docs/roadmap.md b/docs/roadmap.md
index 062667a..bf0ed20 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -38,7 +38,7 @@ Technique is *relocated*, not eliminated: the user is freed from craft, but the
One window that opens a project directory, shows a chat panel wired to a stub agent loop, invokes `scorekit doctor --json` / `build --json` as subprocess tools, and plays a rendered OGG with a canvas spectrum. Proves the four seams: chat ↔ agent core, agent ↔ scorekit subprocess, disk ↔ project state, audio ↔ WebAudio.
-Findings: scorekit's machine contract is *failure-side* JSON — success is exit 0 plus (for `build`) the atomically-written `.meta.json`, which scorebench treats as the build result; human stdout is never parsed. The binary is located via `SCOREBENCH_SCOREKIT` > PATH > well-known prefixes (GUI apps on macOS launch with a stripped PATH). Asset bytes cross IPC as binary (`tauri::ipc::Response`) with a containment check pinning reads inside the project root; playback, FFT, and the spectrum stay entirely in the webview (WebAudio `AnalyserNode`), honoring the no-in-house-audio rule. The July 2026 GUI smoke covered native project opening, both renderers, OGG/WAV playback, seek/pause/loop, live spectrum switching, filesystem watching, scorekit-missing guidance, and the 960×640 minimum layout contract. M1 replaced the temporary slash-command stub with the real ReACT loop while preserving the tagged `AgentEvent` frontend contract.
+Findings: scorekit's machine contract is *failure-side* JSON — success is exit 0 plus (for `build`) the atomically-written `.meta.json`, which scorebench treats as the build result; human stdout is never parsed. The binary is located via `SCOREBENCH_SCOREKIT` > the settings pin (Settings → scorekit binary, for machines carrying several scorekit versions; the handshake reports which channel won) > PATH > well-known prefixes (GUI apps on macOS launch with a stripped PATH). Asset bytes cross IPC as binary (`tauri::ipc::Response`) with a containment check pinning reads inside the project root; playback, FFT, and the spectrum stay entirely in the webview (WebAudio `AnalyserNode`), honoring the no-in-house-audio rule. The July 2026 GUI smoke covered native project opening, both renderers, OGG/WAV playback, seek/pause/loop, live spectrum switching, filesystem watching, scorekit-missing guidance, and the 960×640 minimum layout contract. M1 replaced the temporary slash-command stub with the real ReACT loop while preserving the tagged `AgentEvent` frontend contract.
### M1 — Agent core (status: complete)
@@ -129,7 +129,7 @@ Rules (MUST):
| Feature | Tier | Happy path | Failure path |
| --- | --- | --- | --- |
-| scorekit binary discovery | 1 | integration tests run via PATH-located binary | `locate_missing_everywhere_is_typed_error`, `locate_env_override_must_be_executable` (`scorekit.rs`) |
+| scorekit binary discovery | 1 | integration tests run via PATH-located binary | `locate_missing_everywhere_is_typed_error`, `locate_env_override_must_be_executable`, `locate_settings_pin_wins_over_path_and_well_known`, `locate_settings_pin_must_be_executable` (`scorekit.rs`) |
| scorekit error contract (`--json` stderr) | 1 | `doctor_fixture_shape_holds`, `meta_fixture_shape_holds` (recorded fixtures) | `parses_recorded_io_error`, `falls_back_on_non_json_stderr` |
| Build param → CLI arg mapping | 1 | `build_params_render_full_arg_set` covers renderer and `--texture-profile` | n/a (pure function, no state) |
| Project directory scan | 1 | `scan_finds_scenes_and_assets` | `scan_rejects_non_directory` |
diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs
index 80642b3..c00df44 100644
--- a/src-tauri/src/lib.rs
+++ b/src-tauri/src/lib.rs
@@ -687,9 +687,14 @@ async fn get_settings(app: AppHandle) -> Result Result<(), BenchError> {
let config_dir = app.path().app_config_dir().map_err(BenchError::io)?;
- tauri::async_runtime::spawn_blocking(move || settings::save(&config_dir, &value))
- .await
- .map_err(BenchError::io)?
+ tauri::async_runtime::spawn_blocking(move || {
+ settings::save(&config_dir, &value)?;
+ // Keep the process-wide scorekit pin in sync with what was persisted.
+ scorekit::set_configured_path(value.scorekit_path.map(PathBuf::from));
+ Ok(())
+ })
+ .await
+ .map_err(BenchError::io)?
}
#[tauri::command]
@@ -722,6 +727,16 @@ pub fn run() {
.manage(agent::AgentState::default())
.manage(watcher::ProjectWatcher::default())
.manage(RecordingSink::default())
+ .setup(|app| {
+ // Seed the scorekit settings pin before the first locate() call.
+ // A corrupt settings file degrades to auto-discovery; it must
+ // never block startup.
+ let config_dir = app.path().app_config_dir()?;
+ if let Ok((settings, _)) = settings::load(&config_dir) {
+ scorekit::set_configured_path(settings.scorekit_path.map(PathBuf::from));
+ }
+ Ok(())
+ })
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_dialog::init())
.invoke_handler(tauri::generate_handler![
diff --git a/src-tauri/src/scorekit.rs b/src-tauri/src/scorekit.rs
index 3a60c4b..c008777 100644
--- a/src-tauri/src/scorekit.rs
+++ b/src-tauri/src/scorekit.rs
@@ -1,6 +1,6 @@
//! Subprocess boundary to the `scorekit` CLI.
//!
-//! Contract (recorded through scorekit 0.3.0, see `tests/fixtures/`):
+//! Contract (recorded through scorekit 0.4.0, see `tests/fixtures/`):
//! - success: exit 0; `build` writes `
+ {:else}
+ {#each inspection.texture_profile.mode_mismatches ?? [] as mismatch}
+