@helyx/core 0.2.1 → 0.2.3

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.
@@ -0,0 +1,14 @@
1
+ import { type HelpRetrievalPolicy, type HelpRuntimeResult } from "./help-retrieval.js";
2
+ import type { HelpRuntimeFaq } from "./help-runtime.js";
3
+ /** Candidate count alone cannot establish relevance. Only the explicitly
4
+ * versioned policy may promote a well-corroborated semantic singleton. */
5
+ export declare function helpReliableDirectAnswer(input: {
6
+ faqs: readonly HelpRuntimeFaq[];
7
+ question: string;
8
+ queryVector: readonly number[];
9
+ vectors: ReadonlyMap<string, readonly number[]>;
10
+ result: HelpRuntimeResult;
11
+ area?: string;
12
+ policy: HelpRetrievalPolicy;
13
+ }): HelpRuntimeResult;
14
+ //# sourceMappingURL=help-direct-answer.d.ts.map
@@ -0,0 +1,52 @@
1
+ import { helpKeywordEvidence } from "./help-keywords.js";
2
+ import { helpSearchQuestion, scopeHelpFaqs, } from "./help-retrieval.js";
3
+ /** Candidate count alone cannot establish relevance. Only the explicitly
4
+ * versioned policy may promote a well-corroborated semantic singleton. */
5
+ export function helpReliableDirectAnswer(input) {
6
+ const { result, queryVector } = input;
7
+ if (input.policy !== "conservative-v5" ||
8
+ result.mode !== "hybrid" ||
9
+ result.degraded ||
10
+ result.kind !== "choices" ||
11
+ result.faqs.length !== 1 ||
12
+ queryVector.length !== 384 ||
13
+ queryVector.some((value) => !Number.isFinite(value)) ||
14
+ Math.abs(Math.hypot(...queryVector) - 1) > 0.0001)
15
+ return result;
16
+ const faqs = scopeHelpFaqs(input.faqs, input.area);
17
+ const candidate = faqs.find((faq) => faq.id === result.faqs[0].id);
18
+ if (!candidate ||
19
+ candidate.contentHash !== result.faqs[0].contentHash ||
20
+ candidate.approvedRevision !== result.faqs[0].approvedRevision)
21
+ return result;
22
+ const evidence = helpKeywordEvidence(faqs, helpSearchQuestion(faqs, input.question, input.policy));
23
+ if (!evidence.rareTitles.has(candidate.id))
24
+ return result;
25
+ const semantic = faqs.map((faq) => {
26
+ const vector = input.vectors.get(faq.id);
27
+ if (vector?.length !== 384 ||
28
+ vector.some((value) => !Number.isFinite(value)) ||
29
+ Math.abs(Math.hypot(...vector) - 1) > 0.0001)
30
+ return { id: faq.id, similarity: Number.NaN };
31
+ return {
32
+ id: faq.id,
33
+ similarity: vector.reduce((sum, value, index) => sum + value * queryVector[index], 0),
34
+ };
35
+ });
36
+ // Missing evidence cannot establish a lead over other eligible candidates.
37
+ if (semantic.some((row) => !Number.isFinite(row.similarity)))
38
+ return result;
39
+ semantic.sort((a, b) => b.similarity - a.similarity || a.id.localeCompare(b.id));
40
+ if (semantic.length < 2 ||
41
+ semantic[0].id !== candidate.id ||
42
+ semantic[0].similarity < 0.7 ||
43
+ semantic[0].similarity - semantic[1].similarity < 0.05)
44
+ return result;
45
+ return {
46
+ mode: result.mode,
47
+ revision: result.revision,
48
+ kind: "answer",
49
+ faq: structuredClone(candidate),
50
+ };
51
+ }
52
+ //# sourceMappingURL=help-direct-answer.js.map
@@ -0,0 +1,10 @@
1
+ import type { HelpRuntimeFaq } from "./help-runtime.js";
2
+ /** BM25 field ranks support semantic choices; their raw scores are never added
3
+ * to cosine scores or treated as confidence. Input is current eligible content.
4
+ */
5
+ export declare function helpKeywordEvidence(faqs: readonly HelpRuntimeFaq[], question: string): {
6
+ ranks: Map<string, number>;
7
+ areas: Set<string>;
8
+ rareTitles: Set<string>;
9
+ };
10
+ //# sourceMappingURL=help-keywords.d.ts.map
@@ -0,0 +1,98 @@
1
+ import { normaliseHelpQuestion } from "./help.js";
2
+ const stop = new Set("a an and are as at be been but by can could did do does for from had has have how i if in is it its me my of on or our should that the their them there these they this to us was we were what when where which who why will with would you your help helyx module please all any another anyone someone just ordinary".split(" "));
3
+ function terms(text) {
4
+ return normaliseHelpQuestion(text)
5
+ .split(/\s+/u)
6
+ .filter((term) => term && !stop.has(term))
7
+ .map((term) => {
8
+ if (["mods", "moderators", "moderator", "manager", "managers"].includes(term))
9
+ return "manager";
10
+ if (["cannot", "cant", "unable"].includes(term))
11
+ return "not";
12
+ if (["access", "open"].includes(term))
13
+ return "access";
14
+ if (["user", "users", "member", "members"].includes(term))
15
+ return "member";
16
+ if (term.length > 4 && term.endsWith("ies"))
17
+ return term.slice(0, -3) + "y";
18
+ if (term.length > 4 && term.endsWith("s") && !term.endsWith("ss"))
19
+ return term.slice(0, -1);
20
+ if (term.length > 5 && term.endsWith("ing"))
21
+ return term.slice(0, -3).replace(/(.)\1$/u, "$1");
22
+ if (term.length > 4 && term.endsWith("ed"))
23
+ return term.slice(0, -2);
24
+ return term;
25
+ });
26
+ }
27
+ /** BM25 field ranks support semantic choices; their raw scores are never added
28
+ * to cosine scores or treated as confidence. Input is current eligible content.
29
+ */
30
+ export function helpKeywordEvidence(faqs, question) {
31
+ const query = new Set(terms(question));
32
+ const documents = faqs.map((faq) => {
33
+ const fields = [
34
+ terms(faq.question),
35
+ terms(faq.answerText),
36
+ terms(faq.areaName),
37
+ ].map((tokens) => {
38
+ const frequency = new Map();
39
+ for (const token of tokens)
40
+ frequency.set(token, (frequency.get(token) ?? 0) + 1);
41
+ return { length: tokens.length, frequency };
42
+ });
43
+ return {
44
+ faq,
45
+ fields,
46
+ vocabulary: new Set(fields.flatMap((field) => [...field.frequency.keys()])),
47
+ };
48
+ });
49
+ const frequency = new Map();
50
+ for (const doc of documents)
51
+ for (const term of doc.vocabulary)
52
+ frequency.set(term, (frequency.get(term) ?? 0) + 1);
53
+ const averages = [0, 1, 2].map((index) => documents.reduce((sum, doc) => sum + doc.fields[index].length, 0) /
54
+ documents.length || 1);
55
+ const areas = new Set();
56
+ for (const faq of faqs) {
57
+ const name = terms(faq.areaName);
58
+ if (!["900", "901"].includes(faq.areaCode) &&
59
+ name.length &&
60
+ name.every((term) => query.has(term)))
61
+ areas.add(faq.areaCode);
62
+ }
63
+ const scored = documents.map((doc) => {
64
+ const matched = [...query].filter((term) => doc.vocabulary.has(term)).length;
65
+ const rareTitleMatch = [...query].some((term) => term.length >= 3 &&
66
+ doc.fields[0].frequency.has(term) &&
67
+ Math.log(1 +
68
+ (documents.length - (frequency.get(term) ?? 0) + 0.5) /
69
+ ((frequency.get(term) ?? 0) + 0.5)) >= 3);
70
+ const score = doc.fields.reduce((sum, field, index) => sum +
71
+ [...query].reduce((total, term) => {
72
+ const count = field.frequency.get(term) ?? 0;
73
+ const occurrences = frequency.get(term) ?? 0;
74
+ const inverseFrequency = Math.log(1 + (documents.length - occurrences + 0.5) / (occurrences + 0.5));
75
+ return (total +
76
+ (inverseFrequency * count * 2.2) /
77
+ (count + 1.2 * (0.25 + (0.75 * field.length) / averages[index])));
78
+ }, 0) *
79
+ [2, 1, 0.5][index], 0);
80
+ return {
81
+ id: doc.faq.id,
82
+ matched,
83
+ rareTitleMatch,
84
+ coverage: matched / query.size,
85
+ score,
86
+ };
87
+ });
88
+ const ranked = scored
89
+ .filter((doc) => (doc.matched >= 2 || (doc.matched === 1 && doc.rareTitleMatch)) &&
90
+ doc.coverage >= 0.4)
91
+ .sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
92
+ return {
93
+ ranks: new Map(ranked.slice(0, 3).map((doc, index) => [doc.id, index + 1])),
94
+ areas,
95
+ rareTitles: new Set(scored.filter((doc) => doc.rareTitleMatch).map((doc) => doc.id)),
96
+ };
97
+ }
98
+ //# sourceMappingURL=help-keywords.js.map
@@ -0,0 +1,27 @@
1
+ /** Provider-free matching shared by bundled Help and the staff approved preview. */
2
+ export interface HelpLexicalDocument {
3
+ id: string;
4
+ question: string;
5
+ area: string;
6
+ areaName: string;
7
+ answerText: string;
8
+ }
9
+ export type HelpLexicalResult<T> = {
10
+ mode: "lexical";
11
+ revision: string;
12
+ } & ({
13
+ kind: "answer";
14
+ faq: T;
15
+ } | {
16
+ kind: "choices";
17
+ faqs: T[];
18
+ } | {
19
+ kind: "no_match";
20
+ reason: "empty" | "weak_match";
21
+ docsUrl: string;
22
+ } | {
23
+ kind: "invalid";
24
+ reason: "too_long";
25
+ });
26
+ export declare function createHelpLexicalSearch<T extends HelpLexicalDocument>(faqs: readonly T[], revision: string, copy?: (faq: T) => T): (question: string) => HelpLexicalResult<T>;
27
+ //# sourceMappingURL=help-lexical.d.ts.map
@@ -0,0 +1,99 @@
1
+ const stopWords = new Set("a an and are as at be been but by can could did do does for from had has have how i if in is it its me my of on or our should that the their them there these they this to us was we were what when where which who why will with would you your help helyx module please".split(" "));
2
+ const normalise = (text) => text
3
+ .normalize("NFKC")
4
+ .toLowerCase()
5
+ .replace(/[\p{Cc}\p{Cf}]/gu, " ")
6
+ .replace(/[^\p{L}\p{N}]+/gu, " ")
7
+ .trim();
8
+ const terms = (text) => new Set(normalise(text)
9
+ .split(/\s+/u)
10
+ .filter((term) => term && !stopWords.has(term)));
11
+ export function createHelpLexicalSearch(faqs, revision, copy = (faq) => structuredClone(faq)) {
12
+ const documents = faqs.map((faq) => ({
13
+ faq,
14
+ normalised: normalise(faq.question),
15
+ question: terms(faq.question),
16
+ area: terms(`${faq.area} ${faq.areaName}`),
17
+ answer: terms(faq.answerText),
18
+ }));
19
+ const frequency = new Map();
20
+ for (const doc of documents)
21
+ for (const term of doc.question)
22
+ frequency.set(term, (frequency.get(term) ?? 0) + 1);
23
+ const weight = (term) => 1 + Math.log(1 + documents.length / (1 + (frequency.get(term) ?? 0)));
24
+ const base = { mode: "lexical", revision };
25
+ const choose = (results) => results.length === 1
26
+ ? { ...base, kind: "answer", faq: copy(results[0]) }
27
+ : { ...base, kind: "choices", faqs: results.slice(0, 3).map(copy) };
28
+ return (question) => {
29
+ if (question.length > 600)
30
+ return { ...base, kind: "invalid", reason: "too_long" };
31
+ const query = normalise(question), tokens = terms(question);
32
+ if (!query || !tokens.size)
33
+ return {
34
+ ...base,
35
+ kind: "no_match",
36
+ reason: "empty",
37
+ docsUrl: "https://docs.helyx.gg/",
38
+ };
39
+ const byId = documents.find(({ faq }) => faq.id === question.trim().replace(/^#/u, ""));
40
+ if (byId)
41
+ return choose([byId.faq]);
42
+ const exact = documents.filter((doc) => doc.normalised === query);
43
+ if (exact.length)
44
+ return choose(exact.map(({ faq }) => faq));
45
+ const total = [...tokens].reduce((sum, term) => sum + weight(term), 0);
46
+ const ranked = documents
47
+ .map((doc) => {
48
+ let matched = 0, overlap = 0, areaOverlap = 0, answerOverlap = 0;
49
+ for (const term of tokens) {
50
+ if (doc.question.has(term)) {
51
+ overlap += weight(term);
52
+ matched++;
53
+ }
54
+ if (doc.area.has(term))
55
+ areaOverlap += weight(term);
56
+ if (doc.answer.has(term))
57
+ answerOverlap += weight(term);
58
+ }
59
+ const coverage = overlap / total, length = [...doc.question].reduce((sum, term) => sum + weight(term), 0);
60
+ const score = (2 * overlap) / (total + length || 1) +
61
+ (0.2 * areaOverlap) / total +
62
+ (0.05 * answerOverlap) / total;
63
+ return { ...doc, score, coverage, matched };
64
+ })
65
+ .filter((doc) => doc.matched > 0 && doc.score >= 0.3)
66
+ .sort((a, b) => b.score - a.score ||
67
+ (a.faq.id < b.faq.id ? -1 : a.faq.id > b.faq.id ? 1 : 0));
68
+ const best = ranked[0];
69
+ if (!best)
70
+ return {
71
+ ...base,
72
+ kind: "no_match",
73
+ reason: "weak_match",
74
+ docsUrl: "https://docs.helyx.gg/",
75
+ };
76
+ const next = ranked[1];
77
+ if (best.matched >= 2 &&
78
+ best.coverage >= 0.8 &&
79
+ best.score >= 0.65 &&
80
+ (!next || best.score - next.score >= 0.18))
81
+ return choose([best.faq]);
82
+ const alternatives = ranked
83
+ .filter((doc) => doc.coverage >= 0.4 && doc.score >= best.score - 0.18)
84
+ .slice(0, 3);
85
+ if (alternatives.length >= 2)
86
+ return {
87
+ ...base,
88
+ kind: "choices",
89
+ faqs: alternatives.map(({ faq }) => copy(faq)),
90
+ };
91
+ return {
92
+ ...base,
93
+ kind: "no_match",
94
+ reason: "weak_match",
95
+ docsUrl: "https://docs.helyx.gg/",
96
+ };
97
+ };
98
+ }
99
+ //# sourceMappingURL=help-lexical.js.map
@@ -0,0 +1,34 @@
1
+ import type { HelpRuntimeFaq, HelpSemanticArtifact } from "./help-runtime.js";
2
+ export type HelpRetrievalPolicy = HelpSemanticArtifact["manifest"]["retrievalPolicy"];
3
+ /** Only repair a transposed long word when the eligible vocabulary has one candidate.
4
+ * IDs, known words and ambiguous corrections are left as written. This is search
5
+ * evidence, never permission to turn a corrected question into an exact answer.
6
+ */
7
+ export declare function helpSearchQuestion(faqs: readonly HelpRuntimeFaq[], question: string, policy?: HelpRetrievalPolicy): string;
8
+ export type HelpRuntimeResult = {
9
+ mode: "lexical" | "hybrid";
10
+ revision: string;
11
+ degraded?: "provider_disabled" | "provider_unavailable" | "budget_exhausted";
12
+ } & ({
13
+ kind: "answer";
14
+ faq: HelpRuntimeFaq;
15
+ } | {
16
+ kind: "choices";
17
+ faqs: HelpRuntimeFaq[];
18
+ } | {
19
+ kind: "no_match";
20
+ reason: "empty" | "weak_match";
21
+ docsUrl: string;
22
+ } | {
23
+ kind: "invalid";
24
+ reason: "too_long";
25
+ });
26
+ export declare function scopeHelpFaqs(faqs: readonly HelpRuntimeFaq[], area?: string): HelpRuntimeFaq[];
27
+ /** Module-specific exact questions precede a manually approved generic comparison. */
28
+ export declare function helpExactMatch(faqs: readonly HelpRuntimeFaq[], question: string, revision: string, area?: string): HelpRuntimeResult | null;
29
+ export declare function helpLexicalResult(faqs: readonly HelpRuntimeFaq[], question: string, revision: string, area?: string, policy?: HelpRetrievalPolicy): HelpRuntimeResult;
30
+ export declare const HELP_SEMANTIC_CHOICE_FLOOR = 0.75;
31
+ export declare const HELP_SHARED_SUPPORT_FLOOR = 0.7;
32
+ /** Development-calibrated ranking; approximate evidence still requires selection. */
33
+ export declare function helpHybridChoices(faqs: readonly HelpRuntimeFaq[], queryVector: readonly number[], vectors: ReadonlyMap<string, readonly number[]>, lexical: HelpRuntimeResult, area?: string, policy?: HelpRetrievalPolicy, question?: string): HelpRuntimeResult;
34
+ //# sourceMappingURL=help-retrieval.d.ts.map
@@ -0,0 +1,263 @@
1
+ import { createHelpLexicalSearch, normaliseHelpQuestion, HELP_DOCS_URL, } from "./help.js";
2
+ import { helpKeywordEvidence } from "./help-keywords.js";
3
+ /** Only repair a transposed long word when the eligible vocabulary has one candidate.
4
+ * IDs, known words and ambiguous corrections are left as written. This is search
5
+ * evidence, never permission to turn a corrected question into an exact answer.
6
+ */
7
+ export function helpSearchQuestion(faqs, question, policy = "conservative-v2") {
8
+ const vocabulary = new Set(faqs.flatMap((faq) => normaliseHelpQuestion(`${faq.areaName} ${faq.question} ${faq.answerText}`).split(/\s+/u)));
9
+ const moduleTerms = new Set(faqs
10
+ .filter((faq) => !["900", "901"].includes(faq.areaCode))
11
+ .flatMap((faq) => normaliseHelpQuestion(faq.areaName).split(/\s+/u))
12
+ .map((term) => term.endsWith("s") && !term.endsWith("ss") ? term.slice(0, -1) : term)
13
+ .filter((term) => term.length >= 8));
14
+ const transposed = (a, b) => {
15
+ if (a.length !== b.length)
16
+ return false;
17
+ let index = 0;
18
+ while (index < a.length && a[index] === b[index])
19
+ index++;
20
+ return (a[index] === b[index + 1] &&
21
+ a[index + 1] === b[index] &&
22
+ a.slice(index + 2) === b.slice(index + 2));
23
+ };
24
+ return question
25
+ .replace(/\b(?:cant|mods)\b/giu, (word) => word.toLowerCase() === "cant" ? "cannot" : "moderators")
26
+ .replace(/\b[a-zA-Z]{6,}\b/gu, (word) => {
27
+ const lower = word.toLowerCase();
28
+ if (vocabulary.has(lower))
29
+ return word;
30
+ let correction;
31
+ for (const candidate of vocabulary) {
32
+ if (!transposed(lower, candidate))
33
+ continue;
34
+ if (correction)
35
+ return word;
36
+ correction = candidate;
37
+ }
38
+ if (correction)
39
+ return correction;
40
+ if (!["conservative-v3", "conservative-v4", "conservative-v5"].includes(policy) ||
41
+ lower.length < 8)
42
+ return word;
43
+ // Only recover missing letters in long, known module/area names. Broad
44
+ // edit-distance correction can turn valid words such as rules into roles.
45
+ const candidates = [...moduleTerms].filter((term) => {
46
+ if (term.length <= lower.length || term.length - lower.length > 2)
47
+ return false;
48
+ let index = 0;
49
+ for (const character of term)
50
+ if (character === lower[index])
51
+ index++;
52
+ return index === lower.length;
53
+ });
54
+ return candidates.length === 1 ? candidates[0] : word;
55
+ });
56
+ }
57
+ // Search synonyms describe the subject of a question; they do not grant a
58
+ // dashboard role or change any stored question/answer/permission policy.
59
+ function lexicalQuestion(question) {
60
+ return normaliseHelpQuestion(question)
61
+ .split(/\s+/u)
62
+ .map((term) => {
63
+ if ([
64
+ "mod",
65
+ "mods",
66
+ "moderator",
67
+ "moderators",
68
+ "manager",
69
+ "managers",
70
+ ].includes(term))
71
+ return "manager";
72
+ if (["open", "access"].includes(term))
73
+ return "access";
74
+ if (["cannot", "cant", "unable"].includes(term))
75
+ return "not";
76
+ if (["role", "roles"].includes(term))
77
+ return "role";
78
+ if (["member", "members"].includes(term))
79
+ return "member";
80
+ if (["add", "adding", "assign", "assigned", "assigning"].includes(term))
81
+ return "assign";
82
+ return term;
83
+ })
84
+ .join(" ");
85
+ }
86
+ export function scopeHelpFaqs(faqs, area) {
87
+ if (!area)
88
+ return [...faqs];
89
+ return faqs.filter((faq) => faq.areaCode === area ||
90
+ faq.area === area ||
91
+ faq.moduleId === area ||
92
+ (faq.source.kind === "generic" &&
93
+ faq.source.comparisons.some((ref) => ref.id.slice(0, 3) === area ||
94
+ ref.area === area ||
95
+ `helyx.${ref.area}` === area)));
96
+ }
97
+ /** Module-specific exact questions precede a manually approved generic comparison. */
98
+ export function helpExactMatch(faqs, question, revision, area) {
99
+ const scoped = scopeHelpFaqs(faqs, area), query = normaliseHelpQuestion(question);
100
+ const byId = scoped.find((faq) => faq.id === question.trim().replace(/^#/u, ""));
101
+ if (byId)
102
+ return {
103
+ mode: "lexical",
104
+ revision,
105
+ kind: "answer",
106
+ faq: structuredClone(byId),
107
+ };
108
+ const specific = scoped.filter((faq) => faq.source.kind !== "generic" &&
109
+ normaliseHelpQuestion(faq.question) === query);
110
+ const generic = scoped.filter((faq) => faq.source.kind === "generic" &&
111
+ (normaliseHelpQuestion(faq.question) === query ||
112
+ faq.source.comparisons.some((ref) => normaliseHelpQuestion(ref.question) === query)));
113
+ const choose = (records) => records.length === 1
114
+ ? {
115
+ mode: "lexical",
116
+ revision,
117
+ kind: "answer",
118
+ faq: structuredClone(records[0]),
119
+ }
120
+ : {
121
+ mode: "lexical",
122
+ revision,
123
+ kind: "choices",
124
+ faqs: records.slice(0, 3).map((faq) => structuredClone(faq)),
125
+ };
126
+ if (area && specific.length)
127
+ return choose(specific);
128
+ const shared = generic.filter((faq) => faq.source.kind === "generic" &&
129
+ specific.every((record) => faq.source.kind === "generic" &&
130
+ faq.source.comparisons.some((ref) => ref.id === record.id)));
131
+ if (shared.length === 1)
132
+ return choose(shared);
133
+ if (specific.length || generic.length)
134
+ return choose([...specific, ...generic]);
135
+ return null;
136
+ }
137
+ export function helpLexicalResult(faqs, question, revision, area, policy = "conservative-v2") {
138
+ const exact = helpExactMatch(faqs, question, revision, area);
139
+ if (exact)
140
+ return exact;
141
+ const scoped = scopeHelpFaqs(faqs, area);
142
+ const records = new Map(scoped.map((faq) => [faq.id, faq]));
143
+ const transform = (text) => policy !== "conservative-v1" ? lexicalQuestion(text) : text;
144
+ const ranked = createHelpLexicalSearch(scoped.map((faq) => ({
145
+ ...faq,
146
+ question: transform(faq.question),
147
+ answerText: transform(faq.answerText),
148
+ })), revision, (faq) => structuredClone(records.get(faq.id)))(transform(policy !== "conservative-v1"
149
+ ? helpSearchQuestion(scoped, question, policy)
150
+ : question));
151
+ // Uncalibrated ranking scores cannot authorise an automatic answer. Even one
152
+ // plausible approximate match needs the member to select it explicitly.
153
+ return ranked.kind === "answer"
154
+ ? { mode: "lexical", revision, kind: "choices", faqs: [ranked.faq] }
155
+ : ranked;
156
+ }
157
+ export const HELP_SEMANTIC_CHOICE_FLOOR = 0.75;
158
+ export const HELP_SHARED_SUPPORT_FLOOR = 0.7;
159
+ /** Development-calibrated ranking; approximate evidence still requires selection. */
160
+ export function helpHybridChoices(faqs, queryVector, vectors, lexical, area, policy = "conservative-v2", question) {
161
+ if (queryVector.length !== 384 ||
162
+ queryVector.some((value) => !Number.isFinite(value)) ||
163
+ Math.abs(Math.hypot(...queryVector) - 1) > 0.0001)
164
+ throw new Error("Invalid Help query vector");
165
+ if (["conservative-v3", "conservative-v4", "conservative-v5"].includes(policy) &&
166
+ (!question || question.length > 600))
167
+ throw new Error("Invalid Help search context");
168
+ const scoped = scopeHelpFaqs(faqs, area), semantic = scoped
169
+ .map((faq) => {
170
+ const vector = vectors.get(faq.id);
171
+ return {
172
+ faq,
173
+ similarity: vector?.length === 384
174
+ ? vector.reduce((sum, value, index) => sum + value * queryVector[index], 0)
175
+ : -1,
176
+ };
177
+ })
178
+ .sort((a, b) => b.similarity - a.similarity || a.faq.id.localeCompare(b.faq.id));
179
+ const collapsed = new Set();
180
+ const lexicalIds = lexical.kind === "choices" ? lexical.faqs.map((faq) => faq.id) : [];
181
+ const keyword = [
182
+ "conservative-v3",
183
+ "conservative-v4",
184
+ "conservative-v5",
185
+ ].includes(policy)
186
+ ? helpKeywordEvidence(scoped, helpSearchQuestion(scoped, question, policy))
187
+ : null;
188
+ const candidates = semantic.map((candidate) => {
189
+ if (area || candidate.faq.source.kind !== "generic")
190
+ return candidate;
191
+ const comparisons = new Set(candidate.faq.source.comparisons.map((ref) => ref.id));
192
+ const support = semantic.filter((other) => comparisons.has(other.faq.id) &&
193
+ other.similarity >= HELP_SHARED_SUPPORT_FLOOR);
194
+ if (new Set(support.map((other) => other.faq.areaCode)).size < 2)
195
+ return candidate;
196
+ // An approved comparison can represent several matching module questions.
197
+ // Its ranking support is not a probability and never authorises a direct answer.
198
+ for (const other of support)
199
+ collapsed.add(other.faq.id);
200
+ return {
201
+ ...candidate,
202
+ similarity: Math.max(candidate.similarity, support[0].similarity),
203
+ sharedSupport: true,
204
+ };
205
+ });
206
+ const semanticRanks = new Map([...candidates]
207
+ .sort((a, b) => b.similarity - a.similarity || a.faq.id.localeCompare(b.faq.id))
208
+ .map((candidate, index) => [candidate.faq.id, index + 1]));
209
+ const ranked = candidates
210
+ .filter((candidate) => !collapsed.has(candidate.faq.id) &&
211
+ candidate.similarity >=
212
+ ("sharedSupport" in candidate
213
+ ? HELP_SHARED_SUPPORT_FLOOR
214
+ : keyword
215
+ ? keyword.ranks.has(candidate.faq.id)
216
+ ? 0.65
217
+ : area ||
218
+ keyword.areas.has(candidate.faq.areaCode) ||
219
+ (["conservative-v4", "conservative-v5"].includes(policy) &&
220
+ keyword.rareTitles.has(candidate.faq.id) &&
221
+ semantic[0]?.faq.id === candidate.faq.id &&
222
+ candidate.similarity - (semantic[1]?.similarity ?? -1) >=
223
+ 0.05)
224
+ ? 0.7
225
+ : HELP_SEMANTIC_CHOICE_FLOOR
226
+ : policy === "conservative-v2" &&
227
+ lexicalIds.includes(candidate.faq.id)
228
+ ? 0.65
229
+ : policy === "conservative-v2" &&
230
+ semantic[0].similarity >= HELP_SEMANTIC_CHOICE_FLOOR
231
+ ? Math.max(HELP_SHARED_SUPPORT_FLOOR, semantic[0].similarity - 0.05)
232
+ : HELP_SEMANTIC_CHOICE_FLOOR))
233
+ .sort((a, b) => {
234
+ const score = (candidate) => keyword
235
+ ? 1 / (10 + semanticRanks.get(candidate.faq.id)) +
236
+ (keyword.ranks.has(candidate.faq.id)
237
+ ? 0.5 / (10 + keyword.ranks.get(candidate.faq.id))
238
+ : 0)
239
+ : candidate.similarity +
240
+ (policy === "conservative-v2" &&
241
+ lexicalIds.includes(candidate.faq.id)
242
+ ? 0.05 / (lexicalIds.indexOf(candidate.faq.id) + 1)
243
+ : 0);
244
+ return score(b) - score(a) || a.faq.id.localeCompare(b.faq.id);
245
+ });
246
+ return ranked.length
247
+ ? {
248
+ mode: "hybrid",
249
+ revision: lexical.revision,
250
+ kind: "choices",
251
+ faqs: ranked
252
+ .slice(0, 3)
253
+ .map((candidate) => structuredClone(candidate.faq)),
254
+ }
255
+ : {
256
+ mode: "hybrid",
257
+ revision: lexical.revision,
258
+ kind: "no_match",
259
+ reason: "weak_match",
260
+ docsUrl: HELP_DOCS_URL,
261
+ };
262
+ }
263
+ //# sourceMappingURL=help-retrieval.js.map
@@ -0,0 +1,4 @@
1
+ import type { HelpRuntimeCollection } from "./help-runtime.js";
2
+ /** Approved published public FAQs only. Generated by prepare-help-runtime-bundle.mjs. */
3
+ export declare const BUNDLED_HELP_RUNTIME_COLLECTION: HelpRuntimeCollection;
4
+ //# sourceMappingURL=help-runtime-collection.d.ts.map