bc-observatory 0.5.0 → 0.7.1
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 +3 -3
- package/dist/server.js +454 -72
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ Microsoft 365 roadmap features, videos and community posts), every claim with a
|
|
|
8
8
|
claude mcp add bc-observatory -- npx -y bc-observatory@latest
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
Or the Claude Code plugin (MCP server plus skills): `claude plugin marketplace add waldo1001/waldo.BCObservatory`.
|
|
11
|
+
Or the Claude Code plugin (MCP server plus skills): `claude plugin marketplace add waldo1001/waldo.BCObservatory`. The server never fetches Learn pages (D01); the plugin also connects Microsoft's Learn MCP (`https://learn.microsoft.com/api/mcp`), and a Learn hit's url is read there with `microsoft_docs_fetch`.
|
|
12
12
|
|
|
13
13
|
Tools: `search`, `ls`, `cat`, `get_object`, `diff_object`, `localization`, `whats_new`, `blog_footprint`, `feedback`.
|
|
14
14
|
|
|
@@ -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,
|
|
@@ -387,6 +463,7 @@ var push = (m, k, v) => {
|
|
|
387
463
|
if (l) l.push(v);
|
|
388
464
|
else m.set(k, [v]);
|
|
389
465
|
};
|
|
466
|
+
var INDEX_PAGE_IDS = 16;
|
|
390
467
|
function normOf(r) {
|
|
391
468
|
const cached = NORM.get(r);
|
|
392
469
|
if (cached) return cached;
|
|
@@ -395,12 +472,21 @@ function normOf(r) {
|
|
|
395
472
|
const own = r.type === "topic" ? new Set(tokenize(r.title)) : null;
|
|
396
473
|
const learn = r.type === "learn";
|
|
397
474
|
const tags = (r.tags ?? []).flatMap((t) => indexTokens(t, learn)).filter((t) => !own?.has(t));
|
|
398
|
-
const
|
|
475
|
+
const coded = learn ? [...new Set((r.tags ?? []).filter(isCodedId))] : [];
|
|
476
|
+
const eqs = r.type === "object" || symbol ? [tokenize(r.name), ...r.caption ? [tokenize(r.caption)] : []] : [tokenize(r.title), ...learn ? [...(r.title.match(/\b[A-Z]{2,3}\d{4}\b/g) ?? []).filter(isCodedId), ...coded.length <= INDEX_PAGE_IDS ? coded : []].map((t) => tokenize(t)).filter((t) => t.length === 1) : []];
|
|
399
477
|
const n = { main: [...main], tags: learn ? tags : [...new Set(tags)], text: symbol ? null : indexTokens(r.text ?? ""), eqs };
|
|
400
478
|
NORM.set(r, n);
|
|
401
479
|
return n;
|
|
402
480
|
}
|
|
403
481
|
var textOf = (r, n) => n.text ??= indexTokens(r.text ?? "");
|
|
482
|
+
function seqs(r) {
|
|
483
|
+
let main, text2;
|
|
484
|
+
const symbol = r.kind !== "page";
|
|
485
|
+
return {
|
|
486
|
+
main: () => main ??= [seqTokens(r.name, symbol), seqTokens(r.title, symbol), ...r.caption ? [seqTokens(r.caption)] : []].flatMap((f, k) => k ? ["", ...f] : f),
|
|
487
|
+
text: () => text2 ??= seqTokens(r.text ?? "")
|
|
488
|
+
};
|
|
489
|
+
}
|
|
404
490
|
function prepare(records, prev) {
|
|
405
491
|
const index2 = prev ?? { records: [], postings: /* @__PURE__ */ new Map(), vocab: /* @__PURE__ */ new Map(), byType: /* @__PURE__ */ new Map(), maxInbound: /* @__PURE__ */ new Map() };
|
|
406
492
|
let st = STATE.get(index2);
|
|
@@ -522,7 +608,7 @@ function matchersOf(index2, q) {
|
|
|
522
608
|
}
|
|
523
609
|
return m;
|
|
524
610
|
}
|
|
525
|
-
function best(m, toks) {
|
|
611
|
+
function best(m, toks, seq = () => toks) {
|
|
526
612
|
let b = null;
|
|
527
613
|
for (const t of toks) {
|
|
528
614
|
const f = m.words.get(t);
|
|
@@ -530,7 +616,8 @@ function best(m, toks) {
|
|
|
530
616
|
}
|
|
531
617
|
for (const p of m.phrases) {
|
|
532
618
|
if (b && b.f >= p.f) continue;
|
|
533
|
-
|
|
619
|
+
if (!p.words.every((w) => holds(toks, w))) continue;
|
|
620
|
+
const f = phraseForm(p.words, seq());
|
|
534
621
|
if (f) {
|
|
535
622
|
const v = p.f * f;
|
|
536
623
|
if (!b || v > b.f) b = { f: v, how: p.how, ...p.via ? { via: p.via } : {} };
|
|
@@ -538,6 +625,21 @@ function best(m, toks) {
|
|
|
538
625
|
}
|
|
539
626
|
return b;
|
|
540
627
|
}
|
|
628
|
+
var STEMS = /* @__PURE__ */ new Map();
|
|
629
|
+
var stemOf = (t) => {
|
|
630
|
+
let s = STEMS.get(t);
|
|
631
|
+
if (s === void 0) {
|
|
632
|
+
s = stem(t);
|
|
633
|
+
STEMS.set(t, s);
|
|
634
|
+
}
|
|
635
|
+
return s;
|
|
636
|
+
};
|
|
637
|
+
function holds(toks, w) {
|
|
638
|
+
if (toks.includes(w)) return true;
|
|
639
|
+
const sw = stemOf(w);
|
|
640
|
+
for (const t of toks) if (t[0] === w[0] && stemOf(t) === sw) return true;
|
|
641
|
+
return false;
|
|
642
|
+
}
|
|
541
643
|
function phraseForm(words, toks) {
|
|
542
644
|
let bestF = 0;
|
|
543
645
|
for (let i = 0; i + words.length <= toks.length; i++) {
|
|
@@ -551,7 +653,7 @@ function phraseForm(words, toks) {
|
|
|
551
653
|
}
|
|
552
654
|
return bestF;
|
|
553
655
|
}
|
|
554
|
-
function nameMatch(eq, ms,
|
|
656
|
+
function nameMatch(eq, ms, plural2) {
|
|
555
657
|
let pos = 0, f = 1, lastPrefix = false;
|
|
556
658
|
for (const m of ms) {
|
|
557
659
|
const ws = m.term.phrase ? m.term.text.split(" ") : null;
|
|
@@ -559,7 +661,7 @@ function nameMatch(eq, ms, plural) {
|
|
|
559
661
|
for (const w of ws) {
|
|
560
662
|
const t2 = eq[pos++];
|
|
561
663
|
if (t2 === void 0) return { eq: 0, starts: 0 };
|
|
562
|
-
f = Math.min(f, t2 === w ? 1 : stem(t2) === stem(w) ?
|
|
664
|
+
f = Math.min(f, t2 === w ? 1 : stem(t2) === stem(w) ? plural2 ? 1 : 0.75 : 0);
|
|
563
665
|
}
|
|
564
666
|
if (!f) return { eq: 0, starts: 0 };
|
|
565
667
|
lastPrefix = false;
|
|
@@ -576,10 +678,10 @@ function nameMatch(eq, ms, plural) {
|
|
|
576
678
|
lastPrefix = p.how === "prefix";
|
|
577
679
|
continue;
|
|
578
680
|
}
|
|
579
|
-
f = Math.min(f, form.how === "stem" &&
|
|
681
|
+
f = Math.min(f, form.how === "stem" && plural2 ? 1 : form.f);
|
|
580
682
|
lastPrefix = form.how === "prefix";
|
|
581
683
|
}
|
|
582
|
-
if (pos === eq.length && !lastPrefix && (
|
|
684
|
+
if (pos === eq.length && !lastPrefix && (plural2 || f === 1)) return { eq: f, starts: 0 };
|
|
583
685
|
return { eq: 0, starts: pos <= eq.length ? f : 0 };
|
|
584
686
|
}
|
|
585
687
|
var NARRATIVE = { reviewed: 2, unreviewed: 0, none: -2, derived: -2 };
|
|
@@ -600,7 +702,7 @@ function scoreRecord(index2, r, q) {
|
|
|
600
702
|
if (q.ref.country) return { s: country === q.ref.country ? 1e3 : country === null ? 900 : 850, why };
|
|
601
703
|
return { s: country === null ? 1e3 : 900, why };
|
|
602
704
|
}
|
|
603
|
-
const prior =
|
|
705
|
+
const prior = objectish ? TYPE_PRIOR[r.objectType ?? ""] ?? 0 : 0;
|
|
604
706
|
const lift = (base) => base + (object ? LAYER[r.layer] : symbol ? 0 : 1) + (objectish ? 2 * r.importance : 0) + prior;
|
|
605
707
|
if (q.number !== null) {
|
|
606
708
|
if (!object || r.objectId !== q.number) return none;
|
|
@@ -616,10 +718,10 @@ function scoreRecord(index2, r, q) {
|
|
|
616
718
|
if (object && country && q.countries.includes(country)) return { s: 3 + 2 * r.importance + prior, why };
|
|
617
719
|
return none;
|
|
618
720
|
}
|
|
619
|
-
const n = normOf(r), ms = matchersOf(index2, q);
|
|
620
|
-
let s = 0, named = false, gate = !symbol || !!q.kind;
|
|
721
|
+
const n = normOf(r), ms = matchersOf(index2, q), sq = seqs(r);
|
|
722
|
+
let s = 0, textPts = 0, named = false, gate = !symbol || !!q.kind;
|
|
621
723
|
for (const m of ms) {
|
|
622
|
-
const main = best(m, n.main), tags = best(m, n.tags), text2 = best(m, textOf(r, n));
|
|
724
|
+
const main = best(m, n.main, sq.main), tags = best(m, n.tags), text2 = best(m, textOf(r, n), sq.text);
|
|
623
725
|
if (!main && !tags && !text2) return none;
|
|
624
726
|
if (main || tags) named = true;
|
|
625
727
|
if (symbol && !gate && n.eqs[0].some((t) => {
|
|
@@ -628,21 +730,25 @@ function scoreRecord(index2, r, q) {
|
|
|
628
730
|
})) gate = true;
|
|
629
731
|
if (symbol && /^on[a-z]/.test(m.term.text)) gate = true;
|
|
630
732
|
s += 3 * (main?.f ?? 0) + 2 * (tags?.f ?? 0) + (text2?.f ?? 0);
|
|
733
|
+
textPts += text2?.f ?? 0;
|
|
631
734
|
const via = [main, tags, text2].find((x) => x?.via && (x.how === "syn" || x.how === "fuzzy"))?.via;
|
|
632
735
|
if (via && !why.includes(`matched "${via}"`)) why.push(`matched "${via}"`);
|
|
633
736
|
}
|
|
634
737
|
if (!gate) return none;
|
|
738
|
+
const learn = r.type === "learn";
|
|
635
739
|
let eq = 0, starts = 0, byName = false;
|
|
636
740
|
n.eqs.forEach((e, k) => {
|
|
637
741
|
const x = nameMatch(e, ms, objectish);
|
|
638
742
|
if (x.eq > eq) {
|
|
639
743
|
eq = x.eq;
|
|
640
|
-
byName = k === 0;
|
|
744
|
+
byName = k === 0 || learn;
|
|
641
745
|
}
|
|
642
|
-
starts = Math.max(starts, x.starts);
|
|
746
|
+
if (!learn || k === 0) starts = Math.max(starts, x.starts);
|
|
643
747
|
});
|
|
748
|
+
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 ")));
|
|
749
|
+
const owned = symbol && !!lifted;
|
|
644
750
|
if (eq) {
|
|
645
|
-
s += 10 * eq;
|
|
751
|
+
s += 10 * eq - (owned ? textPts : 0);
|
|
646
752
|
why.push(!byName ? "caption" : eq >= 0.75 ? "name" : "name~");
|
|
647
753
|
} else if (starts) {
|
|
648
754
|
s += 3 * starts;
|
|
@@ -652,8 +758,7 @@ function scoreRecord(index2, r, q) {
|
|
|
652
758
|
if ((r.type === "topic" || r.type === "app") && !symbol) s += NARRATIVE[r.narrative ?? (r.type === "app" ? "none" : "unreviewed")] ?? 0;
|
|
653
759
|
s += countryPoints(q, r, country);
|
|
654
760
|
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 ")));
|
|
761
|
+
s = symbol ? s + 2 * r.importance + (owned ? LAYER[r.layer] + prior : 0) : lift(s);
|
|
657
762
|
if (objectish && !lifted) {
|
|
658
763
|
s *= 0.6;
|
|
659
764
|
why.push("demoted");
|
|
@@ -695,11 +800,11 @@ function search(index2, q, opts = {}) {
|
|
|
695
800
|
return opts.limit ? hits.slice(0, opts.limit) : hits;
|
|
696
801
|
}
|
|
697
802
|
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}${
|
|
803
|
+
const of2 = `${r.owner ? ` of ${r.owner.title}` : ""}${r.country && r.app === `${r.country.toUpperCase()} layer` ? ` \xB7 ${r.app}` : ""}`, x = r.extra ?? {};
|
|
804
|
+
if (r.kind === "field") return `field ${x.id}${of2} \xB7 ${x.type}`;
|
|
805
|
+
if (r.kind === "event") return `event${of2} \xB7 ${x.kind} \xB7 ${x.subscribers} ${x.subscribers === 1 ? "subscriber" : "subscribers"}${x.obsolete ? ` \xB7 obsolete ${x.obsolete}` : ""}`;
|
|
806
|
+
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}` : ""}`;
|
|
807
|
+
if (r.kind === "value") return `value ${x.ordinal}${of2}${x.caption && x.caption !== r.name ? ` \xB7 "${x.caption}"` : ""}`;
|
|
703
808
|
return r.title;
|
|
704
809
|
}
|
|
705
810
|
function objectsAt(index2, type, id) {
|
|
@@ -720,7 +825,8 @@ function typoWords(index2, q) {
|
|
|
720
825
|
function namedBy(index2, r, q) {
|
|
721
826
|
if (!STATE.get(index2)) return 0;
|
|
722
827
|
const n = normOf(r);
|
|
723
|
-
|
|
828
|
+
const sq = seqs(r);
|
|
829
|
+
return matchersOf(index2, q).reduce((a, m) => a + (best(m, n.main, sq.main)?.f ?? best(m, n.tags)?.f ?? 0), 0);
|
|
724
830
|
}
|
|
725
831
|
function symbolRecords(kind, rows, ownerOf) {
|
|
726
832
|
const out = [];
|
|
@@ -736,7 +842,7 @@ function countriesOf(index2) {
|
|
|
736
842
|
}
|
|
737
843
|
|
|
738
844
|
// ../search/src/hints.ts
|
|
739
|
-
function hints(index2, q, hits) {
|
|
845
|
+
function hints(index2, q, hits, opts = {}) {
|
|
740
846
|
const out = [];
|
|
741
847
|
const unknown = q.warnings.map((w) => /^unknown abbreviation (\w+)$/.exec(w)?.[1]).find(Boolean);
|
|
742
848
|
const digits = /(\d+)\s*$/.exec(q.raw)?.[1];
|
|
@@ -768,21 +874,89 @@ function hints(index2, q, hits) {
|
|
|
768
874
|
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
875
|
}
|
|
770
876
|
if (q.question) {
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
const
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
best2
|
|
778
|
-
|
|
877
|
+
const hubOf = () => {
|
|
878
|
+
let hub = null, best2 = 0;
|
|
879
|
+
for (const i of index2.byType.get("topic") ?? []) {
|
|
880
|
+
const r = index2.records[i], named = namedBy(index2, r, q);
|
|
881
|
+
if (!named) continue;
|
|
882
|
+
const s = 3 * named + Math.log2((r.members ?? 0) + 1) + (r.narrative === "reviewed" ? 2 : r.narrative === "none" ? -2 : 0);
|
|
883
|
+
if (s > best2) {
|
|
884
|
+
best2 = s;
|
|
885
|
+
hub = r;
|
|
886
|
+
}
|
|
779
887
|
}
|
|
888
|
+
return hub;
|
|
889
|
+
};
|
|
890
|
+
if (q.shape === "what-changed" || !q.shape) {
|
|
891
|
+
const hub = hubOf();
|
|
892
|
+
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 } : {} });
|
|
893
|
+
} else {
|
|
894
|
+
const hub = learnHub(index2, hits) ?? hubOf();
|
|
895
|
+
if (hub) out.push({ text: `For ${q.shape === "how-to" ? "how-to" : "a concept"}, start with the hub: ${hub.title}.`, path: hub.id });
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
const kind = q.kind ?? opts.kind ?? null;
|
|
899
|
+
if (kind && !hits.length) {
|
|
900
|
+
const name = nameOf2(q);
|
|
901
|
+
if (opts.unfiltered?.length) {
|
|
902
|
+
const n = opts.unfiltered.length;
|
|
903
|
+
out.unshift({ text: `No ${KIND_WORD[kind]} named "${name}" matches the filters${opts.filters ? ` (${opts.filters})` : ""}; without them: ${n} ${n === 1 ? "hit" : "hits"}.` });
|
|
904
|
+
} else {
|
|
905
|
+
const h = nearestSymbols(index2, kind, name, opts.keep);
|
|
906
|
+
if (h) out.unshift(h);
|
|
780
907
|
}
|
|
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
908
|
}
|
|
783
|
-
void hits;
|
|
784
909
|
return out;
|
|
785
910
|
}
|
|
911
|
+
function learnHub(index2, hits) {
|
|
912
|
+
for (const h of hits.slice(0, 10)) {
|
|
913
|
+
const path = h.r.type === "learn" ? h.r.page?.hub : void 0;
|
|
914
|
+
if (!path) continue;
|
|
915
|
+
const hub = (index2.byType.get("topic") ?? []).map((i) => index2.records[i]).find((r) => r.id === path);
|
|
916
|
+
if (hub) return hub;
|
|
917
|
+
}
|
|
918
|
+
return null;
|
|
919
|
+
}
|
|
920
|
+
var KIND_WORD = { field: "field", event: "event", proc: "procedure", value: "value" };
|
|
921
|
+
function nameOf2(q) {
|
|
922
|
+
const typed = q.terms.map((t) => t.raw || t.text).join(" ").replace(/"/g, "").trim();
|
|
923
|
+
return typed || (q.kind ? q.raw.replace(/^\s*\w+\s*:\s*/, "").replace(/"/g, "").trim() : "");
|
|
924
|
+
}
|
|
925
|
+
function nearestSymbols(index2, kind, name, keep2) {
|
|
926
|
+
const all = index2.byType.get(kind) ?? [];
|
|
927
|
+
const want = name.toLowerCase();
|
|
928
|
+
if (!all.length || !want) return null;
|
|
929
|
+
const word = KIND_WORD[kind], head1 = `No ${word} named "${name}".`;
|
|
930
|
+
const near = [];
|
|
931
|
+
if (want.length >= 4) {
|
|
932
|
+
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)));
|
|
933
|
+
for (const i of all) {
|
|
934
|
+
const r = index2.records[i], n = r.name.toLowerCase();
|
|
935
|
+
if (n === want) return null;
|
|
936
|
+
if (keep2 && !keep2(r)) continue;
|
|
937
|
+
let ok = editDistance(want, n, reach) <= reach;
|
|
938
|
+
if (!ok && n.startsWith(head)) {
|
|
939
|
+
let k = 0;
|
|
940
|
+
while (k < n.length && k < want.length && n[k] === want[k]) k++;
|
|
941
|
+
ok = k >= shared;
|
|
942
|
+
}
|
|
943
|
+
if (ok) near.push({ r, d: editDistance(want, n, 60) });
|
|
944
|
+
}
|
|
945
|
+
} else if (all.some((i) => index2.records[i].name.toLowerCase() === want)) return null;
|
|
946
|
+
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));
|
|
947
|
+
const seen = /* @__PURE__ */ new Set(), pick = [];
|
|
948
|
+
for (const { r } of near) {
|
|
949
|
+
if (pick.length >= 3) break;
|
|
950
|
+
if (seen.has(r.id)) continue;
|
|
951
|
+
seen.add(r.id);
|
|
952
|
+
pick.push(r);
|
|
953
|
+
}
|
|
954
|
+
if (!pick.length) return { text: head1 };
|
|
955
|
+
const plural2 = word === "procedure" ? "procedures" : `${word}s`;
|
|
956
|
+
const label = (r) => r.owner ? `${r.name} (${r.owner.title})` : r.name;
|
|
957
|
+
const qk = kind === "proc" ? "proc" : kind;
|
|
958
|
+
return { text: `${head1} Nearest ${plural2}: ${pick.map(label).join(", ")}.`, query: `${qk}: ${pick[0].name}` };
|
|
959
|
+
}
|
|
786
960
|
|
|
787
961
|
// ../search/src/learn.ts
|
|
788
962
|
function learnRecords(doc) {
|
|
@@ -912,6 +1086,139 @@ function dot(a, b, bOffset = 0) {
|
|
|
912
1086
|
return s;
|
|
913
1087
|
}
|
|
914
1088
|
|
|
1089
|
+
// src/country.ts
|
|
1090
|
+
var BLOCK_MEMBERS = 40;
|
|
1091
|
+
var DELTA_CLIP = 140;
|
|
1092
|
+
var KINDS2 = ["fields", "values", "events", "procedures", "keys"];
|
|
1093
|
+
var NOUN = { fields: ["field", "fields"], values: ["value", "values"], events: ["event", "events"], procedures: ["procedure", "procedures"], keys: ["key", "keys"] };
|
|
1094
|
+
var plural = (n, kind) => `${n} ${NOUN[kind][n === 1 ? 0 : 1]}`;
|
|
1095
|
+
var of = (o, kind, change) => (o[kind] ?? []).filter((m) => m.change === change);
|
|
1096
|
+
var clip = (s, n = DELTA_CLIP) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
|
|
1097
|
+
function titleOfKey(key, name) {
|
|
1098
|
+
const [type, id] = key.split("/");
|
|
1099
|
+
return `${type.charAt(0).toUpperCase()}${type.slice(1)} ${id} "${name}"`;
|
|
1100
|
+
}
|
|
1101
|
+
function addedSummary(o) {
|
|
1102
|
+
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(", ");
|
|
1103
|
+
}
|
|
1104
|
+
function replacedInLine(layers) {
|
|
1105
|
+
if (!layers.length) return null;
|
|
1106
|
+
const parts2 = [...layers].sort((a, b) => a.cc.localeCompare(b.cc)).map(({ cc, object }) => {
|
|
1107
|
+
const s = addedSummary(object);
|
|
1108
|
+
return `${cc.toUpperCase()}${s ? ` (${s})` : ""}`;
|
|
1109
|
+
});
|
|
1110
|
+
return `Replaced in: ${parts2.join(", ")}.`;
|
|
1111
|
+
}
|
|
1112
|
+
var paramsOf = (ps) => (ps ?? []).map((p) => `${p.var ? "var " : ""}${p.name}: ${p.type}`).join("; ");
|
|
1113
|
+
var signature = (name, to) => `${name}(${paramsOf(to.params)})${to.returns ? `: ${to.returns}` : ""}`;
|
|
1114
|
+
var procLine = (m) => {
|
|
1115
|
+
const to = m.to ?? {};
|
|
1116
|
+
return `${signature(m.name, to)} (${to.event && to.event !== "subscriber" ? to.event : to.scope ?? "global"})${to.obsolete?.state ? ` [obsolete ${to.obsolete.state}]` : ""}`;
|
|
1117
|
+
};
|
|
1118
|
+
var fieldLine = (m) => `${m.id} "${m.name}" ${(m.to ?? {}).type ?? ""}`.trimEnd();
|
|
1119
|
+
function propertiesText(p) {
|
|
1120
|
+
return Object.entries(p ?? {}).map(([k, v]) => v === "true" ? k : /^(Caption|ToolTip)/.test(k) ? `${k} "${v}"` : `${k} ${v}`).join("; ");
|
|
1121
|
+
}
|
|
1122
|
+
function deltaText(d) {
|
|
1123
|
+
if (!d) return "";
|
|
1124
|
+
const show = (v) => typeof v === "string" ? v : JSON.stringify(v);
|
|
1125
|
+
const parts2 = [];
|
|
1126
|
+
for (const [k, v] of Object.entries(d)) {
|
|
1127
|
+
if (k === "properties") {
|
|
1128
|
+
for (const [pk, pv] of Object.entries(v)) parts2.push(`${pk}: ${pv[0] ?? "-"} -> ${pv[1] ?? "-"}`);
|
|
1129
|
+
continue;
|
|
1130
|
+
}
|
|
1131
|
+
const [a, b] = v;
|
|
1132
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
1133
|
+
const add = b.filter((x) => !a.includes(x)), rem = a.filter((x) => !b.includes(x));
|
|
1134
|
+
parts2.push(`${k} ${[...add.map((x) => `+${x}`), ...rem.map((x) => `-${x}`)].join(", ") || "reordered"}`);
|
|
1135
|
+
} else parts2.push(`${k}: ${show(a ?? "-")} -> ${show(b ?? "-")}`);
|
|
1136
|
+
}
|
|
1137
|
+
return clip(parts2.join("; "));
|
|
1138
|
+
}
|
|
1139
|
+
function keyLine(m) {
|
|
1140
|
+
if (m.change === "added") return `${m.name} (new: ${((m.to ?? {}).fields ?? []).join(", ")})`;
|
|
1141
|
+
if (m.change === "removed") return `${m.name} (removed)`;
|
|
1142
|
+
return `${m.name} (${deltaText(m.delta) || "changed"})`.replace(/\(fields /, "(");
|
|
1143
|
+
}
|
|
1144
|
+
function headline(o, cc, major, title) {
|
|
1145
|
+
const added = addedSummary(o);
|
|
1146
|
+
const other = [];
|
|
1147
|
+
for (const k of KINDS2) {
|
|
1148
|
+
const changed = of(o, k, "changed").length + (k === "keys" ? of(o, k, "added").length : 0), removed = of(o, k, "removed").length;
|
|
1149
|
+
if (changed) other.push(`${plural(changed, k)} changed`);
|
|
1150
|
+
if (removed) other.push(`${plural(removed, k)} removed`);
|
|
1151
|
+
}
|
|
1152
|
+
if (o.properties?.length) other.push(`${o.properties.length} ${o.properties.length === 1 ? "property" : "properties"} changed`);
|
|
1153
|
+
const tail = [added || "no member added", ...other.length ? [other.join(", ")] : []].join("; ");
|
|
1154
|
+
return `${cc.toUpperCase()} replaces ${title} (BC${major}): ${tail}.`;
|
|
1155
|
+
}
|
|
1156
|
+
function countryBlock(o, cc, major, opts = {}) {
|
|
1157
|
+
const C = cc.toUpperCase(), title = opts.title ?? titleOfKey(o.key, o.name);
|
|
1158
|
+
const groups = [
|
|
1159
|
+
["Fields added", of(o, "fields", "added").map(fieldLine)],
|
|
1160
|
+
["Values added", of(o, "values", "added").map((m) => `${m.id} ${m.name}`)],
|
|
1161
|
+
["Events added", of(o, "events", "added").map(procLine)],
|
|
1162
|
+
["Procedures added", of(o, "procedures", "added").map(procLine)],
|
|
1163
|
+
["Keys changed", [...of(o, "keys", "changed"), ...of(o, "keys", "added"), ...of(o, "keys", "removed")].map(keyLine)],
|
|
1164
|
+
["Fields changed", of(o, "fields", "changed").map((m) => `${m.id} "${m.name}"${m.delta ? ` (${deltaText(m.delta)})` : ""}`)],
|
|
1165
|
+
["Events changed", of(o, "events", "changed").map((m) => `${m.name}${m.delta ? ` (${deltaText(m.delta)})` : ""}`)],
|
|
1166
|
+
["Procedures changed", of(o, "procedures", "changed").map((m) => `${m.name}${m.delta ? ` (${deltaText(m.delta)})` : ""}`)]
|
|
1167
|
+
];
|
|
1168
|
+
const out = [headline(o, cc, major, title)];
|
|
1169
|
+
let room = BLOCK_MEMBERS, more = 0;
|
|
1170
|
+
for (const [label, items] of groups) {
|
|
1171
|
+
if (!items.length) continue;
|
|
1172
|
+
const shown = items.slice(0, Math.max(0, room));
|
|
1173
|
+
room -= shown.length;
|
|
1174
|
+
more += items.length - shown.length;
|
|
1175
|
+
if (shown.length) out.push(`${label}: ${shown.join("; ")}`);
|
|
1176
|
+
}
|
|
1177
|
+
if (more) out.push(`... and ${more} more: localization("${C}", section: "w1_objects_this_country_changes")`);
|
|
1178
|
+
out.push(`Source: data/code/diffs/country/${major}-${cc.toLowerCase()}.json; the ${C} page: localizations/${cc.toLowerCase()}.`);
|
|
1179
|
+
return out.join("\n");
|
|
1180
|
+
}
|
|
1181
|
+
function parseAnchor(anchor) {
|
|
1182
|
+
const m = /^#?(field|value|event|proc)-(.+)$/.exec(anchor);
|
|
1183
|
+
return m ? { kind: m[1], key: m[2] } : null;
|
|
1184
|
+
}
|
|
1185
|
+
function memberAnswer(o, a, cc, opts) {
|
|
1186
|
+
const C = cc.toUpperCase(), title = opts.title ?? titleOfKey(o.key, o.name);
|
|
1187
|
+
const head = (what) => `${title}, ${what} (added by the ${C} layer, not in W1):`;
|
|
1188
|
+
const foot = `W1 page: ${opts.site}${opts.path}/ \xB7 ${C} layer: localizations/${cc.toLowerCase()}`;
|
|
1189
|
+
if (a.kind === "field") {
|
|
1190
|
+
const m = of(o, "fields", "added").find((x) => x.id === a.key);
|
|
1191
|
+
if (!m) return null;
|
|
1192
|
+
const to = m.to ?? {};
|
|
1193
|
+
return `${head(`field ${m.id}`)}
|
|
1194
|
+
| No. | Name | Type | Properties | Layer |
|
|
1195
|
+
|---|---|---|---|---|
|
|
1196
|
+
| ${m.id} | ${m.name} | ${to.type ?? ""} | ${propertiesText(to.properties)} | ${C} |
|
|
1197
|
+
|
|
1198
|
+
${foot}
|
|
1199
|
+
`;
|
|
1200
|
+
}
|
|
1201
|
+
if (a.kind === "value") {
|
|
1202
|
+
const m = of(o, "values", "added").find((x) => x.id === a.key);
|
|
1203
|
+
if (!m) return null;
|
|
1204
|
+
return `${head(`value ${m.id}`)}
|
|
1205
|
+
| Ordinal | Name | Layer |
|
|
1206
|
+
|---|---|---|
|
|
1207
|
+
| ${m.id} | ${m.name} | ${C} |
|
|
1208
|
+
|
|
1209
|
+
${foot}
|
|
1210
|
+
`;
|
|
1211
|
+
}
|
|
1212
|
+
const list = a.kind === "event" ? of(o, "events", "added") : of(o, "procedures", "added");
|
|
1213
|
+
const hits = list.filter((x) => x.name === a.key);
|
|
1214
|
+
if (!hits.length) return null;
|
|
1215
|
+
return `${head(`${a.kind === "event" ? "event" : "procedure"} ${a.key}`)}
|
|
1216
|
+
${hits.map((m) => `- \`${signature(m.name, m.to ?? {})}\` (${(m.to ?? {}).event ?? (m.to ?? {}).scope ?? "global"})`).join("\n")}
|
|
1217
|
+
|
|
1218
|
+
${foot}
|
|
1219
|
+
`;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
915
1222
|
// src/sections.ts
|
|
916
1223
|
var DEFAULT_BUDGET = 3e4;
|
|
917
1224
|
var MIN_BUDGET = 5e3;
|
|
@@ -1089,7 +1396,7 @@ function findSection(p, want) {
|
|
|
1089
1396
|
}
|
|
1090
1397
|
var keyList = (p, counts = false) => [...p.sections.map((s) => counts && s.rows.length ? `${s.key} (${s.rows.length})` : s.key), "frontmatter"].join(", ");
|
|
1091
1398
|
var titleOf = (p, path) => p.preamble.split("\n").find((l) => l.startsWith("# "))?.slice(2).trim() || path;
|
|
1092
|
-
function
|
|
1399
|
+
function clip2(s, n, mark = "\u2026") {
|
|
1093
1400
|
if (s.length <= n) return s;
|
|
1094
1401
|
const end = /[\uD800-\uDBFF]/.test(s[n - 2]) ? n - 2 : n - 1;
|
|
1095
1402
|
return `${s.slice(0, end)}${mark}`;
|
|
@@ -1152,7 +1459,7 @@ function compactView(md, path, budget) {
|
|
|
1152
1459
|
const more = `\u2026 (section: "${s.key}")`;
|
|
1153
1460
|
if (!s.rows.length) {
|
|
1154
1461
|
const text2 = s.head.filter((l) => !blank(l)).join("\n");
|
|
1155
|
-
if (text2) out.push(
|
|
1462
|
+
if (text2) out.push(clip2(text2, PROSE_CLIP, more));
|
|
1156
1463
|
} else {
|
|
1157
1464
|
const lead = s.head.find((l) => !blank(l) && !l.startsWith("|") && !l.startsWith("### "));
|
|
1158
1465
|
if (lead && lead.length <= HEAD_LINE_MAX) out.push(lead);
|
|
@@ -1165,12 +1472,12 @@ function compactView(md, path, budget) {
|
|
|
1165
1472
|
const t = r.table ? r.table.join("\n") : null;
|
|
1166
1473
|
if (r.table && t !== table2) out.push(...r.table);
|
|
1167
1474
|
table2 = t;
|
|
1168
|
-
out.push(
|
|
1475
|
+
out.push(clip2(r.lines[0], ROW_CLIP));
|
|
1169
1476
|
}
|
|
1170
1477
|
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
1478
|
else {
|
|
1172
1479
|
const text2 = s.tail.filter((l) => !blank(l)).join("\n");
|
|
1173
|
-
if (text2) out.push(
|
|
1480
|
+
if (text2) out.push(clip2(text2, PROSE_CLIP, more));
|
|
1174
1481
|
}
|
|
1175
1482
|
}
|
|
1176
1483
|
out.push("");
|
|
@@ -1264,7 +1571,7 @@ ${pageView(md, clean, {}, budget, site)}`;
|
|
|
1264
1571
|
}
|
|
1265
1572
|
|
|
1266
1573
|
// src/server.ts
|
|
1267
|
-
var VERSION = "0.
|
|
1574
|
+
var VERSION = "0.7.1";
|
|
1268
1575
|
var SITE = (process.env.BC_OBSERVATORY_SITE ?? "https://waldo1001.github.io/waldo.BCObservatory/").replace(/\/?$/, "/");
|
|
1269
1576
|
var LOCAL = process.env.BC_OBSERVATORY_LOCAL ? resolve(process.env.BC_OBSERVATORY_LOCAL) : null;
|
|
1270
1577
|
var BUDGET = budgetOf(process.env.BC_OBSERVATORY_MAX_CHARS);
|
|
@@ -1293,6 +1600,7 @@ async function pageMarkdown(path) {
|
|
|
1293
1600
|
var pages = [];
|
|
1294
1601
|
var index = null;
|
|
1295
1602
|
var loadedAt = 0;
|
|
1603
|
+
var symbolsMajor = null;
|
|
1296
1604
|
async function shard(file, sha) {
|
|
1297
1605
|
const cached = join(cacheDir, `${sha}.json`);
|
|
1298
1606
|
if (!LOCAL && existsSync(cached)) return readFileSync(cached, "utf8");
|
|
@@ -1320,6 +1628,7 @@ async function loadIndex() {
|
|
|
1320
1628
|
} catch {
|
|
1321
1629
|
sm = null;
|
|
1322
1630
|
}
|
|
1631
|
+
symbolsMajor = sm?.major ?? null;
|
|
1323
1632
|
if (sm) {
|
|
1324
1633
|
const byPath = new Map(idx.records.map((r) => [r.id, r]));
|
|
1325
1634
|
for (const [k, e] of Object.entries(sm.kinds)) {
|
|
@@ -1332,7 +1641,37 @@ async function loadIndex() {
|
|
|
1332
1641
|
}
|
|
1333
1642
|
index = idx;
|
|
1334
1643
|
loadedAt = Date.now();
|
|
1644
|
+
countryDiffs.clear();
|
|
1645
|
+
}
|
|
1646
|
+
var countryDiffs = /* @__PURE__ */ new Map();
|
|
1647
|
+
function diffMajor() {
|
|
1648
|
+
if (symbolsMajor) return symbolsMajor;
|
|
1649
|
+
let best2 = 0;
|
|
1650
|
+
for (const r of index?.records ?? []) for (const part of (r.major ?? "").split(/[\s,]+/)) {
|
|
1651
|
+
const n = Number(part.split("-").pop());
|
|
1652
|
+
if (n > best2) best2 = n;
|
|
1653
|
+
}
|
|
1654
|
+
return best2 ? String(best2) : null;
|
|
1335
1655
|
}
|
|
1656
|
+
function replacedObjects(major, cc) {
|
|
1657
|
+
const key = `${major}-${cc}`, hit = countryDiffs.get(key);
|
|
1658
|
+
if (hit && Date.now() - hit.at < DAY) return Promise.resolve(hit.objects);
|
|
1659
|
+
const loading = (async () => {
|
|
1660
|
+
try {
|
|
1661
|
+
const diff = JSON.parse(await dataFile(`code/diffs/country/${key}.json`));
|
|
1662
|
+
return new Map(diff.objects.filter((o) => o.change === "replaced").map((o) => [o.key, o]));
|
|
1663
|
+
} catch {
|
|
1664
|
+
return null;
|
|
1665
|
+
}
|
|
1666
|
+
})();
|
|
1667
|
+
countryDiffs.set(key, { at: Date.now(), objects: loading });
|
|
1668
|
+
void loading.then((o) => {
|
|
1669
|
+
const e = countryDiffs.get(key);
|
|
1670
|
+
if (e) e.objects = o;
|
|
1671
|
+
});
|
|
1672
|
+
return loading;
|
|
1673
|
+
}
|
|
1674
|
+
var replacedBy = async (major, cc, key) => (await replacedObjects(major, cc))?.get(key) ?? null;
|
|
1336
1675
|
var url = (path) => `${SITE}${path}/`;
|
|
1337
1676
|
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
1677
|
in: ${r.path_label}` : ""}${r.stats ? `
|
|
@@ -1396,7 +1735,7 @@ function fuse(lists, k = 60) {
|
|
|
1396
1735
|
}
|
|
1397
1736
|
function keepOf(a) {
|
|
1398
1737
|
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);
|
|
1738
|
+
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
1739
|
const system = a.system ? systemOf(a.system) ?? lc(a.system) : void 0;
|
|
1401
1740
|
const objectType = a.object_type ? typeOfWord(a.object_type.replace(/\s+/g, "")) ?? lc(a.object_type) : void 0;
|
|
1402
1741
|
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 +1746,11 @@ async function toolSearch(a) {
|
|
|
1407
1746
|
const limit = a.limit ?? 10, mode = a.mode ?? "hybrid", POOL = 50;
|
|
1408
1747
|
const q = parseQuery(a.query, { countries: countriesOf(index) });
|
|
1409
1748
|
const hits = search(index, q, { filter: keep2 });
|
|
1410
|
-
const
|
|
1749
|
+
const kind = q.kind ?? KINDS[a.kind?.trim().toLowerCase() ?? ""] ?? null;
|
|
1750
|
+
const uid = (r) => r.kind !== "page" && r.country ? `${r.id} ${r.country}` : r.id;
|
|
1751
|
+
const byId = new Map(hits.map((h) => [uid(h.r), h.r]));
|
|
1411
1752
|
let semantic = [], note = "";
|
|
1412
|
-
if (mode !== "keyword") {
|
|
1753
|
+
if (mode !== "keyword" && !kind) {
|
|
1413
1754
|
const m = await loadModel();
|
|
1414
1755
|
if (m) {
|
|
1415
1756
|
const v = embed(m, a.query), data = pageVectors(m);
|
|
@@ -1426,27 +1767,36 @@ async function toolSearch(a) {
|
|
|
1426
1767
|
}
|
|
1427
1768
|
} else if (mode === "semantic") note = ` (semantic search unavailable: ${modelError}; keyword results instead)`;
|
|
1428
1769
|
}
|
|
1429
|
-
const keyword = hits.map((h) => h.r
|
|
1770
|
+
const keyword = hits.map((h) => uid(h.r));
|
|
1430
1771
|
let order, exactBand = 0;
|
|
1431
1772
|
if (mode === "semantic") order = semantic.length ? semantic : keyword.slice(0, POOL);
|
|
1432
1773
|
else if (mode === "keyword" || !semantic.length) order = keyword;
|
|
1433
1774
|
else {
|
|
1434
|
-
const exact = hits.filter((h) => h.band === "exact").map((h) => h.r
|
|
1775
|
+
const exact = hits.filter((h) => h.band === "exact").map((h) => uid(h.r)), pinned = new Set(exact);
|
|
1435
1776
|
exactBand = exact.length;
|
|
1436
|
-
order = [...exact, ...fuse([hits.filter((h) => h.band !== "exact").slice(0, POOL).map((h) => h.r
|
|
1777
|
+
order = [...exact, ...fuse([hits.filter((h) => h.band !== "exact").slice(0, POOL).map((h) => uid(h.r)), semantic.filter((p) => !pinned.has(p))])];
|
|
1437
1778
|
}
|
|
1438
1779
|
if (mode !== "semantic" && mode !== "keyword" && !semantic.length) exactBand = hits.filter((h) => h.band === "exact").length;
|
|
1439
1780
|
const inKeyword = new Set(keyword.slice(0, Math.max(POOL, limit))), inMeaning = new Set(semantic);
|
|
1440
1781
|
const tag = (id) => inKeyword.has(id) && inMeaning.has(id) ? " [both]" : inMeaning.has(id) ? " [meaning]" : " [keyword]";
|
|
1441
1782
|
const shown = order.slice(0, limit).map((id) => byId.get(id)).filter(Boolean);
|
|
1442
|
-
const
|
|
1783
|
+
const filters = ["type", "tier", "system", "country", "app", "object_type"].filter((k) => a[k]?.trim()).map((k) => `${k}=${a[k].trim()}`).join(", ");
|
|
1784
|
+
const unfiltered = kind && !hits.length && filters ? search(index, q, { filter: (r) => r.kind === kind }) : void 0;
|
|
1785
|
+
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
1786
|
const hintText = hs.length ? `
|
|
1444
1787
|
${hs.map((h) => `Hint: ${h.text}${h.query ? ` (search "${h.query}")` : h.path ? ` (cat("${h.path}"))` : ""}`).join("\n")}` : "";
|
|
1788
|
+
if (!shown.length && kind && /^No \w+ named /.test(hs[0]?.text ?? "")) {
|
|
1789
|
+
const [first, ...more] = hs, via = (h) => h.query ? ` (search "${h.query}")` : h.path ? ` (cat("${h.path}"))` : "";
|
|
1790
|
+
return `${first.text}${via(first)}${note}${more.map((h) => `
|
|
1791
|
+
Hint: ${h.text}${via(h)}`).join("")}`;
|
|
1792
|
+
}
|
|
1445
1793
|
if (!shown.length) return `No pages match "${a.query}". Try fewer words, or ls("") to browse sections.${note}${hintText}`;
|
|
1446
1794
|
const how = semantic.length && keyword.length && mode === "hybrid" ? "keyword + meaning" : mode === "semantic" && semantic.length ? "meaning" : "keyword";
|
|
1447
|
-
const learnNote = shown.some((r) => r.type === "learn") ? "; a Learn article's url
|
|
1448
|
-
|
|
1449
|
-
${shown.map((r) => hitLine(r, tag(r
|
|
1795
|
+
const learnNote = shown.some((r) => r.type === "learn") ? "; a Learn article's url reads with the Learn MCP's microsoft_docs_fetch (the plugin connects it), else open it on learn.microsoft.com; cat(hub) reads its hub" : "";
|
|
1796
|
+
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}):
|
|
1797
|
+
${shown.map((r) => hitLine(r, tag(uid(r)))).join("\n")}${hintText}`;
|
|
1798
|
+
return `${shown.length} results, ${how}${note}${how === "keyword + meaning" ? ` (exact band: ${exactBand})` : ""} (read one with cat(path)${learnNote}):
|
|
1799
|
+
${shown.map((r) => hitLine(r, tag(uid(r)))).join("\n")}${hintText}`;
|
|
1450
1800
|
}
|
|
1451
1801
|
async function toolLs(a) {
|
|
1452
1802
|
await loadIndex();
|
|
@@ -1468,7 +1818,27 @@ async function toolLs(a) {
|
|
|
1468
1818
|
async function toolCat(a) {
|
|
1469
1819
|
const [base, anchor] = a.path.split(/(?=#)/);
|
|
1470
1820
|
const clean = `${base.replace(/^\/+|\/+$/g, "").replace(/\.md$/, "")}${anchor ?? ""}`;
|
|
1471
|
-
|
|
1821
|
+
const path = clean.replace(/#.*$/, ""), md = await pageMarkdown(path);
|
|
1822
|
+
if (anchor && anchorView(md, path, anchor, SITE) === null) {
|
|
1823
|
+
const added = await layerMember(path, anchor);
|
|
1824
|
+
if (added) return added;
|
|
1825
|
+
}
|
|
1826
|
+
return pageView(md, clean, a, BUDGET, SITE);
|
|
1827
|
+
}
|
|
1828
|
+
async function layerMember(path, anchor) {
|
|
1829
|
+
const want = parseAnchor(anchor), major = diffMajor();
|
|
1830
|
+
if (!want || !major) return null;
|
|
1831
|
+
await loadIndex();
|
|
1832
|
+
const id = `${path}${anchor}`;
|
|
1833
|
+
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();
|
|
1834
|
+
const owner = index.records.find((r) => r.kind === "page" && r.id === path);
|
|
1835
|
+
const out = [];
|
|
1836
|
+
for (const cc of layers) {
|
|
1837
|
+
const o = owner?.objectId != null ? await replacedBy(major, cc, `${owner.objectType}/${owner.objectId}`) : null;
|
|
1838
|
+
const text2 = o && memberAnswer(o, want, cc, { title: owner?.title, site: SITE, path });
|
|
1839
|
+
if (text2) out.push(text2);
|
|
1840
|
+
}
|
|
1841
|
+
return out.length ? out.join("\n") : null;
|
|
1472
1842
|
}
|
|
1473
1843
|
async function toolGetObject(a) {
|
|
1474
1844
|
await loadIndex();
|
|
@@ -1503,12 +1873,24 @@ async function toolGetObject(a) {
|
|
|
1503
1873
|
const pick = country ? inCountry.length ? inCountry : w1 : w1.length ? w1 : found;
|
|
1504
1874
|
if (pick.length === 1) {
|
|
1505
1875
|
const twins = found.filter((r) => r !== pick[0] && r.country).map((r) => r.country.toUpperCase());
|
|
1506
|
-
const
|
|
1876
|
+
const major = diffMajor(), key = `${type}/${pick[0].objectId ?? pick[0].name.toLowerCase()}`;
|
|
1877
|
+
let note = "";
|
|
1878
|
+
if (country && !inCountry.length) {
|
|
1879
|
+
const replaced = major ? await replacedBy(major, country, key) : null;
|
|
1880
|
+
note = replaced && major ? `${countryBlock(replaced, country, major, { title: pick[0].title })}
|
|
1507
1881
|
|
|
1508
|
-
` :
|
|
1509
|
-
|
|
1882
|
+
` : `(${country.toUpperCase()} has no ${type} of its own here; the W1 ${type} applies.)
|
|
1883
|
+
|
|
1884
|
+
`;
|
|
1885
|
+
}
|
|
1886
|
+
let also = !country && twins.length ? `
|
|
1510
1887
|
|
|
1511
1888
|
(Also in ${twins.join(", ")}: get_object("${type}", "${pick[0].objectId ?? pick[0].name}", country).)` : "";
|
|
1889
|
+
if (!country && !pick[0].country && major && !a.section) {
|
|
1890
|
+
const layers = (await Promise.all(countriesOf(index).map(async (cc2) => ({ cc: cc2, object: await replacedBy(major, cc2, key) })))).filter((l) => !!l.object);
|
|
1891
|
+
const line2 = replacedInLine(layers);
|
|
1892
|
+
if (line2) also += `${also ? "\n" : "\n\n"}${line2}`;
|
|
1893
|
+
}
|
|
1512
1894
|
return `${note}${pageView(await pageMarkdown(pick[0].id), pick[0].id, a, BUDGET, SITE)}${a.section ? "" : also}`;
|
|
1513
1895
|
}
|
|
1514
1896
|
if (pick.length > 1) return `${pick.length} ${type} objects match "${a.idOrName}"; read one with cat(path):
|
|
@@ -1566,13 +1948,13 @@ ${a.message}
|
|
|
1566
1948
|
var VIEW = { section: z.string().optional(), offset: z.number().int().min(0).max(1e5).optional(), limit: z.number().int().min(1).max(5e3).optional() };
|
|
1567
1949
|
function createServer() {
|
|
1568
1950
|
const server = new McpServer({ name: "bc-observatory", version: VERSION }, {
|
|
1569
|
-
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
|
|
1951
|
+
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: read their url with the Learn MCP's microsoft_docs_fetch (the plugin connects it), else open it on learn.microsoft.com."
|
|
1570
1952
|
});
|
|
1571
1953
|
server.registerTool(
|
|
1572
1954
|
"search",
|
|
1573
1955
|
{
|
|
1574
1956
|
title: "Search the knowledge base",
|
|
1575
|
-
description: "Search every page
|
|
1957
|
+
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
1958
|
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
1959
|
},
|
|
1578
1960
|
async (a) => text(await toolSearch(a))
|
|
@@ -1589,7 +1971,7 @@ function createServer() {
|
|
|
1589
1971
|
);
|
|
1590
1972
|
server.registerTool("get_object", {
|
|
1591
1973
|
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.`,
|
|
1974
|
+
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
1975
|
inputSchema: { type: z.string(), idOrName: z.string(), country: z.string().length(2).optional(), ...VIEW }
|
|
1594
1976
|
}, async (a) => text(await toolGetObject(a)));
|
|
1595
1977
|
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.1",
|
|
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": {
|