bc-observatory 0.5.0 → 0.7.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 +2 -2
- package/dist/server.js +450 -70
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ same order: references (`t36`, `cu 80`, `table 11300 BE`) and bare ids (`36`) go
|
|
|
21
21
|
the title and an AL object's caption weigh 3, tags 2, summary 1, through the exact word, a prefix, the plural, a synonym
|
|
22
22
|
(`client` is Customer, `G/L` is general ledger) or one typo; a name equal to the query adds 10; base app before
|
|
23
23
|
first-party app before country layer, then what references, calls and documents an object; topic hubs by their size and
|
|
24
|
-
review state, so "subscription" returns the Subscription billing hub before the codeunits that share the word. Fields,
|
|
24
|
+
review state, so "subscription" returns the Subscription billing hub before the codeunits that share the word. A field, event or procedure ranks where its owner ranks (base application before first-party app before country layer, then importance), and its tooltip adds nothing when its name equals the query; a `kind:` query that finds nothing says `No event named "..."`, names the nearest symbols and is never padded with pages by meaning. A question (`how to ...`, `difference between A and B`, `A vs B`) drops its question words when a content word is left, a quoted phrase keeps all its words, and the hint names the hub of the best Learn article. Fields,
|
|
25
25
|
events, global procedures and enum values are searchable too (`OnAfterPostSalesDoc`, `field: Posting Date`); their path
|
|
26
26
|
ends in an anchor (`objects/codeunit/80#event-OnAfterPostSalesDoc`). Filters: `type`, `tier`, `system`, `country`,
|
|
27
27
|
`app`, `object_type`, `kind`. Each result says where it sits (`in:` the Learn TOC path, the app, the channel) and, for a
|
|
@@ -35,7 +35,7 @@ plain JavaScript; the 30 MB model is downloaded once on the first search, pinned
|
|
|
35
35
|
`BC_OBSERVATORY_MODEL_DIR=<dir with config.json, tokenizer.json, model.safetensors>` uses a model on disk (offline). Without a
|
|
36
36
|
model, search is keyword-only and says so.
|
|
37
37
|
|
|
38
|
-
`get_object` takes an id, a reference (`t36`, `36-be`), a name or a caption, and a `country`; `whats_new` lists nothing
|
|
38
|
+
`get_object` takes an id, a reference (`t36`, `36-be`), a name or a caption, and a `country`; when that country's layer replaces a W1 object (`get_object("table", "18", "BE")`) the fields, events and procedures the layer adds print before the W1 page, and a lookup without a country ends with a `Replaced in:` line naming the layers; `search("field: Enterprise No.", country: "BE")` finds a member a layer adds. `whats_new` lists nothing
|
|
39
39
|
dated after today unless `include_future: true`.
|
|
40
40
|
|
|
41
41
|
`cat`, `get_object` and `localization` return a page as it is when it fits 30,000 characters. A longer page
|
package/dist/server.js
CHANGED
|
@@ -37,6 +37,7 @@ var KIND_OF = { fields: "field", events: "event", procs: "proc", values: "value"
|
|
|
37
37
|
var fold = (s) => s.normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase();
|
|
38
38
|
var TOKEN = /[\p{L}\p{N}]+(?:[.\-/][\p{L}\p{N}]+)*/gu;
|
|
39
39
|
var keep = (t) => t.length > 1 || /\d/.test(t);
|
|
40
|
+
var isCodedId = (w) => /^[A-Z]{2,3}\d{4}$/.test(w);
|
|
40
41
|
function tokenize(s) {
|
|
41
42
|
const out = [];
|
|
42
43
|
for (const m of fold(s).matchAll(TOKEN)) if (keep(m[0])) out.push(m[0]);
|
|
@@ -50,14 +51,17 @@ function camelParts(raw) {
|
|
|
50
51
|
if (!/[a-z][A-Z]|[A-Z]{2}[a-z]/.test(raw)) return [];
|
|
51
52
|
return (raw.match(/[A-Z]+(?![a-z])|[A-Z]?[a-z]+|\d+/g) ?? []).map((w) => fold(w)).filter(keep);
|
|
52
53
|
}
|
|
53
|
-
function
|
|
54
|
-
const out =
|
|
54
|
+
function seqTokens(raw, camel = false) {
|
|
55
|
+
const out = [];
|
|
55
56
|
for (const t of tokenize(raw)) {
|
|
56
|
-
out.
|
|
57
|
-
|
|
57
|
+
out.push(t);
|
|
58
|
+
out.push(...parts(t));
|
|
58
59
|
}
|
|
59
|
-
if (camel) for (const w of raw.split(/[^\p{L}\p{N}]+/u))
|
|
60
|
-
return
|
|
60
|
+
if (camel) for (const w of raw.split(/[^\p{L}\p{N}]+/u)) out.push(...camelParts(w));
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
function indexTokens(raw, camel = false) {
|
|
64
|
+
return [...new Set(seqTokens(raw, camel))];
|
|
61
65
|
}
|
|
62
66
|
function stem(t) {
|
|
63
67
|
if (t.length < 5 || /\d/.test(t) || /[.\-/]/.test(t)) return t;
|
|
@@ -181,7 +185,53 @@ var SYSTEM_ALIASES = {
|
|
|
181
185
|
development: ["al", "extension", "codeunit", "table", "page", "event", "interface", "namespace", "al language", "vs code", "analyzer", "compiler", "testability", "permission set"],
|
|
182
186
|
platform: ["al-go", "pipeline", "docker", "container", "telemetry", "application insights", "performance", "database", "sql", "upgrade", "update", "release", "wave", "deprecation", "obsolete", "web client", "user experience"]
|
|
183
187
|
};
|
|
184
|
-
var QUESTION_WORDS = /* @__PURE__ */ new Set([
|
|
188
|
+
var QUESTION_WORDS = /* @__PURE__ */ new Set([
|
|
189
|
+
"how",
|
|
190
|
+
"what",
|
|
191
|
+
"why",
|
|
192
|
+
"where",
|
|
193
|
+
"when",
|
|
194
|
+
"which",
|
|
195
|
+
"who",
|
|
196
|
+
"do",
|
|
197
|
+
"does",
|
|
198
|
+
"did",
|
|
199
|
+
"can",
|
|
200
|
+
"could",
|
|
201
|
+
"should",
|
|
202
|
+
"is",
|
|
203
|
+
"are",
|
|
204
|
+
"was",
|
|
205
|
+
"were",
|
|
206
|
+
"the",
|
|
207
|
+
"an",
|
|
208
|
+
"to",
|
|
209
|
+
"in",
|
|
210
|
+
"of",
|
|
211
|
+
"for",
|
|
212
|
+
"on",
|
|
213
|
+
"my",
|
|
214
|
+
"me",
|
|
215
|
+
"it",
|
|
216
|
+
"from",
|
|
217
|
+
"with",
|
|
218
|
+
"and",
|
|
219
|
+
"or",
|
|
220
|
+
"changed",
|
|
221
|
+
"change",
|
|
222
|
+
"difference",
|
|
223
|
+
"differences",
|
|
224
|
+
"between",
|
|
225
|
+
"vs",
|
|
226
|
+
"versus",
|
|
227
|
+
"compare",
|
|
228
|
+
"comparison",
|
|
229
|
+
"use",
|
|
230
|
+
"used",
|
|
231
|
+
"using",
|
|
232
|
+
"mean",
|
|
233
|
+
"means"
|
|
234
|
+
]);
|
|
185
235
|
var norm = (s) => s.toLowerCase().replace(/\.(?=\s|$)/g, "").trim();
|
|
186
236
|
var table = null;
|
|
187
237
|
function synonymsOf(word) {
|
|
@@ -223,11 +273,22 @@ function term(text2, phrase = false, raw = text2) {
|
|
|
223
273
|
const alts = [...synonymsOf(text2)];
|
|
224
274
|
const camel = camelParts(raw);
|
|
225
275
|
if (camel.length > 1) alts.push(camel.join(" "));
|
|
226
|
-
return { text: text2, stem: phrase ? text2.split(" ").map(stem).join(" ") : stem(text2), alts, phrase, numeric: /^\d+$/.test(text2) };
|
|
227
|
-
}
|
|
276
|
+
return { text: text2, stem: phrase ? text2.split(" ").map(stem).join(" ") : stem(text2), alts, phrase, numeric: /^\d+$/.test(text2), raw: raw || text2 };
|
|
277
|
+
}
|
|
278
|
+
var WH_START = /^(how|what|why|where|when|which|who)\b/i;
|
|
279
|
+
var AUX_PRONOUN = /^(is|are|can|could|should|do|does)\s+(i|you|we|it|there|this|that|my|they|he|she)\b/i;
|
|
280
|
+
var AUX_ARTICLE = /^(is|are|can|could|should|do|does)\s+(a|an|the)\b/i;
|
|
281
|
+
var COMPARE_START = /^compare\s+\S+(?:\s+\S+)*?\s+(?:and|with|to|vs|versus)\s+\S/i;
|
|
282
|
+
var VERSUS = /[\p{L}\p{N}]\S*\s+(?:vs|versus)(?![.\p{L}\p{N}])\s+(?!code\b)[\p{L}\p{N}]/iu;
|
|
283
|
+
var USE_AFTER = /* @__PURE__ */ new Set(["to", "i", "we", "you", "it", "can", "should", "between", "and", "or"]);
|
|
284
|
+
var isQuestion = (s) => {
|
|
285
|
+
if (WH_START.test(s)) return true;
|
|
286
|
+
if (!/^IS\b/.test(s) && (AUX_PRONOUN.test(s) || AUX_ARTICLE.test(s) && (s.split(/\s+/).length >= 5 || s.endsWith("?")))) return true;
|
|
287
|
+
return COMPARE_START.test(s) || /\bchanged in\b/i.test(s) || /\bdifferences? between\b/i.test(s) || VERSUS.test(s);
|
|
288
|
+
};
|
|
228
289
|
function parseQuery(raw, opts) {
|
|
229
290
|
const countries = new Set(opts.countries.map((c) => c.toLowerCase()));
|
|
230
|
-
const q = { raw, ref: null, number: null, types: [], kind: null, major: null, countries: [], question: false, terms: [], warnings: [] };
|
|
291
|
+
const q = { raw, ref: null, number: null, types: [], kind: null, major: null, countries: [], question: false, dropped: [], shape: null, terms: [], warnings: [] };
|
|
231
292
|
let s = raw.trim();
|
|
232
293
|
const phrases = [];
|
|
233
294
|
s = s.replace(/"([^"]*)"/g, (_, p) => {
|
|
@@ -242,7 +303,7 @@ function parseQuery(raw, opts) {
|
|
|
242
303
|
}
|
|
243
304
|
if (!q.kind && !phrases.length) {
|
|
244
305
|
const m = /^([a-z]+)\s*(\d+)(?:(?:\s+|\s*-\s*)([a-z]{2}))?$/i.exec(s);
|
|
245
|
-
if (m) {
|
|
306
|
+
if (m && !(isCodedId(s.toUpperCase()) && !typeOfWord(m[1]))) {
|
|
246
307
|
const type = typeOfWord(m[1]);
|
|
247
308
|
const cc = m[3]?.toLowerCase() ?? null;
|
|
248
309
|
if (type && (!cc || countries.has(cc))) {
|
|
@@ -262,7 +323,8 @@ function parseQuery(raw, opts) {
|
|
|
262
323
|
q.major = v;
|
|
263
324
|
return " ";
|
|
264
325
|
}).trim();
|
|
265
|
-
q.question =
|
|
326
|
+
q.question = !q.kind && isQuestion(s);
|
|
327
|
+
if (q.question) q.shape = /\bchanged in\b/i.test(s) ? "what-changed" : /^(how (do|to)|when to)\b/i.test(s) ? "how-to" : "concept";
|
|
266
328
|
const words = s.split(/\s+/).filter(Boolean);
|
|
267
329
|
const typeAt = (i) => i >= 0 && i < words.length && !!TYPE_WORDS[words[i].toLowerCase()];
|
|
268
330
|
const typeWords = [];
|
|
@@ -291,12 +353,24 @@ function parseQuery(raw, opts) {
|
|
|
291
353
|
const t = tokenize(w);
|
|
292
354
|
for (const x of t) toks.push({ text: x, raw: t.length === 1 ? w.replace(/[^\p{L}\p{N}.\-/]/gu, "") : x });
|
|
293
355
|
}
|
|
294
|
-
if (q.question)
|
|
356
|
+
if (q.question) {
|
|
357
|
+
const dropWord = (i) => {
|
|
358
|
+
const w = toks[i].text;
|
|
359
|
+
if (w === "vs" || w === "versus") return i > 0 && i < toks.length - 1 && !toks[i].raw.endsWith(".") && toks[i + 1].text !== "code";
|
|
360
|
+
if (w === "use" || w === "used" || w === "using") return USE_AFTER.has(toks[i - 1]?.text);
|
|
361
|
+
return QUESTION_WORDS.has(w);
|
|
362
|
+
};
|
|
363
|
+
const gone = new Set(toks.map((_, i) => i).filter((i) => dropWord(i) || toks[i].text.length === 1));
|
|
364
|
+
if (phrases.length || toks.some((_, i) => !gone.has(i))) {
|
|
365
|
+
for (const i of gone) if (QUESTION_WORDS.has(toks[i].text) && !q.dropped.includes(toks[i].text)) q.dropped.push(toks[i].text);
|
|
366
|
+
toks = toks.filter((_, i) => !gone.has(i));
|
|
367
|
+
}
|
|
368
|
+
}
|
|
295
369
|
const multi = synonymPhrases().map((p) => p.split(" ")).sort((a, b) => b.length - a.length);
|
|
296
370
|
for (let i = 0; i < toks.length; i++) {
|
|
297
371
|
const hit = multi.find((p) => p.every((w, j) => toks[i + j]?.text === w));
|
|
298
372
|
if (hit) {
|
|
299
|
-
q.terms.push(term(hit.join(" "), true));
|
|
373
|
+
q.terms.push({ ...term(hit.join(" "), true), raw: toks.slice(i, i + hit.length).map((t) => t.raw || t.text).join(" ") });
|
|
300
374
|
i += hit.length - 1;
|
|
301
375
|
continue;
|
|
302
376
|
}
|
|
@@ -349,6 +423,8 @@ function pageToRecord(p) {
|
|
|
349
423
|
function symbolToRecord(kind, row, owner) {
|
|
350
424
|
const name = String(row[1]);
|
|
351
425
|
const key = kind === "field" || kind === "value" ? Number(row[2]) : name;
|
|
426
|
+
const last = row[kind === "field" ? 5 : kind === "value" ? 4 : 6];
|
|
427
|
+
const cc = typeof last === "string" && /^[a-z]{2}$/i.test(last) ? last.toLowerCase() : null;
|
|
352
428
|
const base = {
|
|
353
429
|
id: `${owner.id}${anchorOf(kind, key)}`,
|
|
354
430
|
kind,
|
|
@@ -357,9 +433,9 @@ function symbolToRecord(kind, row, owner) {
|
|
|
357
433
|
title: name,
|
|
358
434
|
objectType: owner.objectType,
|
|
359
435
|
objectId: owner.objectId ?? null,
|
|
360
|
-
country: owner.country ?? null,
|
|
361
|
-
app: owner.app ?? null,
|
|
362
|
-
layer: owner.layer,
|
|
436
|
+
country: cc ?? owner.country ?? null,
|
|
437
|
+
app: cc ? `${cc.toUpperCase()} layer` : owner.app ?? null,
|
|
438
|
+
layer: cc ? 2 : owner.layer,
|
|
363
439
|
importance: owner.importance,
|
|
364
440
|
inbound: owner.inbound ?? 0,
|
|
365
441
|
system: owner.system ?? null,
|
|
@@ -395,12 +471,20 @@ function normOf(r) {
|
|
|
395
471
|
const own = r.type === "topic" ? new Set(tokenize(r.title)) : null;
|
|
396
472
|
const learn = r.type === "learn";
|
|
397
473
|
const tags = (r.tags ?? []).flatMap((t) => indexTokens(t, learn)).filter((t) => !own?.has(t));
|
|
398
|
-
const eqs = r.type === "object" || symbol ? [tokenize(r.name), ...r.caption ? [tokenize(r.caption)] : []] : [tokenize(r.title)];
|
|
474
|
+
const eqs = r.type === "object" || symbol ? [tokenize(r.name), ...r.caption ? [tokenize(r.caption)] : []] : [tokenize(r.title), ...learn ? (r.tags ?? []).filter(isCodedId).map((t) => tokenize(t)).filter((t) => t.length === 1) : []];
|
|
399
475
|
const n = { main: [...main], tags: learn ? tags : [...new Set(tags)], text: symbol ? null : indexTokens(r.text ?? ""), eqs };
|
|
400
476
|
NORM.set(r, n);
|
|
401
477
|
return n;
|
|
402
478
|
}
|
|
403
479
|
var textOf = (r, n) => n.text ??= indexTokens(r.text ?? "");
|
|
480
|
+
function seqs(r) {
|
|
481
|
+
let main, text2;
|
|
482
|
+
const symbol = r.kind !== "page";
|
|
483
|
+
return {
|
|
484
|
+
main: () => main ??= [seqTokens(r.name, symbol), seqTokens(r.title, symbol), ...r.caption ? [seqTokens(r.caption)] : []].flatMap((f, k) => k ? ["", ...f] : f),
|
|
485
|
+
text: () => text2 ??= seqTokens(r.text ?? "")
|
|
486
|
+
};
|
|
487
|
+
}
|
|
404
488
|
function prepare(records, prev) {
|
|
405
489
|
const index2 = prev ?? { records: [], postings: /* @__PURE__ */ new Map(), vocab: /* @__PURE__ */ new Map(), byType: /* @__PURE__ */ new Map(), maxInbound: /* @__PURE__ */ new Map() };
|
|
406
490
|
let st = STATE.get(index2);
|
|
@@ -522,7 +606,7 @@ function matchersOf(index2, q) {
|
|
|
522
606
|
}
|
|
523
607
|
return m;
|
|
524
608
|
}
|
|
525
|
-
function best(m, toks) {
|
|
609
|
+
function best(m, toks, seq = () => toks) {
|
|
526
610
|
let b = null;
|
|
527
611
|
for (const t of toks) {
|
|
528
612
|
const f = m.words.get(t);
|
|
@@ -530,7 +614,8 @@ function best(m, toks) {
|
|
|
530
614
|
}
|
|
531
615
|
for (const p of m.phrases) {
|
|
532
616
|
if (b && b.f >= p.f) continue;
|
|
533
|
-
|
|
617
|
+
if (!p.words.every((w) => holds(toks, w))) continue;
|
|
618
|
+
const f = phraseForm(p.words, seq());
|
|
534
619
|
if (f) {
|
|
535
620
|
const v = p.f * f;
|
|
536
621
|
if (!b || v > b.f) b = { f: v, how: p.how, ...p.via ? { via: p.via } : {} };
|
|
@@ -538,6 +623,21 @@ function best(m, toks) {
|
|
|
538
623
|
}
|
|
539
624
|
return b;
|
|
540
625
|
}
|
|
626
|
+
var STEMS = /* @__PURE__ */ new Map();
|
|
627
|
+
var stemOf = (t) => {
|
|
628
|
+
let s = STEMS.get(t);
|
|
629
|
+
if (s === void 0) {
|
|
630
|
+
s = stem(t);
|
|
631
|
+
STEMS.set(t, s);
|
|
632
|
+
}
|
|
633
|
+
return s;
|
|
634
|
+
};
|
|
635
|
+
function holds(toks, w) {
|
|
636
|
+
if (toks.includes(w)) return true;
|
|
637
|
+
const sw = stemOf(w);
|
|
638
|
+
for (const t of toks) if (t[0] === w[0] && stemOf(t) === sw) return true;
|
|
639
|
+
return false;
|
|
640
|
+
}
|
|
541
641
|
function phraseForm(words, toks) {
|
|
542
642
|
let bestF = 0;
|
|
543
643
|
for (let i = 0; i + words.length <= toks.length; i++) {
|
|
@@ -551,7 +651,7 @@ function phraseForm(words, toks) {
|
|
|
551
651
|
}
|
|
552
652
|
return bestF;
|
|
553
653
|
}
|
|
554
|
-
function nameMatch(eq, ms,
|
|
654
|
+
function nameMatch(eq, ms, plural2) {
|
|
555
655
|
let pos = 0, f = 1, lastPrefix = false;
|
|
556
656
|
for (const m of ms) {
|
|
557
657
|
const ws = m.term.phrase ? m.term.text.split(" ") : null;
|
|
@@ -559,7 +659,7 @@ function nameMatch(eq, ms, plural) {
|
|
|
559
659
|
for (const w of ws) {
|
|
560
660
|
const t2 = eq[pos++];
|
|
561
661
|
if (t2 === void 0) return { eq: 0, starts: 0 };
|
|
562
|
-
f = Math.min(f, t2 === w ? 1 : stem(t2) === stem(w) ?
|
|
662
|
+
f = Math.min(f, t2 === w ? 1 : stem(t2) === stem(w) ? plural2 ? 1 : 0.75 : 0);
|
|
563
663
|
}
|
|
564
664
|
if (!f) return { eq: 0, starts: 0 };
|
|
565
665
|
lastPrefix = false;
|
|
@@ -576,10 +676,10 @@ function nameMatch(eq, ms, plural) {
|
|
|
576
676
|
lastPrefix = p.how === "prefix";
|
|
577
677
|
continue;
|
|
578
678
|
}
|
|
579
|
-
f = Math.min(f, form.how === "stem" &&
|
|
679
|
+
f = Math.min(f, form.how === "stem" && plural2 ? 1 : form.f);
|
|
580
680
|
lastPrefix = form.how === "prefix";
|
|
581
681
|
}
|
|
582
|
-
if (pos === eq.length && !lastPrefix && (
|
|
682
|
+
if (pos === eq.length && !lastPrefix && (plural2 || f === 1)) return { eq: f, starts: 0 };
|
|
583
683
|
return { eq: 0, starts: pos <= eq.length ? f : 0 };
|
|
584
684
|
}
|
|
585
685
|
var NARRATIVE = { reviewed: 2, unreviewed: 0, none: -2, derived: -2 };
|
|
@@ -600,7 +700,7 @@ function scoreRecord(index2, r, q) {
|
|
|
600
700
|
if (q.ref.country) return { s: country === q.ref.country ? 1e3 : country === null ? 900 : 850, why };
|
|
601
701
|
return { s: country === null ? 1e3 : 900, why };
|
|
602
702
|
}
|
|
603
|
-
const prior =
|
|
703
|
+
const prior = objectish ? TYPE_PRIOR[r.objectType ?? ""] ?? 0 : 0;
|
|
604
704
|
const lift = (base) => base + (object ? LAYER[r.layer] : symbol ? 0 : 1) + (objectish ? 2 * r.importance : 0) + prior;
|
|
605
705
|
if (q.number !== null) {
|
|
606
706
|
if (!object || r.objectId !== q.number) return none;
|
|
@@ -616,10 +716,10 @@ function scoreRecord(index2, r, q) {
|
|
|
616
716
|
if (object && country && q.countries.includes(country)) return { s: 3 + 2 * r.importance + prior, why };
|
|
617
717
|
return none;
|
|
618
718
|
}
|
|
619
|
-
const n = normOf(r), ms = matchersOf(index2, q);
|
|
620
|
-
let s = 0, named = false, gate = !symbol || !!q.kind;
|
|
719
|
+
const n = normOf(r), ms = matchersOf(index2, q), sq = seqs(r);
|
|
720
|
+
let s = 0, textPts = 0, named = false, gate = !symbol || !!q.kind;
|
|
621
721
|
for (const m of ms) {
|
|
622
|
-
const main = best(m, n.main), tags = best(m, n.tags), text2 = best(m, textOf(r, n));
|
|
722
|
+
const main = best(m, n.main, sq.main), tags = best(m, n.tags), text2 = best(m, textOf(r, n), sq.text);
|
|
623
723
|
if (!main && !tags && !text2) return none;
|
|
624
724
|
if (main || tags) named = true;
|
|
625
725
|
if (symbol && !gate && n.eqs[0].some((t) => {
|
|
@@ -628,21 +728,25 @@ function scoreRecord(index2, r, q) {
|
|
|
628
728
|
})) gate = true;
|
|
629
729
|
if (symbol && /^on[a-z]/.test(m.term.text)) gate = true;
|
|
630
730
|
s += 3 * (main?.f ?? 0) + 2 * (tags?.f ?? 0) + (text2?.f ?? 0);
|
|
731
|
+
textPts += text2?.f ?? 0;
|
|
631
732
|
const via = [main, tags, text2].find((x) => x?.via && (x.how === "syn" || x.how === "fuzzy"))?.via;
|
|
632
733
|
if (via && !why.includes(`matched "${via}"`)) why.push(`matched "${via}"`);
|
|
633
734
|
}
|
|
634
735
|
if (!gate) return none;
|
|
736
|
+
const learn = r.type === "learn";
|
|
635
737
|
let eq = 0, starts = 0, byName = false;
|
|
636
738
|
n.eqs.forEach((e, k) => {
|
|
637
739
|
const x = nameMatch(e, ms, objectish);
|
|
638
740
|
if (x.eq > eq) {
|
|
639
741
|
eq = x.eq;
|
|
640
|
-
byName = k === 0;
|
|
742
|
+
byName = k === 0 || learn;
|
|
641
743
|
}
|
|
642
|
-
starts = Math.max(starts, x.starts);
|
|
744
|
+
if (!learn || k === 0) starts = Math.max(starts, x.starts);
|
|
643
745
|
});
|
|
746
|
+
const lifted = q.kind || q.types.length || /\d/.test(q.raw) || OBJECT_WORDS.test(q.raw) || object && eq && byName || symbol && ms.some((m) => /^on[a-z]/.test(m.term.text) && m.term.alts.some((a) => a.startsWith("on ")));
|
|
747
|
+
const owned = symbol && !!lifted;
|
|
644
748
|
if (eq) {
|
|
645
|
-
s += 10 * eq;
|
|
749
|
+
s += 10 * eq - (owned ? textPts : 0);
|
|
646
750
|
why.push(!byName ? "caption" : eq >= 0.75 ? "name" : "name~");
|
|
647
751
|
} else if (starts) {
|
|
648
752
|
s += 3 * starts;
|
|
@@ -652,8 +756,7 @@ function scoreRecord(index2, r, q) {
|
|
|
652
756
|
if ((r.type === "topic" || r.type === "app") && !symbol) s += NARRATIVE[r.narrative ?? (r.type === "app" ? "none" : "unreviewed")] ?? 0;
|
|
653
757
|
s += countryPoints(q, r, country);
|
|
654
758
|
if (q.types.length && objectish) s += 3;
|
|
655
|
-
s = lift(s);
|
|
656
|
-
const lifted = q.kind || q.types.length || /\d/.test(q.raw) || OBJECT_WORDS.test(q.raw) || object && eq && byName || symbol && ms.some((m) => /^on[a-z]/.test(m.term.text) && m.term.alts.some((a) => a.startsWith("on ")));
|
|
759
|
+
s = symbol ? s + 2 * r.importance + (owned ? LAYER[r.layer] + prior : 0) : lift(s);
|
|
657
760
|
if (objectish && !lifted) {
|
|
658
761
|
s *= 0.6;
|
|
659
762
|
why.push("demoted");
|
|
@@ -695,11 +798,11 @@ function search(index2, q, opts = {}) {
|
|
|
695
798
|
return opts.limit ? hits.slice(0, opts.limit) : hits;
|
|
696
799
|
}
|
|
697
800
|
function symbolLine(r) {
|
|
698
|
-
const
|
|
699
|
-
if (r.kind === "field") return `field ${x.id}${
|
|
700
|
-
if (r.kind === "event") return `event${
|
|
701
|
-
if (r.kind === "proc") return `procedure${
|
|
702
|
-
if (r.kind === "value") return `value ${x.ordinal}${
|
|
801
|
+
const of2 = `${r.owner ? ` of ${r.owner.title}` : ""}${r.country && r.app === `${r.country.toUpperCase()} layer` ? ` \xB7 ${r.app}` : ""}`, x = r.extra ?? {};
|
|
802
|
+
if (r.kind === "field") return `field ${x.id}${of2} \xB7 ${x.type}`;
|
|
803
|
+
if (r.kind === "event") return `event${of2} \xB7 ${x.kind} \xB7 ${x.subscribers} ${x.subscribers === 1 ? "subscriber" : "subscribers"}${x.obsolete ? ` \xB7 obsolete ${x.obsolete}` : ""}`;
|
|
804
|
+
if (r.kind === "proc") return `procedure${of2} \xB7 ${x.params} ${x.params === 1 ? "parameter" : "parameters"}${x.returns ? ` \xB7 returns ${x.returns}` : ""}${x.obsolete ? ` \xB7 obsolete ${x.obsolete}` : ""}`;
|
|
805
|
+
if (r.kind === "value") return `value ${x.ordinal}${of2}${x.caption && x.caption !== r.name ? ` \xB7 "${x.caption}"` : ""}`;
|
|
703
806
|
return r.title;
|
|
704
807
|
}
|
|
705
808
|
function objectsAt(index2, type, id) {
|
|
@@ -720,7 +823,8 @@ function typoWords(index2, q) {
|
|
|
720
823
|
function namedBy(index2, r, q) {
|
|
721
824
|
if (!STATE.get(index2)) return 0;
|
|
722
825
|
const n = normOf(r);
|
|
723
|
-
|
|
826
|
+
const sq = seqs(r);
|
|
827
|
+
return matchersOf(index2, q).reduce((a, m) => a + (best(m, n.main, sq.main)?.f ?? best(m, n.tags)?.f ?? 0), 0);
|
|
724
828
|
}
|
|
725
829
|
function symbolRecords(kind, rows, ownerOf) {
|
|
726
830
|
const out = [];
|
|
@@ -736,7 +840,7 @@ function countriesOf(index2) {
|
|
|
736
840
|
}
|
|
737
841
|
|
|
738
842
|
// ../search/src/hints.ts
|
|
739
|
-
function hints(index2, q, hits) {
|
|
843
|
+
function hints(index2, q, hits, opts = {}) {
|
|
740
844
|
const out = [];
|
|
741
845
|
const unknown = q.warnings.map((w) => /^unknown abbreviation (\w+)$/.exec(w)?.[1]).find(Boolean);
|
|
742
846
|
const digits = /(\d+)\s*$/.exec(q.raw)?.[1];
|
|
@@ -768,21 +872,89 @@ function hints(index2, q, hits) {
|
|
|
768
872
|
out.push({ text: `${t.term}: no page; ${words.map((x) => `${x.w} (${x.n.toLocaleString("en")} ${x.n === 1 ? "page" : "pages"})`).join(", ")}`, query: swap(words[0].w) });
|
|
769
873
|
}
|
|
770
874
|
if (q.question) {
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
const
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
best2
|
|
778
|
-
|
|
875
|
+
const hubOf = () => {
|
|
876
|
+
let hub = null, best2 = 0;
|
|
877
|
+
for (const i of index2.byType.get("topic") ?? []) {
|
|
878
|
+
const r = index2.records[i], named = namedBy(index2, r, q);
|
|
879
|
+
if (!named) continue;
|
|
880
|
+
const s = 3 * named + Math.log2((r.members ?? 0) + 1) + (r.narrative === "reviewed" ? 2 : r.narrative === "none" ? -2 : 0);
|
|
881
|
+
if (s > best2) {
|
|
882
|
+
best2 = s;
|
|
883
|
+
hub = r;
|
|
884
|
+
}
|
|
779
885
|
}
|
|
886
|
+
return hub;
|
|
887
|
+
};
|
|
888
|
+
if (q.shape === "what-changed" || !q.shape) {
|
|
889
|
+
const hub = hubOf();
|
|
890
|
+
out.push({ text: `For what changed, open the object's Versions section or Changes; for how-to, start with the hub${hub ? `: ${hub.title}` : ""}.`, ...hub ? { path: hub.id } : {} });
|
|
891
|
+
} else {
|
|
892
|
+
const hub = learnHub(index2, hits) ?? hubOf();
|
|
893
|
+
if (hub) out.push({ text: `For ${q.shape === "how-to" ? "how-to" : "a concept"}, start with the hub: ${hub.title}.`, path: hub.id });
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
const kind = q.kind ?? opts.kind ?? null;
|
|
897
|
+
if (kind && !hits.length) {
|
|
898
|
+
const name = nameOf2(q);
|
|
899
|
+
if (opts.unfiltered?.length) {
|
|
900
|
+
const n = opts.unfiltered.length;
|
|
901
|
+
out.unshift({ text: `No ${KIND_WORD[kind]} named "${name}" matches the filters${opts.filters ? ` (${opts.filters})` : ""}; without them: ${n} ${n === 1 ? "hit" : "hits"}.` });
|
|
902
|
+
} else {
|
|
903
|
+
const h = nearestSymbols(index2, kind, name, opts.keep);
|
|
904
|
+
if (h) out.unshift(h);
|
|
780
905
|
}
|
|
781
|
-
out.push({ text: `For what changed, open the object's Versions section or Changes; for how-to, start with the hub${hub ? `: ${hub.title}` : ""}.`, ...hub ? { path: hub.id } : {} });
|
|
782
906
|
}
|
|
783
|
-
void hits;
|
|
784
907
|
return out;
|
|
785
908
|
}
|
|
909
|
+
function learnHub(index2, hits) {
|
|
910
|
+
for (const h of hits.slice(0, 10)) {
|
|
911
|
+
const path = h.r.type === "learn" ? h.r.page?.hub : void 0;
|
|
912
|
+
if (!path) continue;
|
|
913
|
+
const hub = (index2.byType.get("topic") ?? []).map((i) => index2.records[i]).find((r) => r.id === path);
|
|
914
|
+
if (hub) return hub;
|
|
915
|
+
}
|
|
916
|
+
return null;
|
|
917
|
+
}
|
|
918
|
+
var KIND_WORD = { field: "field", event: "event", proc: "procedure", value: "value" };
|
|
919
|
+
function nameOf2(q) {
|
|
920
|
+
const typed = q.terms.map((t) => t.raw || t.text).join(" ").replace(/"/g, "").trim();
|
|
921
|
+
return typed || (q.kind ? q.raw.replace(/^\s*\w+\s*:\s*/, "").replace(/"/g, "").trim() : "");
|
|
922
|
+
}
|
|
923
|
+
function nearestSymbols(index2, kind, name, keep2) {
|
|
924
|
+
const all = index2.byType.get(kind) ?? [];
|
|
925
|
+
const want = name.toLowerCase();
|
|
926
|
+
if (!all.length || !want) return null;
|
|
927
|
+
const word = KIND_WORD[kind], head1 = `No ${word} named "${name}".`;
|
|
928
|
+
const near = [];
|
|
929
|
+
if (want.length >= 4) {
|
|
930
|
+
const head = want.slice(0, 5), shared = Math.ceil(0.75 * want.length), reach = Math.min(2, Math.max(1, Math.floor(want.length / 4)));
|
|
931
|
+
for (const i of all) {
|
|
932
|
+
const r = index2.records[i], n = r.name.toLowerCase();
|
|
933
|
+
if (n === want) return null;
|
|
934
|
+
if (keep2 && !keep2(r)) continue;
|
|
935
|
+
let ok = editDistance(want, n, reach) <= reach;
|
|
936
|
+
if (!ok && n.startsWith(head)) {
|
|
937
|
+
let k = 0;
|
|
938
|
+
while (k < n.length && k < want.length && n[k] === want[k]) k++;
|
|
939
|
+
ok = k >= shared;
|
|
940
|
+
}
|
|
941
|
+
if (ok) near.push({ r, d: editDistance(want, n, 60) });
|
|
942
|
+
}
|
|
943
|
+
} else if (all.some((i) => index2.records[i].name.toLowerCase() === want)) return null;
|
|
944
|
+
near.sort((a, b) => a.d - b.d || b.r.importance - a.r.importance || a.r.name.localeCompare(b.r.name) || a.r.id.localeCompare(b.r.id));
|
|
945
|
+
const seen = /* @__PURE__ */ new Set(), pick = [];
|
|
946
|
+
for (const { r } of near) {
|
|
947
|
+
if (pick.length >= 3) break;
|
|
948
|
+
if (seen.has(r.id)) continue;
|
|
949
|
+
seen.add(r.id);
|
|
950
|
+
pick.push(r);
|
|
951
|
+
}
|
|
952
|
+
if (!pick.length) return { text: head1 };
|
|
953
|
+
const plural2 = word === "procedure" ? "procedures" : `${word}s`;
|
|
954
|
+
const label = (r) => r.owner ? `${r.name} (${r.owner.title})` : r.name;
|
|
955
|
+
const qk = kind === "proc" ? "proc" : kind;
|
|
956
|
+
return { text: `${head1} Nearest ${plural2}: ${pick.map(label).join(", ")}.`, query: `${qk}: ${pick[0].name}` };
|
|
957
|
+
}
|
|
786
958
|
|
|
787
959
|
// ../search/src/learn.ts
|
|
788
960
|
function learnRecords(doc) {
|
|
@@ -912,6 +1084,139 @@ function dot(a, b, bOffset = 0) {
|
|
|
912
1084
|
return s;
|
|
913
1085
|
}
|
|
914
1086
|
|
|
1087
|
+
// src/country.ts
|
|
1088
|
+
var BLOCK_MEMBERS = 40;
|
|
1089
|
+
var DELTA_CLIP = 140;
|
|
1090
|
+
var KINDS2 = ["fields", "values", "events", "procedures", "keys"];
|
|
1091
|
+
var NOUN = { fields: ["field", "fields"], values: ["value", "values"], events: ["event", "events"], procedures: ["procedure", "procedures"], keys: ["key", "keys"] };
|
|
1092
|
+
var plural = (n, kind) => `${n} ${NOUN[kind][n === 1 ? 0 : 1]}`;
|
|
1093
|
+
var of = (o, kind, change) => (o[kind] ?? []).filter((m) => m.change === change);
|
|
1094
|
+
var clip = (s, n = DELTA_CLIP) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
|
|
1095
|
+
function titleOfKey(key, name) {
|
|
1096
|
+
const [type, id] = key.split("/");
|
|
1097
|
+
return `${type.charAt(0).toUpperCase()}${type.slice(1)} ${id} "${name}"`;
|
|
1098
|
+
}
|
|
1099
|
+
function addedSummary(o) {
|
|
1100
|
+
return KINDS2.filter((k) => k !== "keys").map((k) => [k, of(o, k, "added").length]).filter(([, n]) => n > 0).map(([k, n]) => `+${plural(n, k)}`).join(", ");
|
|
1101
|
+
}
|
|
1102
|
+
function replacedInLine(layers) {
|
|
1103
|
+
if (!layers.length) return null;
|
|
1104
|
+
const parts2 = [...layers].sort((a, b) => a.cc.localeCompare(b.cc)).map(({ cc, object }) => {
|
|
1105
|
+
const s = addedSummary(object);
|
|
1106
|
+
return `${cc.toUpperCase()}${s ? ` (${s})` : ""}`;
|
|
1107
|
+
});
|
|
1108
|
+
return `Replaced in: ${parts2.join(", ")}.`;
|
|
1109
|
+
}
|
|
1110
|
+
var paramsOf = (ps) => (ps ?? []).map((p) => `${p.var ? "var " : ""}${p.name}: ${p.type}`).join("; ");
|
|
1111
|
+
var signature = (name, to) => `${name}(${paramsOf(to.params)})${to.returns ? `: ${to.returns}` : ""}`;
|
|
1112
|
+
var procLine = (m) => {
|
|
1113
|
+
const to = m.to ?? {};
|
|
1114
|
+
return `${signature(m.name, to)} (${to.event && to.event !== "subscriber" ? to.event : to.scope ?? "global"})${to.obsolete?.state ? ` [obsolete ${to.obsolete.state}]` : ""}`;
|
|
1115
|
+
};
|
|
1116
|
+
var fieldLine = (m) => `${m.id} "${m.name}" ${(m.to ?? {}).type ?? ""}`.trimEnd();
|
|
1117
|
+
function propertiesText(p) {
|
|
1118
|
+
return Object.entries(p ?? {}).map(([k, v]) => v === "true" ? k : /^(Caption|ToolTip)/.test(k) ? `${k} "${v}"` : `${k} ${v}`).join("; ");
|
|
1119
|
+
}
|
|
1120
|
+
function deltaText(d) {
|
|
1121
|
+
if (!d) return "";
|
|
1122
|
+
const show = (v) => typeof v === "string" ? v : JSON.stringify(v);
|
|
1123
|
+
const parts2 = [];
|
|
1124
|
+
for (const [k, v] of Object.entries(d)) {
|
|
1125
|
+
if (k === "properties") {
|
|
1126
|
+
for (const [pk, pv] of Object.entries(v)) parts2.push(`${pk}: ${pv[0] ?? "-"} -> ${pv[1] ?? "-"}`);
|
|
1127
|
+
continue;
|
|
1128
|
+
}
|
|
1129
|
+
const [a, b] = v;
|
|
1130
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
1131
|
+
const add = b.filter((x) => !a.includes(x)), rem = a.filter((x) => !b.includes(x));
|
|
1132
|
+
parts2.push(`${k} ${[...add.map((x) => `+${x}`), ...rem.map((x) => `-${x}`)].join(", ") || "reordered"}`);
|
|
1133
|
+
} else parts2.push(`${k}: ${show(a ?? "-")} -> ${show(b ?? "-")}`);
|
|
1134
|
+
}
|
|
1135
|
+
return clip(parts2.join("; "));
|
|
1136
|
+
}
|
|
1137
|
+
function keyLine(m) {
|
|
1138
|
+
if (m.change === "added") return `${m.name} (new: ${((m.to ?? {}).fields ?? []).join(", ")})`;
|
|
1139
|
+
if (m.change === "removed") return `${m.name} (removed)`;
|
|
1140
|
+
return `${m.name} (${deltaText(m.delta) || "changed"})`.replace(/\(fields /, "(");
|
|
1141
|
+
}
|
|
1142
|
+
function headline(o, cc, major, title) {
|
|
1143
|
+
const added = addedSummary(o);
|
|
1144
|
+
const other = [];
|
|
1145
|
+
for (const k of KINDS2) {
|
|
1146
|
+
const changed = of(o, k, "changed").length + (k === "keys" ? of(o, k, "added").length : 0), removed = of(o, k, "removed").length;
|
|
1147
|
+
if (changed) other.push(`${plural(changed, k)} changed`);
|
|
1148
|
+
if (removed) other.push(`${plural(removed, k)} removed`);
|
|
1149
|
+
}
|
|
1150
|
+
if (o.properties?.length) other.push(`${o.properties.length} ${o.properties.length === 1 ? "property" : "properties"} changed`);
|
|
1151
|
+
const tail = [added || "no member added", ...other.length ? [other.join(", ")] : []].join("; ");
|
|
1152
|
+
return `${cc.toUpperCase()} replaces ${title} (BC${major}): ${tail}.`;
|
|
1153
|
+
}
|
|
1154
|
+
function countryBlock(o, cc, major, opts = {}) {
|
|
1155
|
+
const C = cc.toUpperCase(), title = opts.title ?? titleOfKey(o.key, o.name);
|
|
1156
|
+
const groups = [
|
|
1157
|
+
["Fields added", of(o, "fields", "added").map(fieldLine)],
|
|
1158
|
+
["Values added", of(o, "values", "added").map((m) => `${m.id} ${m.name}`)],
|
|
1159
|
+
["Events added", of(o, "events", "added").map(procLine)],
|
|
1160
|
+
["Procedures added", of(o, "procedures", "added").map(procLine)],
|
|
1161
|
+
["Keys changed", [...of(o, "keys", "changed"), ...of(o, "keys", "added"), ...of(o, "keys", "removed")].map(keyLine)],
|
|
1162
|
+
["Fields changed", of(o, "fields", "changed").map((m) => `${m.id} "${m.name}"${m.delta ? ` (${deltaText(m.delta)})` : ""}`)],
|
|
1163
|
+
["Events changed", of(o, "events", "changed").map((m) => `${m.name}${m.delta ? ` (${deltaText(m.delta)})` : ""}`)],
|
|
1164
|
+
["Procedures changed", of(o, "procedures", "changed").map((m) => `${m.name}${m.delta ? ` (${deltaText(m.delta)})` : ""}`)]
|
|
1165
|
+
];
|
|
1166
|
+
const out = [headline(o, cc, major, title)];
|
|
1167
|
+
let room = BLOCK_MEMBERS, more = 0;
|
|
1168
|
+
for (const [label, items] of groups) {
|
|
1169
|
+
if (!items.length) continue;
|
|
1170
|
+
const shown = items.slice(0, Math.max(0, room));
|
|
1171
|
+
room -= shown.length;
|
|
1172
|
+
more += items.length - shown.length;
|
|
1173
|
+
if (shown.length) out.push(`${label}: ${shown.join("; ")}`);
|
|
1174
|
+
}
|
|
1175
|
+
if (more) out.push(`... and ${more} more: localization("${C}", section: "w1_objects_this_country_changes")`);
|
|
1176
|
+
out.push(`Source: data/code/diffs/country/${major}-${cc.toLowerCase()}.json; the ${C} page: localizations/${cc.toLowerCase()}.`);
|
|
1177
|
+
return out.join("\n");
|
|
1178
|
+
}
|
|
1179
|
+
function parseAnchor(anchor) {
|
|
1180
|
+
const m = /^#?(field|value|event|proc)-(.+)$/.exec(anchor);
|
|
1181
|
+
return m ? { kind: m[1], key: m[2] } : null;
|
|
1182
|
+
}
|
|
1183
|
+
function memberAnswer(o, a, cc, opts) {
|
|
1184
|
+
const C = cc.toUpperCase(), title = opts.title ?? titleOfKey(o.key, o.name);
|
|
1185
|
+
const head = (what) => `${title}, ${what} (added by the ${C} layer, not in W1):`;
|
|
1186
|
+
const foot = `W1 page: ${opts.site}${opts.path}/ \xB7 ${C} layer: localizations/${cc.toLowerCase()}`;
|
|
1187
|
+
if (a.kind === "field") {
|
|
1188
|
+
const m = of(o, "fields", "added").find((x) => x.id === a.key);
|
|
1189
|
+
if (!m) return null;
|
|
1190
|
+
const to = m.to ?? {};
|
|
1191
|
+
return `${head(`field ${m.id}`)}
|
|
1192
|
+
| No. | Name | Type | Properties | Layer |
|
|
1193
|
+
|---|---|---|---|---|
|
|
1194
|
+
| ${m.id} | ${m.name} | ${to.type ?? ""} | ${propertiesText(to.properties)} | ${C} |
|
|
1195
|
+
|
|
1196
|
+
${foot}
|
|
1197
|
+
`;
|
|
1198
|
+
}
|
|
1199
|
+
if (a.kind === "value") {
|
|
1200
|
+
const m = of(o, "values", "added").find((x) => x.id === a.key);
|
|
1201
|
+
if (!m) return null;
|
|
1202
|
+
return `${head(`value ${m.id}`)}
|
|
1203
|
+
| Ordinal | Name | Layer |
|
|
1204
|
+
|---|---|---|
|
|
1205
|
+
| ${m.id} | ${m.name} | ${C} |
|
|
1206
|
+
|
|
1207
|
+
${foot}
|
|
1208
|
+
`;
|
|
1209
|
+
}
|
|
1210
|
+
const list = a.kind === "event" ? of(o, "events", "added") : of(o, "procedures", "added");
|
|
1211
|
+
const hits = list.filter((x) => x.name === a.key);
|
|
1212
|
+
if (!hits.length) return null;
|
|
1213
|
+
return `${head(`${a.kind === "event" ? "event" : "procedure"} ${a.key}`)}
|
|
1214
|
+
${hits.map((m) => `- \`${signature(m.name, m.to ?? {})}\` (${(m.to ?? {}).event ?? (m.to ?? {}).scope ?? "global"})`).join("\n")}
|
|
1215
|
+
|
|
1216
|
+
${foot}
|
|
1217
|
+
`;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
915
1220
|
// src/sections.ts
|
|
916
1221
|
var DEFAULT_BUDGET = 3e4;
|
|
917
1222
|
var MIN_BUDGET = 5e3;
|
|
@@ -1089,7 +1394,7 @@ function findSection(p, want) {
|
|
|
1089
1394
|
}
|
|
1090
1395
|
var keyList = (p, counts = false) => [...p.sections.map((s) => counts && s.rows.length ? `${s.key} (${s.rows.length})` : s.key), "frontmatter"].join(", ");
|
|
1091
1396
|
var titleOf = (p, path) => p.preamble.split("\n").find((l) => l.startsWith("# "))?.slice(2).trim() || path;
|
|
1092
|
-
function
|
|
1397
|
+
function clip2(s, n, mark = "\u2026") {
|
|
1093
1398
|
if (s.length <= n) return s;
|
|
1094
1399
|
const end = /[\uD800-\uDBFF]/.test(s[n - 2]) ? n - 2 : n - 1;
|
|
1095
1400
|
return `${s.slice(0, end)}${mark}`;
|
|
@@ -1152,7 +1457,7 @@ function compactView(md, path, budget) {
|
|
|
1152
1457
|
const more = `\u2026 (section: "${s.key}")`;
|
|
1153
1458
|
if (!s.rows.length) {
|
|
1154
1459
|
const text2 = s.head.filter((l) => !blank(l)).join("\n");
|
|
1155
|
-
if (text2) out.push(
|
|
1460
|
+
if (text2) out.push(clip2(text2, PROSE_CLIP, more));
|
|
1156
1461
|
} else {
|
|
1157
1462
|
const lead = s.head.find((l) => !blank(l) && !l.startsWith("|") && !l.startsWith("### "));
|
|
1158
1463
|
if (lead && lead.length <= HEAD_LINE_MAX) out.push(lead);
|
|
@@ -1165,12 +1470,12 @@ function compactView(md, path, budget) {
|
|
|
1165
1470
|
const t = r.table ? r.table.join("\n") : null;
|
|
1166
1471
|
if (r.table && t !== table2) out.push(...r.table);
|
|
1167
1472
|
table2 = t;
|
|
1168
|
-
out.push(
|
|
1473
|
+
out.push(clip2(r.lines[0], ROW_CLIP));
|
|
1169
1474
|
}
|
|
1170
1475
|
if (s.rows.length > PREVIEW_ROWS) out.push(`\u2192 ${s.rows.length - PREVIEW_ROWS} more of ${s.rows.length}: cat("${path}", section: "${s.key}", offset: ${PREVIEW_ROWS})`);
|
|
1171
1476
|
else {
|
|
1172
1477
|
const text2 = s.tail.filter((l) => !blank(l)).join("\n");
|
|
1173
|
-
if (text2) out.push(
|
|
1478
|
+
if (text2) out.push(clip2(text2, PROSE_CLIP, more));
|
|
1174
1479
|
}
|
|
1175
1480
|
}
|
|
1176
1481
|
out.push("");
|
|
@@ -1264,7 +1569,7 @@ ${pageView(md, clean, {}, budget, site)}`;
|
|
|
1264
1569
|
}
|
|
1265
1570
|
|
|
1266
1571
|
// src/server.ts
|
|
1267
|
-
var VERSION = "0.
|
|
1572
|
+
var VERSION = "0.7.0";
|
|
1268
1573
|
var SITE = (process.env.BC_OBSERVATORY_SITE ?? "https://waldo1001.github.io/waldo.BCObservatory/").replace(/\/?$/, "/");
|
|
1269
1574
|
var LOCAL = process.env.BC_OBSERVATORY_LOCAL ? resolve(process.env.BC_OBSERVATORY_LOCAL) : null;
|
|
1270
1575
|
var BUDGET = budgetOf(process.env.BC_OBSERVATORY_MAX_CHARS);
|
|
@@ -1293,6 +1598,7 @@ async function pageMarkdown(path) {
|
|
|
1293
1598
|
var pages = [];
|
|
1294
1599
|
var index = null;
|
|
1295
1600
|
var loadedAt = 0;
|
|
1601
|
+
var symbolsMajor = null;
|
|
1296
1602
|
async function shard(file, sha) {
|
|
1297
1603
|
const cached = join(cacheDir, `${sha}.json`);
|
|
1298
1604
|
if (!LOCAL && existsSync(cached)) return readFileSync(cached, "utf8");
|
|
@@ -1320,6 +1626,7 @@ async function loadIndex() {
|
|
|
1320
1626
|
} catch {
|
|
1321
1627
|
sm = null;
|
|
1322
1628
|
}
|
|
1629
|
+
symbolsMajor = sm?.major ?? null;
|
|
1323
1630
|
if (sm) {
|
|
1324
1631
|
const byPath = new Map(idx.records.map((r) => [r.id, r]));
|
|
1325
1632
|
for (const [k, e] of Object.entries(sm.kinds)) {
|
|
@@ -1332,7 +1639,37 @@ async function loadIndex() {
|
|
|
1332
1639
|
}
|
|
1333
1640
|
index = idx;
|
|
1334
1641
|
loadedAt = Date.now();
|
|
1642
|
+
countryDiffs.clear();
|
|
1643
|
+
}
|
|
1644
|
+
var countryDiffs = /* @__PURE__ */ new Map();
|
|
1645
|
+
function diffMajor() {
|
|
1646
|
+
if (symbolsMajor) return symbolsMajor;
|
|
1647
|
+
let best2 = 0;
|
|
1648
|
+
for (const r of index?.records ?? []) for (const part of (r.major ?? "").split(/[\s,]+/)) {
|
|
1649
|
+
const n = Number(part.split("-").pop());
|
|
1650
|
+
if (n > best2) best2 = n;
|
|
1651
|
+
}
|
|
1652
|
+
return best2 ? String(best2) : null;
|
|
1335
1653
|
}
|
|
1654
|
+
function replacedObjects(major, cc) {
|
|
1655
|
+
const key = `${major}-${cc}`, hit = countryDiffs.get(key);
|
|
1656
|
+
if (hit && Date.now() - hit.at < DAY) return Promise.resolve(hit.objects);
|
|
1657
|
+
const loading = (async () => {
|
|
1658
|
+
try {
|
|
1659
|
+
const diff = JSON.parse(await dataFile(`code/diffs/country/${key}.json`));
|
|
1660
|
+
return new Map(diff.objects.filter((o) => o.change === "replaced").map((o) => [o.key, o]));
|
|
1661
|
+
} catch {
|
|
1662
|
+
return null;
|
|
1663
|
+
}
|
|
1664
|
+
})();
|
|
1665
|
+
countryDiffs.set(key, { at: Date.now(), objects: loading });
|
|
1666
|
+
void loading.then((o) => {
|
|
1667
|
+
const e = countryDiffs.get(key);
|
|
1668
|
+
if (e) e.objects = o;
|
|
1669
|
+
});
|
|
1670
|
+
return loading;
|
|
1671
|
+
}
|
|
1672
|
+
var replacedBy = async (major, cc, key) => (await replacedObjects(major, cc))?.get(key) ?? null;
|
|
1336
1673
|
var url = (path) => `${SITE}${path}/`;
|
|
1337
1674
|
var line = (r, tag = "") => `- ${r.title}${r.caption ? ` (captioned "${r.caption}")` : ""} [${r.type}${r.tier ? `, ${r.tier}` : ""}${r.date ? `, ${r.date}` : ""}] path=${r.path}${tag}${r.path_label ? `
|
|
1338
1675
|
in: ${r.path_label}` : ""}${r.stats ? `
|
|
@@ -1396,7 +1733,7 @@ function fuse(lists, k = 60) {
|
|
|
1396
1733
|
}
|
|
1397
1734
|
function keepOf(a) {
|
|
1398
1735
|
const lc = (x) => x?.trim().toLowerCase() || void 0;
|
|
1399
|
-
const type = lc(a.type), tier = lc(a.tier), country = lc(a.country), app = lc(a.app), kind = lc(a.kind);
|
|
1736
|
+
const type = lc(a.type), tier = lc(a.tier), country = lc(a.country), app = lc(a.app), kind = KINDS[lc(a.kind) ?? ""] ?? lc(a.kind);
|
|
1400
1737
|
const system = a.system ? systemOf(a.system) ?? lc(a.system) : void 0;
|
|
1401
1738
|
const objectType = a.object_type ? typeOfWord(a.object_type.replace(/\s+/g, "")) ?? lc(a.object_type) : void 0;
|
|
1402
1739
|
return (r) => (!type || r.type === type) && (!tier || r.tier === tier) && (!system || r.system === system) && (!country || (r.country ?? "") === country) && (!app || (r.app ?? "").toLowerCase() === app) && (!objectType || r.objectType === objectType) && (!kind || r.kind === kind);
|
|
@@ -1407,9 +1744,11 @@ async function toolSearch(a) {
|
|
|
1407
1744
|
const limit = a.limit ?? 10, mode = a.mode ?? "hybrid", POOL = 50;
|
|
1408
1745
|
const q = parseQuery(a.query, { countries: countriesOf(index) });
|
|
1409
1746
|
const hits = search(index, q, { filter: keep2 });
|
|
1410
|
-
const
|
|
1747
|
+
const kind = q.kind ?? KINDS[a.kind?.trim().toLowerCase() ?? ""] ?? null;
|
|
1748
|
+
const uid = (r) => r.kind !== "page" && r.country ? `${r.id} ${r.country}` : r.id;
|
|
1749
|
+
const byId = new Map(hits.map((h) => [uid(h.r), h.r]));
|
|
1411
1750
|
let semantic = [], note = "";
|
|
1412
|
-
if (mode !== "keyword") {
|
|
1751
|
+
if (mode !== "keyword" && !kind) {
|
|
1413
1752
|
const m = await loadModel();
|
|
1414
1753
|
if (m) {
|
|
1415
1754
|
const v = embed(m, a.query), data = pageVectors(m);
|
|
@@ -1426,27 +1765,36 @@ async function toolSearch(a) {
|
|
|
1426
1765
|
}
|
|
1427
1766
|
} else if (mode === "semantic") note = ` (semantic search unavailable: ${modelError}; keyword results instead)`;
|
|
1428
1767
|
}
|
|
1429
|
-
const keyword = hits.map((h) => h.r
|
|
1768
|
+
const keyword = hits.map((h) => uid(h.r));
|
|
1430
1769
|
let order, exactBand = 0;
|
|
1431
1770
|
if (mode === "semantic") order = semantic.length ? semantic : keyword.slice(0, POOL);
|
|
1432
1771
|
else if (mode === "keyword" || !semantic.length) order = keyword;
|
|
1433
1772
|
else {
|
|
1434
|
-
const exact = hits.filter((h) => h.band === "exact").map((h) => h.r
|
|
1773
|
+
const exact = hits.filter((h) => h.band === "exact").map((h) => uid(h.r)), pinned = new Set(exact);
|
|
1435
1774
|
exactBand = exact.length;
|
|
1436
|
-
order = [...exact, ...fuse([hits.filter((h) => h.band !== "exact").slice(0, POOL).map((h) => h.r
|
|
1775
|
+
order = [...exact, ...fuse([hits.filter((h) => h.band !== "exact").slice(0, POOL).map((h) => uid(h.r)), semantic.filter((p) => !pinned.has(p))])];
|
|
1437
1776
|
}
|
|
1438
1777
|
if (mode !== "semantic" && mode !== "keyword" && !semantic.length) exactBand = hits.filter((h) => h.band === "exact").length;
|
|
1439
1778
|
const inKeyword = new Set(keyword.slice(0, Math.max(POOL, limit))), inMeaning = new Set(semantic);
|
|
1440
1779
|
const tag = (id) => inKeyword.has(id) && inMeaning.has(id) ? " [both]" : inMeaning.has(id) ? " [meaning]" : " [keyword]";
|
|
1441
1780
|
const shown = order.slice(0, limit).map((id) => byId.get(id)).filter(Boolean);
|
|
1442
|
-
const
|
|
1781
|
+
const filters = ["type", "tier", "system", "country", "app", "object_type"].filter((k) => a[k]?.trim()).map((k) => `${k}=${a[k].trim()}`).join(", ");
|
|
1782
|
+
const unfiltered = kind && !hits.length && filters ? search(index, q, { filter: (r) => r.kind === kind }) : void 0;
|
|
1783
|
+
const hs = hints(index, q, hits, { kind, unfiltered, filters, keep: keep2 }).filter((h) => !hits.length || hits[0].s < 6 || q.question || !!q.ref || /: no page; /.test(h.text));
|
|
1443
1784
|
const hintText = hs.length ? `
|
|
1444
1785
|
${hs.map((h) => `Hint: ${h.text}${h.query ? ` (search "${h.query}")` : h.path ? ` (cat("${h.path}"))` : ""}`).join("\n")}` : "";
|
|
1786
|
+
if (!shown.length && kind && /^No \w+ named /.test(hs[0]?.text ?? "")) {
|
|
1787
|
+
const [first, ...more] = hs, via = (h) => h.query ? ` (search "${h.query}")` : h.path ? ` (cat("${h.path}"))` : "";
|
|
1788
|
+
return `${first.text}${via(first)}${note}${more.map((h) => `
|
|
1789
|
+
Hint: ${h.text}${via(h)}`).join("")}`;
|
|
1790
|
+
}
|
|
1445
1791
|
if (!shown.length) return `No pages match "${a.query}". Try fewer words, or ls("") to browse sections.${note}${hintText}`;
|
|
1446
1792
|
const how = semantic.length && keyword.length && mode === "hybrid" ? "keyword + meaning" : mode === "semantic" && semantic.length ? "meaning" : "keyword";
|
|
1447
1793
|
const learnNote = shown.some((r) => r.type === "learn") ? "; a Learn article's url opens on learn.microsoft.com, cat(hub) reads its hub" : "";
|
|
1448
|
-
|
|
1449
|
-
${shown.map((r) => hitLine(r, tag(r
|
|
1794
|
+
if (mode === "hybrid" && !keyword.length && semantic.length) return `No keyword match for "${a.query}"; ${shown.length} pages by meaning only (read one with cat(path)${learnNote}):
|
|
1795
|
+
${shown.map((r) => hitLine(r, tag(uid(r)))).join("\n")}${hintText}`;
|
|
1796
|
+
return `${shown.length} results, ${how}${note}${how === "keyword + meaning" ? ` (exact band: ${exactBand})` : ""} (read one with cat(path)${learnNote}):
|
|
1797
|
+
${shown.map((r) => hitLine(r, tag(uid(r)))).join("\n")}${hintText}`;
|
|
1450
1798
|
}
|
|
1451
1799
|
async function toolLs(a) {
|
|
1452
1800
|
await loadIndex();
|
|
@@ -1468,7 +1816,27 @@ async function toolLs(a) {
|
|
|
1468
1816
|
async function toolCat(a) {
|
|
1469
1817
|
const [base, anchor] = a.path.split(/(?=#)/);
|
|
1470
1818
|
const clean = `${base.replace(/^\/+|\/+$/g, "").replace(/\.md$/, "")}${anchor ?? ""}`;
|
|
1471
|
-
|
|
1819
|
+
const path = clean.replace(/#.*$/, ""), md = await pageMarkdown(path);
|
|
1820
|
+
if (anchor && anchorView(md, path, anchor, SITE) === null) {
|
|
1821
|
+
const added = await layerMember(path, anchor);
|
|
1822
|
+
if (added) return added;
|
|
1823
|
+
}
|
|
1824
|
+
return pageView(md, clean, a, BUDGET, SITE);
|
|
1825
|
+
}
|
|
1826
|
+
async function layerMember(path, anchor) {
|
|
1827
|
+
const want = parseAnchor(anchor), major = diffMajor();
|
|
1828
|
+
if (!want || !major) return null;
|
|
1829
|
+
await loadIndex();
|
|
1830
|
+
const id = `${path}${anchor}`;
|
|
1831
|
+
const layers = [...new Set(index.records.filter((r) => r.kind !== "page" && r.id === id && r.country && r.app === `${r.country.toUpperCase()} layer`).map((r) => r.country))].sort();
|
|
1832
|
+
const owner = index.records.find((r) => r.kind === "page" && r.id === path);
|
|
1833
|
+
const out = [];
|
|
1834
|
+
for (const cc of layers) {
|
|
1835
|
+
const o = owner?.objectId != null ? await replacedBy(major, cc, `${owner.objectType}/${owner.objectId}`) : null;
|
|
1836
|
+
const text2 = o && memberAnswer(o, want, cc, { title: owner?.title, site: SITE, path });
|
|
1837
|
+
if (text2) out.push(text2);
|
|
1838
|
+
}
|
|
1839
|
+
return out.length ? out.join("\n") : null;
|
|
1472
1840
|
}
|
|
1473
1841
|
async function toolGetObject(a) {
|
|
1474
1842
|
await loadIndex();
|
|
@@ -1503,12 +1871,24 @@ async function toolGetObject(a) {
|
|
|
1503
1871
|
const pick = country ? inCountry.length ? inCountry : w1 : w1.length ? w1 : found;
|
|
1504
1872
|
if (pick.length === 1) {
|
|
1505
1873
|
const twins = found.filter((r) => r !== pick[0] && r.country).map((r) => r.country.toUpperCase());
|
|
1506
|
-
const
|
|
1874
|
+
const major = diffMajor(), key = `${type}/${pick[0].objectId ?? pick[0].name.toLowerCase()}`;
|
|
1875
|
+
let note = "";
|
|
1876
|
+
if (country && !inCountry.length) {
|
|
1877
|
+
const replaced = major ? await replacedBy(major, country, key) : null;
|
|
1878
|
+
note = replaced && major ? `${countryBlock(replaced, country, major, { title: pick[0].title })}
|
|
1507
1879
|
|
|
1508
|
-
` :
|
|
1509
|
-
|
|
1880
|
+
` : `(${country.toUpperCase()} has no ${type} of its own here; the W1 ${type} applies.)
|
|
1881
|
+
|
|
1882
|
+
`;
|
|
1883
|
+
}
|
|
1884
|
+
let also = !country && twins.length ? `
|
|
1510
1885
|
|
|
1511
1886
|
(Also in ${twins.join(", ")}: get_object("${type}", "${pick[0].objectId ?? pick[0].name}", country).)` : "";
|
|
1887
|
+
if (!country && !pick[0].country && major && !a.section) {
|
|
1888
|
+
const layers = (await Promise.all(countriesOf(index).map(async (cc2) => ({ cc: cc2, object: await replacedBy(major, cc2, key) })))).filter((l) => !!l.object);
|
|
1889
|
+
const line2 = replacedInLine(layers);
|
|
1890
|
+
if (line2) also += `${also ? "\n" : "\n\n"}${line2}`;
|
|
1891
|
+
}
|
|
1512
1892
|
return `${note}${pageView(await pageMarkdown(pick[0].id), pick[0].id, a, BUDGET, SITE)}${a.section ? "" : also}`;
|
|
1513
1893
|
}
|
|
1514
1894
|
if (pick.length > 1) return `${pick.length} ${type} objects match "${a.idOrName}"; read one with cat(path):
|
|
@@ -1572,7 +1952,7 @@ function createServer() {
|
|
|
1572
1952
|
"search",
|
|
1573
1953
|
{
|
|
1574
1954
|
title: "Search the knowledge base",
|
|
1575
|
-
description: "Search every page
|
|
1955
|
+
description: "Search every page and the fields, events, global procedures and enum values of AL objects. Takes names ('Sales Header'), references ('t36', 'cu 80'), bare ids ('36') and symbol prefixes ('event: OnAfterPost', 'proc: CopyToTempLines'); symbols rank by their owner's layer and importance; a kind: query with no hit names the nearest symbols; a symbol hit's path ends in its anchor (#event-<Name>, #field-<id>, #proc-<Name>, #value-<ordinal>); cat(path) reads that member. Learn articles: type learn, a url not a path. Hybrid by default: exact matches on top, the rest fused with a meaning ranking; each hit says [keyword], [meaning] or [both]; mode 'keyword' or 'semantic' for one list. Filters: type (topic, learn, app, feature, object, localization, video, post, change, source, digest, release), tier (official, community), system (finance, sales, ... or alias 'g/l'), country ('BE'), app ('Subscription Billing'), object_type ('table'), kind (page, field, event, proc, value).",
|
|
1576
1956
|
inputSchema: { query: z.string(), type: z.string().optional(), tier: z.string().optional(), system: z.string().optional(), country: z.string().optional(), app: z.string().optional(), object_type: z.string().optional(), kind: z.enum(["page", "field", "event", "proc", "value"]).optional(), limit: z.number().int().min(1).max(50).optional(), mode: z.enum(["hybrid", "keyword", "semantic"]).optional() }
|
|
1577
1957
|
},
|
|
1578
1958
|
async (a) => text(await toolSearch(a))
|
|
@@ -1589,7 +1969,7 @@ function createServer() {
|
|
|
1589
1969
|
);
|
|
1590
1970
|
server.registerTool("get_object", {
|
|
1591
1971
|
title: "Look up an AL object",
|
|
1592
|
-
description: `An AL object of W1, a Microsoft first-party app or a country layer by type and id, reference, name or caption: ('table', '18'), ('table', 't36'), ('table', '36-be'), ('codeunit', 'Sales-Post'), ('page', 'Customers'), or with country 'BE'. Returns its page: fields, keys, events, procedures, calls, obsolete state, versions, countries, Learn pages; a W1 object wins over its country twins unless a country is given, and two candidates are listed with their paths. A page over ${B} characters comes back compact (frontmatter, the first 5 rows of each section, the section keys): pass section ('fields', 'events', 'procedures', 'calls', ...) with offset and limit to read one. To check that a field, event or procedure exists, search('event: <Name>') answers without the page.`,
|
|
1972
|
+
description: `An AL object of W1, a Microsoft first-party app or a country layer by type and id, reference, name or caption: ('table', '18'), ('table', 't36'), ('table', '36-be'), ('codeunit', 'Sales-Post'), ('page', 'Customers'), or with country 'BE' (what that layer adds prints first). Returns its page: fields, keys, events, procedures, calls, obsolete state, versions, countries, Learn pages; a W1 object wins over its country twins unless a country is given, and two candidates are listed with their paths. A page over ${B} characters comes back compact (frontmatter, the first 5 rows of each section, the section keys): pass section ('fields', 'events', 'procedures', 'calls', ...) with offset and limit to read one. To check that a field, event or procedure exists, search('event: <Name>') answers without the page. counts.procedures counts every procedure (public, local, events, subscribers); the summary's 'public procedures' excludes locals and events.`,
|
|
1593
1973
|
inputSchema: { type: z.string(), idOrName: z.string(), country: z.string().length(2).optional(), ...VIEW }
|
|
1594
1974
|
}, async (a) => text(await toolGetObject(a)));
|
|
1595
1975
|
server.registerTool("diff_object", {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bc-observatory",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "MCP server for BC Observatory: an agent-first, cross-referenced knowledge base of Microsoft Dynamics 365 Business Central (Learn, code, roadmap, videos, community blogs).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|