From c6de2defb5353d8eca207d0aa629b5ac3f402e1e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Jul 2026 16:21:10 +0000 Subject: [PATCH] feat(tui): generate fuzzyMatch/fuzzyFilter via fluessig (f64/uint32 path) Swap the hand-written `fuzzyMatch`/`fuzzyFilter` napi exports (packages/tui/ src/fuzzy.ts) to fluessig-generated ops, exercising the #77 float/unsigned scalar arms now available at the bumped pin (e056bb9): - `FuzzyMatchResult.score` is authored as `float64` -> lowers to Rust `f64`, crossing to JS `number` (byte-identical `score: number`). - `fuzzyFilter` returns `{ list: uint32 }` -> Rust `Vec`, crossing to JS `Array` (byte-identical), the ranked surviving indices. Author the two ops + the `FuzzyMatchResult` model in schema/api.json, add the `core_impl` delegations into `pidgin_tui`'s fuzzy layer (same logic the hand-written exports called), regenerate src/generated.rs, and delete the hand-written `#[napi]` exports. The generated `.d.ts`/`.js`/nativeBinding entries for all three symbols are byte-identical to the pre-swap build; pi's own test/fuzzy.test.ts oracle passes 14/14 against the locally-built addon. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_017xL2W7dkhPsTSRuXQNCDeY --- crates/pidgin-napi/schema/api.json | 68 +++++++++++++++++++++++++++++ crates/pidgin-napi/src/core_impl.rs | 22 ++++++++++ crates/pidgin-napi/src/generated.rs | 26 +++++++++++ crates/pidgin-napi/src/lib.rs | 49 +++++---------------- 4 files changed, 127 insertions(+), 38 deletions(-) diff --git a/crates/pidgin-napi/schema/api.json b/crates/pidgin-napi/schema/api.json index 68fa3fc6..eef6b28c 100644 --- a/crates/pidgin-napi/schema/api.json +++ b/crates/pidgin-napi/schema/api.json @@ -84,6 +84,22 @@ "nullable": false } ] + }, + { + "name": "FuzzyMatchResult", + "doc": "Result of [`fuzzy_match`]; serialized to pi's `{ matches, score }`.", + "fields": [ + { + "name": "matches", + "type": "boolean", + "nullable": false + }, + { + "name": "score", + "type": "float64", + "nullable": false + } + ] } ], "unions": [], @@ -870,6 +886,58 @@ "name": "hasTrustRequiringProjectResources" } } + }, + { + "name": "fuzzyMatch", + "doc": "`fuzzyMatch` (fuzzy.ts): fuzzy-match `query` against `text`, returning pi's\n`{ matches, score }` (lower score = better).", + "shape": "unary", + "infallible": true, + "readonly": true, + "params": [ + { + "name": "query", + "type": "string" + }, + { + "name": "text", + "type": "string" + } + ], + "returns": { + "model": "FuzzyMatchResult" + }, + "bindings": { + "node": { + "name": "fuzzyMatch" + } + } + }, + { + "name": "fuzzyFilter", + "doc": "`fuzzyFilter` (fuzzy.ts): run pi's whole filter orchestration in Rust. Given\neach candidate's already-materialized text and the query, return the\nsurviving candidates' original indices ranked best-match-first. The shim\nmaps these indices back to items, so pi's `getText` callback stays in JS.", + "shape": "unary", + "infallible": true, + "readonly": true, + "params": [ + { + "name": "texts", + "type": { + "list": "string" + } + }, + { + "name": "query", + "type": "string" + } + ], + "returns": { + "list": "uint32" + }, + "bindings": { + "node": { + "name": "fuzzyFilter" + } + } } ] }, diff --git a/crates/pidgin-napi/src/core_impl.rs b/crates/pidgin-napi/src/core_impl.rs index fa7a18e9..b64d6983 100644 --- a/crates/pidgin-napi/src/core_impl.rs +++ b/crates/pidgin-napi/src/core_impl.rs @@ -33,6 +33,12 @@ /// native `word-navigation.ts` shim. Cursors are UTF-16 string indices crossing /// as `int32` (JS `number`) and widened to the engine's `usize` at the seam — /// the JS-visible cursor values are identical to the pre-swap wrappers. +/// - the tui fuzzy ops (`fuzzyMatch`, `fuzzyFilter`) route into `pidgin_tui`'s +/// fuzzy layer, backing the native `fuzzy.ts` shim. `fuzzyMatch` returns +/// `FuzzyMatchResult { matches, score }` with the score crossing as `float64` +/// (JS `number`); `fuzzyFilter` returns the surviving indices as `uint32` (JS +/// `number`), widened from the engine's `usize` at the seam — the JS-visible +/// scores and indices are identical to the pre-swap wrappers. pub struct PidginImpl; impl crate::generated::PidginCore for PidginImpl { @@ -262,6 +268,22 @@ impl crate::generated::PidginCore for PidginImpl { &cwd, &home_dir, ) } + + fn fuzzy_match(query: String, text: String) -> crate::generated::FuzzyMatchResult { + let m = pidgin_tui::fuzzy_match(&query, &text); + crate::generated::FuzzyMatchResult { + matches: m.matches, + score: m.score, + } + } + + fn fuzzy_filter(texts: Vec, query: String) -> Vec { + let text_refs: Vec<&str> = texts.iter().map(String::as_str).collect(); + pidgin_tui::fuzzy_filter_indices(&text_refs, &query) + .into_iter() + .map(|i| i as u32) + .collect() + } } // --- coding-agent session-cwd seam (core/session-cwd.ts) -------------------- diff --git a/crates/pidgin-napi/src/generated.rs b/crates/pidgin-napi/src/generated.rs index ae96ba2b..c1da3745 100644 --- a/crates/pidgin-napi/src/generated.rs +++ b/crates/pidgin-napi/src/generated.rs @@ -53,6 +53,14 @@ pub struct SessionCwdIssueJs { pub fallback_cwd: String, } +/// Result of [`fuzzy_match`]; serialized to pi's `{ matches, score }`. +#[napi(object)] +#[derive(Clone)] +pub struct FuzzyMatchResult { + pub matches: bool, + pub score: f64, +} + /// The `Pidgin` contract — implement over the engine in `crate::core_impl`. pub trait PidginCore: Sized + Send + Sync + 'static { fn version() -> String; @@ -100,6 +108,8 @@ pub trait PidginCore: Sized + Send + Sync + 'static { fn is_newer_package_version(candidate_version: String, current_version: String) -> bool; fn get_project_trust_parent_path(cwd: String) -> Option; fn has_trust_requiring_project_resources(cwd: String, home_dir: String) -> bool; + fn fuzzy_match(query: String, text: String) -> FuzzyMatchResult; + fn fuzzy_filter(texts: Vec, query: String) -> Vec; } /// The `KeybindingsManagerCore` contract — implement over the engine in `crate::core_impl`. @@ -424,6 +434,22 @@ pub fn has_trust_requiring_project_resources(cwd: String, home_dir: String) -> b ) } +/// `fuzzyMatch` (fuzzy.ts): fuzzy-match `query` against `text`, returning pi's +/// `{ matches, score }` (lower score = better). +#[napi(js_name = "fuzzyMatch")] +pub fn fuzzy_match(query: String, text: String) -> FuzzyMatchResult { + ::fuzzy_match(query, text) +} + +/// `fuzzyFilter` (fuzzy.ts): run pi's whole filter orchestration in Rust. Given +/// each candidate's already-materialized text and the query, return the +/// surviving candidates' original indices ranked best-match-first. The shim +/// maps these indices back to items, so pi's `getText` callback stays in JS. +#[napi(js_name = "fuzzyFilter")] +pub fn fuzzy_filter(texts: Vec, query: String) -> Vec { + ::fuzzy_filter(texts, query) +} + /// The Rust-backed keybindings core, exposed to JavaScript as /// `KeybindingsManagerCore`. #[napi] diff --git a/crates/pidgin-napi/src/lib.rs b/crates/pidgin-napi/src/lib.rs index 45b78a7d..9d976aab 100644 --- a/crates/pidgin-napi/src/lib.rs +++ b/crates/pidgin-napi/src/lib.rs @@ -807,44 +807,17 @@ pub fn migrate_keybindings_config(raw_json: String) -> napi::Result { // --- tui fuzzy layer (packages/tui/src/fuzzy.ts) --------------------------- // -// Thin wrappers over `pidgin_tui::fuzzy`, backing the hand-written native -// `fuzzy.ts` shim. `fuzzyMatch` crosses as a plain `{ matches, score }` -// object. `fuzzyFilter` crosses as `(texts, query) -> ranked indices`: the -// shim materializes each item's text via its `getText` callback in JS, calls -// this, and maps the returned indices back to items — so pi's `getText` stays -// on the JS side while the whole tokenize/AND-gate/score-sum/sort orchestration -// runs in Rust. - -/// Result of [`fuzzy_match`]; serialized to pi's `{ matches, score }`. -#[napi(object)] -pub struct FuzzyMatchResult { - pub matches: bool, - pub score: f64, -} - -/// `fuzzyMatch` (fuzzy.ts): fuzzy-match `query` against `text`, returning pi's -/// `{ matches, score }` (lower score = better). -#[napi(js_name = "fuzzyMatch")] -pub fn fuzzy_match(query: String, text: String) -> FuzzyMatchResult { - let m = pidgin_tui::fuzzy_match(&query, &text); - FuzzyMatchResult { - matches: m.matches, - score: m.score, - } -} - -/// `fuzzyFilter` (fuzzy.ts): run pi's whole filter orchestration in Rust. Given -/// each candidate's already-materialized text and the query, return the -/// surviving candidates' original indices ranked best-match-first. The shim -/// maps these indices back to items, so pi's `getText` callback stays in JS. -#[napi(js_name = "fuzzyFilter")] -pub fn fuzzy_filter(texts: Vec, query: String) -> Vec { - let text_refs: Vec<&str> = texts.iter().map(String::as_str).collect(); - pidgin_tui::fuzzy_filter_indices(&text_refs, &query) - .into_iter() - .map(|i| i as u32) - .collect() -} +// The two fuzzy ops (`fuzzyMatch`, `fuzzyFilter`) now generate from the fluessig +// api schema through `crate::generated` + `crate::core_impl`, routing into +// `pidgin_tui`'s fuzzy layer. `fuzzyMatch` returns `FuzzyMatchResult`, whose +// `score` crosses as `float64` (JS `number`); `fuzzyFilter` returns the ranked +// surviving indices as `uint32` (JS `number`), widened from the engine's `usize` +// at the core seam. `fuzzyFilter`'s shim materializes each item's text via its +// `getText` callback in JS and maps the returned indices back to items, so pi's +// `getText` stays JS-side while the whole tokenize/AND-gate/score-sum/sort +// orchestration runs in Rust. The hand-written `#[napi]` exports that lived here +// were deleted; edit `schema/api.json` and rerun `regen.sh` instead of re-adding +// them. // --- tui word-navigation layer (packages/tui/src/word-navigation.ts) -------- //