@odla-ai/kg 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +143 -0
- package/dist/index.cjs +486 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +412 -1
- package/dist/index.d.ts +412 -1
- package/dist/index.js +486 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -650,6 +650,59 @@ function httpsUrl(value) {
|
|
|
650
650
|
}
|
|
651
651
|
}
|
|
652
652
|
|
|
653
|
+
// src/enrich/grade.ts
|
|
654
|
+
var DEFAULT_STANDARD = {
|
|
655
|
+
minimumSources: 2,
|
|
656
|
+
minimumFacts: 3,
|
|
657
|
+
anchors: ["first-party", "authoritative"],
|
|
658
|
+
anchorRequiredForMedium: false
|
|
659
|
+
};
|
|
660
|
+
function independentHosts(sources) {
|
|
661
|
+
const hosts = /* @__PURE__ */ new Set();
|
|
662
|
+
for (const source of sources) {
|
|
663
|
+
try {
|
|
664
|
+
hosts.add(new URL(source.sourceUrl).hostname.toLowerCase().replace(/^www\./, ""));
|
|
665
|
+
} catch {
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return [...hosts];
|
|
669
|
+
}
|
|
670
|
+
function gradeSources(sources, standard = DEFAULT_STANDARD) {
|
|
671
|
+
const supporting = sources.filter((s) => s.corroboration === "confirmed" || s.corroboration === "name-only");
|
|
672
|
+
const contradicting = sources.filter((s) => s.corroboration === "absent");
|
|
673
|
+
const unreadable = sources.filter((s) => s.corroboration === "unreachable" || s.corroboration === "unchecked");
|
|
674
|
+
const facts = [...new Set(supporting.flatMap((s) => s.facts))];
|
|
675
|
+
const hosts = independentHosts(supporting);
|
|
676
|
+
const hasAnchor = supporting.some((s) => standard.anchors.includes(s.sourceClass));
|
|
677
|
+
const base = {
|
|
678
|
+
independentSources: hosts.length,
|
|
679
|
+
factsCorroborated: facts,
|
|
680
|
+
hasAnchor,
|
|
681
|
+
contradictions: contradicting.map((s) => s.sourceUrl)
|
|
682
|
+
};
|
|
683
|
+
if (supporting.length === 0) {
|
|
684
|
+
return unreadable.length > 0 && contradicting.length === 0 ? { ...base, grade: "unverifiable", reason: `no source could be read (${unreadable.length} unreachable)` } : { ...base, grade: "unresolved", reason: "no source corroborated the subject" };
|
|
685
|
+
}
|
|
686
|
+
if (base.contradictions.length > 0) {
|
|
687
|
+
return { ...base, grade: "unresolved", reason: `${base.contradictions.length} source(s) named someone else` };
|
|
688
|
+
}
|
|
689
|
+
if (hosts.length >= standard.minimumSources && hasAnchor && facts.length >= standard.minimumFacts) {
|
|
690
|
+
return { ...base, grade: "high", reason: `${hosts.length} independent sources, anchored, agreeing on ${facts.length} facts` };
|
|
691
|
+
}
|
|
692
|
+
if (hosts.length >= standard.minimumSources && (hasAnchor || !standard.anchorRequiredForMedium)) {
|
|
693
|
+
return {
|
|
694
|
+
...base,
|
|
695
|
+
grade: "medium",
|
|
696
|
+
reason: hasAnchor ? `${hosts.length} sources but only ${facts.length} corroborated facts` : `${hosts.length} sources, none first-party or authoritative`
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
return { ...base, grade: "unresolved", reason: hosts.length < standard.minimumSources ? "only one independent source" : "no anchoring source" };
|
|
700
|
+
}
|
|
701
|
+
var ACTIONABLE = ["high", "medium"];
|
|
702
|
+
function isActionable(value) {
|
|
703
|
+
return ACTIONABLE.includes(value);
|
|
704
|
+
}
|
|
705
|
+
|
|
653
706
|
// src/enrich/run.ts
|
|
654
707
|
async function enrichCompany(ai, models, company) {
|
|
655
708
|
const where = `${company.city ? `, ${company.city}` : ""}${company.region ? ` (${company.region})` : ""}`;
|
|
@@ -711,6 +764,409 @@ async function resolveCounties(ai, model, rows, gazetteer) {
|
|
|
711
764
|
return out;
|
|
712
765
|
}
|
|
713
766
|
|
|
767
|
+
// src/enrich/match.ts
|
|
768
|
+
function fold(value) {
|
|
769
|
+
return value.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
|
|
770
|
+
}
|
|
771
|
+
function mentionsName(haystack, name) {
|
|
772
|
+
const tokens = fold(name).split(" ").filter((t) => t.length > 2);
|
|
773
|
+
if (tokens.length < 2) return false;
|
|
774
|
+
const words = haystack.split(" ");
|
|
775
|
+
const positions = /* @__PURE__ */ new Map();
|
|
776
|
+
for (const [i, word] of words.entries()) {
|
|
777
|
+
if (tokens.includes(word)) positions.set(word, [...positions.get(word) ?? [], i]);
|
|
778
|
+
}
|
|
779
|
+
if (tokens.some((t) => !positions.has(t))) return false;
|
|
780
|
+
const span = tokens.length;
|
|
781
|
+
const anchors = positions.get(tokens[0]) ?? [];
|
|
782
|
+
return anchors.some(
|
|
783
|
+
(anchor) => tokens.every((token) => (positions.get(token) ?? []).some((at) => Math.abs(at - anchor) <= span))
|
|
784
|
+
);
|
|
785
|
+
}
|
|
786
|
+
function snippetAround(text, name, width = 240) {
|
|
787
|
+
const hay = fold(text);
|
|
788
|
+
const first = fold(name).split(" ")[0] ?? "";
|
|
789
|
+
const at = hay.indexOf(first);
|
|
790
|
+
if (at < 0) return "";
|
|
791
|
+
const start = Math.max(0, at - width / 2);
|
|
792
|
+
return text.slice(start, start + width).replace(/\s+/g, " ").trim();
|
|
793
|
+
}
|
|
794
|
+
function stripMarkup(html) {
|
|
795
|
+
return html.replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<[^>]+>/g, " ").replace(/ /gi, " ").replace(/&/gi, "&").replace(/\s+/g, " ").slice(0, 2e5);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// src/enrich/strategy.ts
|
|
799
|
+
async function investigatePerson(context, strategies, standard = DEFAULT_STANDARD) {
|
|
800
|
+
const skipped = [];
|
|
801
|
+
const runnable = [];
|
|
802
|
+
for (const strategy of strategies) {
|
|
803
|
+
if (strategy.applicable(context)) runnable.push(strategy);
|
|
804
|
+
else skipped.push(strategy.name);
|
|
805
|
+
}
|
|
806
|
+
const findings = await Promise.all(
|
|
807
|
+
runnable.map(async (strategy) => {
|
|
808
|
+
try {
|
|
809
|
+
return await strategy.run(context);
|
|
810
|
+
} catch (err) {
|
|
811
|
+
return {
|
|
812
|
+
strategy: strategy.name,
|
|
813
|
+
sources: [],
|
|
814
|
+
links: [],
|
|
815
|
+
notes: "",
|
|
816
|
+
error: err instanceof Error ? err.message : String(err)
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
})
|
|
820
|
+
);
|
|
821
|
+
const sources = dedupeBy(findings.flatMap((f) => f.sources), (s) => s.sourceUrl);
|
|
822
|
+
const links = dedupeBy(findings.flatMap((f) => f.links), (l) => l.url);
|
|
823
|
+
const report = gradeSources(sources, standard);
|
|
824
|
+
return { person: context.person, grade: report.grade, report, sources, links, findings, skipped };
|
|
825
|
+
}
|
|
826
|
+
function dedupeBy(items, key) {
|
|
827
|
+
const seen = /* @__PURE__ */ new Map();
|
|
828
|
+
for (const item of items) {
|
|
829
|
+
const at = key(item);
|
|
830
|
+
if (!seen.has(at)) seen.set(at, item);
|
|
831
|
+
}
|
|
832
|
+
return [...seen.values()];
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// src/enrich/strategies/org-site.ts
|
|
836
|
+
var TEAM_PATHS = [
|
|
837
|
+
"/team",
|
|
838
|
+
"/about",
|
|
839
|
+
"/about-us",
|
|
840
|
+
"/our-team",
|
|
841
|
+
"/leadership",
|
|
842
|
+
"/people",
|
|
843
|
+
"/staff",
|
|
844
|
+
"/who-we-are",
|
|
845
|
+
"/company/team",
|
|
846
|
+
"/press",
|
|
847
|
+
"/news"
|
|
848
|
+
];
|
|
849
|
+
var PEOPLE_WORDS = [
|
|
850
|
+
"team",
|
|
851
|
+
"about",
|
|
852
|
+
"leadership",
|
|
853
|
+
"staff",
|
|
854
|
+
"people",
|
|
855
|
+
"who we are",
|
|
856
|
+
"our story",
|
|
857
|
+
"founders",
|
|
858
|
+
"management",
|
|
859
|
+
"board",
|
|
860
|
+
"press",
|
|
861
|
+
"news",
|
|
862
|
+
"advisors",
|
|
863
|
+
"bios"
|
|
864
|
+
];
|
|
865
|
+
function rootOf(website) {
|
|
866
|
+
try {
|
|
867
|
+
return new URL(website.startsWith("http") ? website : `https://${website}`).origin;
|
|
868
|
+
} catch {
|
|
869
|
+
return null;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
function candidatePages(website, limit = 4) {
|
|
873
|
+
const origin = rootOf(website);
|
|
874
|
+
if (!origin) return [];
|
|
875
|
+
return TEAM_PATHS.map((p) => `${origin}${p}`).slice(0, limit);
|
|
876
|
+
}
|
|
877
|
+
function peopleLinks(html, origin, limit = 5) {
|
|
878
|
+
const found = /* @__PURE__ */ new Map();
|
|
879
|
+
const anchor = /<a\b[^>]*href=["']([^"']+)["'][^>]*>([\s\S]{0,200}?)<\/a>/gi;
|
|
880
|
+
for (const match of html.matchAll(anchor)) {
|
|
881
|
+
let url;
|
|
882
|
+
try {
|
|
883
|
+
url = new URL(match[1] ?? "", origin);
|
|
884
|
+
} catch {
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
if (url.origin !== origin) continue;
|
|
888
|
+
const text = (match[2] ?? "").replace(/<[^>]+>/g, " ").toLowerCase();
|
|
889
|
+
const rank = PEOPLE_WORDS.findIndex((w) => `${text} ${url.pathname.toLowerCase()}`.includes(w));
|
|
890
|
+
if (rank < 0) continue;
|
|
891
|
+
const clean = `${url.origin}${url.pathname}`.replace(/\/$/, "");
|
|
892
|
+
if (clean === origin) continue;
|
|
893
|
+
const prior = found.get(clean);
|
|
894
|
+
if (prior === void 0 || rank < prior) found.set(clean, rank);
|
|
895
|
+
}
|
|
896
|
+
return [...found.entries()].sort((a, b) => a[1] - b[1]).slice(0, limit).map(([url]) => url);
|
|
897
|
+
}
|
|
898
|
+
function judgeOrgPage(name, url, text) {
|
|
899
|
+
const named = mentionsName(fold(text), name);
|
|
900
|
+
return { url, named, snippet: named ? snippetAround(text, name) : "" };
|
|
901
|
+
}
|
|
902
|
+
var orgSiteStrategy = {
|
|
903
|
+
name: "org-site",
|
|
904
|
+
applicable(context) {
|
|
905
|
+
return Boolean(context.readPage && rootOf(context.person.companyWebsite ?? ""));
|
|
906
|
+
},
|
|
907
|
+
async run(context) {
|
|
908
|
+
const { person, readPage, retrievedAt } = context;
|
|
909
|
+
const origin = rootOf(person.companyWebsite ?? "");
|
|
910
|
+
if (!origin || !readPage) {
|
|
911
|
+
return { strategy: "org-site", sources: [], links: [], notes: "no employer website to read" };
|
|
912
|
+
}
|
|
913
|
+
const hits = [];
|
|
914
|
+
let read = 0;
|
|
915
|
+
const visit = async (url) => {
|
|
916
|
+
const html = await readPage(url);
|
|
917
|
+
if (html === null) return null;
|
|
918
|
+
read += 1;
|
|
919
|
+
const hit = judgeOrgPage(person.name, url, stripMarkup(html));
|
|
920
|
+
if (hit.named) hits.push(hit);
|
|
921
|
+
return html;
|
|
922
|
+
};
|
|
923
|
+
const home = await visit(origin);
|
|
924
|
+
const followable = home && hits.length === 0 ? peopleLinks(home, origin) : [];
|
|
925
|
+
const fallback = read <= 1 ? candidatePages(person.companyWebsite ?? "") : [];
|
|
926
|
+
for (const url of [...followable, ...fallback]) {
|
|
927
|
+
if (hits.length > 0) break;
|
|
928
|
+
await visit(url);
|
|
929
|
+
}
|
|
930
|
+
const sources = hits.map((hit) => ({
|
|
931
|
+
sourceUrl: hit.url,
|
|
932
|
+
sourceClass: "first-party",
|
|
933
|
+
facts: ["employer", "affiliation"],
|
|
934
|
+
verifiedOn: retrievedAt,
|
|
935
|
+
corroboration: "confirmed"
|
|
936
|
+
}));
|
|
937
|
+
return {
|
|
938
|
+
strategy: "org-site",
|
|
939
|
+
sources,
|
|
940
|
+
links: [],
|
|
941
|
+
notes: hits[0] ? `named on ${hits[0].url}
|
|
942
|
+
|
|
943
|
+
${hits[0].snippet}` : read > 0 ? `read ${read} page(s) on the employer's site; none named them` : "no page on the employer's site could be read"
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
};
|
|
947
|
+
|
|
948
|
+
// src/enrich/strategies/research.ts
|
|
949
|
+
var SELF_ASSERTED_HOSTS = ["linkedin.com", "x.com", "twitter.com", "bsky.app", "github.com", "substack.com"];
|
|
950
|
+
function hostOf(url) {
|
|
951
|
+
try {
|
|
952
|
+
return new URL(url).hostname.toLowerCase().replace(/^www\./, "");
|
|
953
|
+
} catch {
|
|
954
|
+
return "";
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
function classifySource(url, companyWebsite) {
|
|
958
|
+
const host = hostOf(url);
|
|
959
|
+
if (!host) return "third-party";
|
|
960
|
+
const company = companyWebsite ? hostOf(companyWebsite.startsWith("http") ? companyWebsite : `https://${companyWebsite}`) : "";
|
|
961
|
+
if (company && (host === company || host.endsWith(`.${company}`))) return "first-party";
|
|
962
|
+
if (SELF_ASSERTED_HOSTS.some((h) => host === h || host.endsWith(`.${h}`))) return "self-asserted";
|
|
963
|
+
return "third-party";
|
|
964
|
+
}
|
|
965
|
+
function isAttributed(citation) {
|
|
966
|
+
return Boolean(citation.citedText || citation.startIndex !== void 0 || citation.endIndex !== void 0);
|
|
967
|
+
}
|
|
968
|
+
var researchStrategy = {
|
|
969
|
+
name: "research",
|
|
970
|
+
applicable(context) {
|
|
971
|
+
return Boolean(context.ai && context.models?.search && context.models?.extract);
|
|
972
|
+
},
|
|
973
|
+
async run(context) {
|
|
974
|
+
const { ai, models, person, retrievedAt } = context;
|
|
975
|
+
if (!ai || !models) return { strategy: "research", sources: [], links: [], notes: "no models available" };
|
|
976
|
+
const result = await enrichPerson(ai, models, person, retrievedAt);
|
|
977
|
+
const sources = result.citations.map((citation) => {
|
|
978
|
+
const corroboration = isAttributed(citation) ? "name-only" : "unchecked";
|
|
979
|
+
return {
|
|
980
|
+
sourceUrl: citation.url,
|
|
981
|
+
sourceClass: classifySource(citation.url, person.companyWebsite),
|
|
982
|
+
facts: ["identity"],
|
|
983
|
+
verifiedOn: retrievedAt,
|
|
984
|
+
corroboration
|
|
985
|
+
};
|
|
986
|
+
});
|
|
987
|
+
return {
|
|
988
|
+
strategy: "research",
|
|
989
|
+
sources,
|
|
990
|
+
links: result.links,
|
|
991
|
+
notes: `[${result.verification.verdict}] ${result.notes}`
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
};
|
|
995
|
+
|
|
996
|
+
// src/enrich/strategies/targeted-profile.ts
|
|
997
|
+
var PROFILE_HOSTS = {
|
|
998
|
+
"linkedin.com": /\/in\/[^/?#]+/i,
|
|
999
|
+
"github.com": /^\/[^/?#]+\/?$/i,
|
|
1000
|
+
"bsky.app": /\/profile\/[^/?#]+/i,
|
|
1001
|
+
"x.com": /^\/[^/?#]+\/?$/i,
|
|
1002
|
+
"twitter.com": /^\/[^/?#]+\/?$/i
|
|
1003
|
+
};
|
|
1004
|
+
function kindOf(host) {
|
|
1005
|
+
if (host.startsWith("linkedin")) return "linkedin";
|
|
1006
|
+
if (host.startsWith("github")) return "github";
|
|
1007
|
+
if (host.startsWith("bsky")) return "bluesky";
|
|
1008
|
+
if (host.startsWith("x.") || host.startsWith("twitter")) return "x";
|
|
1009
|
+
return "other";
|
|
1010
|
+
}
|
|
1011
|
+
function targetedQuery(person, platform = "linkedin") {
|
|
1012
|
+
const company = person.companyName ? ` "${person.companyName}"` : "";
|
|
1013
|
+
return `"${person.name}"${company} ${platform}`;
|
|
1014
|
+
}
|
|
1015
|
+
function candidatesFrom(citations) {
|
|
1016
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1017
|
+
for (const citation of citations) {
|
|
1018
|
+
let url;
|
|
1019
|
+
try {
|
|
1020
|
+
url = new URL(citation.url);
|
|
1021
|
+
} catch {
|
|
1022
|
+
continue;
|
|
1023
|
+
}
|
|
1024
|
+
const host = url.hostname.toLowerCase().replace(/^www\./, "").replace(/^[a-z]{2}\./, "");
|
|
1025
|
+
const match = PROFILE_HOSTS[host]?.exec(url.pathname);
|
|
1026
|
+
if (!match) continue;
|
|
1027
|
+
const handle = match[0].replace(/\/+$/, "").toLowerCase();
|
|
1028
|
+
if (!seen.has(`${host}${handle}`)) seen.set(`${host}${handle}`, { url: citation.url, host, handle });
|
|
1029
|
+
}
|
|
1030
|
+
return [...seen.values()];
|
|
1031
|
+
}
|
|
1032
|
+
var targetedProfileStrategy = {
|
|
1033
|
+
name: "targeted-profile",
|
|
1034
|
+
applicable(context) {
|
|
1035
|
+
return Boolean(context.ai && context.models?.search);
|
|
1036
|
+
},
|
|
1037
|
+
async run(context) {
|
|
1038
|
+
const { ai, models, person, retrievedAt } = context;
|
|
1039
|
+
if (!ai || !models) return { strategy: "targeted-profile", sources: [], links: [], notes: "no search model" };
|
|
1040
|
+
const query = targetedQuery(person);
|
|
1041
|
+
const { citations } = await ai.search({
|
|
1042
|
+
model: models.search,
|
|
1043
|
+
system: "Search for the exact query given and report what you find. Do not guess a profile URL that did not appear in the results.",
|
|
1044
|
+
user: query,
|
|
1045
|
+
maxTokens: 600
|
|
1046
|
+
});
|
|
1047
|
+
const candidates = candidatesFrom(citations);
|
|
1048
|
+
if (candidates.length !== 1) {
|
|
1049
|
+
return {
|
|
1050
|
+
strategy: "targeted-profile",
|
|
1051
|
+
sources: [],
|
|
1052
|
+
links: [],
|
|
1053
|
+
notes: candidates.length === 0 ? `${query}
|
|
1054
|
+
|
|
1055
|
+
no profile matched` : `${query}
|
|
1056
|
+
|
|
1057
|
+
${candidates.length} distinct profiles matched \u2014 the name is ambiguous`
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
const only = candidates[0];
|
|
1061
|
+
const link = { kind: kindOf(only.host), url: only.url };
|
|
1062
|
+
const source = {
|
|
1063
|
+
sourceUrl: only.url,
|
|
1064
|
+
sourceClass: "self-asserted",
|
|
1065
|
+
facts: ["profile"],
|
|
1066
|
+
verifiedOn: retrievedAt,
|
|
1067
|
+
corroboration: "confirmed"
|
|
1068
|
+
};
|
|
1069
|
+
return {
|
|
1070
|
+
strategy: "targeted-profile",
|
|
1071
|
+
sources: [source],
|
|
1072
|
+
links: [link],
|
|
1073
|
+
notes: `${query}
|
|
1074
|
+
|
|
1075
|
+
exactly one distinct profile matched the name and company`
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
};
|
|
1079
|
+
|
|
1080
|
+
// src/identity/person.ts
|
|
1081
|
+
var DURABLE_KINDS = ["linkedin", "github", "bluesky", "x"];
|
|
1082
|
+
function settled(config) {
|
|
1083
|
+
return {
|
|
1084
|
+
aliasType: config.aliasType,
|
|
1085
|
+
aliasKeyAttr: config.aliasKeyAttr ?? "alias",
|
|
1086
|
+
personKeyAttr: config.personKeyAttr ?? "personKey",
|
|
1087
|
+
personType: config.personType ?? "person",
|
|
1088
|
+
edgeLabel: config.edgeLabel ?? "of_person"
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
function mintPersonKey(random = () => crypto.randomUUID()) {
|
|
1092
|
+
return `p_${random()}`;
|
|
1093
|
+
}
|
|
1094
|
+
function isMintedKey(key) {
|
|
1095
|
+
return key.startsWith("p_");
|
|
1096
|
+
}
|
|
1097
|
+
function profileAlias(kind, url) {
|
|
1098
|
+
try {
|
|
1099
|
+
const parsed = new URL(url);
|
|
1100
|
+
const handle = parsed.pathname.replace(/\/+$/, "").toLowerCase();
|
|
1101
|
+
return handle && handle !== "/" ? `${kind}:${handle}` : null;
|
|
1102
|
+
} catch {
|
|
1103
|
+
return null;
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
function nameAlias(name, companyName) {
|
|
1107
|
+
const person = identityKey(name);
|
|
1108
|
+
const company = companyName ? identityKey(companyName) : "";
|
|
1109
|
+
return person && company ? `name:${person}~${company}` : null;
|
|
1110
|
+
}
|
|
1111
|
+
function aliasesFor(person, links = []) {
|
|
1112
|
+
const out = [];
|
|
1113
|
+
for (const kind of DURABLE_KINDS) {
|
|
1114
|
+
for (const link of links.filter((l) => l.kind === kind)) {
|
|
1115
|
+
const alias = profileAlias(kind, link.url);
|
|
1116
|
+
if (alias && !out.includes(alias)) out.push(alias);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
const byName = nameAlias(person.name, person.companyName);
|
|
1120
|
+
if (byName) out.push(byName);
|
|
1121
|
+
return out;
|
|
1122
|
+
}
|
|
1123
|
+
async function resolvePersonKey(db, config, aliases) {
|
|
1124
|
+
const { aliasType, aliasKeyAttr, personKeyAttr } = settled(config);
|
|
1125
|
+
if (aliases.length === 0) return null;
|
|
1126
|
+
const result = await db.query({
|
|
1127
|
+
[aliasType]: { $: { where: { [aliasKeyAttr]: { $in: [...aliases] } } } }
|
|
1128
|
+
});
|
|
1129
|
+
const found = /* @__PURE__ */ new Map();
|
|
1130
|
+
for (const row of result[aliasType] ?? []) {
|
|
1131
|
+
const alias = row[aliasKeyAttr];
|
|
1132
|
+
const personKey = row[personKeyAttr];
|
|
1133
|
+
if (typeof alias === "string" && typeof personKey === "string" && personKey) found.set(alias, personKey);
|
|
1134
|
+
}
|
|
1135
|
+
for (const alias of aliases) {
|
|
1136
|
+
const hit = found.get(alias);
|
|
1137
|
+
if (hit) return hit;
|
|
1138
|
+
}
|
|
1139
|
+
return null;
|
|
1140
|
+
}
|
|
1141
|
+
function applyPersonIdentity(fragment, from, to, aliases, config) {
|
|
1142
|
+
const { aliasType, aliasKeyAttr, personKeyAttr, personType, edgeLabel } = settled(config);
|
|
1143
|
+
const rekey = (type, key) => type === personType && key === from ? to : key;
|
|
1144
|
+
const nodes = fragment.nodes.map((n) => ({ ...n, key: rekey(n.type, n.key) }));
|
|
1145
|
+
const edges = fragment.edges.map((e) => ({
|
|
1146
|
+
from: { type: e.from.type, key: rekey(e.from.type, e.from.key) },
|
|
1147
|
+
to: { type: e.to.type, key: rekey(e.to.type, e.to.key) },
|
|
1148
|
+
label: e.label
|
|
1149
|
+
}));
|
|
1150
|
+
for (const alias of [...new Set(aliases)]) {
|
|
1151
|
+
nodes.push({ type: aliasType, key: alias, attrs: { [personKeyAttr]: to } });
|
|
1152
|
+
edges.push({ from: { type: aliasType, key: alias }, to: { type: personType, key: to }, label: edgeLabel });
|
|
1153
|
+
}
|
|
1154
|
+
return { nodes, edges };
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// src/identity/identify.ts
|
|
1158
|
+
async function identifyPerson(db, config, person, links, fragment) {
|
|
1159
|
+
const aliases = aliasesFor(person, links);
|
|
1160
|
+
const existing = await resolvePersonKey(db, config, aliases);
|
|
1161
|
+
const personKey = existing ?? mintPersonKey();
|
|
1162
|
+
return {
|
|
1163
|
+
personKey,
|
|
1164
|
+
minted: existing === null,
|
|
1165
|
+
aliases,
|
|
1166
|
+
fragment: applyPersonIdentity(fragment, person.slug, personKey, aliases, config)
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
|
|
714
1170
|
// src/llm/provider.ts
|
|
715
1171
|
import { init, extractText } from "@odla-ai/ai";
|
|
716
1172
|
var MODEL_TIERS = {
|
|
@@ -1390,7 +1846,9 @@ async function ingestFresh(step, deps, fresh, kind, resolve = false) {
|
|
|
1390
1846
|
);
|
|
1391
1847
|
}
|
|
1392
1848
|
export {
|
|
1849
|
+
ACTIONABLE,
|
|
1393
1850
|
COMMIT_STEP_CONFIG,
|
|
1851
|
+
DEFAULT_STANDARD,
|
|
1394
1852
|
FACTS_EXTRACT_SYSTEM,
|
|
1395
1853
|
FACTS_EXTRACT_TOOL,
|
|
1396
1854
|
FACTS_SEARCH_SYSTEM,
|
|
@@ -1403,11 +1861,17 @@ export {
|
|
|
1403
1861
|
PERSON_EXTRACT_TOOL,
|
|
1404
1862
|
PERSON_SEARCH_SYSTEM,
|
|
1405
1863
|
Store,
|
|
1864
|
+
TEAM_PATHS,
|
|
1406
1865
|
WRITABLE_VERDICTS,
|
|
1407
1866
|
addUsage,
|
|
1867
|
+
aliasesFor,
|
|
1408
1868
|
allJobs,
|
|
1869
|
+
applyPersonIdentity,
|
|
1409
1870
|
asStepLike,
|
|
1871
|
+
candidatePages,
|
|
1872
|
+
candidatesFrom,
|
|
1410
1873
|
canonicalizeFragment,
|
|
1874
|
+
classifySource,
|
|
1411
1875
|
connectorIds,
|
|
1412
1876
|
countyKey,
|
|
1413
1877
|
createProvider,
|
|
@@ -1422,34 +1886,56 @@ export {
|
|
|
1422
1886
|
fetchText,
|
|
1423
1887
|
fetchUrl,
|
|
1424
1888
|
findConnectors,
|
|
1889
|
+
fold,
|
|
1425
1890
|
fragmentToOps,
|
|
1426
1891
|
getConnector,
|
|
1427
1892
|
getProvider,
|
|
1893
|
+
gradeSources,
|
|
1428
1894
|
htmlToText,
|
|
1429
1895
|
humanAttrsOf,
|
|
1896
|
+
identifyPerson,
|
|
1430
1897
|
identityKey,
|
|
1898
|
+
independentHosts,
|
|
1431
1899
|
ingestFresh,
|
|
1900
|
+
investigatePerson,
|
|
1901
|
+
isActionable,
|
|
1902
|
+
isAttributed,
|
|
1432
1903
|
isHttpUrl,
|
|
1433
1904
|
isLikelyPersonName,
|
|
1905
|
+
isMintedKey,
|
|
1434
1906
|
isVerdict,
|
|
1435
1907
|
isWritable,
|
|
1436
1908
|
jobsForHour,
|
|
1909
|
+
judgeOrgPage,
|
|
1437
1910
|
locatePrompt,
|
|
1438
1911
|
locationFragment,
|
|
1439
1912
|
makeContext,
|
|
1913
|
+
mentionsName,
|
|
1440
1914
|
mergeAttrs,
|
|
1441
1915
|
mergeRulesOf,
|
|
1442
1916
|
mergeScalar,
|
|
1917
|
+
mintPersonKey,
|
|
1918
|
+
nameAlias,
|
|
1443
1919
|
naturalKeysOf,
|
|
1444
1920
|
normalizeUrl,
|
|
1445
1921
|
normalizeVerification,
|
|
1922
|
+
orgSiteStrategy,
|
|
1923
|
+
peopleLinks,
|
|
1446
1924
|
personPrompt,
|
|
1447
1925
|
personToFragment,
|
|
1926
|
+
profileAlias,
|
|
1927
|
+
researchStrategy,
|
|
1448
1928
|
resilientStep,
|
|
1449
1929
|
resolveCounties,
|
|
1450
1930
|
resolveName,
|
|
1931
|
+
resolvePersonKey,
|
|
1932
|
+
rootOf,
|
|
1451
1933
|
searchEntities,
|
|
1452
1934
|
slugify,
|
|
1935
|
+
snippetAround,
|
|
1936
|
+
stripMarkup,
|
|
1937
|
+
targetedProfileStrategy,
|
|
1938
|
+
targetedQuery,
|
|
1453
1939
|
toSerializedSchema,
|
|
1454
1940
|
urlHash,
|
|
1455
1941
|
validateLinks,
|