mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-29 19:01:39 +00:00
Bare names resolve only when prefix expansion finds exactly one canonical candidate; ambiguous collisions and low-specificity multi-token fuzzy matches fall through to the guarded holding path instead of confident wrong attribution. Fixes #2723.
This commit is contained in:
@@ -10,15 +10,15 @@
|
||||
* Lives under `src/core/entities/` so signal-detector can reuse it for the
|
||||
* Sonnet pass too without circular import through facts/.
|
||||
*
|
||||
* Prefix-expansion step lives between fuzzy match and slugify fallback.
|
||||
* Bare-name prefix expansion lives before fuzzy match and slugify fallback.
|
||||
* Bare first names like "Alice" score too low on pg_trgm (short strings
|
||||
* have terrible trigram overlap), so without this step they fall through
|
||||
* to slugify("Alice") → "alice", which spawns a phantom `people/alice.md`
|
||||
* stub at brain root instead of resolving to the existing
|
||||
* `people/alice-example` page. The fix queries `slug LIKE 'people/X-%'`
|
||||
* (then `companies/X-%`) when fuzzy fails on a single-word bare name, and
|
||||
* uses connection count (links + chunks) as the tiebreaker when multiple
|
||||
* candidates match.
|
||||
* resolves only a single candidate; collisions fall through to the guarded
|
||||
* unprefixed holding path rather than guessing from connection count.
|
||||
*/
|
||||
|
||||
import type { BrainEngine } from '../engine.ts';
|
||||
@@ -29,9 +29,10 @@ import type { BrainEngine } from '../engine.ts';
|
||||
* Resolution order:
|
||||
* 1. If `raw` is already a page slug shape (contains a "/" or matches an
|
||||
* exact pages.slug row in this source), return it untouched.
|
||||
* 2. Try fuzzy match against pages.slug + pages.title within the source
|
||||
* (case-insensitive). Pick the highest-trgm-score match if any.
|
||||
* 3. Fall back to a deterministic slugify: lowercase-no-spaces with
|
||||
* 2. Resolve a bare name only when prefix expansion finds one candidate.
|
||||
* 3. For multi-token input, require a high-specificity fuzzy match against
|
||||
* pages.slug + pages.title within the source (case-insensitive).
|
||||
* 4. Fall back to a deterministic slugify: lowercase-no-spaces with
|
||||
* hyphen-collapse. NOT prefixed with a directory — caller decides
|
||||
* whether to prefix `people/`, `companies/`, etc.
|
||||
*
|
||||
@@ -54,21 +55,22 @@ export async function resolveEntitySlug(
|
||||
if (exact) return exact;
|
||||
}
|
||||
|
||||
// 2. Fuzzy match against existing pages within the source. Match either
|
||||
// on slug fragment or on title.
|
||||
const fuzzy = await tryFuzzyMatch(engine, source_id, trimmed);
|
||||
if (fuzzy) return fuzzy;
|
||||
|
||||
// 3. Prefix-expansion match: when the input looks like a bare first name
|
||||
// 2. Prefix-expansion match: when the input looks like a bare first name
|
||||
// (no slash, no prefix, slugifies to a single short token), try
|
||||
// `people/<token>-%` then `companies/<token>-%`. Short bare names
|
||||
// score terribly on pg_trgm — similarity('alice', 'alice-example')
|
||||
// is below the 0.4 threshold — so this is the layer that catches
|
||||
// is below the fuzzy threshold — so this is the layer that catches
|
||||
// `"Alice"` → `people/alice-example` before we phantom-stub a bare
|
||||
// `people/alice.md`.
|
||||
if (isBareName(trimmed)) {
|
||||
const expanded = await tryPrefixExpansion(engine, source_id, slugify(trimmed));
|
||||
const expanded = await tryUnambiguousPrefixExpansion(engine, source_id, slugify(trimmed));
|
||||
if (expanded) return expanded;
|
||||
} else {
|
||||
// 3. Fuzzy match against existing pages within the source. Bare names
|
||||
// deliberately skip this arm: a shared first name is not specific
|
||||
// enough to choose one person by trigram score or popularity.
|
||||
const fuzzy = await tryFuzzyMatch(engine, source_id, trimmed);
|
||||
if (fuzzy) return fuzzy;
|
||||
}
|
||||
|
||||
// 4. Fallback: deterministic slugify.
|
||||
@@ -131,12 +133,12 @@ export async function resolveEntitySlugWithSource(
|
||||
if (exact) return { slug: exact, source: 'exact_page' };
|
||||
}
|
||||
|
||||
const fuzzy = await tryFuzzyMatch(engine, source_id, trimmed);
|
||||
if (fuzzy) return { slug: fuzzy, source: 'fuzzy_match' };
|
||||
|
||||
if (isBareName(trimmed)) {
|
||||
const expanded = await tryPrefixExpansion(engine, source_id, slugify(trimmed));
|
||||
const expanded = await tryUnambiguousPrefixExpansion(engine, source_id, slugify(trimmed));
|
||||
if (expanded) return { slug: expanded, source: 'fuzzy_match' };
|
||||
} else {
|
||||
const fuzzy = await tryFuzzyMatch(engine, source_id, trimmed);
|
||||
if (fuzzy) return { slug: fuzzy, source: 'fuzzy_match' };
|
||||
}
|
||||
|
||||
return { slug: slugify(trimmed), source: 'fallback_slugify' };
|
||||
@@ -240,6 +242,15 @@ export async function findPrefixCandidates(
|
||||
}
|
||||
}
|
||||
|
||||
async function tryUnambiguousPrefixExpansion(
|
||||
engine: BrainEngine,
|
||||
source_id: string,
|
||||
token: string,
|
||||
): Promise<string | null> {
|
||||
const candidates = await findPrefixCandidates(engine, source_id, token);
|
||||
return candidates.length === 1 ? candidates[0].slug : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up pages whose slug starts with `<dir>/<token>-` for each known
|
||||
* entity directory. When multiple candidates match within a directory,
|
||||
@@ -359,7 +370,11 @@ async function tryFuzzyMatch(
|
||||
LIMIT 3`,
|
||||
[source_id, lc, fragment],
|
||||
);
|
||||
if (rows.length > 0 && rows[0].score >= 0.4) return rows[0].slug;
|
||||
// 0.4 confidently misattributes names that share only a generic company
|
||||
// token (for example "Beacon Capital" → "Benton Capital"). Keep fuzzy
|
||||
// typo tolerance, but require high-specificity overlap before writing a
|
||||
// fact to an existing entity.
|
||||
if (rows.length > 0 && rows[0].score >= 0.7) return rows[0].slug;
|
||||
} catch {
|
||||
// pg_trgm functions might not be available on every engine config;
|
||||
// fall through to slugify.
|
||||
|
||||
Reference in New Issue
Block a user