-module(starfuzz@resolver). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/starfuzz/resolver.gleam"). -export([new/1, with_threshold/2, with_confidence_thresholds/3, component/3, resolve/3]). -export_type([confidence/0, resolution/1, resolver/2]). -if(?OTP_RELEASE >= 27). -define(MODULEDOC(Str), -moduledoc(Str)). -define(DOC(Str), -doc(Str)). -else. -define(MODULEDOC(Str), -compile([])). -define(DOC(Str), -compile([])). -endif. ?MODULEDOC( " A module providing declarative entity resolution matching capabilities.\n" " Structured records can be compared using weighted components, outputting matched status\n" " and confidence levels (High, Medium, Low) for each candidate.\n" ). -type confidence() :: high | medium | low. -type resolution(DYO) :: {resolution, DYO, float(), list(starfuzz@rank:component_score()), confidence(), boolean()}. -type resolver(DYP, DYQ) :: {resolver, fun((DYP, DYQ) -> list(starfuzz@rank:component_score())), float(), {float(), float()}}. -file("src/starfuzz/resolver.gleam", 38). ?DOC( " Creates a new Resolver with a composite scoring function.\n" " Defaults to a threshold of 0.7, medium confidence at 0.75, and high confidence at 0.9.\n" ). -spec new(fun((DYR, DYS) -> list(starfuzz@rank:component_score()))) -> resolver(DYR, DYS). new(Scorer) -> {resolver, Scorer, 0.7, {0.75, 0.9}}. -file("src/starfuzz/resolver.gleam", 47). ?DOC(" Configures the minimum score threshold to consider a record \"matched\".\n"). -spec with_threshold(resolver(DYW, DYX), float()) -> resolver(DYW, DYX). with_threshold(Resolver, Threshold) -> {resolver, erlang:element(2, Resolver), Threshold, erlang:element(4, Resolver)}. -file("src/starfuzz/resolver.gleam", 52). ?DOC(" Configures the thresholds for Medium and High confidence tiers.\n"). -spec with_confidence_thresholds(resolver(DZC, DZD), float(), float()) -> resolver(DZC, DZD). with_confidence_thresholds(Resolver, Medium, High) -> {resolver, erlang:element(2, Resolver), erlang:element(3, Resolver), {Medium, High}}. -file("src/starfuzz/resolver.gleam", 57). ?DOC(" Helper function to construct a ComponentScore inside the resolver scorer.\n"). -spec component(binary(), float(), float()) -> starfuzz@rank:component_score(). component(Name, Weight, Score) -> {component_score, Name, Score, Weight}. -file("src/starfuzz/resolver.gleam", 63). ?DOC( " Resolves a query entity against a list of candidates.\n" " Returns resolution records sorted descending by composite score, with stable ordering.\n" ). -spec resolve(resolver(DZI, DZJ), DZI, list(DZJ)) -> {ok, list(resolution(DZJ))} | {error, starfuzz@rank:rank_error()}. resolve(Resolver, Query, Candidates) -> Candidates_indexed = gleam@list:index_map( Candidates, fun(C, I) -> {C, I} end ), Result = gleam@list:fold( Candidates_indexed, {ok, []}, fun(Acc_state, Item) -> case Acc_state of {error, E} -> {error, E}; {ok, Acc} -> {Candidate, _} = Item, Components = (erlang:element(2, Resolver))(Query, Candidate), case starfuzz@rank:combine(Components) of {error, E@1} -> {error, E@1}; {ok, Score} -> Matched = Score >= erlang:element(3, Resolver), {Med_th, High_th} = erlang:element(4, Resolver), Confidence = case Score >= High_th of true -> high; false -> case Score >= Med_th of true -> medium; false -> low end end, {ok, [{resolution, Candidate, Score, Components, Confidence, Matched} | Acc]} end end end ), case Result of {error, E@2} -> {error, E@2}; {ok, Resolutions} -> Indexed = gleam@list:index_map( lists:reverse(Resolutions), fun(R, Idx) -> {R, Idx} end ), Sorted = gleam@list:sort( Indexed, fun(A, B) -> case erlang:element(3, erlang:element(1, A)) > erlang:element( 3, erlang:element(1, B) ) of true -> lt; false -> case erlang:element(3, erlang:element(1, A)) < erlang:element( 3, erlang:element(1, B) ) of true -> gt; false -> case erlang:element(2, A) < erlang:element( 2, B ) of true -> lt; false -> gt end end end end ), {ok, gleam@list:map(Sorted, fun(X) -> erlang:element(1, X) end)} end.