bc-observatory 0.4.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 +485 -74
- 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,
|
|
@@ -393,13 +469,22 @@ function normOf(r) {
|
|
|
393
469
|
const symbol = r.kind !== "page";
|
|
394
470
|
const main = /* @__PURE__ */ new Set([...indexTokens(r.name, symbol), ...indexTokens(r.title, symbol), ...r.caption ? indexTokens(r.caption) : []]);
|
|
395
471
|
const own = r.type === "topic" ? new Set(tokenize(r.title)) : null;
|
|
396
|
-
const
|
|
397
|
-
const
|
|
398
|
-
const
|
|
472
|
+
const learn = r.type === "learn";
|
|
473
|
+
const tags = (r.tags ?? []).flatMap((t) => indexTokens(t, learn)).filter((t) => !own?.has(t));
|
|
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) : []];
|
|
475
|
+
const n = { main: [...main], tags: learn ? tags : [...new Set(tags)], text: symbol ? null : indexTokens(r.text ?? ""), eqs };
|
|
399
476
|
NORM.set(r, n);
|
|
400
477
|
return n;
|
|
401
478
|
}
|
|
402
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
|
+
}
|
|
403
488
|
function prepare(records, prev) {
|
|
404
489
|
const index2 = prev ?? { records: [], postings: /* @__PURE__ */ new Map(), vocab: /* @__PURE__ */ new Map(), byType: /* @__PURE__ */ new Map(), maxInbound: /* @__PURE__ */ new Map() };
|
|
405
490
|
let st = STATE.get(index2);
|
|
@@ -521,7 +606,7 @@ function matchersOf(index2, q) {
|
|
|
521
606
|
}
|
|
522
607
|
return m;
|
|
523
608
|
}
|
|
524
|
-
function best(m, toks) {
|
|
609
|
+
function best(m, toks, seq = () => toks) {
|
|
525
610
|
let b = null;
|
|
526
611
|
for (const t of toks) {
|
|
527
612
|
const f = m.words.get(t);
|
|
@@ -529,7 +614,8 @@ function best(m, toks) {
|
|
|
529
614
|
}
|
|
530
615
|
for (const p of m.phrases) {
|
|
531
616
|
if (b && b.f >= p.f) continue;
|
|
532
|
-
|
|
617
|
+
if (!p.words.every((w) => holds(toks, w))) continue;
|
|
618
|
+
const f = phraseForm(p.words, seq());
|
|
533
619
|
if (f) {
|
|
534
620
|
const v = p.f * f;
|
|
535
621
|
if (!b || v > b.f) b = { f: v, how: p.how, ...p.via ? { via: p.via } : {} };
|
|
@@ -537,6 +623,21 @@ function best(m, toks) {
|
|
|
537
623
|
}
|
|
538
624
|
return b;
|
|
539
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
|
+
}
|
|
540
641
|
function phraseForm(words, toks) {
|
|
541
642
|
let bestF = 0;
|
|
542
643
|
for (let i = 0; i + words.length <= toks.length; i++) {
|
|
@@ -550,7 +651,7 @@ function phraseForm(words, toks) {
|
|
|
550
651
|
}
|
|
551
652
|
return bestF;
|
|
552
653
|
}
|
|
553
|
-
function nameMatch(eq, ms,
|
|
654
|
+
function nameMatch(eq, ms, plural2) {
|
|
554
655
|
let pos = 0, f = 1, lastPrefix = false;
|
|
555
656
|
for (const m of ms) {
|
|
556
657
|
const ws = m.term.phrase ? m.term.text.split(" ") : null;
|
|
@@ -558,7 +659,7 @@ function nameMatch(eq, ms, plural) {
|
|
|
558
659
|
for (const w of ws) {
|
|
559
660
|
const t2 = eq[pos++];
|
|
560
661
|
if (t2 === void 0) return { eq: 0, starts: 0 };
|
|
561
|
-
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);
|
|
562
663
|
}
|
|
563
664
|
if (!f) return { eq: 0, starts: 0 };
|
|
564
665
|
lastPrefix = false;
|
|
@@ -575,10 +676,10 @@ function nameMatch(eq, ms, plural) {
|
|
|
575
676
|
lastPrefix = p.how === "prefix";
|
|
576
677
|
continue;
|
|
577
678
|
}
|
|
578
|
-
f = Math.min(f, form.how === "stem" &&
|
|
679
|
+
f = Math.min(f, form.how === "stem" && plural2 ? 1 : form.f);
|
|
579
680
|
lastPrefix = form.how === "prefix";
|
|
580
681
|
}
|
|
581
|
-
if (pos === eq.length && !lastPrefix && (
|
|
682
|
+
if (pos === eq.length && !lastPrefix && (plural2 || f === 1)) return { eq: f, starts: 0 };
|
|
582
683
|
return { eq: 0, starts: pos <= eq.length ? f : 0 };
|
|
583
684
|
}
|
|
584
685
|
var NARRATIVE = { reviewed: 2, unreviewed: 0, none: -2, derived: -2 };
|
|
@@ -599,7 +700,7 @@ function scoreRecord(index2, r, q) {
|
|
|
599
700
|
if (q.ref.country) return { s: country === q.ref.country ? 1e3 : country === null ? 900 : 850, why };
|
|
600
701
|
return { s: country === null ? 1e3 : 900, why };
|
|
601
702
|
}
|
|
602
|
-
const prior =
|
|
703
|
+
const prior = objectish ? TYPE_PRIOR[r.objectType ?? ""] ?? 0 : 0;
|
|
603
704
|
const lift = (base) => base + (object ? LAYER[r.layer] : symbol ? 0 : 1) + (objectish ? 2 * r.importance : 0) + prior;
|
|
604
705
|
if (q.number !== null) {
|
|
605
706
|
if (!object || r.objectId !== q.number) return none;
|
|
@@ -615,10 +716,10 @@ function scoreRecord(index2, r, q) {
|
|
|
615
716
|
if (object && country && q.countries.includes(country)) return { s: 3 + 2 * r.importance + prior, why };
|
|
616
717
|
return none;
|
|
617
718
|
}
|
|
618
|
-
const n = normOf(r), ms = matchersOf(index2, q);
|
|
619
|
-
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;
|
|
620
721
|
for (const m of ms) {
|
|
621
|
-
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);
|
|
622
723
|
if (!main && !tags && !text2) return none;
|
|
623
724
|
if (main || tags) named = true;
|
|
624
725
|
if (symbol && !gate && n.eqs[0].some((t) => {
|
|
@@ -627,21 +728,25 @@ function scoreRecord(index2, r, q) {
|
|
|
627
728
|
})) gate = true;
|
|
628
729
|
if (symbol && /^on[a-z]/.test(m.term.text)) gate = true;
|
|
629
730
|
s += 3 * (main?.f ?? 0) + 2 * (tags?.f ?? 0) + (text2?.f ?? 0);
|
|
731
|
+
textPts += text2?.f ?? 0;
|
|
630
732
|
const via = [main, tags, text2].find((x) => x?.via && (x.how === "syn" || x.how === "fuzzy"))?.via;
|
|
631
733
|
if (via && !why.includes(`matched "${via}"`)) why.push(`matched "${via}"`);
|
|
632
734
|
}
|
|
633
735
|
if (!gate) return none;
|
|
736
|
+
const learn = r.type === "learn";
|
|
634
737
|
let eq = 0, starts = 0, byName = false;
|
|
635
738
|
n.eqs.forEach((e, k) => {
|
|
636
739
|
const x = nameMatch(e, ms, objectish);
|
|
637
740
|
if (x.eq > eq) {
|
|
638
741
|
eq = x.eq;
|
|
639
|
-
byName = k === 0;
|
|
742
|
+
byName = k === 0 || learn;
|
|
640
743
|
}
|
|
641
|
-
starts = Math.max(starts, x.starts);
|
|
744
|
+
if (!learn || k === 0) starts = Math.max(starts, x.starts);
|
|
642
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;
|
|
643
748
|
if (eq) {
|
|
644
|
-
s += 10 * eq;
|
|
749
|
+
s += 10 * eq - (owned ? textPts : 0);
|
|
645
750
|
why.push(!byName ? "caption" : eq >= 0.75 ? "name" : "name~");
|
|
646
751
|
} else if (starts) {
|
|
647
752
|
s += 3 * starts;
|
|
@@ -651,8 +756,7 @@ function scoreRecord(index2, r, q) {
|
|
|
651
756
|
if ((r.type === "topic" || r.type === "app") && !symbol) s += NARRATIVE[r.narrative ?? (r.type === "app" ? "none" : "unreviewed")] ?? 0;
|
|
652
757
|
s += countryPoints(q, r, country);
|
|
653
758
|
if (q.types.length && objectish) s += 3;
|
|
654
|
-
s = lift(s);
|
|
655
|
-
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);
|
|
656
760
|
if (objectish && !lifted) {
|
|
657
761
|
s *= 0.6;
|
|
658
762
|
why.push("demoted");
|
|
@@ -694,11 +798,11 @@ function search(index2, q, opts = {}) {
|
|
|
694
798
|
return opts.limit ? hits.slice(0, opts.limit) : hits;
|
|
695
799
|
}
|
|
696
800
|
function symbolLine(r) {
|
|
697
|
-
const
|
|
698
|
-
if (r.kind === "field") return `field ${x.id}${
|
|
699
|
-
if (r.kind === "event") return `event${
|
|
700
|
-
if (r.kind === "proc") return `procedure${
|
|
701
|
-
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}"` : ""}`;
|
|
702
806
|
return r.title;
|
|
703
807
|
}
|
|
704
808
|
function objectsAt(index2, type, id) {
|
|
@@ -719,7 +823,8 @@ function typoWords(index2, q) {
|
|
|
719
823
|
function namedBy(index2, r, q) {
|
|
720
824
|
if (!STATE.get(index2)) return 0;
|
|
721
825
|
const n = normOf(r);
|
|
722
|
-
|
|
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);
|
|
723
828
|
}
|
|
724
829
|
function symbolRecords(kind, rows, ownerOf) {
|
|
725
830
|
const out = [];
|
|
@@ -735,7 +840,7 @@ function countriesOf(index2) {
|
|
|
735
840
|
}
|
|
736
841
|
|
|
737
842
|
// ../search/src/hints.ts
|
|
738
|
-
function hints(index2, q, hits) {
|
|
843
|
+
function hints(index2, q, hits, opts = {}) {
|
|
739
844
|
const out = [];
|
|
740
845
|
const unknown = q.warnings.map((w) => /^unknown abbreviation (\w+)$/.exec(w)?.[1]).find(Boolean);
|
|
741
846
|
const digits = /(\d+)\s*$/.exec(q.raw)?.[1];
|
|
@@ -767,21 +872,109 @@ function hints(index2, q, hits) {
|
|
|
767
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) });
|
|
768
873
|
}
|
|
769
874
|
if (q.question) {
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
const
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
best2
|
|
777
|
-
|
|
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
|
+
}
|
|
778
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);
|
|
779
905
|
}
|
|
780
|
-
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 } : {} });
|
|
781
906
|
}
|
|
782
|
-
void hits;
|
|
783
907
|
return out;
|
|
784
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
|
+
}
|
|
958
|
+
|
|
959
|
+
// ../search/src/learn.ts
|
|
960
|
+
function learnRecords(doc) {
|
|
961
|
+
return doc.rows.map(([path, title, summary, date, h, system, ids]) => {
|
|
962
|
+
const hub = h >= 0 ? doc.hubs[h] : void 0;
|
|
963
|
+
const sys = system ?? hub?.[2] ?? null;
|
|
964
|
+
return {
|
|
965
|
+
path: /^https?:\/\//.test(path) ? path : `${doc.base}${path}`,
|
|
966
|
+
type: "learn",
|
|
967
|
+
title,
|
|
968
|
+
summary,
|
|
969
|
+
tier: "official",
|
|
970
|
+
...sys ? { system: sys } : {},
|
|
971
|
+
...date ? { date } : {},
|
|
972
|
+
...ids?.length ? { tags: ids } : {},
|
|
973
|
+
...hub?.[1] ? { path_label: hub[1] } : {},
|
|
974
|
+
...hub?.[0] ? { hub: hub[0] } : {}
|
|
975
|
+
};
|
|
976
|
+
});
|
|
977
|
+
}
|
|
785
978
|
|
|
786
979
|
// src/embed.ts
|
|
787
980
|
var MAX_TOKENS = 512;
|
|
@@ -891,6 +1084,139 @@ function dot(a, b, bOffset = 0) {
|
|
|
891
1084
|
return s;
|
|
892
1085
|
}
|
|
893
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
|
+
|
|
894
1220
|
// src/sections.ts
|
|
895
1221
|
var DEFAULT_BUDGET = 3e4;
|
|
896
1222
|
var MIN_BUDGET = 5e3;
|
|
@@ -1068,7 +1394,7 @@ function findSection(p, want) {
|
|
|
1068
1394
|
}
|
|
1069
1395
|
var keyList = (p, counts = false) => [...p.sections.map((s) => counts && s.rows.length ? `${s.key} (${s.rows.length})` : s.key), "frontmatter"].join(", ");
|
|
1070
1396
|
var titleOf = (p, path) => p.preamble.split("\n").find((l) => l.startsWith("# "))?.slice(2).trim() || path;
|
|
1071
|
-
function
|
|
1397
|
+
function clip2(s, n, mark = "\u2026") {
|
|
1072
1398
|
if (s.length <= n) return s;
|
|
1073
1399
|
const end = /[\uD800-\uDBFF]/.test(s[n - 2]) ? n - 2 : n - 1;
|
|
1074
1400
|
return `${s.slice(0, end)}${mark}`;
|
|
@@ -1131,7 +1457,7 @@ function compactView(md, path, budget) {
|
|
|
1131
1457
|
const more = `\u2026 (section: "${s.key}")`;
|
|
1132
1458
|
if (!s.rows.length) {
|
|
1133
1459
|
const text2 = s.head.filter((l) => !blank(l)).join("\n");
|
|
1134
|
-
if (text2) out.push(
|
|
1460
|
+
if (text2) out.push(clip2(text2, PROSE_CLIP, more));
|
|
1135
1461
|
} else {
|
|
1136
1462
|
const lead = s.head.find((l) => !blank(l) && !l.startsWith("|") && !l.startsWith("### "));
|
|
1137
1463
|
if (lead && lead.length <= HEAD_LINE_MAX) out.push(lead);
|
|
@@ -1144,12 +1470,12 @@ function compactView(md, path, budget) {
|
|
|
1144
1470
|
const t = r.table ? r.table.join("\n") : null;
|
|
1145
1471
|
if (r.table && t !== table2) out.push(...r.table);
|
|
1146
1472
|
table2 = t;
|
|
1147
|
-
out.push(
|
|
1473
|
+
out.push(clip2(r.lines[0], ROW_CLIP));
|
|
1148
1474
|
}
|
|
1149
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})`);
|
|
1150
1476
|
else {
|
|
1151
1477
|
const text2 = s.tail.filter((l) => !blank(l)).join("\n");
|
|
1152
|
-
if (text2) out.push(
|
|
1478
|
+
if (text2) out.push(clip2(text2, PROSE_CLIP, more));
|
|
1153
1479
|
}
|
|
1154
1480
|
}
|
|
1155
1481
|
out.push("");
|
|
@@ -1243,7 +1569,7 @@ ${pageView(md, clean, {}, budget, site)}`;
|
|
|
1243
1569
|
}
|
|
1244
1570
|
|
|
1245
1571
|
// src/server.ts
|
|
1246
|
-
var VERSION = "0.
|
|
1572
|
+
var VERSION = "0.7.0";
|
|
1247
1573
|
var SITE = (process.env.BC_OBSERVATORY_SITE ?? "https://waldo1001.github.io/waldo.BCObservatory/").replace(/\/?$/, "/");
|
|
1248
1574
|
var LOCAL = process.env.BC_OBSERVATORY_LOCAL ? resolve(process.env.BC_OBSERVATORY_LOCAL) : null;
|
|
1249
1575
|
var BUDGET = budgetOf(process.env.BC_OBSERVATORY_MAX_CHARS);
|
|
@@ -1272,6 +1598,7 @@ async function pageMarkdown(path) {
|
|
|
1272
1598
|
var pages = [];
|
|
1273
1599
|
var index = null;
|
|
1274
1600
|
var loadedAt = 0;
|
|
1601
|
+
var symbolsMajor = null;
|
|
1275
1602
|
async function shard(file, sha) {
|
|
1276
1603
|
const cached = join(cacheDir, `${sha}.json`);
|
|
1277
1604
|
if (!LOCAL && existsSync(cached)) return readFileSync(cached, "utf8");
|
|
@@ -1287,12 +1614,19 @@ async function loadIndex() {
|
|
|
1287
1614
|
for (const s of manifest.shards) all.push(...JSON.parse(await shard(s.file, s.sha256)));
|
|
1288
1615
|
pages = all;
|
|
1289
1616
|
const idx = prepare(all.map(pageToRecord));
|
|
1617
|
+
if (manifest.learn) {
|
|
1618
|
+
try {
|
|
1619
|
+
prepare(learnRecords(JSON.parse(await shard(manifest.learn.file, manifest.learn.sha256))).map(pageToRecord), idx);
|
|
1620
|
+
} catch {
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1290
1623
|
let sm = null;
|
|
1291
1624
|
try {
|
|
1292
1625
|
sm = JSON.parse(await dataFile("index/symbols-manifest.json"));
|
|
1293
1626
|
} catch {
|
|
1294
1627
|
sm = null;
|
|
1295
1628
|
}
|
|
1629
|
+
symbolsMajor = sm?.major ?? null;
|
|
1296
1630
|
if (sm) {
|
|
1297
1631
|
const byPath = new Map(idx.records.map((r) => [r.id, r]));
|
|
1298
1632
|
for (const [k, e] of Object.entries(sm.kinds)) {
|
|
@@ -1305,7 +1639,37 @@ async function loadIndex() {
|
|
|
1305
1639
|
}
|
|
1306
1640
|
index = idx;
|
|
1307
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;
|
|
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;
|
|
1308
1671
|
}
|
|
1672
|
+
var replacedBy = async (major, cc, key) => (await replacedObjects(major, cc))?.get(key) ?? null;
|
|
1309
1673
|
var url = (path) => `${SITE}${path}/`;
|
|
1310
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 ? `
|
|
1311
1675
|
in: ${r.path_label}` : ""}${r.stats ? `
|
|
@@ -1314,7 +1678,10 @@ var line = (r, tag = "") => `- ${r.title}${r.caption ? ` (captioned "${r.caption
|
|
|
1314
1678
|
var text = (t) => ({ content: [{ type: "text", text: t }] });
|
|
1315
1679
|
var symbolHit = (r, tag = "") => `- ${r.name} (${symbolLine(r).replace(/ · /g, ", ")}) path=${r.id}${tag}${r.text ? `
|
|
1316
1680
|
${r.text}` : ""}`;
|
|
1317
|
-
var
|
|
1681
|
+
var learnLine = (r, tag = "") => `- ${r.title} [learn, official${r.date ? `, ${r.date}` : ""}] url=${r.path}${tag}${r.path_label ? `
|
|
1682
|
+
in: ${r.path_label}${r.hub ? ` (hub: ${r.hub})` : ""}` : ""}
|
|
1683
|
+
${r.summary}`;
|
|
1684
|
+
var hitLine = (r, tag = "") => r.kind === "page" && r.page ? r.type === "learn" ? learnLine(r.page, tag) : line(r.page, tag) : symbolHit(r, tag);
|
|
1318
1685
|
var MODEL = {
|
|
1319
1686
|
repo: "minishlab/potion-base-8M",
|
|
1320
1687
|
revision: "bf8b056651a2c21b8d2565580b8569da283cab23",
|
|
@@ -1366,7 +1733,7 @@ function fuse(lists, k = 60) {
|
|
|
1366
1733
|
}
|
|
1367
1734
|
function keepOf(a) {
|
|
1368
1735
|
const lc = (x) => x?.trim().toLowerCase() || void 0;
|
|
1369
|
-
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);
|
|
1370
1737
|
const system = a.system ? systemOf(a.system) ?? lc(a.system) : void 0;
|
|
1371
1738
|
const objectType = a.object_type ? typeOfWord(a.object_type.replace(/\s+/g, "")) ?? lc(a.object_type) : void 0;
|
|
1372
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);
|
|
@@ -1377,9 +1744,11 @@ async function toolSearch(a) {
|
|
|
1377
1744
|
const limit = a.limit ?? 10, mode = a.mode ?? "hybrid", POOL = 50;
|
|
1378
1745
|
const q = parseQuery(a.query, { countries: countriesOf(index) });
|
|
1379
1746
|
const hits = search(index, q, { filter: keep2 });
|
|
1380
|
-
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]));
|
|
1381
1750
|
let semantic = [], note = "";
|
|
1382
|
-
if (mode !== "keyword") {
|
|
1751
|
+
if (mode !== "keyword" && !kind) {
|
|
1383
1752
|
const m = await loadModel();
|
|
1384
1753
|
if (m) {
|
|
1385
1754
|
const v = embed(m, a.query), data = pageVectors(m);
|
|
@@ -1396,26 +1765,36 @@ async function toolSearch(a) {
|
|
|
1396
1765
|
}
|
|
1397
1766
|
} else if (mode === "semantic") note = ` (semantic search unavailable: ${modelError}; keyword results instead)`;
|
|
1398
1767
|
}
|
|
1399
|
-
const keyword = hits.map((h) => h.r
|
|
1768
|
+
const keyword = hits.map((h) => uid(h.r));
|
|
1400
1769
|
let order, exactBand = 0;
|
|
1401
1770
|
if (mode === "semantic") order = semantic.length ? semantic : keyword.slice(0, POOL);
|
|
1402
1771
|
else if (mode === "keyword" || !semantic.length) order = keyword;
|
|
1403
1772
|
else {
|
|
1404
|
-
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);
|
|
1405
1774
|
exactBand = exact.length;
|
|
1406
|
-
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))])];
|
|
1407
1776
|
}
|
|
1408
1777
|
if (mode !== "semantic" && mode !== "keyword" && !semantic.length) exactBand = hits.filter((h) => h.band === "exact").length;
|
|
1409
1778
|
const inKeyword = new Set(keyword.slice(0, Math.max(POOL, limit))), inMeaning = new Set(semantic);
|
|
1410
1779
|
const tag = (id) => inKeyword.has(id) && inMeaning.has(id) ? " [both]" : inMeaning.has(id) ? " [meaning]" : " [keyword]";
|
|
1411
1780
|
const shown = order.slice(0, limit).map((id) => byId.get(id)).filter(Boolean);
|
|
1412
|
-
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));
|
|
1413
1784
|
const hintText = hs.length ? `
|
|
1414
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
|
+
}
|
|
1415
1791
|
if (!shown.length) return `No pages match "${a.query}". Try fewer words, or ls("") to browse sections.${note}${hintText}`;
|
|
1416
1792
|
const how = semantic.length && keyword.length && mode === "hybrid" ? "keyword + meaning" : mode === "semantic" && semantic.length ? "meaning" : "keyword";
|
|
1417
|
-
|
|
1418
|
-
${shown.
|
|
1793
|
+
const learnNote = shown.some((r) => r.type === "learn") ? "; a Learn article's url opens on learn.microsoft.com, cat(hub) reads its hub" : "";
|
|
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}`;
|
|
1419
1798
|
}
|
|
1420
1799
|
async function toolLs(a) {
|
|
1421
1800
|
await loadIndex();
|
|
@@ -1437,7 +1816,27 @@ async function toolLs(a) {
|
|
|
1437
1816
|
async function toolCat(a) {
|
|
1438
1817
|
const [base, anchor] = a.path.split(/(?=#)/);
|
|
1439
1818
|
const clean = `${base.replace(/^\/+|\/+$/g, "").replace(/\.md$/, "")}${anchor ?? ""}`;
|
|
1440
|
-
|
|
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;
|
|
1441
1840
|
}
|
|
1442
1841
|
async function toolGetObject(a) {
|
|
1443
1842
|
await loadIndex();
|
|
@@ -1472,12 +1871,24 @@ async function toolGetObject(a) {
|
|
|
1472
1871
|
const pick = country ? inCountry.length ? inCountry : w1 : w1.length ? w1 : found;
|
|
1473
1872
|
if (pick.length === 1) {
|
|
1474
1873
|
const twins = found.filter((r) => r !== pick[0] && r.country).map((r) => r.country.toUpperCase());
|
|
1475
|
-
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 })}
|
|
1476
1879
|
|
|
1477
|
-
` :
|
|
1478
|
-
|
|
1880
|
+
` : `(${country.toUpperCase()} has no ${type} of its own here; the W1 ${type} applies.)
|
|
1881
|
+
|
|
1882
|
+
`;
|
|
1883
|
+
}
|
|
1884
|
+
let also = !country && twins.length ? `
|
|
1479
1885
|
|
|
1480
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
|
+
}
|
|
1481
1892
|
return `${note}${pageView(await pageMarkdown(pick[0].id), pick[0].id, a, BUDGET, SITE)}${a.section ? "" : also}`;
|
|
1482
1893
|
}
|
|
1483
1894
|
if (pick.length > 1) return `${pick.length} ${type} objects match "${a.idOrName}"; read one with cat(path):
|
|
@@ -1535,13 +1946,13 @@ ${a.message}
|
|
|
1535
1946
|
var VIEW = { section: z.string().optional(), offset: z.number().int().min(0).max(1e5).optional(), limit: z.number().int().min(1).max(5e3).optional() };
|
|
1536
1947
|
function createServer() {
|
|
1537
1948
|
const server = new McpServer({ name: "bc-observatory", version: VERSION }, {
|
|
1538
|
-
instructions: "BC Observatory: an agent-first knowledge base of Microsoft Dynamics 365 Business Central (Learn hubs, AL objects from the code for BC28-30, localizations, roadmap features, videos, community posts, code changes: merged pull requests of Microsoft's Business Central repositories, BCApps joined to the AL objects they changed, under changes/, and AL Language extension releases (the marketplace changelog, under releases/)). Every page carries a trust tier (official = Microsoft, community = everyone else) and a review state: say which tier a claim comes from. Never invent AL object ids or version numbers: look them up with get_object. Start with search(), read pages with cat(path), browse with ls(path)."
|
|
1949
|
+
instructions: "BC Observatory: an agent-first knowledge base of Microsoft Dynamics 365 Business Central (Learn hubs, AL objects from the code for BC28-30, localizations, roadmap features, videos, community posts, code changes: merged pull requests of Microsoft's Business Central repositories, BCApps joined to the AL objects they changed, under changes/, and AL Language extension releases (the marketplace changelog, under releases/)). Every page carries a trust tier (official = Microsoft, community = everyone else) and a review state: say which tier a claim comes from. Never invent AL object ids or version numbers: look them up with get_object. Start with search(), read pages with cat(path), browse with ls(path). search() also finds Microsoft Learn articles by title, summary and the identifiers they document (type learn): those are not pages here, open their url on learn.microsoft.com."
|
|
1539
1950
|
});
|
|
1540
1951
|
server.registerTool(
|
|
1541
1952
|
"search",
|
|
1542
1953
|
{
|
|
1543
1954
|
title: "Search the knowledge base",
|
|
1544
|
-
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).",
|
|
1545
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() }
|
|
1546
1957
|
},
|
|
1547
1958
|
async (a) => text(await toolSearch(a))
|
|
@@ -1558,7 +1969,7 @@ function createServer() {
|
|
|
1558
1969
|
);
|
|
1559
1970
|
server.registerTool("get_object", {
|
|
1560
1971
|
title: "Look up an AL object",
|
|
1561
|
-
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.`,
|
|
1562
1973
|
inputSchema: { type: z.string(), idOrName: z.string(), country: z.string().length(2).optional(), ...VIEW }
|
|
1563
1974
|
}, async (a) => text(await toolGetObject(a)));
|
|
1564
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": {
|