@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.
- package/CHANGELOG.md +14 -0
- package/dist/help-areas.d.ts +6 -0
- package/dist/help-areas.js +373 -0
- package/dist/help-collection.d.ts +81 -0
- package/dist/help-collection.js +4124 -0
- package/dist/help-direct-answer.d.ts +14 -0
- package/dist/help-direct-answer.js +52 -0
- package/dist/help-keywords.d.ts +10 -0
- package/dist/help-keywords.js +98 -0
- package/dist/help-lexical.d.ts +27 -0
- package/dist/help-lexical.js +99 -0
- package/dist/help-retrieval.d.ts +34 -0
- package/dist/help-retrieval.js +263 -0
- package/dist/help-runtime-collection.d.ts +4 -0
- package/dist/help-runtime-collection.js +5439 -0
- package/dist/help-runtime.d.ts +66 -0
- package/dist/help-runtime.js +221 -0
- package/dist/help.d.ts +123 -0
- package/dist/help.js +304 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/package.json +3 -3
package/dist/help.js
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
export const HELP_COLLECTION_SCHEMA_VERSION = 2;
|
|
3
|
+
export const HELP_QUESTION_MAX_LENGTH = 600;
|
|
4
|
+
export const HELP_DOCS_URL = "https://docs.helyx.gg/";
|
|
5
|
+
const compare = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
6
|
+
const hash = (value) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
7
|
+
/** Explicit field order makes hashes independent of JSON property order. */
|
|
8
|
+
export function helpFaqContent(faq) {
|
|
9
|
+
return {
|
|
10
|
+
id: faq.id,
|
|
11
|
+
moduleCode: faq.moduleCode,
|
|
12
|
+
area: faq.area,
|
|
13
|
+
areaName: faq.areaName,
|
|
14
|
+
moduleId: faq.moduleId,
|
|
15
|
+
question: faq.question,
|
|
16
|
+
answerMarkdown: faq.answerMarkdown,
|
|
17
|
+
answerText: faq.answerText,
|
|
18
|
+
sourceUrl: faq.sourceUrl,
|
|
19
|
+
sourcePath: faq.sourcePath,
|
|
20
|
+
releaseStatus: faq.releaseStatus,
|
|
21
|
+
deploymentSupport: faq.deploymentSupport,
|
|
22
|
+
context: [...faq.context],
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export function hashHelpFaq(faq) {
|
|
26
|
+
return hash(helpFaqContent(faq));
|
|
27
|
+
}
|
|
28
|
+
export function createHelpCollection(faqs) {
|
|
29
|
+
const records = faqs
|
|
30
|
+
.map((faq) => ({ ...helpFaqContent(faq), contentHash: faq.contentHash }))
|
|
31
|
+
.sort((a, b) => compare(a.id, b.id));
|
|
32
|
+
return parseHelpCollection({
|
|
33
|
+
schemaVersion: HELP_COLLECTION_SCHEMA_VERSION,
|
|
34
|
+
revision: hash(records),
|
|
35
|
+
faqs: records,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/** Validate persisted/build input before it can enter retrieval or packaging. */
|
|
39
|
+
export function parseHelpCollection(value) {
|
|
40
|
+
if (!value || typeof value !== "object")
|
|
41
|
+
throw new Error("Invalid Help collection");
|
|
42
|
+
const collection = value;
|
|
43
|
+
if (collection.schemaVersion !== 2 || !Array.isArray(collection.faqs))
|
|
44
|
+
throw new Error("Unsupported Help collection schema");
|
|
45
|
+
const seen = new Set();
|
|
46
|
+
const faqs = [];
|
|
47
|
+
for (const input of collection.faqs) {
|
|
48
|
+
if (!input || typeof input !== "object")
|
|
49
|
+
throw new Error("Invalid Help FAQ");
|
|
50
|
+
const faq = input;
|
|
51
|
+
for (const field of [
|
|
52
|
+
"id",
|
|
53
|
+
"moduleCode",
|
|
54
|
+
"area",
|
|
55
|
+
"areaName",
|
|
56
|
+
"question",
|
|
57
|
+
"answerMarkdown",
|
|
58
|
+
"answerText",
|
|
59
|
+
"sourceUrl",
|
|
60
|
+
"sourcePath",
|
|
61
|
+
"contentHash",
|
|
62
|
+
]) {
|
|
63
|
+
if (typeof faq[field] !== "string" || !faq[field].trim())
|
|
64
|
+
throw new Error(`Invalid Help FAQ ${field}`);
|
|
65
|
+
}
|
|
66
|
+
if (!/^[1-9]\d{5}$/u.test(faq.id) ||
|
|
67
|
+
faq.id.slice(3) === "000" ||
|
|
68
|
+
faq.moduleCode !== faq.id.slice(0, 3) ||
|
|
69
|
+
seen.has(faq.id))
|
|
70
|
+
throw new Error(`Invalid or duplicate Help FAQ ID: ${faq.id}`);
|
|
71
|
+
seen.add(faq.id);
|
|
72
|
+
if (faq.moduleId !== null && typeof faq.moduleId !== "string")
|
|
73
|
+
throw new Error("Invalid Help module identity");
|
|
74
|
+
if (!["live", "testing", "planned", "alpha", "reference", "unknown"].includes(faq.releaseStatus))
|
|
75
|
+
throw new Error("Invalid Help release status");
|
|
76
|
+
if (!["both", "hosted", "self-hosted", "not_declared"].includes(faq.deploymentSupport))
|
|
77
|
+
throw new Error("Invalid Help deployment support");
|
|
78
|
+
if (!Array.isArray(faq.context) ||
|
|
79
|
+
faq.context.some((item) => typeof item !== "string"))
|
|
80
|
+
throw new Error("Invalid Help context");
|
|
81
|
+
const url = new URL(faq.sourceUrl);
|
|
82
|
+
if (url.origin !== "https://docs.helyx.gg" ||
|
|
83
|
+
url.username ||
|
|
84
|
+
url.password ||
|
|
85
|
+
!url.hash)
|
|
86
|
+
throw new Error("Help source must be a public documentation fragment");
|
|
87
|
+
if (!/^apps\/docs\/src\/content\/docs\/.+\.mdx?$/u.test(faq.sourcePath) ||
|
|
88
|
+
faq.sourcePath.includes(".."))
|
|
89
|
+
throw new Error("Invalid Help source path");
|
|
90
|
+
if (faq.contentHash !== hashHelpFaq(faq))
|
|
91
|
+
throw new Error(`Help FAQ checksum mismatch: ${faq.id}`);
|
|
92
|
+
faqs.push({ ...helpFaqContent(faq), contentHash: faq.contentHash });
|
|
93
|
+
}
|
|
94
|
+
faqs.sort((a, b) => compare(a.id, b.id));
|
|
95
|
+
if (collection.revision !== hash(faqs))
|
|
96
|
+
throw new Error("Help collection checksum mismatch");
|
|
97
|
+
return { schemaVersion: 2, revision: collection.revision, faqs };
|
|
98
|
+
}
|
|
99
|
+
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(" "));
|
|
100
|
+
const normalise = (text) => text
|
|
101
|
+
.normalize("NFKC")
|
|
102
|
+
.toLowerCase()
|
|
103
|
+
.replace(/[\p{Cc}\p{Cf}]/gu, " ")
|
|
104
|
+
.replace(/[^\p{L}\p{N}]+/gu, " ")
|
|
105
|
+
.trim();
|
|
106
|
+
export const normaliseHelpQuestion = normalise;
|
|
107
|
+
const terms = (text) => new Set(normalise(text)
|
|
108
|
+
.split(/\s+/u)
|
|
109
|
+
.filter((term) => term && !stopWords.has(term)));
|
|
110
|
+
export function createHelpLexicalSearch(faqs, revision, copy = (faq) => structuredClone(faq)) {
|
|
111
|
+
const documents = faqs.map((faq) => ({
|
|
112
|
+
faq,
|
|
113
|
+
normalised: normalise(faq.question),
|
|
114
|
+
question: terms(faq.question),
|
|
115
|
+
area: terms(`${faq.area} ${faq.areaName}`),
|
|
116
|
+
answer: terms(faq.answerText),
|
|
117
|
+
}));
|
|
118
|
+
const frequency = new Map();
|
|
119
|
+
for (const doc of documents)
|
|
120
|
+
for (const term of doc.question)
|
|
121
|
+
frequency.set(term, (frequency.get(term) ?? 0) + 1);
|
|
122
|
+
const weight = (term) => 1 + Math.log(1 + documents.length / (1 + (frequency.get(term) ?? 0)));
|
|
123
|
+
const base = { mode: "lexical", revision };
|
|
124
|
+
const choose = (results) => results.length === 1
|
|
125
|
+
? { ...base, kind: "answer", faq: copy(results[0]) }
|
|
126
|
+
: { ...base, kind: "choices", faqs: results.slice(0, 3).map(copy) };
|
|
127
|
+
return (question) => {
|
|
128
|
+
if (question.length > 600)
|
|
129
|
+
return { ...base, kind: "invalid", reason: "too_long" };
|
|
130
|
+
const query = normalise(question), tokens = terms(question);
|
|
131
|
+
if (!query || !tokens.size)
|
|
132
|
+
return {
|
|
133
|
+
...base,
|
|
134
|
+
kind: "no_match",
|
|
135
|
+
reason: "empty",
|
|
136
|
+
docsUrl: "https://docs.helyx.gg/",
|
|
137
|
+
};
|
|
138
|
+
const byId = documents.find(({ faq }) => faq.id === question.trim().replace(/^#/u, ""));
|
|
139
|
+
if (byId)
|
|
140
|
+
return choose([byId.faq]);
|
|
141
|
+
const exact = documents.filter((doc) => doc.normalised === query);
|
|
142
|
+
if (exact.length)
|
|
143
|
+
return choose(exact.map(({ faq }) => faq));
|
|
144
|
+
const total = [...tokens].reduce((sum, term) => sum + weight(term), 0);
|
|
145
|
+
const ranked = documents
|
|
146
|
+
.map((doc) => {
|
|
147
|
+
let matched = 0, overlap = 0, areaOverlap = 0, answerOverlap = 0;
|
|
148
|
+
for (const term of tokens) {
|
|
149
|
+
if (doc.question.has(term)) {
|
|
150
|
+
overlap += weight(term);
|
|
151
|
+
matched++;
|
|
152
|
+
}
|
|
153
|
+
if (doc.area.has(term))
|
|
154
|
+
areaOverlap += weight(term);
|
|
155
|
+
if (doc.answer.has(term))
|
|
156
|
+
answerOverlap += weight(term);
|
|
157
|
+
}
|
|
158
|
+
const coverage = overlap / total, length = [...doc.question].reduce((sum, term) => sum + weight(term), 0);
|
|
159
|
+
const score = (2 * overlap) / (total + length || 1) +
|
|
160
|
+
(0.2 * areaOverlap) / total +
|
|
161
|
+
(0.05 * answerOverlap) / total;
|
|
162
|
+
return { ...doc, score, coverage, matched };
|
|
163
|
+
})
|
|
164
|
+
.filter((doc) => doc.matched > 0 && doc.score >= 0.3)
|
|
165
|
+
.sort((a, b) => b.score - a.score ||
|
|
166
|
+
(a.faq.id < b.faq.id ? -1 : a.faq.id > b.faq.id ? 1 : 0));
|
|
167
|
+
const best = ranked[0];
|
|
168
|
+
if (!best)
|
|
169
|
+
return {
|
|
170
|
+
...base,
|
|
171
|
+
kind: "no_match",
|
|
172
|
+
reason: "weak_match",
|
|
173
|
+
docsUrl: "https://docs.helyx.gg/",
|
|
174
|
+
};
|
|
175
|
+
const next = ranked[1];
|
|
176
|
+
if (best.matched >= 2 &&
|
|
177
|
+
best.coverage >= 0.8 &&
|
|
178
|
+
best.score >= 0.65 &&
|
|
179
|
+
(!next || best.score - next.score >= 0.18))
|
|
180
|
+
return choose([best.faq]);
|
|
181
|
+
const alternatives = ranked
|
|
182
|
+
.filter((doc) => doc.coverage >= 0.4 && doc.score >= best.score - 0.18)
|
|
183
|
+
.slice(0, 3);
|
|
184
|
+
if (alternatives.length >= 2)
|
|
185
|
+
return {
|
|
186
|
+
...base,
|
|
187
|
+
kind: "choices",
|
|
188
|
+
faqs: alternatives.map(({ faq }) => copy(faq)),
|
|
189
|
+
};
|
|
190
|
+
return {
|
|
191
|
+
...base,
|
|
192
|
+
kind: "no_match",
|
|
193
|
+
reason: "weak_match",
|
|
194
|
+
docsUrl: "https://docs.helyx.gg/",
|
|
195
|
+
};
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
/** Conservative local baseline. Scores are ranking signals, not probabilities. */
|
|
199
|
+
export function createHelpSearch(input) {
|
|
200
|
+
const collection = parseHelpCollection(input);
|
|
201
|
+
return createHelpLexicalSearch(collection.faqs, collection.revision, (faq) => ({ ...faq, context: [...faq.context] }));
|
|
202
|
+
}
|
|
203
|
+
export const HELP_PUBLISHED_SOURCE_URL = "https://docs.helyx.gg/help-sources.json";
|
|
204
|
+
function publishedSourceContent(value) {
|
|
205
|
+
if (!value || typeof value !== "object")
|
|
206
|
+
throw new Error("Invalid published Help source");
|
|
207
|
+
const feed = value;
|
|
208
|
+
if (feed.schemaVersion !== 1 ||
|
|
209
|
+
!Array.isArray(feed.groups) ||
|
|
210
|
+
feed.groups.length > 898 ||
|
|
211
|
+
!Array.isArray(feed.records) ||
|
|
212
|
+
feed.records.length > 20000 ||
|
|
213
|
+
!/^[a-f0-9]{64}$/u.test(feed.sourceRevision) ||
|
|
214
|
+
!Number.isInteger(feed.nextModuleCode) ||
|
|
215
|
+
!Number.isInteger(feed.nextGeneralCode) ||
|
|
216
|
+
feed.nextModuleCode < 100 ||
|
|
217
|
+
feed.nextModuleCode > 1000 ||
|
|
218
|
+
feed.nextGeneralCode < 99 ||
|
|
219
|
+
feed.nextGeneralCode > 999 ||
|
|
220
|
+
feed.nextModuleCode > feed.nextGeneralCode + 1)
|
|
221
|
+
throw new Error("Invalid published Help source bounds");
|
|
222
|
+
const groups = feed.groups
|
|
223
|
+
.map((group) => ({
|
|
224
|
+
code: group.code,
|
|
225
|
+
area: group.area,
|
|
226
|
+
name: group.name,
|
|
227
|
+
nextFaq: group.nextFaq,
|
|
228
|
+
}))
|
|
229
|
+
.sort((a, b) => compare(a.code, b.code));
|
|
230
|
+
if (new Set(groups.map((group) => group.code)).size !== groups.length ||
|
|
231
|
+
new Set(groups.map((group) => group.area)).size !== groups.length)
|
|
232
|
+
throw new Error("Duplicate published Help area");
|
|
233
|
+
for (const group of groups)
|
|
234
|
+
if (!/^[1-9]\d{2}$/u.test(group.code) ||
|
|
235
|
+
["900", "901"].includes(group.code) ||
|
|
236
|
+
typeof group.area !== "string" ||
|
|
237
|
+
!group.area.trim() ||
|
|
238
|
+
group.area.length > 200 ||
|
|
239
|
+
["platform:generic", "platform:custom"].includes(group.area) ||
|
|
240
|
+
typeof group.name !== "string" ||
|
|
241
|
+
!group.name.trim() ||
|
|
242
|
+
group.name.length > 200 ||
|
|
243
|
+
!Number.isInteger(group.nextFaq) ||
|
|
244
|
+
group.nextFaq < 1 ||
|
|
245
|
+
group.nextFaq > 1000 ||
|
|
246
|
+
(group.area.startsWith("platform:")
|
|
247
|
+
? Number(group.code) <= feed.nextGeneralCode
|
|
248
|
+
: Number(group.code) >= feed.nextModuleCode))
|
|
249
|
+
throw new Error("Invalid published Help area");
|
|
250
|
+
const canonicalFaqs = new Map(createHelpCollection(feed.records.map((record) => record.faq)).faqs.map((faq) => [faq.id, faq]));
|
|
251
|
+
const groupMap = new Map(groups.map((group) => [group.code, group]));
|
|
252
|
+
const records = feed.records
|
|
253
|
+
.map((record) => {
|
|
254
|
+
const faq = canonicalFaqs.get(record.faq.id), group = groupMap.get(faq.moduleCode);
|
|
255
|
+
if (!group ||
|
|
256
|
+
group.area !== faq.area ||
|
|
257
|
+
Number(faq.id.slice(3)) >= group.nextFaq ||
|
|
258
|
+
faq.question.length > 600 ||
|
|
259
|
+
faq.answerMarkdown.length > 100000 ||
|
|
260
|
+
faq.answerText.length > 100000 ||
|
|
261
|
+
!["dedicated_faq", "guide_question"].includes(record.sourceKind) ||
|
|
262
|
+
typeof record.pageTitle !== "string" ||
|
|
263
|
+
!record.pageTitle.trim() ||
|
|
264
|
+
record.pageTitle.length > 500 ||
|
|
265
|
+
!Number.isInteger(record.sourceLine) ||
|
|
266
|
+
record.sourceLine < 1 ||
|
|
267
|
+
record.sourceLine > 1000000 ||
|
|
268
|
+
!/^[a-f0-9]{64}$/u.test(record.sourceAnswerHash) ||
|
|
269
|
+
!Array.isArray(record.issues) ||
|
|
270
|
+
record.issues.length > 20 ||
|
|
271
|
+
record.issues.some((issue) => typeof issue !== "string" || issue.length > 500))
|
|
272
|
+
throw new Error("Invalid published Help source record");
|
|
273
|
+
if (new URL(faq.sourceUrl).hash !== `#help-faq-${faq.id}`)
|
|
274
|
+
throw new Error("Published Help anchor does not match its permanent ID");
|
|
275
|
+
return {
|
|
276
|
+
faq,
|
|
277
|
+
sourceKind: record.sourceKind,
|
|
278
|
+
pageTitle: record.pageTitle,
|
|
279
|
+
sourceLine: record.sourceLine,
|
|
280
|
+
sourceAnswerHash: record.sourceAnswerHash,
|
|
281
|
+
issues: [...record.issues],
|
|
282
|
+
};
|
|
283
|
+
})
|
|
284
|
+
.sort((a, b) => compare(a.faq.id, b.faq.id));
|
|
285
|
+
return {
|
|
286
|
+
schemaVersion: 1,
|
|
287
|
+
sourceRevision: feed.sourceRevision,
|
|
288
|
+
nextModuleCode: feed.nextModuleCode,
|
|
289
|
+
nextGeneralCode: feed.nextGeneralCode,
|
|
290
|
+
groups,
|
|
291
|
+
records,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
export function createHelpPublishedSources(input) {
|
|
295
|
+
const content = publishedSourceContent({ ...input, schemaVersion: 1 });
|
|
296
|
+
return { ...content, revision: hash(content) };
|
|
297
|
+
}
|
|
298
|
+
export function parseHelpPublishedSources(value) {
|
|
299
|
+
const content = publishedSourceContent(value), revision = hash(content);
|
|
300
|
+
if (value.revision !== revision)
|
|
301
|
+
throw new Error("Published Help source checksum mismatch");
|
|
302
|
+
return { ...content, revision };
|
|
303
|
+
}
|
|
304
|
+
//# sourceMappingURL=help.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import type { HealthResult, Logger, ModuleDefinition, ModulePermissionConfiguration, PermissionActor, GuildAccessPolicy, HelyxAccessLevel, PermissionPolicy, ServiceAccess } from "@helyx/sdk";
|
|
2
2
|
export * from "./async-cleanup-stack.js";
|
|
3
|
+
export * from "./help.js";
|
|
4
|
+
export * from "./help-collection.js";
|
|
5
|
+
export * from "./help-runtime.js";
|
|
6
|
+
export * from "./help-runtime-collection.js";
|
|
7
|
+
export * from "./help-retrieval.js";
|
|
8
|
+
export * from "./help-direct-answer.js";
|
|
9
|
+
export * from "./help-areas.js";
|
|
3
10
|
export type RuntimeState = "created" | "starting" | "running" | "stopping" | "stopped" | "failed";
|
|
4
11
|
export interface ModuleHealth {
|
|
5
12
|
id: string;
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { orderModuleManifests, parseModuleManifest, } from "@helyx/module-manifest";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
export * from "./async-cleanup-stack.js";
|
|
4
|
+
export * from "./help.js";
|
|
5
|
+
export * from "./help-collection.js";
|
|
6
|
+
export * from "./help-runtime.js";
|
|
7
|
+
export * from "./help-runtime-collection.js";
|
|
8
|
+
export * from "./help-retrieval.js";
|
|
9
|
+
export * from "./help-direct-answer.js";
|
|
10
|
+
export * from "./help-areas.js";
|
|
4
11
|
export class ServiceRegistry {
|
|
5
12
|
#services;
|
|
6
13
|
constructor(services = new Map()) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@helyx/core",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Module lifecycle and runtime registry for Helyx.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"helyx",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"test": "vitest run"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@helyx/module-manifest": ">=0.
|
|
51
|
-
"@helyx/sdk": ">=0.
|
|
50
|
+
"@helyx/module-manifest": ">=0.14.0 <0.15.0",
|
|
51
|
+
"@helyx/sdk": ">=0.17.0 <0.18.0"
|
|
52
52
|
}
|
|
53
53
|
}
|