diff --git a/crates/fff-c/include/fff.h b/crates/fff-c/include/fff.h index 1a60fbcd5..2574cfb45 100644 --- a/crates/fff-c/include/fff.h +++ b/crates/fff-c/include/fff.h @@ -1238,6 +1238,94 @@ bool fff_grep_match_get_is_definition(const struct FffGrepMatch *m); */ bool fff_grep_match_get_is_binary(const struct FffGrepMatch *m); +/** + * Final combined score used for ranking. Returns `0` if `s` is null. + * + * ## Safety + * `s` must be a valid `FffScore` pointer or null. + */ +int32_t fff_score_get_total(const struct FffScore *s); + +/** + * Raw fuzzy match score before bonuses and penalties. Returns `0` if `s` is null. + * + * ## Safety + * `s` must be a valid `FffScore` pointer or null. + */ +int32_t fff_score_get_base_score(const struct FffScore *s); + +/** + * Bonus for matching the file name rather than only the path. Returns `0` if `s` is null. + * + * ## Safety + * `s` must be a valid `FffScore` pointer or null. + */ +int32_t fff_score_get_filename_bonus(const struct FffScore *s); + +/** + * Bonus for special file names (e.g. `README`, `main.rs`). Returns `0` if `s` is null. + * + * ## Safety + * `s` must be a valid `FffScore` pointer or null. + */ +int32_t fff_score_get_special_filename_bonus(const struct FffScore *s); + +/** + * Boost from the frecency database. Returns `0` if `s` is null. + * + * ## Safety + * `s` must be a valid `FffScore` pointer or null. + */ +int32_t fff_score_get_frecency_boost(const struct FffScore *s); + +/** + * Penalty based on directory distance from the current file. Returns `0` if `s` is null. + * + * ## Safety + * `s` must be a valid `FffScore` pointer or null. + */ +int32_t fff_score_get_distance_penalty(const struct FffScore *s); + +/** + * Penalty applied to the currently open file. Returns `0` if `s` is null. + * + * ## Safety + * `s` must be a valid `FffScore` pointer or null. + */ +int32_t fff_score_get_current_file_penalty(const struct FffScore *s); + +/** + * Boost when several scoring signals agree. Returns `0` if `s` is null. + * + * ## Safety + * `s` must be a valid `FffScore` pointer or null. + */ +int32_t fff_score_get_combo_match_boost(const struct FffScore *s); + +/** + * Bonus for query segments aligning with path segments. Returns `0` if `s` is null. + * + * ## Safety + * `s` must be a valid `FffScore` pointer or null. + */ +int32_t fff_score_get_path_alignment_bonus(const struct FffScore *s); + +/** + * `true` if the query matched the file name or directory exactly; `false` if `s` is null. + * + * ## Safety + * `s` must be a valid `FffScore` pointer or null. + */ +bool fff_score_get_exact_match(const struct FffScore *s); + +/** + * Match type label (e.g. `"exact_filename"`, `"fuzzy_path"`); null if `s` is null. Do not free. + * + * ## Safety + * `s` must be a valid `FffScore` pointer or null. + */ +const char *fff_score_get_match_type(const struct FffScore *s); + /** * Number of items in the result; `0` if `r` is null. * diff --git a/crates/fff-c/src/accessors.rs b/crates/fff-c/src/accessors.rs index 84da47873..150806f46 100644 --- a/crates/fff-c/src/accessors.rs +++ b/crates/fff-c/src/accessors.rs @@ -7,7 +7,7 @@ use std::ffi::c_char; use std::ptr; use crate::ffi_types::{ - FffFileItem, FffGrepMatch, FffGrepResult, FffMatchRange, FffResult, FffSearchResult, + FffFileItem, FffGrepMatch, FffGrepResult, FffMatchRange, FffResult, FffScore, FffSearchResult, }; // ── FffResult ──────────────────────────────────────────────────────────────── @@ -474,6 +474,140 @@ pub unsafe extern "C" fn fff_grep_match_get_is_binary(m: *const FffGrepMatch) -> unsafe { (*m).is_binary } } +// ── FffScore ───────────────────────────────────────────────────────────────── + +/// Final combined score used for ranking. Returns `0` if `s` is null. +/// +/// ## Safety +/// `s` must be a valid `FffScore` pointer or null. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn fff_score_get_total(s: *const FffScore) -> i32 { + if s.is_null() { + return 0; + } + unsafe { (*s).total } +} + +/// Raw fuzzy match score before bonuses and penalties. Returns `0` if `s` is null. +/// +/// ## Safety +/// `s` must be a valid `FffScore` pointer or null. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn fff_score_get_base_score(s: *const FffScore) -> i32 { + if s.is_null() { + return 0; + } + unsafe { (*s).base_score } +} + +/// Bonus for matching the file name rather than only the path. Returns `0` if `s` is null. +/// +/// ## Safety +/// `s` must be a valid `FffScore` pointer or null. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn fff_score_get_filename_bonus(s: *const FffScore) -> i32 { + if s.is_null() { + return 0; + } + unsafe { (*s).filename_bonus } +} + +/// Bonus for special file names (e.g. `README`, `main.rs`). Returns `0` if `s` is null. +/// +/// ## Safety +/// `s` must be a valid `FffScore` pointer or null. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn fff_score_get_special_filename_bonus(s: *const FffScore) -> i32 { + if s.is_null() { + return 0; + } + unsafe { (*s).special_filename_bonus } +} + +/// Boost from the frecency database. Returns `0` if `s` is null. +/// +/// ## Safety +/// `s` must be a valid `FffScore` pointer or null. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn fff_score_get_frecency_boost(s: *const FffScore) -> i32 { + if s.is_null() { + return 0; + } + unsafe { (*s).frecency_boost } +} + +/// Penalty based on directory distance from the current file. Returns `0` if `s` is null. +/// +/// ## Safety +/// `s` must be a valid `FffScore` pointer or null. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn fff_score_get_distance_penalty(s: *const FffScore) -> i32 { + if s.is_null() { + return 0; + } + unsafe { (*s).distance_penalty } +} + +/// Penalty applied to the currently open file. Returns `0` if `s` is null. +/// +/// ## Safety +/// `s` must be a valid `FffScore` pointer or null. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn fff_score_get_current_file_penalty(s: *const FffScore) -> i32 { + if s.is_null() { + return 0; + } + unsafe { (*s).current_file_penalty } +} + +/// Boost when several scoring signals agree. Returns `0` if `s` is null. +/// +/// ## Safety +/// `s` must be a valid `FffScore` pointer or null. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn fff_score_get_combo_match_boost(s: *const FffScore) -> i32 { + if s.is_null() { + return 0; + } + unsafe { (*s).combo_match_boost } +} + +/// Bonus for query segments aligning with path segments. Returns `0` if `s` is null. +/// +/// ## Safety +/// `s` must be a valid `FffScore` pointer or null. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn fff_score_get_path_alignment_bonus(s: *const FffScore) -> i32 { + if s.is_null() { + return 0; + } + unsafe { (*s).path_alignment_bonus } +} + +/// `true` if the query matched the file name or directory exactly; `false` if `s` is null. +/// +/// ## Safety +/// `s` must be a valid `FffScore` pointer or null. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn fff_score_get_exact_match(s: *const FffScore) -> bool { + if s.is_null() { + return false; + } + unsafe { (*s).exact_match } +} + +/// Match type label (e.g. `"exact_filename"`, `"fuzzy_path"`); null if `s` is null. Do not free. +/// +/// ## Safety +/// `s` must be a valid `FffScore` pointer or null. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn fff_score_get_match_type(s: *const FffScore) -> *const c_char { + if s.is_null() { + return ptr::null(); + } + unsafe { (*s).match_type } +} + // ── FffSearchResult ────────────────────────────────────────────────────────── /// Number of items in the result; `0` if `r` is null. diff --git a/packages/fff-bun/src/fff-api.ts b/packages/fff-bun/src/fff-api.ts index f0e23dd6e..50c42c26a 100644 --- a/packages/fff-bun/src/fff-api.ts +++ b/packages/fff-bun/src/fff-api.ts @@ -179,6 +179,8 @@ export interface Score { currentFilePenalty: number; /** Boost from query history combo matching */ comboMatchBoost: number; + /** Bonus for query segments aligning with path segments */ + pathAlignmentBonus: number; /** Whether this was an exact match */ exactMatch: boolean; /** Type of match: 'fuzzy', 'exact', 'prefix', etc. */ diff --git a/packages/fff-bun/src/ffi.ts b/packages/fff-bun/src/ffi.ts index 32cc44413..f37b01105 100644 --- a/packages/fff-bun/src/ffi.ts +++ b/packages/fff-bun/src/ffi.ts @@ -661,7 +661,8 @@ const SC_FREC = 16; // i32 (4) const SC_DIST = 20; // i32 (4) const SC_CURFILE = 24; // i32 (4) const SC_COMBO = 28; // i32 (4) -const SC_EXACT = 32; // bool (1 + 7 pad) +const SC_PATH_ALIGN = 32; // i32 (4) +const SC_EXACT = 36; // bool (1 + 3 pad) const SC_MTYPE = 40; // *mut c_char (8) const SC_SIZE_OF = 48; @@ -701,6 +702,7 @@ function readScoreStruct(p: number): Score { distancePenalty: read.i32(pp, SC_DIST), currentFilePenalty: read.i32(pp, SC_CURFILE), comboMatchBoost: read.i32(pp, SC_COMBO), + pathAlignmentBonus: read.i32(pp, SC_PATH_ALIGN), exactMatch: read.u8(pp, SC_EXACT) !== 0, matchType: readCString(read.ptr(pp, SC_MTYPE)) ?? "", }; diff --git a/packages/fff-bun/test/index.test.ts b/packages/fff-bun/test/index.test.ts index 4459187f8..fab188b86 100644 --- a/packages/fff-bun/test/index.test.ts +++ b/packages/fff-bun/test/index.test.ts @@ -145,6 +145,19 @@ describe("FileFinder - Full Lifecycle", () => { } }); + test("exact filename query decodes exactMatch=true (#888)", () => { + const result = finder.fileSearch("package.json", { pageSize: 5 }); + expect(result.ok).toBe(true); + if (result.ok) { + const idx = result.value.items.findIndex((i) => i.relativePath === "package.json"); + expect(idx).toBeGreaterThanOrEqual(0); + const score = result.value.scores[idx]; + expect(score.matchType).toBe("exact_filename"); + expect(score.exactMatch).toBe(true); + expect(typeof score.pathAlignmentBonus).toBe("number"); + } + }); + test("search returns empty for non-matching query", () => { const result = finder.fileSearch("xyznonexistentfilenamexyz123456"); expect(result.ok).toBe(true); diff --git a/packages/fff-node/src/fff-api.ts b/packages/fff-node/src/fff-api.ts index f0e23dd6e..50c42c26a 100644 --- a/packages/fff-node/src/fff-api.ts +++ b/packages/fff-node/src/fff-api.ts @@ -179,6 +179,8 @@ export interface Score { currentFilePenalty: number; /** Boost from query history combo matching */ comboMatchBoost: number; + /** Bonus for query segments aligning with path segments */ + pathAlignmentBonus: number; /** Whether this was an exact match */ exactMatch: boolean; /** Type of match: 'fuzzy', 'exact', 'prefix', etc. */ diff --git a/packages/fff-node/src/ffi.ts b/packages/fff-node/src/ffi.ts index fcd86f97d..5bf060d3c 100644 --- a/packages/fff-node/src/ffi.ts +++ b/packages/fff-node/src/ffi.ts @@ -474,6 +474,7 @@ const FFF_SCORE_STRUCT = { distance_penalty: DataType.I32, current_file_penalty: DataType.I32, combo_match_boost: DataType.I32, + path_alignment_bonus: DataType.I32, exact_match: DataType.U8, match_type: DataType.External, }; @@ -487,6 +488,7 @@ interface FffScoreRaw { distance_penalty: number; current_file_penalty: number; combo_match_boost: number; + path_alignment_bonus: number; exact_match: number; match_type: JsExternal; } @@ -623,7 +625,7 @@ const FFF_GREP_MATCH_STRUCT = { match_ranges_count: DataType.U32, context_before_count: DataType.U32, context_after_count: DataType.U32, - fuzzy_score: DataType.U32, // actually u16 in C, but ffi-rs doesn't so we read it as u32 with padding + fuzzy_score: DataType.I16, // u16 in C; ffi-rs has no U16, so read I16 and mask (#888) has_fuzzy_score: DataType.U8, is_binary: DataType.U8, is_definition: DataType.U8, @@ -713,6 +715,7 @@ function readScoreFromRaw(raw: FffScoreRaw): Score { distancePenalty: raw.distance_penalty, currentFilePenalty: raw.current_file_penalty, comboMatchBoost: raw.combo_match_boost, + pathAlignmentBonus: raw.path_alignment_bonus, exactMatch: raw.exact_match !== 0, matchType: readCString(raw.match_type) ?? "", }; @@ -841,7 +844,7 @@ function readGrepMatchFromRaw(raw: FffGrepMatchRaw): GrepMatch { }; if (raw.has_fuzzy_score !== 0) { - match.fuzzyScore = raw.fuzzy_score; + match.fuzzyScore = raw.fuzzy_score & 0xffff; } if (raw.context_before_count > 0) { match.contextBefore = readCStringArray(raw.context_before, raw.context_before_count); diff --git a/packages/fff-node/test/e2e.mjs b/packages/fff-node/test/e2e.mjs index 86db7b7b3..7850375e0 100644 --- a/packages/fff-node/test/e2e.mjs +++ b/packages/fff-node/test/e2e.mjs @@ -100,6 +100,17 @@ describe("fff-node", { concurrency: 1 }, () => { assert.equal(typeof s.matchType, "string"); }); + it("exact filename query decodes exactMatch=true (#888)", () => { + const r = finder.fileSearch("Cargo.toml", { pageSize: 5 }); + assert.ok(r.ok); + const idx = r.value.items.findIndex((i) => i.relativePath === "Cargo.toml"); + assert.ok(idx >= 0, "root Cargo.toml should be in results"); + const s = r.value.scores[idx]; + assert.equal(s.matchType, "exact_filename"); + assert.equal(s.exactMatch, true); + assert.equal(typeof s.pathAlignmentBonus, "number"); + }); + it("totalMatched <= totalFiles", () => { const r = finder.fileSearch("rs", { pageSize: 1 }); assert.ok(r.ok); @@ -194,6 +205,27 @@ describe("fff-node", { concurrency: 1 }, () => { console.log(m.matchRanges); }); + it("plain grep on text files never reports binary/definition/fuzzy flags (#888)", () => { + const r = finder.grep("FffResult", { mode: "plain", pageSize: 100 }); + assert.ok(r.ok); + assert.ok(r.value.items.length > 1); + for (const m of r.value.items) { + assert.equal(m.isBinary, false, `${m.relativePath}:${m.lineNumber} flagged binary`); + assert.equal(m.isDefinition, undefined, `${m.relativePath}:${m.lineNumber} flagged definition`); + assert.equal(m.fuzzyScore, undefined, `${m.relativePath}:${m.lineNumber} has fuzzyScore in plain mode`); + } + }); + + it("fuzzy grep decodes fuzzyScore as u16", () => { + const r = finder.grep("FfRslt", { mode: "fuzzy", pageSize: 20 }); + assert.ok(r.ok); + assert.ok(r.value.items.length > 0); + for (const m of r.value.items) { + assert.equal(typeof m.fuzzyScore, "number"); + assert.ok(m.fuzzyScore >= 0 && m.fuzzyScore <= 0xffff, `fuzzyScore out of u16 range: ${m.fuzzyScore}`); + } + }); + it("pagination returns a second page", () => { const r = finder.grep("fn", { mode: "plain" }); assert.ok(r.ok); diff --git a/packages/shared/fff-api.ts b/packages/shared/fff-api.ts index 26cc03ab4..cc53a3aa8 100644 --- a/packages/shared/fff-api.ts +++ b/packages/shared/fff-api.ts @@ -173,6 +173,8 @@ export interface Score { currentFilePenalty: number; /** Boost from query history combo matching */ comboMatchBoost: number; + /** Bonus for query segments aligning with path segments */ + pathAlignmentBonus: number; /** Whether this was an exact match */ exactMatch: boolean; /** Type of match: 'fuzzy', 'exact', 'prefix', etc. */