bc-observatory 0.3.0 → 0.5.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 +8 -0
- package/dist/server.js +403 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -38,4 +38,12 @@ model, search is keyword-only and says so.
|
|
|
38
38
|
`get_object` takes an id, a reference (`t36`, `36-be`), a name or a caption, and a `country`; `whats_new` lists nothing
|
|
39
39
|
dated after today unless `include_future: true`.
|
|
40
40
|
|
|
41
|
+
`cat`, `get_object` and `localization` return a page as it is when it fits 30,000 characters. A longer page
|
|
42
|
+
(Codeunit 80 "Sales-Post" is 177,746) comes back as a compact view: the frontmatter with long lists cut, the title
|
|
43
|
+
block, the section keys with their row counts and the first 5 rows of each section. `section` reads one section
|
|
44
|
+
(`fields`, `events`, `procedures`, `calls`, ..., or `frontmatter`), `offset` and `limit` page through it by rows, and
|
|
45
|
+
no answer exceeds 30,000 characters. A symbol's path from `search` (`objects/codeunit/80#event-OnAfterPostSalesDoc`)
|
|
46
|
+
returns just that member. `BC_OBSERVATORY_MAX_CHARS` changes the limit (`0` returns every page whole, as before
|
|
47
|
+
0.4.0).
|
|
48
|
+
|
|
41
49
|
MIT. Content: see the repository's CONTENT-NOTICE.md (Microsoft content attributed; community content derived only).
|
package/dist/server.js
CHANGED
|
@@ -393,9 +393,10 @@ function normOf(r) {
|
|
|
393
393
|
const symbol = r.kind !== "page";
|
|
394
394
|
const main = /* @__PURE__ */ new Set([...indexTokens(r.name, symbol), ...indexTokens(r.title, symbol), ...r.caption ? indexTokens(r.caption) : []]);
|
|
395
395
|
const own = r.type === "topic" ? new Set(tokenize(r.title)) : null;
|
|
396
|
-
const
|
|
396
|
+
const learn = r.type === "learn";
|
|
397
|
+
const tags = (r.tags ?? []).flatMap((t) => indexTokens(t, learn)).filter((t) => !own?.has(t));
|
|
397
398
|
const eqs = r.type === "object" || symbol ? [tokenize(r.name), ...r.caption ? [tokenize(r.caption)] : []] : [tokenize(r.title)];
|
|
398
|
-
const n = { main: [...main], tags: [...new Set(tags)], text: symbol ? null : indexTokens(r.text ?? ""), eqs };
|
|
399
|
+
const n = { main: [...main], tags: learn ? tags : [...new Set(tags)], text: symbol ? null : indexTokens(r.text ?? ""), eqs };
|
|
399
400
|
NORM.set(r, n);
|
|
400
401
|
return n;
|
|
401
402
|
}
|
|
@@ -783,6 +784,26 @@ function hints(index2, q, hits) {
|
|
|
783
784
|
return out;
|
|
784
785
|
}
|
|
785
786
|
|
|
787
|
+
// ../search/src/learn.ts
|
|
788
|
+
function learnRecords(doc) {
|
|
789
|
+
return doc.rows.map(([path, title, summary, date, h, system, ids]) => {
|
|
790
|
+
const hub = h >= 0 ? doc.hubs[h] : void 0;
|
|
791
|
+
const sys = system ?? hub?.[2] ?? null;
|
|
792
|
+
return {
|
|
793
|
+
path: /^https?:\/\//.test(path) ? path : `${doc.base}${path}`,
|
|
794
|
+
type: "learn",
|
|
795
|
+
title,
|
|
796
|
+
summary,
|
|
797
|
+
tier: "official",
|
|
798
|
+
...sys ? { system: sys } : {},
|
|
799
|
+
...date ? { date } : {},
|
|
800
|
+
...ids?.length ? { tags: ids } : {},
|
|
801
|
+
...hub?.[1] ? { path_label: hub[1] } : {},
|
|
802
|
+
...hub?.[0] ? { hub: hub[0] } : {}
|
|
803
|
+
};
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
|
|
786
807
|
// src/embed.ts
|
|
787
808
|
var MAX_TOKENS = 512;
|
|
788
809
|
function parseModel(tokenizerJson, safetensors, config = {}) {
|
|
@@ -891,10 +912,363 @@ function dot(a, b, bOffset = 0) {
|
|
|
891
912
|
return s;
|
|
892
913
|
}
|
|
893
914
|
|
|
915
|
+
// src/sections.ts
|
|
916
|
+
var DEFAULT_BUDGET = 3e4;
|
|
917
|
+
var MIN_BUDGET = 5e3;
|
|
918
|
+
var PREVIEW_ROWS = 5;
|
|
919
|
+
var ROW_CLIP = 200;
|
|
920
|
+
var PROSE_CLIP = 800;
|
|
921
|
+
var HEAD_LINE_MAX = 120;
|
|
922
|
+
var FM_LIST_CHARS = 600;
|
|
923
|
+
var FM_LIST_MIN = 3;
|
|
924
|
+
var RESERVE = 300;
|
|
925
|
+
var OBJECT_KEYS = {
|
|
926
|
+
"Properties": "properties",
|
|
927
|
+
"Fields": "fields",
|
|
928
|
+
"Fields on this page": "controls",
|
|
929
|
+
"Actions": "actions",
|
|
930
|
+
"Keys": "keys",
|
|
931
|
+
"Values": "values",
|
|
932
|
+
"Events published": "events",
|
|
933
|
+
"Trigger event subscribers": "trigger_subscribers",
|
|
934
|
+
"Event subscriptions": "subscriptions",
|
|
935
|
+
"Procedures": "procedures",
|
|
936
|
+
"Triggers": "triggers",
|
|
937
|
+
"Relations": "relations",
|
|
938
|
+
"Referenced by": "referenced_by",
|
|
939
|
+
"Pages and codeunits on this table": "pages",
|
|
940
|
+
"Extended by": "extended_by",
|
|
941
|
+
"Calls": "calls",
|
|
942
|
+
"Called by": "called_by",
|
|
943
|
+
"Implements": "implements",
|
|
944
|
+
"Implemented by": "implemented_by",
|
|
945
|
+
"Recent changes": "changes",
|
|
946
|
+
"Across versions": "versions",
|
|
947
|
+
"Countries that replace it": "countries",
|
|
948
|
+
"Documented on Microsoft Learn": "learn",
|
|
949
|
+
"Deprecations": "deprecations",
|
|
950
|
+
"Ask your agent": "ask"
|
|
951
|
+
};
|
|
952
|
+
var snake = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
953
|
+
var blank = (l) => l.trim() === "";
|
|
954
|
+
var fmt = (n) => n.toLocaleString("en-US");
|
|
955
|
+
var FENCE = /^\s*```/;
|
|
956
|
+
var SEPARATOR = /^\|\s*:?-/;
|
|
957
|
+
var CAP_NOTE = /^- and \d+ more/;
|
|
958
|
+
function budgetOf(env) {
|
|
959
|
+
const n = env === void 0 || env.trim() === "" ? NaN : Number(env);
|
|
960
|
+
if (!Number.isFinite(n) || n < 0) return DEFAULT_BUDGET;
|
|
961
|
+
if (n === 0) return Infinity;
|
|
962
|
+
return Math.max(MIN_BUDGET, Math.floor(n));
|
|
963
|
+
}
|
|
964
|
+
function sectionKey(heading) {
|
|
965
|
+
return OBJECT_KEYS[heading] ?? snake(heading);
|
|
966
|
+
}
|
|
967
|
+
function splitSection(lines) {
|
|
968
|
+
const head = [], rows = [];
|
|
969
|
+
let pending = [];
|
|
970
|
+
let table2 = null, group = null;
|
|
971
|
+
let inTable = false, afterRow = false, fence = null;
|
|
972
|
+
const other = (line2, kind = "text") => {
|
|
973
|
+
if (rows.length) pending.push({ line: line2, kind });
|
|
974
|
+
else head.push(line2);
|
|
975
|
+
};
|
|
976
|
+
const addRow = (first) => {
|
|
977
|
+
if (rows.length) {
|
|
978
|
+
const prev = rows[rows.length - 1];
|
|
979
|
+
for (const p of pending) {
|
|
980
|
+
if (p.kind === "group" && p.line === group) continue;
|
|
981
|
+
if (p.kind === "table" && table2 && (p.line === table2[0] || p.line === table2[1])) continue;
|
|
982
|
+
if (!blank(p.line)) prev.lines.push(p.line);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
pending = [];
|
|
986
|
+
rows.push({ lines: [first], table: table2, group });
|
|
987
|
+
afterRow = true;
|
|
988
|
+
};
|
|
989
|
+
for (let i = 0; i < lines.length; i++) {
|
|
990
|
+
const l = lines[i];
|
|
991
|
+
if (fence) {
|
|
992
|
+
if (fence === "row") rows[rows.length - 1].lines.push(l);
|
|
993
|
+
else other(l);
|
|
994
|
+
if (FENCE.test(l)) fence = null;
|
|
995
|
+
continue;
|
|
996
|
+
}
|
|
997
|
+
if (FENCE.test(l)) {
|
|
998
|
+
if (afterRow && /^ {2,}/.test(l)) {
|
|
999
|
+
rows[rows.length - 1].lines.push(l);
|
|
1000
|
+
fence = "row";
|
|
1001
|
+
} else {
|
|
1002
|
+
other(l);
|
|
1003
|
+
fence = "text";
|
|
1004
|
+
afterRow = false;
|
|
1005
|
+
inTable = false;
|
|
1006
|
+
table2 = null;
|
|
1007
|
+
}
|
|
1008
|
+
continue;
|
|
1009
|
+
}
|
|
1010
|
+
if (afterRow && /^ {2,}\S/.test(l)) {
|
|
1011
|
+
rows[rows.length - 1].lines.push(l);
|
|
1012
|
+
continue;
|
|
1013
|
+
}
|
|
1014
|
+
if (l.startsWith("|")) {
|
|
1015
|
+
if (!inTable && i + 1 < lines.length && SEPARATOR.test(lines[i + 1])) {
|
|
1016
|
+
table2 = [l, lines[i + 1]];
|
|
1017
|
+
inTable = true;
|
|
1018
|
+
afterRow = false;
|
|
1019
|
+
other(l, "table");
|
|
1020
|
+
other(lines[++i], "table");
|
|
1021
|
+
continue;
|
|
1022
|
+
}
|
|
1023
|
+
if (inTable) {
|
|
1024
|
+
addRow(l);
|
|
1025
|
+
continue;
|
|
1026
|
+
}
|
|
1027
|
+
} else if (inTable) {
|
|
1028
|
+
inTable = false;
|
|
1029
|
+
table2 = null;
|
|
1030
|
+
}
|
|
1031
|
+
if (l.startsWith("- ") && !CAP_NOTE.test(l)) {
|
|
1032
|
+
addRow(l);
|
|
1033
|
+
continue;
|
|
1034
|
+
}
|
|
1035
|
+
afterRow = false;
|
|
1036
|
+
if (l.startsWith("### ")) {
|
|
1037
|
+
group = l;
|
|
1038
|
+
other(l, "group");
|
|
1039
|
+
continue;
|
|
1040
|
+
}
|
|
1041
|
+
other(l);
|
|
1042
|
+
}
|
|
1043
|
+
const tail = pending.map((p) => p.line);
|
|
1044
|
+
return { head, rows, tail };
|
|
1045
|
+
}
|
|
1046
|
+
var memo = null;
|
|
1047
|
+
function parsePage(md) {
|
|
1048
|
+
if (memo && memo.md === md) return memo.page;
|
|
1049
|
+
const all = md.split("\n");
|
|
1050
|
+
let i = 0, frontmatter = "";
|
|
1051
|
+
if (all[0] === "---") {
|
|
1052
|
+
const end = all.indexOf("---", 1);
|
|
1053
|
+
if (end > 0) {
|
|
1054
|
+
frontmatter = all.slice(1, end).join("\n");
|
|
1055
|
+
i = end + 1;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
const pre = [], chunks = [];
|
|
1059
|
+
let fence = false;
|
|
1060
|
+
for (; i < all.length; i++) {
|
|
1061
|
+
const l = all[i];
|
|
1062
|
+
if (!fence && l.startsWith("## ")) {
|
|
1063
|
+
chunks.push({ heading: l.slice(3).trim(), lines: [] });
|
|
1064
|
+
continue;
|
|
1065
|
+
}
|
|
1066
|
+
if (FENCE.test(l)) fence = !fence;
|
|
1067
|
+
(chunks.length ? chunks[chunks.length - 1].lines : pre).push(l);
|
|
1068
|
+
}
|
|
1069
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1070
|
+
const sections = chunks.map(({ heading, lines }) => {
|
|
1071
|
+
while (lines.length && blank(lines[lines.length - 1])) lines.pop();
|
|
1072
|
+
const base = sectionKey(heading), n = (seen.get(base) ?? 0) + 1;
|
|
1073
|
+
seen.set(base, n);
|
|
1074
|
+
return { heading, key: n === 1 ? base : `${base}_${n}`, lines, ...splitSection(lines) };
|
|
1075
|
+
});
|
|
1076
|
+
const preamble = pre.join("\n").replace(/^\s*\n/, "").replace(/\s+$/, "");
|
|
1077
|
+
const page = { frontmatter, preamble, sections };
|
|
1078
|
+
memo = { md, page };
|
|
1079
|
+
return page;
|
|
1080
|
+
}
|
|
1081
|
+
function frontmatterSection(p) {
|
|
1082
|
+
const lines = p.frontmatter ? p.frontmatter.split("\n") : [];
|
|
1083
|
+
return { heading: "", key: "frontmatter", lines, head: [], rows: lines.map((l) => ({ lines: [l], table: null, group: null })), tail: [] };
|
|
1084
|
+
}
|
|
1085
|
+
function findSection(p, want) {
|
|
1086
|
+
const w = want.trim().toLowerCase(), ws = snake(want);
|
|
1087
|
+
if (w === "frontmatter") return frontmatterSection(p);
|
|
1088
|
+
return p.sections.find((s) => s.key === w) ?? p.sections.find((s) => s.heading.toLowerCase() === w) ?? p.sections.find((s) => ws !== "" && snake(s.heading) === ws) ?? null;
|
|
1089
|
+
}
|
|
1090
|
+
var keyList = (p, counts = false) => [...p.sections.map((s) => counts && s.rows.length ? `${s.key} (${s.rows.length})` : s.key), "frontmatter"].join(", ");
|
|
1091
|
+
var titleOf = (p, path) => p.preamble.split("\n").find((l) => l.startsWith("# "))?.slice(2).trim() || path;
|
|
1092
|
+
function clip(s, n, mark = "\u2026") {
|
|
1093
|
+
if (s.length <= n) return s;
|
|
1094
|
+
const end = /[\uD800-\uDBFF]/.test(s[n - 2]) ? n - 2 : n - 1;
|
|
1095
|
+
return `${s.slice(0, end)}${mark}`;
|
|
1096
|
+
}
|
|
1097
|
+
var indentOf = (l) => l.length - l.trimStart().length;
|
|
1098
|
+
var BLOCK_SCALAR = /(?:^|:|-)\s*[|>][-+0-9]*\s*$/;
|
|
1099
|
+
var LIST_ITEM = /^( *)-(?: |$)/;
|
|
1100
|
+
function cutLists(lines) {
|
|
1101
|
+
const out = [];
|
|
1102
|
+
let i = 0;
|
|
1103
|
+
while (i < lines.length) {
|
|
1104
|
+
const l = lines[i], m = LIST_ITEM.exec(l);
|
|
1105
|
+
if (!m) {
|
|
1106
|
+
out.push(l);
|
|
1107
|
+
i++;
|
|
1108
|
+
if (BLOCK_SCALAR.test(l)) {
|
|
1109
|
+
const ind2 = indentOf(l);
|
|
1110
|
+
while (i < lines.length && (blank(lines[i]) || indentOf(lines[i]) > ind2)) out.push(lines[i++]);
|
|
1111
|
+
}
|
|
1112
|
+
continue;
|
|
1113
|
+
}
|
|
1114
|
+
const ind = m[1].length, items = [];
|
|
1115
|
+
while (i < lines.length && indentOf(lines[i]) === ind && LIST_ITEM.test(lines[i])) {
|
|
1116
|
+
const item = [lines[i++]];
|
|
1117
|
+
while (i < lines.length && (indentOf(lines[i]) > ind || blank(lines[i]) && i + 1 < lines.length && indentOf(lines[i + 1]) > ind)) item.push(lines[i++]);
|
|
1118
|
+
items.push(BLOCK_SCALAR.test(item[0]) ? item : [item[0], ...cutLists(item.slice(1))]);
|
|
1119
|
+
}
|
|
1120
|
+
const size = (k2) => items.slice(0, k2).map((it) => it.join("\n")).join("\n").length;
|
|
1121
|
+
if (size(items.length) <= FM_LIST_CHARS) {
|
|
1122
|
+
out.push(...items.flat());
|
|
1123
|
+
continue;
|
|
1124
|
+
}
|
|
1125
|
+
let k = 0;
|
|
1126
|
+
while (k < items.length && size(k + 1) <= FM_LIST_CHARS) k++;
|
|
1127
|
+
k = Math.max(k, FM_LIST_MIN);
|
|
1128
|
+
out.push(...items.slice(0, k).flat());
|
|
1129
|
+
if (k < items.length) out.push(`${" ".repeat(ind)}# \u2026 ${items.length - k} more; section: "frontmatter"`);
|
|
1130
|
+
}
|
|
1131
|
+
return out;
|
|
1132
|
+
}
|
|
1133
|
+
function compactFrontmatter(yaml) {
|
|
1134
|
+
const lines = yaml.split("\n"), kept = [];
|
|
1135
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1136
|
+
if (/^generated:/.test(lines[i])) {
|
|
1137
|
+
while (i + 1 < lines.length && (indentOf(lines[i + 1]) > 0 || blank(lines[i + 1]))) i++;
|
|
1138
|
+
continue;
|
|
1139
|
+
}
|
|
1140
|
+
kept.push(lines[i]);
|
|
1141
|
+
}
|
|
1142
|
+
return cutLists(kept).join("\n");
|
|
1143
|
+
}
|
|
1144
|
+
function compactView(md, path, budget) {
|
|
1145
|
+
const p = parsePage(md);
|
|
1146
|
+
const out = [];
|
|
1147
|
+
if (md.startsWith("---\n") && p.frontmatter !== "") out.push("---", compactFrontmatter(p.frontmatter), "---", "");
|
|
1148
|
+
if (p.preamble) out.push(p.preamble, "");
|
|
1149
|
+
out.push(`Compact view: this page is ${fmt(md.length)} characters and one answer holds ${fmt(budget)}, so each section shows its first ${PREVIEW_ROWS} rows. Sections: ${keyList(p, true)}. Read one with cat("${path}", section: "<key>"); offset and limit page through a long one.`, "");
|
|
1150
|
+
for (const s of p.sections) {
|
|
1151
|
+
out.push(`## ${s.heading}`);
|
|
1152
|
+
const more = `\u2026 (section: "${s.key}")`;
|
|
1153
|
+
if (!s.rows.length) {
|
|
1154
|
+
const text2 = s.head.filter((l) => !blank(l)).join("\n");
|
|
1155
|
+
if (text2) out.push(clip(text2, PROSE_CLIP, more));
|
|
1156
|
+
} else {
|
|
1157
|
+
const lead = s.head.find((l) => !blank(l) && !l.startsWith("|") && !l.startsWith("### "));
|
|
1158
|
+
if (lead && lead.length <= HEAD_LINE_MAX) out.push(lead);
|
|
1159
|
+
let group = null, table2 = null;
|
|
1160
|
+
for (const r of s.rows.slice(0, PREVIEW_ROWS)) {
|
|
1161
|
+
if (r.group && r.group !== group) {
|
|
1162
|
+
out.push(r.group);
|
|
1163
|
+
group = r.group;
|
|
1164
|
+
}
|
|
1165
|
+
const t = r.table ? r.table.join("\n") : null;
|
|
1166
|
+
if (r.table && t !== table2) out.push(...r.table);
|
|
1167
|
+
table2 = t;
|
|
1168
|
+
out.push(clip(r.lines[0], ROW_CLIP));
|
|
1169
|
+
}
|
|
1170
|
+
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
|
+
else {
|
|
1172
|
+
const text2 = s.tail.filter((l) => !blank(l)).join("\n");
|
|
1173
|
+
if (text2) out.push(clip(text2, PROSE_CLIP, more));
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
out.push("");
|
|
1177
|
+
}
|
|
1178
|
+
return `${out.join("\n").replace(/\n{3,}/g, "\n\n").replace(/\n+$/, "")}
|
|
1179
|
+
`;
|
|
1180
|
+
}
|
|
1181
|
+
var dropLeadingBlank = (lines) => {
|
|
1182
|
+
let i = 0;
|
|
1183
|
+
while (i < lines.length && blank(lines[i])) i++;
|
|
1184
|
+
return lines.slice(i);
|
|
1185
|
+
};
|
|
1186
|
+
function sectionView(md, path, want, offset, limit, budget, site) {
|
|
1187
|
+
const p = parsePage(md);
|
|
1188
|
+
const nl = (lines) => `${lines.join("\n")}
|
|
1189
|
+
`;
|
|
1190
|
+
const s = findSection(p, want);
|
|
1191
|
+
if (!s) return `No section "${want}" on ${path}. Sections: ${keyList(p)}.`;
|
|
1192
|
+
const fm = s.key === "frontmatter" && s.heading === "";
|
|
1193
|
+
const first = `${titleOf(p, path)}, section "${s.key}": ${site}${path}/`;
|
|
1194
|
+
const heading = fm ? [] : [`## ${s.heading}`];
|
|
1195
|
+
const n = s.rows.length, off = Math.max(0, Math.floor(offset ?? 0));
|
|
1196
|
+
const verbatim = nl(fm ? [first, "", "---", ...s.lines, "---"] : [first, "", ...heading, ...s.lines]);
|
|
1197
|
+
if (!n || off === 0 && limit === void 0 && verbatim.length <= budget) return verbatim;
|
|
1198
|
+
const tail = dropLeadingBlank(s.tail);
|
|
1199
|
+
if (off >= n) return nl([first, "", ...heading, "", ...tail.length ? tail : [`No rows after ${n} in section "${s.key}" of ${path}.`]]);
|
|
1200
|
+
const out = [first, "", ...heading, ...fm ? [] : [""]];
|
|
1201
|
+
let group = null, table2 = null;
|
|
1202
|
+
if (off === 0) {
|
|
1203
|
+
out.push(...dropLeadingBlank(s.head));
|
|
1204
|
+
group = s.rows[0].group;
|
|
1205
|
+
table2 = s.rows[0].table ? s.rows[0].table.join("\n") : null;
|
|
1206
|
+
}
|
|
1207
|
+
let len = out.join("\n").length, b = off;
|
|
1208
|
+
const end = limit === void 0 ? n : Math.min(n, off + Math.max(1, Math.floor(limit)));
|
|
1209
|
+
for (let r = off; r < end; r++) {
|
|
1210
|
+
const row = s.rows[r], add = [];
|
|
1211
|
+
let g = group, t = table2;
|
|
1212
|
+
if (row.group && row.group !== g) {
|
|
1213
|
+
add.push(...out.length && !blank(out[out.length - 1]) ? [""] : [], row.group, "");
|
|
1214
|
+
g = row.group;
|
|
1215
|
+
t = null;
|
|
1216
|
+
}
|
|
1217
|
+
const rt = row.table ? row.table.join("\n") : null;
|
|
1218
|
+
if (row.table && rt !== t) add.push(...row.table);
|
|
1219
|
+
t = rt;
|
|
1220
|
+
add.push(...row.lines);
|
|
1221
|
+
const size = add.join("\n").length + 1;
|
|
1222
|
+
if (r > off && len + size > budget - RESERVE) break;
|
|
1223
|
+
out.push(...add);
|
|
1224
|
+
len += size;
|
|
1225
|
+
group = g;
|
|
1226
|
+
table2 = t;
|
|
1227
|
+
b = r + 1;
|
|
1228
|
+
}
|
|
1229
|
+
const rows = `Rows ${off + 1}-${b} of ${n}.`;
|
|
1230
|
+
if (b < n) return nl([...out, "", `${rows} Next: cat("${path}", section: "${s.key}", offset: ${b})`]);
|
|
1231
|
+
if (!tail.length) return nl([...out, "", rows]);
|
|
1232
|
+
const withTail = nl([...out, "", ...tail, "", rows]);
|
|
1233
|
+
if (limit === void 0 && withTail.length <= budget) return withTail;
|
|
1234
|
+
return nl([...out, "", `${rows} The section's closing text: cat("${path}", section: "${s.key}", offset: ${n})`]);
|
|
1235
|
+
}
|
|
1236
|
+
var ANCHOR_SECTION = { field: ["fields", "field"], value: ["values", "value"], event: ["events", "event"], proc: ["procedures", "procedure"] };
|
|
1237
|
+
function anchorView(md, path, anchor, site) {
|
|
1238
|
+
const a = anchor.replace(/^#/, ""), m = /^(field|value|event|proc)-(.+)$/.exec(a);
|
|
1239
|
+
if (!m) return null;
|
|
1240
|
+
const [key, label] = ANCHOR_SECTION[m[1]];
|
|
1241
|
+
const p = parsePage(md);
|
|
1242
|
+
const s = p.sections.find((x) => x.key === key);
|
|
1243
|
+
if (!s) return null;
|
|
1244
|
+
const match = m[1] === "field" || m[1] === "value" ? (l) => l.startsWith(`| ${m[2]} |`) : (l) => l.startsWith(`- \`${m[2]}(`);
|
|
1245
|
+
const rows = s.rows.filter((r) => match(r.lines[0]));
|
|
1246
|
+
if (!rows.length) return null;
|
|
1247
|
+
return `${[`${titleOf(p, path)}, ${label} ${m[2]}: ${site}${path}/#${a}`, "", `## ${s.heading}`, ...rows[0].table ?? [], ...rows.flatMap((r) => r.lines)].join("\n")}
|
|
1248
|
+
`;
|
|
1249
|
+
}
|
|
1250
|
+
function pageView(md, path, a, budget, site) {
|
|
1251
|
+
const hash = path.indexOf("#");
|
|
1252
|
+
if (hash >= 0) {
|
|
1253
|
+
const clean = path.slice(0, hash), anchor = path.slice(hash);
|
|
1254
|
+
return anchorView(md, clean, anchor, site) ?? `(No row for ${anchor} on this page.)
|
|
1255
|
+
|
|
1256
|
+
${pageView(md, clean, {}, budget, site)}`;
|
|
1257
|
+
}
|
|
1258
|
+
if (a.section !== void 0 && a.section.trim() !== "") return sectionView(md, path, a.section, a.offset, a.limit, budget, site);
|
|
1259
|
+
if (a.offset !== void 0 || a.limit !== void 0) {
|
|
1260
|
+
return `offset and limit read one section: cat("${path}", section: "<key>", offset: ${a.offset ?? 0}). Sections: ${keyList(parsePage(md))}.`;
|
|
1261
|
+
}
|
|
1262
|
+
if (md.length <= budget) return md;
|
|
1263
|
+
return compactView(md, path, budget);
|
|
1264
|
+
}
|
|
1265
|
+
|
|
894
1266
|
// src/server.ts
|
|
895
|
-
var VERSION = "0.
|
|
1267
|
+
var VERSION = "0.5.0";
|
|
896
1268
|
var SITE = (process.env.BC_OBSERVATORY_SITE ?? "https://waldo1001.github.io/waldo.BCObservatory/").replace(/\/?$/, "/");
|
|
897
1269
|
var LOCAL = process.env.BC_OBSERVATORY_LOCAL ? resolve(process.env.BC_OBSERVATORY_LOCAL) : null;
|
|
1270
|
+
var BUDGET = budgetOf(process.env.BC_OBSERVATORY_MAX_CHARS);
|
|
1271
|
+
var B = (Number.isFinite(BUDGET) ? BUDGET : DEFAULT_BUDGET).toLocaleString("en-US");
|
|
898
1272
|
var REPO = "https://github.com/waldo1001/waldo.BCObservatory";
|
|
899
1273
|
var DAY = 864e5;
|
|
900
1274
|
var cacheDir = join(process.env.XDG_CACHE_HOME ?? join(homedir(), ".cache"), "bc-observatory", createHash("sha256").update(SITE).digest("hex").slice(0, 12));
|
|
@@ -934,6 +1308,12 @@ async function loadIndex() {
|
|
|
934
1308
|
for (const s of manifest.shards) all.push(...JSON.parse(await shard(s.file, s.sha256)));
|
|
935
1309
|
pages = all;
|
|
936
1310
|
const idx = prepare(all.map(pageToRecord));
|
|
1311
|
+
if (manifest.learn) {
|
|
1312
|
+
try {
|
|
1313
|
+
prepare(learnRecords(JSON.parse(await shard(manifest.learn.file, manifest.learn.sha256))).map(pageToRecord), idx);
|
|
1314
|
+
} catch {
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
937
1317
|
let sm = null;
|
|
938
1318
|
try {
|
|
939
1319
|
sm = JSON.parse(await dataFile("index/symbols-manifest.json"));
|
|
@@ -961,7 +1341,10 @@ var line = (r, tag = "") => `- ${r.title}${r.caption ? ` (captioned "${r.caption
|
|
|
961
1341
|
var text = (t) => ({ content: [{ type: "text", text: t }] });
|
|
962
1342
|
var symbolHit = (r, tag = "") => `- ${r.name} (${symbolLine(r).replace(/ · /g, ", ")}) path=${r.id}${tag}${r.text ? `
|
|
963
1343
|
${r.text}` : ""}`;
|
|
964
|
-
var
|
|
1344
|
+
var learnLine = (r, tag = "") => `- ${r.title} [learn, official${r.date ? `, ${r.date}` : ""}] url=${r.path}${tag}${r.path_label ? `
|
|
1345
|
+
in: ${r.path_label}${r.hub ? ` (hub: ${r.hub})` : ""}` : ""}
|
|
1346
|
+
${r.summary}`;
|
|
1347
|
+
var hitLine = (r, tag = "") => r.kind === "page" && r.page ? r.type === "learn" ? learnLine(r.page, tag) : line(r.page, tag) : symbolHit(r, tag);
|
|
965
1348
|
var MODEL = {
|
|
966
1349
|
repo: "minishlab/potion-base-8M",
|
|
967
1350
|
revision: "bf8b056651a2c21b8d2565580b8569da283cab23",
|
|
@@ -1061,7 +1444,8 @@ async function toolSearch(a) {
|
|
|
1061
1444
|
${hs.map((h) => `Hint: ${h.text}${h.query ? ` (search "${h.query}")` : h.path ? ` (cat("${h.path}"))` : ""}`).join("\n")}` : "";
|
|
1062
1445
|
if (!shown.length) return `No pages match "${a.query}". Try fewer words, or ls("") to browse sections.${note}${hintText}`;
|
|
1063
1446
|
const how = semantic.length && keyword.length && mode === "hybrid" ? "keyword + meaning" : mode === "semantic" && semantic.length ? "meaning" : "keyword";
|
|
1064
|
-
|
|
1447
|
+
const learnNote = shown.some((r) => r.type === "learn") ? "; a Learn article's url opens on learn.microsoft.com, cat(hub) reads its hub" : "";
|
|
1448
|
+
return `${shown.length} results, ${how}${note}${how === "keyword + meaning" || mode === "hybrid" ? ` (exact band: ${exactBand})` : ""} (read one with cat(path)${learnNote}):
|
|
1065
1449
|
${shown.map((r) => hitLine(r, tag(r.id))).join("\n")}${hintText}`;
|
|
1066
1450
|
}
|
|
1067
1451
|
async function toolLs(a) {
|
|
@@ -1082,7 +1466,9 @@ async function toolLs(a) {
|
|
|
1082
1466
|
... and ${files.length - MAX} more pages; use search() to narrow down` : ""}`;
|
|
1083
1467
|
}
|
|
1084
1468
|
async function toolCat(a) {
|
|
1085
|
-
|
|
1469
|
+
const [base, anchor] = a.path.split(/(?=#)/);
|
|
1470
|
+
const clean = `${base.replace(/^\/+|\/+$/g, "").replace(/\.md$/, "")}${anchor ?? ""}`;
|
|
1471
|
+
return pageView(await pageMarkdown(clean.replace(/#.*$/, "")), clean, a, BUDGET, SITE);
|
|
1086
1472
|
}
|
|
1087
1473
|
async function toolGetObject(a) {
|
|
1088
1474
|
await loadIndex();
|
|
@@ -1123,7 +1509,7 @@ async function toolGetObject(a) {
|
|
|
1123
1509
|
const also = !country && twins.length ? `
|
|
1124
1510
|
|
|
1125
1511
|
(Also in ${twins.join(", ")}: get_object("${type}", "${pick[0].objectId ?? pick[0].name}", country).)` : "";
|
|
1126
|
-
return `${note}${await pageMarkdown(pick[0].id)}${also}`;
|
|
1512
|
+
return `${note}${pageView(await pageMarkdown(pick[0].id), pick[0].id, a, BUDGET, SITE)}${a.section ? "" : also}`;
|
|
1127
1513
|
}
|
|
1128
1514
|
if (pick.length > 1) return `${pick.length} ${type} objects match "${a.idOrName}"; read one with cat(path):
|
|
1129
1515
|
${pick.map((r) => `- ${r.title} path=${r.id}`).join("\n")}`;
|
|
@@ -1144,7 +1530,8 @@ async function toolDiffObject(a) {
|
|
|
1144
1530
|
${JSON.stringify(o, null, 1)}`;
|
|
1145
1531
|
}
|
|
1146
1532
|
async function toolLocalization(a) {
|
|
1147
|
-
|
|
1533
|
+
const p = `localizations/${a.country.toLowerCase()}`;
|
|
1534
|
+
return pageView(await pageMarkdown(p), p, a, BUDGET, SITE);
|
|
1148
1535
|
}
|
|
1149
1536
|
async function toolWhatsNew(a, today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)) {
|
|
1150
1537
|
await loadIndex();
|
|
@@ -1176,15 +1563,16 @@ ${a.message}
|
|
|
1176
1563
|
(sent from the bc-observatory MCP server ${VERSION})`;
|
|
1177
1564
|
return `Open this link to file the issue (no sign-in through the agent): ${REPO}/issues/new?title=${encodeURIComponent(title)}&body=${encodeURIComponent(body)}&labels=feedback`;
|
|
1178
1565
|
}
|
|
1566
|
+
var VIEW = { section: z.string().optional(), offset: z.number().int().min(0).max(1e5).optional(), limit: z.number().int().min(1).max(5e3).optional() };
|
|
1179
1567
|
function createServer() {
|
|
1180
1568
|
const server = new McpServer({ name: "bc-observatory", version: VERSION }, {
|
|
1181
|
-
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)."
|
|
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, open their url on learn.microsoft.com."
|
|
1182
1570
|
});
|
|
1183
1571
|
server.registerTool(
|
|
1184
1572
|
"search",
|
|
1185
1573
|
{
|
|
1186
1574
|
title: "Search the knowledge base",
|
|
1187
|
-
description: "Search every page
|
|
1575
|
+
description: "Search every page, and the fields, published events, global procedures and enum values of AL objects, ranked like the site's search. Takes names ('Sales Header'), references ('t36', 'cu 80', 'table 11300 BE'), bare ids ('36') and symbol prefixes ('event: OnAfterPost', 'field: Posting Date', 'proc: CopyToTempLines'); a symbol hit's path ends in its anchor (#event-<Name>, #field-<id>, #proc-<Name>, #value-<ordinal>); cat(path) reads that member alone. Learn articles too: type learn, a url, not a path. Hybrid by default: exact matches stay on top and the rest is 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 an alias such as 'g/l'), country ('BE'), app ('Subscription Billing'), object_type ('table'), kind (page, field, event, proc, value).",
|
|
1188
1576
|
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() }
|
|
1189
1577
|
},
|
|
1190
1578
|
async (a) => text(await toolSearch(a))
|
|
@@ -1196,13 +1584,13 @@ function createServer() {
|
|
|
1196
1584
|
);
|
|
1197
1585
|
server.registerTool(
|
|
1198
1586
|
"cat",
|
|
1199
|
-
{ title: "Read a page", description:
|
|
1587
|
+
{ title: "Read a page", description: `A page's markdown by its path from search or ls: frontmatter with evidence and links, then the body. A page over ${B} characters comes back compact with its section keys; section (or 'frontmatter'), offset and limit read one part. A symbol path ('objects/codeunit/80#event-OnAfterPostSalesDoc') returns that member's row.`, inputSchema: { path: z.string(), ...VIEW } },
|
|
1200
1588
|
async (a) => text(await toolCat(a))
|
|
1201
1589
|
);
|
|
1202
1590
|
server.registerTool("get_object", {
|
|
1203
1591
|
title: "Look up an AL object",
|
|
1204
|
-
description:
|
|
1205
|
-
inputSchema: { type: z.string(), idOrName: z.string(), country: z.string().length(2).optional() }
|
|
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.`,
|
|
1593
|
+
inputSchema: { type: z.string(), idOrName: z.string(), country: z.string().length(2).optional(), ...VIEW }
|
|
1206
1594
|
}, async (a) => text(await toolGetObject(a)));
|
|
1207
1595
|
server.registerTool("diff_object", {
|
|
1208
1596
|
title: "What changed in an object",
|
|
@@ -1211,12 +1599,12 @@ function createServer() {
|
|
|
1211
1599
|
}, async (a) => text(await toolDiffObject(a)));
|
|
1212
1600
|
server.registerTool(
|
|
1213
1601
|
"localization",
|
|
1214
|
-
{ title: "A country localization", description:
|
|
1602
|
+
{ title: "A country localization", description: `What a country layer (BE, NL, DE, ...) adds to or changes in W1, with its Learn local functionality hub. Over ${B} characters it comes back compact; section, offset and limit read one section, as in cat.`, inputSchema: { country: z.string().length(2), ...VIEW } },
|
|
1215
1603
|
async (a) => text(await toolLocalization(a))
|
|
1216
1604
|
);
|
|
1217
1605
|
server.registerTool("whats_new", {
|
|
1218
1606
|
title: "What is new",
|
|
1219
|
-
description: "Videos, posts, roadmap features, AL extension releases (type 'release') and
|
|
1607
|
+
description: "Videos, posts, roadmap features, AL extension releases (type 'release') and merged pull requests of BCApps, AL-Go and BCQuality (type 'change') dated on or after a date (YYYY-MM-DD) up to today, newest first. A roadmap feature's date is its GA month: features after today are counted at the end and listed with include_future: true.",
|
|
1220
1608
|
inputSchema: { since: z.string(), type: z.string().optional(), limit: z.number().int().min(1).max(100).optional(), include_future: z.boolean().optional() }
|
|
1221
1609
|
}, async (a) => text(await toolWhatsNew(a)));
|
|
1222
1610
|
server.registerTool("blog_footprint", {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bc-observatory",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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": {
|