@odla-ai/kg 0.4.1 → 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 +174 -0
- package/dist/index.cjs +683 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +555 -2
- package/dist/index.d.ts +555 -2
- package/dist/index.js +683 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -20,7 +20,9 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
ACTIONABLE: () => ACTIONABLE,
|
|
23
24
|
COMMIT_STEP_CONFIG: () => COMMIT_STEP_CONFIG,
|
|
25
|
+
DEFAULT_STANDARD: () => DEFAULT_STANDARD,
|
|
24
26
|
FACTS_EXTRACT_SYSTEM: () => FACTS_EXTRACT_SYSTEM,
|
|
25
27
|
FACTS_EXTRACT_TOOL: () => FACTS_EXTRACT_TOOL,
|
|
26
28
|
FACTS_SEARCH_SYSTEM: () => FACTS_SEARCH_SYSTEM,
|
|
@@ -29,17 +31,28 @@ __export(index_exports, {
|
|
|
29
31
|
LOCATE_SYSTEM: () => LOCATE_SYSTEM,
|
|
30
32
|
LOCATE_TOOL: () => LOCATE_TOOL,
|
|
31
33
|
MODEL_TIERS: () => MODEL_TIERS,
|
|
34
|
+
PERSON_EXTRACT_SYSTEM: () => PERSON_EXTRACT_SYSTEM,
|
|
35
|
+
PERSON_EXTRACT_TOOL: () => PERSON_EXTRACT_TOOL,
|
|
36
|
+
PERSON_SEARCH_SYSTEM: () => PERSON_SEARCH_SYSTEM,
|
|
32
37
|
Store: () => Store,
|
|
38
|
+
TEAM_PATHS: () => TEAM_PATHS,
|
|
39
|
+
WRITABLE_VERDICTS: () => WRITABLE_VERDICTS,
|
|
33
40
|
addUsage: () => addUsage,
|
|
41
|
+
aliasesFor: () => aliasesFor,
|
|
34
42
|
allJobs: () => allJobs,
|
|
43
|
+
applyPersonIdentity: () => applyPersonIdentity,
|
|
35
44
|
asStepLike: () => asStepLike,
|
|
45
|
+
candidatePages: () => candidatePages,
|
|
46
|
+
candidatesFrom: () => candidatesFrom,
|
|
36
47
|
canonicalizeFragment: () => canonicalizeFragment,
|
|
48
|
+
classifySource: () => classifySource,
|
|
37
49
|
connectorIds: () => connectorIds,
|
|
38
50
|
countyKey: () => countyKey,
|
|
39
51
|
createProvider: () => createProvider,
|
|
40
52
|
emptyUsage: () => emptyUsage,
|
|
41
53
|
enabledSources: () => enabledSources,
|
|
42
54
|
enrichCompany: () => enrichCompany,
|
|
55
|
+
enrichPerson: () => enrichPerson,
|
|
43
56
|
exportFragment: () => exportFragment,
|
|
44
57
|
extractJson: () => extractJson,
|
|
45
58
|
extractJsonArray: () => extractJsonArray,
|
|
@@ -47,29 +60,59 @@ __export(index_exports, {
|
|
|
47
60
|
fetchText: () => fetchText,
|
|
48
61
|
fetchUrl: () => fetchUrl,
|
|
49
62
|
findConnectors: () => findConnectors,
|
|
63
|
+
fold: () => fold,
|
|
50
64
|
fragmentToOps: () => fragmentToOps,
|
|
51
65
|
getConnector: () => getConnector,
|
|
52
66
|
getProvider: () => getProvider,
|
|
67
|
+
gradeSources: () => gradeSources,
|
|
53
68
|
htmlToText: () => htmlToText,
|
|
54
69
|
humanAttrsOf: () => humanAttrsOf,
|
|
70
|
+
identifyPerson: () => identifyPerson,
|
|
71
|
+
identityKey: () => identityKey,
|
|
72
|
+
independentHosts: () => independentHosts,
|
|
55
73
|
ingestFresh: () => ingestFresh,
|
|
74
|
+
investigatePerson: () => investigatePerson,
|
|
75
|
+
isActionable: () => isActionable,
|
|
76
|
+
isAttributed: () => isAttributed,
|
|
77
|
+
isHttpUrl: () => isHttpUrl,
|
|
56
78
|
isLikelyPersonName: () => isLikelyPersonName,
|
|
79
|
+
isMintedKey: () => isMintedKey,
|
|
80
|
+
isVerdict: () => isVerdict,
|
|
81
|
+
isWritable: () => isWritable,
|
|
57
82
|
jobsForHour: () => jobsForHour,
|
|
83
|
+
judgeOrgPage: () => judgeOrgPage,
|
|
58
84
|
locatePrompt: () => locatePrompt,
|
|
59
85
|
locationFragment: () => locationFragment,
|
|
60
86
|
makeContext: () => makeContext,
|
|
87
|
+
mentionsName: () => mentionsName,
|
|
61
88
|
mergeAttrs: () => mergeAttrs,
|
|
62
89
|
mergeRulesOf: () => mergeRulesOf,
|
|
63
90
|
mergeScalar: () => mergeScalar,
|
|
91
|
+
mintPersonKey: () => mintPersonKey,
|
|
92
|
+
nameAlias: () => nameAlias,
|
|
64
93
|
naturalKeysOf: () => naturalKeysOf,
|
|
65
94
|
normalizeUrl: () => normalizeUrl,
|
|
95
|
+
normalizeVerification: () => normalizeVerification,
|
|
96
|
+
orgSiteStrategy: () => orgSiteStrategy,
|
|
97
|
+
peopleLinks: () => peopleLinks,
|
|
98
|
+
personPrompt: () => personPrompt,
|
|
99
|
+
personToFragment: () => personToFragment,
|
|
100
|
+
profileAlias: () => profileAlias,
|
|
101
|
+
researchStrategy: () => researchStrategy,
|
|
66
102
|
resilientStep: () => resilientStep,
|
|
67
103
|
resolveCounties: () => resolveCounties,
|
|
68
104
|
resolveName: () => resolveName,
|
|
105
|
+
resolvePersonKey: () => resolvePersonKey,
|
|
106
|
+
rootOf: () => rootOf,
|
|
69
107
|
searchEntities: () => searchEntities,
|
|
70
108
|
slugify: () => slugify,
|
|
109
|
+
snippetAround: () => snippetAround,
|
|
110
|
+
stripMarkup: () => stripMarkup,
|
|
111
|
+
targetedProfileStrategy: () => targetedProfileStrategy,
|
|
112
|
+
targetedQuery: () => targetedQuery,
|
|
71
113
|
toSerializedSchema: () => toSerializedSchema,
|
|
72
114
|
urlHash: () => urlHash,
|
|
115
|
+
validateLinks: () => validateLinks,
|
|
73
116
|
writeFragment: () => writeFragment
|
|
74
117
|
});
|
|
75
118
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -594,6 +637,15 @@ function findConnectors(fragment, config) {
|
|
|
594
637
|
function slugify(input) {
|
|
595
638
|
return input.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60).replace(/-+$/g, "");
|
|
596
639
|
}
|
|
640
|
+
var LEGAL_SUFFIXES = /* @__PURE__ */ new Set(["co", "company", "corp", "corporation", "inc", "incorporated", "llc", "llp", "ltd", "limited", "pllc"]);
|
|
641
|
+
function identityKey(value) {
|
|
642
|
+
const tokens = value.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/&/g, " and ").match(/[a-z0-9]+/g) ?? [];
|
|
643
|
+
while (tokens.length > 1 && LEGAL_SUFFIXES.has(tokens.at(-1) ?? "")) {
|
|
644
|
+
tokens.pop();
|
|
645
|
+
if (tokens.at(-1) === "and") tokens.pop();
|
|
646
|
+
}
|
|
647
|
+
return tokens.join("");
|
|
648
|
+
}
|
|
597
649
|
function normalizeUrl(raw) {
|
|
598
650
|
try {
|
|
599
651
|
const u = new URL(raw);
|
|
@@ -736,6 +788,213 @@ function locationFragment(slug, name, fips) {
|
|
|
736
788
|
};
|
|
737
789
|
}
|
|
738
790
|
|
|
791
|
+
// src/enrich/verify.ts
|
|
792
|
+
var WRITABLE_VERDICTS = ["confirmed", "probable"];
|
|
793
|
+
var ALL = ["confirmed", "probable", "ambiguous", "not_found", "wrong_entity"];
|
|
794
|
+
function isVerdict(value) {
|
|
795
|
+
return typeof value === "string" && ALL.includes(value);
|
|
796
|
+
}
|
|
797
|
+
function isWritable(verdict) {
|
|
798
|
+
return WRITABLE_VERDICTS.includes(verdict);
|
|
799
|
+
}
|
|
800
|
+
function normalizeVerification(draft, retrievedAt) {
|
|
801
|
+
const d = draft ?? {};
|
|
802
|
+
const verdict = isVerdict(d.verdict) ? d.verdict : "ambiguous";
|
|
803
|
+
const raw = typeof d.confidence === "number" && Number.isFinite(d.confidence) ? d.confidence : 0;
|
|
804
|
+
const confidence = Math.min(1, Math.max(0, raw));
|
|
805
|
+
const evidence = [];
|
|
806
|
+
for (const item of Array.isArray(d.evidence) ? d.evidence.slice(0, 12) : []) {
|
|
807
|
+
const e = item ?? {};
|
|
808
|
+
const sourceUrl = typeof e.sourceUrl === "string" ? e.sourceUrl.trim() : "";
|
|
809
|
+
const claim = typeof e.claim === "string" ? e.claim.trim() : "";
|
|
810
|
+
if (!claim || !isHttpUrl(sourceUrl)) continue;
|
|
811
|
+
evidence.push({ claim: claim.slice(0, 300), sourceUrl, ...retrievedAt ? { retrievedAt } : {} });
|
|
812
|
+
}
|
|
813
|
+
if (evidence.length === 0 && isWritable(verdict)) return { verdict: "ambiguous", confidence, evidence };
|
|
814
|
+
return { verdict, confidence, evidence };
|
|
815
|
+
}
|
|
816
|
+
function isHttpUrl(value) {
|
|
817
|
+
try {
|
|
818
|
+
const u = new URL(value);
|
|
819
|
+
return u.protocol === "https:" || u.protocol === "http:";
|
|
820
|
+
} catch {
|
|
821
|
+
return false;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// src/enrich/person.ts
|
|
826
|
+
var LINK_KINDS = ["website", "blog", "substack", "linkedin", "x", "bluesky", "github", "other"];
|
|
827
|
+
var CANONICAL_HOSTS = {
|
|
828
|
+
linkedin: ["linkedin.com"],
|
|
829
|
+
github: ["github.com"],
|
|
830
|
+
x: ["x.com", "twitter.com"],
|
|
831
|
+
bluesky: ["bsky.app"]
|
|
832
|
+
};
|
|
833
|
+
var PLACEHOLDER_HOSTS = ["example.com", "example.org", "example.net", "yourdomain.com", "domain.com"];
|
|
834
|
+
var PERSON_SEARCH_SYSTEM = "You are a research assistant with web search, confirming the identity of a specific named person and finding their PUBLIC PROFESSIONAL presence. Look for: a personal website, blog, or Substack; LinkedIn, X/Bluesky, or GitHub profiles; their current employer and role; and a business email or phone that they or their employer have published openly (a company contact page, a conference bio). Do NOT look for or report home addresses, personal phone numbers, or personal email addresses. Also report disambiguating facts: city or region, alma mater, and other companies named alongside them. Names are not unique. If the results describe more than one person with this name and you cannot tell which is meant, say so explicitly. If you find a different person with the same name, say that too. Cite the URL for every claim. Never guess a profile URL from a name pattern \u2014 report only URLs you actually saw. If you found nothing, say so plainly.";
|
|
835
|
+
var PERSON_EXTRACT_SYSTEM = "Extract the structured profile from the research notes. Use null for anything the notes do not establish. Set verdict to 'confirmed' only when the notes identify this exact person beyond doubt; 'probable' when the evidence is strong but circumstantial; 'ambiguous' when several same-named people cannot be told apart; 'not_found' when nothing was established; 'wrong_entity' when the notes describe a different person with the same name. Every claim in evidence must carry the source URL it came from.";
|
|
836
|
+
var PERSON_EXTRACT_TOOL = {
|
|
837
|
+
name: "person_profile",
|
|
838
|
+
description: "Verified identity plus public professional profile for one person",
|
|
839
|
+
parameters: {
|
|
840
|
+
type: "object",
|
|
841
|
+
properties: {
|
|
842
|
+
verdict: { type: "string", enum: ["confirmed", "probable", "ambiguous", "not_found", "wrong_entity"] },
|
|
843
|
+
confidence: { type: "number", description: "0..1 \u2014 how sure you are this is the right person" },
|
|
844
|
+
links: {
|
|
845
|
+
type: "array",
|
|
846
|
+
description: "Only URLs actually seen in the results; never a guessed profile pattern.",
|
|
847
|
+
items: {
|
|
848
|
+
type: "object",
|
|
849
|
+
properties: {
|
|
850
|
+
kind: { type: "string", enum: [...LINK_KINDS] },
|
|
851
|
+
url: { type: "string" }
|
|
852
|
+
},
|
|
853
|
+
required: ["kind", "url"]
|
|
854
|
+
}
|
|
855
|
+
},
|
|
856
|
+
employer: { type: ["string", "null"], description: "Current employer name" },
|
|
857
|
+
role: { type: ["string", "null"], description: "Current title at that employer" },
|
|
858
|
+
businessEmail: { type: ["string", "null"], description: "Publicly published work email only" },
|
|
859
|
+
businessPhone: { type: ["string", "null"], description: "Publicly published work phone only" },
|
|
860
|
+
city: { type: ["string", "null"] },
|
|
861
|
+
region: { type: ["string", "null"], description: "State, province, or country" },
|
|
862
|
+
almaMater: { type: ["string", "null"] },
|
|
863
|
+
otherCompanies: { type: "array", items: { type: "string" }, description: "Companies named alongside them" },
|
|
864
|
+
evidence: {
|
|
865
|
+
type: "array",
|
|
866
|
+
items: {
|
|
867
|
+
type: "object",
|
|
868
|
+
properties: { claim: { type: "string" }, sourceUrl: { type: "string" } },
|
|
869
|
+
required: ["claim", "sourceUrl"]
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
},
|
|
873
|
+
required: ["verdict", "confidence", "links", "evidence"]
|
|
874
|
+
}
|
|
875
|
+
};
|
|
876
|
+
function personPrompt(person) {
|
|
877
|
+
const at = person.companyName ? ` (${person.role ? `${person.role}, ` : ""}${person.companyName})` : "";
|
|
878
|
+
const where = [person.city, person.region].filter(Boolean).join(", ");
|
|
879
|
+
return `Person: ${person.name}${at}${where ? `
|
|
880
|
+
Known location: ${where}` : ""}
|
|
881
|
+
|
|
882
|
+
Confirm this is a real, identifiable person and find their public professional presence.`;
|
|
883
|
+
}
|
|
884
|
+
function validateLinks(raw, limit = 8) {
|
|
885
|
+
const out = [];
|
|
886
|
+
const seen = /* @__PURE__ */ new Set();
|
|
887
|
+
for (const item of (raw ?? []).slice(0, 24)) {
|
|
888
|
+
const kind = LINK_KINDS.includes(String(item?.kind)) ? item.kind : "other";
|
|
889
|
+
const url = String(item?.url ?? "").trim();
|
|
890
|
+
const parsed = httpsUrl(url);
|
|
891
|
+
if (!parsed) continue;
|
|
892
|
+
const host = parsed.hostname.toLowerCase().replace(/^www\./, "");
|
|
893
|
+
if (PLACEHOLDER_HOSTS.includes(host)) continue;
|
|
894
|
+
const required = CANONICAL_HOSTS[kind];
|
|
895
|
+
if (required && !required.some((h) => host === h || host.endsWith(`.${h}`))) continue;
|
|
896
|
+
if (seen.has(url)) continue;
|
|
897
|
+
seen.add(url);
|
|
898
|
+
out.push({ kind, url });
|
|
899
|
+
if (out.length >= limit) break;
|
|
900
|
+
}
|
|
901
|
+
return out;
|
|
902
|
+
}
|
|
903
|
+
function personToFragment(person, draft, retrievedAt) {
|
|
904
|
+
const verification = normalizeVerification(draft, retrievedAt);
|
|
905
|
+
const links = validateLinks(draft.links);
|
|
906
|
+
if (!isWritable(verification.verdict)) return { verification, links, fragment: { nodes: [], edges: [] } };
|
|
907
|
+
const attrs = { name: person.name };
|
|
908
|
+
for (const link of links) if (!attrs[link.kind]) attrs[link.kind] = link.url;
|
|
909
|
+
const city = str(draft.city, 60);
|
|
910
|
+
if (city && !person.city) attrs.city = city;
|
|
911
|
+
const region = str(draft.region, 60);
|
|
912
|
+
if (region && !person.region) attrs.region = region;
|
|
913
|
+
const email = str(draft.businessEmail, 120);
|
|
914
|
+
if (email && /^[^@\s]+@[^@\s.]+\.[^@\s]+$/.test(email)) attrs.businessEmail = email;
|
|
915
|
+
const phone = str(draft.businessPhone, 40);
|
|
916
|
+
if (phone) attrs.businessPhone = phone;
|
|
917
|
+
const almaMater = str(draft.almaMater, 120);
|
|
918
|
+
if (almaMater) attrs.almaMater = almaMater;
|
|
919
|
+
const nodes = [{ type: "person", key: person.slug, attrs }];
|
|
920
|
+
const edges = [];
|
|
921
|
+
const employer = str(draft.employer, 120);
|
|
922
|
+
const known = person.companySlug && person.companyName && identityKey(person.companyName) === identityKey(employer);
|
|
923
|
+
const companySlug = employer ? known ? person.companySlug : slugify(employer) : "";
|
|
924
|
+
if (employer && companySlug) {
|
|
925
|
+
const key = `${person.slug}~${companySlug}`;
|
|
926
|
+
nodes.push({ type: "company", key: companySlug, attrs: { name: known ? person.companyName : employer } });
|
|
927
|
+
nodes.push({ type: "affiliation", key, attrs: { role: str(draft.role, 60) || person.role || "" } });
|
|
928
|
+
edges.push({ from: { type: "affiliation", key }, to: { type: "person", key: person.slug }, label: "of_person" });
|
|
929
|
+
edges.push({ from: { type: "affiliation", key }, to: { type: "company", key: companySlug }, label: "of_company" });
|
|
930
|
+
}
|
|
931
|
+
return { verification, links, fragment: { nodes, edges } };
|
|
932
|
+
}
|
|
933
|
+
function str(value, max) {
|
|
934
|
+
return typeof value === "string" ? value.trim().slice(0, max) : "";
|
|
935
|
+
}
|
|
936
|
+
function httpsUrl(value) {
|
|
937
|
+
try {
|
|
938
|
+
const url = new URL(value);
|
|
939
|
+
return url.protocol === "https:" ? url : null;
|
|
940
|
+
} catch {
|
|
941
|
+
return null;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// src/enrich/grade.ts
|
|
946
|
+
var DEFAULT_STANDARD = {
|
|
947
|
+
minimumSources: 2,
|
|
948
|
+
minimumFacts: 3,
|
|
949
|
+
anchors: ["first-party", "authoritative"],
|
|
950
|
+
anchorRequiredForMedium: false
|
|
951
|
+
};
|
|
952
|
+
function independentHosts(sources) {
|
|
953
|
+
const hosts = /* @__PURE__ */ new Set();
|
|
954
|
+
for (const source of sources) {
|
|
955
|
+
try {
|
|
956
|
+
hosts.add(new URL(source.sourceUrl).hostname.toLowerCase().replace(/^www\./, ""));
|
|
957
|
+
} catch {
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
return [...hosts];
|
|
961
|
+
}
|
|
962
|
+
function gradeSources(sources, standard = DEFAULT_STANDARD) {
|
|
963
|
+
const supporting = sources.filter((s) => s.corroboration === "confirmed" || s.corroboration === "name-only");
|
|
964
|
+
const contradicting = sources.filter((s) => s.corroboration === "absent");
|
|
965
|
+
const unreadable = sources.filter((s) => s.corroboration === "unreachable" || s.corroboration === "unchecked");
|
|
966
|
+
const facts = [...new Set(supporting.flatMap((s) => s.facts))];
|
|
967
|
+
const hosts = independentHosts(supporting);
|
|
968
|
+
const hasAnchor = supporting.some((s) => standard.anchors.includes(s.sourceClass));
|
|
969
|
+
const base = {
|
|
970
|
+
independentSources: hosts.length,
|
|
971
|
+
factsCorroborated: facts,
|
|
972
|
+
hasAnchor,
|
|
973
|
+
contradictions: contradicting.map((s) => s.sourceUrl)
|
|
974
|
+
};
|
|
975
|
+
if (supporting.length === 0) {
|
|
976
|
+
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" };
|
|
977
|
+
}
|
|
978
|
+
if (base.contradictions.length > 0) {
|
|
979
|
+
return { ...base, grade: "unresolved", reason: `${base.contradictions.length} source(s) named someone else` };
|
|
980
|
+
}
|
|
981
|
+
if (hosts.length >= standard.minimumSources && hasAnchor && facts.length >= standard.minimumFacts) {
|
|
982
|
+
return { ...base, grade: "high", reason: `${hosts.length} independent sources, anchored, agreeing on ${facts.length} facts` };
|
|
983
|
+
}
|
|
984
|
+
if (hosts.length >= standard.minimumSources && (hasAnchor || !standard.anchorRequiredForMedium)) {
|
|
985
|
+
return {
|
|
986
|
+
...base,
|
|
987
|
+
grade: "medium",
|
|
988
|
+
reason: hasAnchor ? `${hosts.length} sources but only ${facts.length} corroborated facts` : `${hosts.length} sources, none first-party or authoritative`
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
return { ...base, grade: "unresolved", reason: hosts.length < standard.minimumSources ? "only one independent source" : "no anchoring source" };
|
|
992
|
+
}
|
|
993
|
+
var ACTIONABLE = ["high", "medium"];
|
|
994
|
+
function isActionable(value) {
|
|
995
|
+
return ACTIONABLE.includes(value);
|
|
996
|
+
}
|
|
997
|
+
|
|
739
998
|
// src/enrich/run.ts
|
|
740
999
|
async function enrichCompany(ai, models, company) {
|
|
741
1000
|
const where = `${company.city ? `, ${company.city}` : ""}${company.region ? ` (${company.region})` : ""}`;
|
|
@@ -759,6 +1018,27 @@ ${text}`,
|
|
|
759
1018
|
const { value } = await extractOnce().catch(extractOnce);
|
|
760
1019
|
return factsToFragment(company, value);
|
|
761
1020
|
}
|
|
1021
|
+
async function enrichPerson(ai, models, person, retrievedAt) {
|
|
1022
|
+
const { text, citations } = await ai.search({
|
|
1023
|
+
model: models.search,
|
|
1024
|
+
system: PERSON_SEARCH_SYSTEM,
|
|
1025
|
+
user: personPrompt(person),
|
|
1026
|
+
maxTokens: 1200
|
|
1027
|
+
});
|
|
1028
|
+
const extractOnce = () => ai.extract({
|
|
1029
|
+
model: models.extract,
|
|
1030
|
+
system: PERSON_EXTRACT_SYSTEM,
|
|
1031
|
+
user: `Person: ${person.name}
|
|
1032
|
+
|
|
1033
|
+
Notes:
|
|
1034
|
+
${text}`,
|
|
1035
|
+
tool: PERSON_EXTRACT_TOOL,
|
|
1036
|
+
maxTokens: 4e3
|
|
1037
|
+
// reasoning headroom before the forced tool call
|
|
1038
|
+
});
|
|
1039
|
+
const { value } = await extractOnce().catch(extractOnce);
|
|
1040
|
+
return { ...personToFragment(person, value, retrievedAt), citations, notes: text };
|
|
1041
|
+
}
|
|
762
1042
|
async function resolveCounties(ai, model, rows, gazetteer) {
|
|
763
1043
|
const { value } = await ai.extract({
|
|
764
1044
|
model,
|
|
@@ -776,6 +1056,409 @@ async function resolveCounties(ai, model, rows, gazetteer) {
|
|
|
776
1056
|
return out;
|
|
777
1057
|
}
|
|
778
1058
|
|
|
1059
|
+
// src/enrich/match.ts
|
|
1060
|
+
function fold(value) {
|
|
1061
|
+
return value.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
|
|
1062
|
+
}
|
|
1063
|
+
function mentionsName(haystack, name) {
|
|
1064
|
+
const tokens = fold(name).split(" ").filter((t) => t.length > 2);
|
|
1065
|
+
if (tokens.length < 2) return false;
|
|
1066
|
+
const words = haystack.split(" ");
|
|
1067
|
+
const positions = /* @__PURE__ */ new Map();
|
|
1068
|
+
for (const [i, word] of words.entries()) {
|
|
1069
|
+
if (tokens.includes(word)) positions.set(word, [...positions.get(word) ?? [], i]);
|
|
1070
|
+
}
|
|
1071
|
+
if (tokens.some((t) => !positions.has(t))) return false;
|
|
1072
|
+
const span = tokens.length;
|
|
1073
|
+
const anchors = positions.get(tokens[0]) ?? [];
|
|
1074
|
+
return anchors.some(
|
|
1075
|
+
(anchor) => tokens.every((token) => (positions.get(token) ?? []).some((at) => Math.abs(at - anchor) <= span))
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1078
|
+
function snippetAround(text, name, width = 240) {
|
|
1079
|
+
const hay = fold(text);
|
|
1080
|
+
const first = fold(name).split(" ")[0] ?? "";
|
|
1081
|
+
const at = hay.indexOf(first);
|
|
1082
|
+
if (at < 0) return "";
|
|
1083
|
+
const start = Math.max(0, at - width / 2);
|
|
1084
|
+
return text.slice(start, start + width).replace(/\s+/g, " ").trim();
|
|
1085
|
+
}
|
|
1086
|
+
function stripMarkup(html) {
|
|
1087
|
+
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);
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
// src/enrich/strategy.ts
|
|
1091
|
+
async function investigatePerson(context, strategies, standard = DEFAULT_STANDARD) {
|
|
1092
|
+
const skipped = [];
|
|
1093
|
+
const runnable = [];
|
|
1094
|
+
for (const strategy of strategies) {
|
|
1095
|
+
if (strategy.applicable(context)) runnable.push(strategy);
|
|
1096
|
+
else skipped.push(strategy.name);
|
|
1097
|
+
}
|
|
1098
|
+
const findings = await Promise.all(
|
|
1099
|
+
runnable.map(async (strategy) => {
|
|
1100
|
+
try {
|
|
1101
|
+
return await strategy.run(context);
|
|
1102
|
+
} catch (err) {
|
|
1103
|
+
return {
|
|
1104
|
+
strategy: strategy.name,
|
|
1105
|
+
sources: [],
|
|
1106
|
+
links: [],
|
|
1107
|
+
notes: "",
|
|
1108
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
})
|
|
1112
|
+
);
|
|
1113
|
+
const sources = dedupeBy(findings.flatMap((f) => f.sources), (s) => s.sourceUrl);
|
|
1114
|
+
const links = dedupeBy(findings.flatMap((f) => f.links), (l) => l.url);
|
|
1115
|
+
const report = gradeSources(sources, standard);
|
|
1116
|
+
return { person: context.person, grade: report.grade, report, sources, links, findings, skipped };
|
|
1117
|
+
}
|
|
1118
|
+
function dedupeBy(items, key) {
|
|
1119
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1120
|
+
for (const item of items) {
|
|
1121
|
+
const at = key(item);
|
|
1122
|
+
if (!seen.has(at)) seen.set(at, item);
|
|
1123
|
+
}
|
|
1124
|
+
return [...seen.values()];
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
// src/enrich/strategies/org-site.ts
|
|
1128
|
+
var TEAM_PATHS = [
|
|
1129
|
+
"/team",
|
|
1130
|
+
"/about",
|
|
1131
|
+
"/about-us",
|
|
1132
|
+
"/our-team",
|
|
1133
|
+
"/leadership",
|
|
1134
|
+
"/people",
|
|
1135
|
+
"/staff",
|
|
1136
|
+
"/who-we-are",
|
|
1137
|
+
"/company/team",
|
|
1138
|
+
"/press",
|
|
1139
|
+
"/news"
|
|
1140
|
+
];
|
|
1141
|
+
var PEOPLE_WORDS = [
|
|
1142
|
+
"team",
|
|
1143
|
+
"about",
|
|
1144
|
+
"leadership",
|
|
1145
|
+
"staff",
|
|
1146
|
+
"people",
|
|
1147
|
+
"who we are",
|
|
1148
|
+
"our story",
|
|
1149
|
+
"founders",
|
|
1150
|
+
"management",
|
|
1151
|
+
"board",
|
|
1152
|
+
"press",
|
|
1153
|
+
"news",
|
|
1154
|
+
"advisors",
|
|
1155
|
+
"bios"
|
|
1156
|
+
];
|
|
1157
|
+
function rootOf(website) {
|
|
1158
|
+
try {
|
|
1159
|
+
return new URL(website.startsWith("http") ? website : `https://${website}`).origin;
|
|
1160
|
+
} catch {
|
|
1161
|
+
return null;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
function candidatePages(website, limit = 4) {
|
|
1165
|
+
const origin = rootOf(website);
|
|
1166
|
+
if (!origin) return [];
|
|
1167
|
+
return TEAM_PATHS.map((p) => `${origin}${p}`).slice(0, limit);
|
|
1168
|
+
}
|
|
1169
|
+
function peopleLinks(html, origin, limit = 5) {
|
|
1170
|
+
const found = /* @__PURE__ */ new Map();
|
|
1171
|
+
const anchor = /<a\b[^>]*href=["']([^"']+)["'][^>]*>([\s\S]{0,200}?)<\/a>/gi;
|
|
1172
|
+
for (const match of html.matchAll(anchor)) {
|
|
1173
|
+
let url;
|
|
1174
|
+
try {
|
|
1175
|
+
url = new URL(match[1] ?? "", origin);
|
|
1176
|
+
} catch {
|
|
1177
|
+
continue;
|
|
1178
|
+
}
|
|
1179
|
+
if (url.origin !== origin) continue;
|
|
1180
|
+
const text = (match[2] ?? "").replace(/<[^>]+>/g, " ").toLowerCase();
|
|
1181
|
+
const rank = PEOPLE_WORDS.findIndex((w) => `${text} ${url.pathname.toLowerCase()}`.includes(w));
|
|
1182
|
+
if (rank < 0) continue;
|
|
1183
|
+
const clean = `${url.origin}${url.pathname}`.replace(/\/$/, "");
|
|
1184
|
+
if (clean === origin) continue;
|
|
1185
|
+
const prior = found.get(clean);
|
|
1186
|
+
if (prior === void 0 || rank < prior) found.set(clean, rank);
|
|
1187
|
+
}
|
|
1188
|
+
return [...found.entries()].sort((a, b) => a[1] - b[1]).slice(0, limit).map(([url]) => url);
|
|
1189
|
+
}
|
|
1190
|
+
function judgeOrgPage(name, url, text) {
|
|
1191
|
+
const named = mentionsName(fold(text), name);
|
|
1192
|
+
return { url, named, snippet: named ? snippetAround(text, name) : "" };
|
|
1193
|
+
}
|
|
1194
|
+
var orgSiteStrategy = {
|
|
1195
|
+
name: "org-site",
|
|
1196
|
+
applicable(context) {
|
|
1197
|
+
return Boolean(context.readPage && rootOf(context.person.companyWebsite ?? ""));
|
|
1198
|
+
},
|
|
1199
|
+
async run(context) {
|
|
1200
|
+
const { person, readPage, retrievedAt } = context;
|
|
1201
|
+
const origin = rootOf(person.companyWebsite ?? "");
|
|
1202
|
+
if (!origin || !readPage) {
|
|
1203
|
+
return { strategy: "org-site", sources: [], links: [], notes: "no employer website to read" };
|
|
1204
|
+
}
|
|
1205
|
+
const hits = [];
|
|
1206
|
+
let read = 0;
|
|
1207
|
+
const visit = async (url) => {
|
|
1208
|
+
const html = await readPage(url);
|
|
1209
|
+
if (html === null) return null;
|
|
1210
|
+
read += 1;
|
|
1211
|
+
const hit = judgeOrgPage(person.name, url, stripMarkup(html));
|
|
1212
|
+
if (hit.named) hits.push(hit);
|
|
1213
|
+
return html;
|
|
1214
|
+
};
|
|
1215
|
+
const home = await visit(origin);
|
|
1216
|
+
const followable = home && hits.length === 0 ? peopleLinks(home, origin) : [];
|
|
1217
|
+
const fallback = read <= 1 ? candidatePages(person.companyWebsite ?? "") : [];
|
|
1218
|
+
for (const url of [...followable, ...fallback]) {
|
|
1219
|
+
if (hits.length > 0) break;
|
|
1220
|
+
await visit(url);
|
|
1221
|
+
}
|
|
1222
|
+
const sources = hits.map((hit) => ({
|
|
1223
|
+
sourceUrl: hit.url,
|
|
1224
|
+
sourceClass: "first-party",
|
|
1225
|
+
facts: ["employer", "affiliation"],
|
|
1226
|
+
verifiedOn: retrievedAt,
|
|
1227
|
+
corroboration: "confirmed"
|
|
1228
|
+
}));
|
|
1229
|
+
return {
|
|
1230
|
+
strategy: "org-site",
|
|
1231
|
+
sources,
|
|
1232
|
+
links: [],
|
|
1233
|
+
notes: hits[0] ? `named on ${hits[0].url}
|
|
1234
|
+
|
|
1235
|
+
${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"
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
};
|
|
1239
|
+
|
|
1240
|
+
// src/enrich/strategies/research.ts
|
|
1241
|
+
var SELF_ASSERTED_HOSTS = ["linkedin.com", "x.com", "twitter.com", "bsky.app", "github.com", "substack.com"];
|
|
1242
|
+
function hostOf(url) {
|
|
1243
|
+
try {
|
|
1244
|
+
return new URL(url).hostname.toLowerCase().replace(/^www\./, "");
|
|
1245
|
+
} catch {
|
|
1246
|
+
return "";
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
function classifySource(url, companyWebsite) {
|
|
1250
|
+
const host = hostOf(url);
|
|
1251
|
+
if (!host) return "third-party";
|
|
1252
|
+
const company = companyWebsite ? hostOf(companyWebsite.startsWith("http") ? companyWebsite : `https://${companyWebsite}`) : "";
|
|
1253
|
+
if (company && (host === company || host.endsWith(`.${company}`))) return "first-party";
|
|
1254
|
+
if (SELF_ASSERTED_HOSTS.some((h) => host === h || host.endsWith(`.${h}`))) return "self-asserted";
|
|
1255
|
+
return "third-party";
|
|
1256
|
+
}
|
|
1257
|
+
function isAttributed(citation) {
|
|
1258
|
+
return Boolean(citation.citedText || citation.startIndex !== void 0 || citation.endIndex !== void 0);
|
|
1259
|
+
}
|
|
1260
|
+
var researchStrategy = {
|
|
1261
|
+
name: "research",
|
|
1262
|
+
applicable(context) {
|
|
1263
|
+
return Boolean(context.ai && context.models?.search && context.models?.extract);
|
|
1264
|
+
},
|
|
1265
|
+
async run(context) {
|
|
1266
|
+
const { ai, models, person, retrievedAt } = context;
|
|
1267
|
+
if (!ai || !models) return { strategy: "research", sources: [], links: [], notes: "no models available" };
|
|
1268
|
+
const result = await enrichPerson(ai, models, person, retrievedAt);
|
|
1269
|
+
const sources = result.citations.map((citation) => {
|
|
1270
|
+
const corroboration = isAttributed(citation) ? "name-only" : "unchecked";
|
|
1271
|
+
return {
|
|
1272
|
+
sourceUrl: citation.url,
|
|
1273
|
+
sourceClass: classifySource(citation.url, person.companyWebsite),
|
|
1274
|
+
facts: ["identity"],
|
|
1275
|
+
verifiedOn: retrievedAt,
|
|
1276
|
+
corroboration
|
|
1277
|
+
};
|
|
1278
|
+
});
|
|
1279
|
+
return {
|
|
1280
|
+
strategy: "research",
|
|
1281
|
+
sources,
|
|
1282
|
+
links: result.links,
|
|
1283
|
+
notes: `[${result.verification.verdict}] ${result.notes}`
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
};
|
|
1287
|
+
|
|
1288
|
+
// src/enrich/strategies/targeted-profile.ts
|
|
1289
|
+
var PROFILE_HOSTS = {
|
|
1290
|
+
"linkedin.com": /\/in\/[^/?#]+/i,
|
|
1291
|
+
"github.com": /^\/[^/?#]+\/?$/i,
|
|
1292
|
+
"bsky.app": /\/profile\/[^/?#]+/i,
|
|
1293
|
+
"x.com": /^\/[^/?#]+\/?$/i,
|
|
1294
|
+
"twitter.com": /^\/[^/?#]+\/?$/i
|
|
1295
|
+
};
|
|
1296
|
+
function kindOf(host) {
|
|
1297
|
+
if (host.startsWith("linkedin")) return "linkedin";
|
|
1298
|
+
if (host.startsWith("github")) return "github";
|
|
1299
|
+
if (host.startsWith("bsky")) return "bluesky";
|
|
1300
|
+
if (host.startsWith("x.") || host.startsWith("twitter")) return "x";
|
|
1301
|
+
return "other";
|
|
1302
|
+
}
|
|
1303
|
+
function targetedQuery(person, platform = "linkedin") {
|
|
1304
|
+
const company = person.companyName ? ` "${person.companyName}"` : "";
|
|
1305
|
+
return `"${person.name}"${company} ${platform}`;
|
|
1306
|
+
}
|
|
1307
|
+
function candidatesFrom(citations) {
|
|
1308
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1309
|
+
for (const citation of citations) {
|
|
1310
|
+
let url;
|
|
1311
|
+
try {
|
|
1312
|
+
url = new URL(citation.url);
|
|
1313
|
+
} catch {
|
|
1314
|
+
continue;
|
|
1315
|
+
}
|
|
1316
|
+
const host = url.hostname.toLowerCase().replace(/^www\./, "").replace(/^[a-z]{2}\./, "");
|
|
1317
|
+
const match = PROFILE_HOSTS[host]?.exec(url.pathname);
|
|
1318
|
+
if (!match) continue;
|
|
1319
|
+
const handle = match[0].replace(/\/+$/, "").toLowerCase();
|
|
1320
|
+
if (!seen.has(`${host}${handle}`)) seen.set(`${host}${handle}`, { url: citation.url, host, handle });
|
|
1321
|
+
}
|
|
1322
|
+
return [...seen.values()];
|
|
1323
|
+
}
|
|
1324
|
+
var targetedProfileStrategy = {
|
|
1325
|
+
name: "targeted-profile",
|
|
1326
|
+
applicable(context) {
|
|
1327
|
+
return Boolean(context.ai && context.models?.search);
|
|
1328
|
+
},
|
|
1329
|
+
async run(context) {
|
|
1330
|
+
const { ai, models, person, retrievedAt } = context;
|
|
1331
|
+
if (!ai || !models) return { strategy: "targeted-profile", sources: [], links: [], notes: "no search model" };
|
|
1332
|
+
const query = targetedQuery(person);
|
|
1333
|
+
const { citations } = await ai.search({
|
|
1334
|
+
model: models.search,
|
|
1335
|
+
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.",
|
|
1336
|
+
user: query,
|
|
1337
|
+
maxTokens: 600
|
|
1338
|
+
});
|
|
1339
|
+
const candidates = candidatesFrom(citations);
|
|
1340
|
+
if (candidates.length !== 1) {
|
|
1341
|
+
return {
|
|
1342
|
+
strategy: "targeted-profile",
|
|
1343
|
+
sources: [],
|
|
1344
|
+
links: [],
|
|
1345
|
+
notes: candidates.length === 0 ? `${query}
|
|
1346
|
+
|
|
1347
|
+
no profile matched` : `${query}
|
|
1348
|
+
|
|
1349
|
+
${candidates.length} distinct profiles matched \u2014 the name is ambiguous`
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
const only = candidates[0];
|
|
1353
|
+
const link = { kind: kindOf(only.host), url: only.url };
|
|
1354
|
+
const source = {
|
|
1355
|
+
sourceUrl: only.url,
|
|
1356
|
+
sourceClass: "self-asserted",
|
|
1357
|
+
facts: ["profile"],
|
|
1358
|
+
verifiedOn: retrievedAt,
|
|
1359
|
+
corroboration: "confirmed"
|
|
1360
|
+
};
|
|
1361
|
+
return {
|
|
1362
|
+
strategy: "targeted-profile",
|
|
1363
|
+
sources: [source],
|
|
1364
|
+
links: [link],
|
|
1365
|
+
notes: `${query}
|
|
1366
|
+
|
|
1367
|
+
exactly one distinct profile matched the name and company`
|
|
1368
|
+
};
|
|
1369
|
+
}
|
|
1370
|
+
};
|
|
1371
|
+
|
|
1372
|
+
// src/identity/person.ts
|
|
1373
|
+
var DURABLE_KINDS = ["linkedin", "github", "bluesky", "x"];
|
|
1374
|
+
function settled(config) {
|
|
1375
|
+
return {
|
|
1376
|
+
aliasType: config.aliasType,
|
|
1377
|
+
aliasKeyAttr: config.aliasKeyAttr ?? "alias",
|
|
1378
|
+
personKeyAttr: config.personKeyAttr ?? "personKey",
|
|
1379
|
+
personType: config.personType ?? "person",
|
|
1380
|
+
edgeLabel: config.edgeLabel ?? "of_person"
|
|
1381
|
+
};
|
|
1382
|
+
}
|
|
1383
|
+
function mintPersonKey(random = () => crypto.randomUUID()) {
|
|
1384
|
+
return `p_${random()}`;
|
|
1385
|
+
}
|
|
1386
|
+
function isMintedKey(key) {
|
|
1387
|
+
return key.startsWith("p_");
|
|
1388
|
+
}
|
|
1389
|
+
function profileAlias(kind, url) {
|
|
1390
|
+
try {
|
|
1391
|
+
const parsed = new URL(url);
|
|
1392
|
+
const handle = parsed.pathname.replace(/\/+$/, "").toLowerCase();
|
|
1393
|
+
return handle && handle !== "/" ? `${kind}:${handle}` : null;
|
|
1394
|
+
} catch {
|
|
1395
|
+
return null;
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
function nameAlias(name, companyName) {
|
|
1399
|
+
const person = identityKey(name);
|
|
1400
|
+
const company = companyName ? identityKey(companyName) : "";
|
|
1401
|
+
return person && company ? `name:${person}~${company}` : null;
|
|
1402
|
+
}
|
|
1403
|
+
function aliasesFor(person, links = []) {
|
|
1404
|
+
const out = [];
|
|
1405
|
+
for (const kind of DURABLE_KINDS) {
|
|
1406
|
+
for (const link of links.filter((l) => l.kind === kind)) {
|
|
1407
|
+
const alias = profileAlias(kind, link.url);
|
|
1408
|
+
if (alias && !out.includes(alias)) out.push(alias);
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
const byName = nameAlias(person.name, person.companyName);
|
|
1412
|
+
if (byName) out.push(byName);
|
|
1413
|
+
return out;
|
|
1414
|
+
}
|
|
1415
|
+
async function resolvePersonKey(db, config, aliases) {
|
|
1416
|
+
const { aliasType, aliasKeyAttr, personKeyAttr } = settled(config);
|
|
1417
|
+
if (aliases.length === 0) return null;
|
|
1418
|
+
const result = await db.query({
|
|
1419
|
+
[aliasType]: { $: { where: { [aliasKeyAttr]: { $in: [...aliases] } } } }
|
|
1420
|
+
});
|
|
1421
|
+
const found = /* @__PURE__ */ new Map();
|
|
1422
|
+
for (const row of result[aliasType] ?? []) {
|
|
1423
|
+
const alias = row[aliasKeyAttr];
|
|
1424
|
+
const personKey = row[personKeyAttr];
|
|
1425
|
+
if (typeof alias === "string" && typeof personKey === "string" && personKey) found.set(alias, personKey);
|
|
1426
|
+
}
|
|
1427
|
+
for (const alias of aliases) {
|
|
1428
|
+
const hit = found.get(alias);
|
|
1429
|
+
if (hit) return hit;
|
|
1430
|
+
}
|
|
1431
|
+
return null;
|
|
1432
|
+
}
|
|
1433
|
+
function applyPersonIdentity(fragment, from, to, aliases, config) {
|
|
1434
|
+
const { aliasType, aliasKeyAttr, personKeyAttr, personType, edgeLabel } = settled(config);
|
|
1435
|
+
const rekey = (type, key) => type === personType && key === from ? to : key;
|
|
1436
|
+
const nodes = fragment.nodes.map((n) => ({ ...n, key: rekey(n.type, n.key) }));
|
|
1437
|
+
const edges = fragment.edges.map((e) => ({
|
|
1438
|
+
from: { type: e.from.type, key: rekey(e.from.type, e.from.key) },
|
|
1439
|
+
to: { type: e.to.type, key: rekey(e.to.type, e.to.key) },
|
|
1440
|
+
label: e.label
|
|
1441
|
+
}));
|
|
1442
|
+
for (const alias of [...new Set(aliases)]) {
|
|
1443
|
+
nodes.push({ type: aliasType, key: alias, attrs: { [personKeyAttr]: to } });
|
|
1444
|
+
edges.push({ from: { type: aliasType, key: alias }, to: { type: personType, key: to }, label: edgeLabel });
|
|
1445
|
+
}
|
|
1446
|
+
return { nodes, edges };
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
// src/identity/identify.ts
|
|
1450
|
+
async function identifyPerson(db, config, person, links, fragment) {
|
|
1451
|
+
const aliases = aliasesFor(person, links);
|
|
1452
|
+
const existing = await resolvePersonKey(db, config, aliases);
|
|
1453
|
+
const personKey = existing ?? mintPersonKey();
|
|
1454
|
+
return {
|
|
1455
|
+
personKey,
|
|
1456
|
+
minted: existing === null,
|
|
1457
|
+
aliases,
|
|
1458
|
+
fragment: applyPersonIdentity(fragment, person.slug, personKey, aliases, config)
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
|
|
779
1462
|
// src/llm/provider.ts
|
|
780
1463
|
var import_ai = require("@odla-ai/ai");
|
|
781
1464
|
var MODEL_TIERS = {
|