bc-observatory 0.3.0 → 0.4.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 +367 -10
- 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
|
@@ -891,10 +891,363 @@ function dot(a, b, bOffset = 0) {
|
|
|
891
891
|
return s;
|
|
892
892
|
}
|
|
893
893
|
|
|
894
|
+
// src/sections.ts
|
|
895
|
+
var DEFAULT_BUDGET = 3e4;
|
|
896
|
+
var MIN_BUDGET = 5e3;
|
|
897
|
+
var PREVIEW_ROWS = 5;
|
|
898
|
+
var ROW_CLIP = 200;
|
|
899
|
+
var PROSE_CLIP = 800;
|
|
900
|
+
var HEAD_LINE_MAX = 120;
|
|
901
|
+
var FM_LIST_CHARS = 600;
|
|
902
|
+
var FM_LIST_MIN = 3;
|
|
903
|
+
var RESERVE = 300;
|
|
904
|
+
var OBJECT_KEYS = {
|
|
905
|
+
"Properties": "properties",
|
|
906
|
+
"Fields": "fields",
|
|
907
|
+
"Fields on this page": "controls",
|
|
908
|
+
"Actions": "actions",
|
|
909
|
+
"Keys": "keys",
|
|
910
|
+
"Values": "values",
|
|
911
|
+
"Events published": "events",
|
|
912
|
+
"Trigger event subscribers": "trigger_subscribers",
|
|
913
|
+
"Event subscriptions": "subscriptions",
|
|
914
|
+
"Procedures": "procedures",
|
|
915
|
+
"Triggers": "triggers",
|
|
916
|
+
"Relations": "relations",
|
|
917
|
+
"Referenced by": "referenced_by",
|
|
918
|
+
"Pages and codeunits on this table": "pages",
|
|
919
|
+
"Extended by": "extended_by",
|
|
920
|
+
"Calls": "calls",
|
|
921
|
+
"Called by": "called_by",
|
|
922
|
+
"Implements": "implements",
|
|
923
|
+
"Implemented by": "implemented_by",
|
|
924
|
+
"Recent changes": "changes",
|
|
925
|
+
"Across versions": "versions",
|
|
926
|
+
"Countries that replace it": "countries",
|
|
927
|
+
"Documented on Microsoft Learn": "learn",
|
|
928
|
+
"Deprecations": "deprecations",
|
|
929
|
+
"Ask your agent": "ask"
|
|
930
|
+
};
|
|
931
|
+
var snake = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
932
|
+
var blank = (l) => l.trim() === "";
|
|
933
|
+
var fmt = (n) => n.toLocaleString("en-US");
|
|
934
|
+
var FENCE = /^\s*```/;
|
|
935
|
+
var SEPARATOR = /^\|\s*:?-/;
|
|
936
|
+
var CAP_NOTE = /^- and \d+ more/;
|
|
937
|
+
function budgetOf(env) {
|
|
938
|
+
const n = env === void 0 || env.trim() === "" ? NaN : Number(env);
|
|
939
|
+
if (!Number.isFinite(n) || n < 0) return DEFAULT_BUDGET;
|
|
940
|
+
if (n === 0) return Infinity;
|
|
941
|
+
return Math.max(MIN_BUDGET, Math.floor(n));
|
|
942
|
+
}
|
|
943
|
+
function sectionKey(heading) {
|
|
944
|
+
return OBJECT_KEYS[heading] ?? snake(heading);
|
|
945
|
+
}
|
|
946
|
+
function splitSection(lines) {
|
|
947
|
+
const head = [], rows = [];
|
|
948
|
+
let pending = [];
|
|
949
|
+
let table2 = null, group = null;
|
|
950
|
+
let inTable = false, afterRow = false, fence = null;
|
|
951
|
+
const other = (line2, kind = "text") => {
|
|
952
|
+
if (rows.length) pending.push({ line: line2, kind });
|
|
953
|
+
else head.push(line2);
|
|
954
|
+
};
|
|
955
|
+
const addRow = (first) => {
|
|
956
|
+
if (rows.length) {
|
|
957
|
+
const prev = rows[rows.length - 1];
|
|
958
|
+
for (const p of pending) {
|
|
959
|
+
if (p.kind === "group" && p.line === group) continue;
|
|
960
|
+
if (p.kind === "table" && table2 && (p.line === table2[0] || p.line === table2[1])) continue;
|
|
961
|
+
if (!blank(p.line)) prev.lines.push(p.line);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
pending = [];
|
|
965
|
+
rows.push({ lines: [first], table: table2, group });
|
|
966
|
+
afterRow = true;
|
|
967
|
+
};
|
|
968
|
+
for (let i = 0; i < lines.length; i++) {
|
|
969
|
+
const l = lines[i];
|
|
970
|
+
if (fence) {
|
|
971
|
+
if (fence === "row") rows[rows.length - 1].lines.push(l);
|
|
972
|
+
else other(l);
|
|
973
|
+
if (FENCE.test(l)) fence = null;
|
|
974
|
+
continue;
|
|
975
|
+
}
|
|
976
|
+
if (FENCE.test(l)) {
|
|
977
|
+
if (afterRow && /^ {2,}/.test(l)) {
|
|
978
|
+
rows[rows.length - 1].lines.push(l);
|
|
979
|
+
fence = "row";
|
|
980
|
+
} else {
|
|
981
|
+
other(l);
|
|
982
|
+
fence = "text";
|
|
983
|
+
afterRow = false;
|
|
984
|
+
inTable = false;
|
|
985
|
+
table2 = null;
|
|
986
|
+
}
|
|
987
|
+
continue;
|
|
988
|
+
}
|
|
989
|
+
if (afterRow && /^ {2,}\S/.test(l)) {
|
|
990
|
+
rows[rows.length - 1].lines.push(l);
|
|
991
|
+
continue;
|
|
992
|
+
}
|
|
993
|
+
if (l.startsWith("|")) {
|
|
994
|
+
if (!inTable && i + 1 < lines.length && SEPARATOR.test(lines[i + 1])) {
|
|
995
|
+
table2 = [l, lines[i + 1]];
|
|
996
|
+
inTable = true;
|
|
997
|
+
afterRow = false;
|
|
998
|
+
other(l, "table");
|
|
999
|
+
other(lines[++i], "table");
|
|
1000
|
+
continue;
|
|
1001
|
+
}
|
|
1002
|
+
if (inTable) {
|
|
1003
|
+
addRow(l);
|
|
1004
|
+
continue;
|
|
1005
|
+
}
|
|
1006
|
+
} else if (inTable) {
|
|
1007
|
+
inTable = false;
|
|
1008
|
+
table2 = null;
|
|
1009
|
+
}
|
|
1010
|
+
if (l.startsWith("- ") && !CAP_NOTE.test(l)) {
|
|
1011
|
+
addRow(l);
|
|
1012
|
+
continue;
|
|
1013
|
+
}
|
|
1014
|
+
afterRow = false;
|
|
1015
|
+
if (l.startsWith("### ")) {
|
|
1016
|
+
group = l;
|
|
1017
|
+
other(l, "group");
|
|
1018
|
+
continue;
|
|
1019
|
+
}
|
|
1020
|
+
other(l);
|
|
1021
|
+
}
|
|
1022
|
+
const tail = pending.map((p) => p.line);
|
|
1023
|
+
return { head, rows, tail };
|
|
1024
|
+
}
|
|
1025
|
+
var memo = null;
|
|
1026
|
+
function parsePage(md) {
|
|
1027
|
+
if (memo && memo.md === md) return memo.page;
|
|
1028
|
+
const all = md.split("\n");
|
|
1029
|
+
let i = 0, frontmatter = "";
|
|
1030
|
+
if (all[0] === "---") {
|
|
1031
|
+
const end = all.indexOf("---", 1);
|
|
1032
|
+
if (end > 0) {
|
|
1033
|
+
frontmatter = all.slice(1, end).join("\n");
|
|
1034
|
+
i = end + 1;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
const pre = [], chunks = [];
|
|
1038
|
+
let fence = false;
|
|
1039
|
+
for (; i < all.length; i++) {
|
|
1040
|
+
const l = all[i];
|
|
1041
|
+
if (!fence && l.startsWith("## ")) {
|
|
1042
|
+
chunks.push({ heading: l.slice(3).trim(), lines: [] });
|
|
1043
|
+
continue;
|
|
1044
|
+
}
|
|
1045
|
+
if (FENCE.test(l)) fence = !fence;
|
|
1046
|
+
(chunks.length ? chunks[chunks.length - 1].lines : pre).push(l);
|
|
1047
|
+
}
|
|
1048
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1049
|
+
const sections = chunks.map(({ heading, lines }) => {
|
|
1050
|
+
while (lines.length && blank(lines[lines.length - 1])) lines.pop();
|
|
1051
|
+
const base = sectionKey(heading), n = (seen.get(base) ?? 0) + 1;
|
|
1052
|
+
seen.set(base, n);
|
|
1053
|
+
return { heading, key: n === 1 ? base : `${base}_${n}`, lines, ...splitSection(lines) };
|
|
1054
|
+
});
|
|
1055
|
+
const preamble = pre.join("\n").replace(/^\s*\n/, "").replace(/\s+$/, "");
|
|
1056
|
+
const page = { frontmatter, preamble, sections };
|
|
1057
|
+
memo = { md, page };
|
|
1058
|
+
return page;
|
|
1059
|
+
}
|
|
1060
|
+
function frontmatterSection(p) {
|
|
1061
|
+
const lines = p.frontmatter ? p.frontmatter.split("\n") : [];
|
|
1062
|
+
return { heading: "", key: "frontmatter", lines, head: [], rows: lines.map((l) => ({ lines: [l], table: null, group: null })), tail: [] };
|
|
1063
|
+
}
|
|
1064
|
+
function findSection(p, want) {
|
|
1065
|
+
const w = want.trim().toLowerCase(), ws = snake(want);
|
|
1066
|
+
if (w === "frontmatter") return frontmatterSection(p);
|
|
1067
|
+
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;
|
|
1068
|
+
}
|
|
1069
|
+
var keyList = (p, counts = false) => [...p.sections.map((s) => counts && s.rows.length ? `${s.key} (${s.rows.length})` : s.key), "frontmatter"].join(", ");
|
|
1070
|
+
var titleOf = (p, path) => p.preamble.split("\n").find((l) => l.startsWith("# "))?.slice(2).trim() || path;
|
|
1071
|
+
function clip(s, n, mark = "\u2026") {
|
|
1072
|
+
if (s.length <= n) return s;
|
|
1073
|
+
const end = /[\uD800-\uDBFF]/.test(s[n - 2]) ? n - 2 : n - 1;
|
|
1074
|
+
return `${s.slice(0, end)}${mark}`;
|
|
1075
|
+
}
|
|
1076
|
+
var indentOf = (l) => l.length - l.trimStart().length;
|
|
1077
|
+
var BLOCK_SCALAR = /(?:^|:|-)\s*[|>][-+0-9]*\s*$/;
|
|
1078
|
+
var LIST_ITEM = /^( *)-(?: |$)/;
|
|
1079
|
+
function cutLists(lines) {
|
|
1080
|
+
const out = [];
|
|
1081
|
+
let i = 0;
|
|
1082
|
+
while (i < lines.length) {
|
|
1083
|
+
const l = lines[i], m = LIST_ITEM.exec(l);
|
|
1084
|
+
if (!m) {
|
|
1085
|
+
out.push(l);
|
|
1086
|
+
i++;
|
|
1087
|
+
if (BLOCK_SCALAR.test(l)) {
|
|
1088
|
+
const ind2 = indentOf(l);
|
|
1089
|
+
while (i < lines.length && (blank(lines[i]) || indentOf(lines[i]) > ind2)) out.push(lines[i++]);
|
|
1090
|
+
}
|
|
1091
|
+
continue;
|
|
1092
|
+
}
|
|
1093
|
+
const ind = m[1].length, items = [];
|
|
1094
|
+
while (i < lines.length && indentOf(lines[i]) === ind && LIST_ITEM.test(lines[i])) {
|
|
1095
|
+
const item = [lines[i++]];
|
|
1096
|
+
while (i < lines.length && (indentOf(lines[i]) > ind || blank(lines[i]) && i + 1 < lines.length && indentOf(lines[i + 1]) > ind)) item.push(lines[i++]);
|
|
1097
|
+
items.push(BLOCK_SCALAR.test(item[0]) ? item : [item[0], ...cutLists(item.slice(1))]);
|
|
1098
|
+
}
|
|
1099
|
+
const size = (k2) => items.slice(0, k2).map((it) => it.join("\n")).join("\n").length;
|
|
1100
|
+
if (size(items.length) <= FM_LIST_CHARS) {
|
|
1101
|
+
out.push(...items.flat());
|
|
1102
|
+
continue;
|
|
1103
|
+
}
|
|
1104
|
+
let k = 0;
|
|
1105
|
+
while (k < items.length && size(k + 1) <= FM_LIST_CHARS) k++;
|
|
1106
|
+
k = Math.max(k, FM_LIST_MIN);
|
|
1107
|
+
out.push(...items.slice(0, k).flat());
|
|
1108
|
+
if (k < items.length) out.push(`${" ".repeat(ind)}# \u2026 ${items.length - k} more; section: "frontmatter"`);
|
|
1109
|
+
}
|
|
1110
|
+
return out;
|
|
1111
|
+
}
|
|
1112
|
+
function compactFrontmatter(yaml) {
|
|
1113
|
+
const lines = yaml.split("\n"), kept = [];
|
|
1114
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1115
|
+
if (/^generated:/.test(lines[i])) {
|
|
1116
|
+
while (i + 1 < lines.length && (indentOf(lines[i + 1]) > 0 || blank(lines[i + 1]))) i++;
|
|
1117
|
+
continue;
|
|
1118
|
+
}
|
|
1119
|
+
kept.push(lines[i]);
|
|
1120
|
+
}
|
|
1121
|
+
return cutLists(kept).join("\n");
|
|
1122
|
+
}
|
|
1123
|
+
function compactView(md, path, budget) {
|
|
1124
|
+
const p = parsePage(md);
|
|
1125
|
+
const out = [];
|
|
1126
|
+
if (md.startsWith("---\n") && p.frontmatter !== "") out.push("---", compactFrontmatter(p.frontmatter), "---", "");
|
|
1127
|
+
if (p.preamble) out.push(p.preamble, "");
|
|
1128
|
+
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.`, "");
|
|
1129
|
+
for (const s of p.sections) {
|
|
1130
|
+
out.push(`## ${s.heading}`);
|
|
1131
|
+
const more = `\u2026 (section: "${s.key}")`;
|
|
1132
|
+
if (!s.rows.length) {
|
|
1133
|
+
const text2 = s.head.filter((l) => !blank(l)).join("\n");
|
|
1134
|
+
if (text2) out.push(clip(text2, PROSE_CLIP, more));
|
|
1135
|
+
} else {
|
|
1136
|
+
const lead = s.head.find((l) => !blank(l) && !l.startsWith("|") && !l.startsWith("### "));
|
|
1137
|
+
if (lead && lead.length <= HEAD_LINE_MAX) out.push(lead);
|
|
1138
|
+
let group = null, table2 = null;
|
|
1139
|
+
for (const r of s.rows.slice(0, PREVIEW_ROWS)) {
|
|
1140
|
+
if (r.group && r.group !== group) {
|
|
1141
|
+
out.push(r.group);
|
|
1142
|
+
group = r.group;
|
|
1143
|
+
}
|
|
1144
|
+
const t = r.table ? r.table.join("\n") : null;
|
|
1145
|
+
if (r.table && t !== table2) out.push(...r.table);
|
|
1146
|
+
table2 = t;
|
|
1147
|
+
out.push(clip(r.lines[0], ROW_CLIP));
|
|
1148
|
+
}
|
|
1149
|
+
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
|
+
else {
|
|
1151
|
+
const text2 = s.tail.filter((l) => !blank(l)).join("\n");
|
|
1152
|
+
if (text2) out.push(clip(text2, PROSE_CLIP, more));
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
out.push("");
|
|
1156
|
+
}
|
|
1157
|
+
return `${out.join("\n").replace(/\n{3,}/g, "\n\n").replace(/\n+$/, "")}
|
|
1158
|
+
`;
|
|
1159
|
+
}
|
|
1160
|
+
var dropLeadingBlank = (lines) => {
|
|
1161
|
+
let i = 0;
|
|
1162
|
+
while (i < lines.length && blank(lines[i])) i++;
|
|
1163
|
+
return lines.slice(i);
|
|
1164
|
+
};
|
|
1165
|
+
function sectionView(md, path, want, offset, limit, budget, site) {
|
|
1166
|
+
const p = parsePage(md);
|
|
1167
|
+
const nl = (lines) => `${lines.join("\n")}
|
|
1168
|
+
`;
|
|
1169
|
+
const s = findSection(p, want);
|
|
1170
|
+
if (!s) return `No section "${want}" on ${path}. Sections: ${keyList(p)}.`;
|
|
1171
|
+
const fm = s.key === "frontmatter" && s.heading === "";
|
|
1172
|
+
const first = `${titleOf(p, path)}, section "${s.key}": ${site}${path}/`;
|
|
1173
|
+
const heading = fm ? [] : [`## ${s.heading}`];
|
|
1174
|
+
const n = s.rows.length, off = Math.max(0, Math.floor(offset ?? 0));
|
|
1175
|
+
const verbatim = nl(fm ? [first, "", "---", ...s.lines, "---"] : [first, "", ...heading, ...s.lines]);
|
|
1176
|
+
if (!n || off === 0 && limit === void 0 && verbatim.length <= budget) return verbatim;
|
|
1177
|
+
const tail = dropLeadingBlank(s.tail);
|
|
1178
|
+
if (off >= n) return nl([first, "", ...heading, "", ...tail.length ? tail : [`No rows after ${n} in section "${s.key}" of ${path}.`]]);
|
|
1179
|
+
const out = [first, "", ...heading, ...fm ? [] : [""]];
|
|
1180
|
+
let group = null, table2 = null;
|
|
1181
|
+
if (off === 0) {
|
|
1182
|
+
out.push(...dropLeadingBlank(s.head));
|
|
1183
|
+
group = s.rows[0].group;
|
|
1184
|
+
table2 = s.rows[0].table ? s.rows[0].table.join("\n") : null;
|
|
1185
|
+
}
|
|
1186
|
+
let len = out.join("\n").length, b = off;
|
|
1187
|
+
const end = limit === void 0 ? n : Math.min(n, off + Math.max(1, Math.floor(limit)));
|
|
1188
|
+
for (let r = off; r < end; r++) {
|
|
1189
|
+
const row = s.rows[r], add = [];
|
|
1190
|
+
let g = group, t = table2;
|
|
1191
|
+
if (row.group && row.group !== g) {
|
|
1192
|
+
add.push(...out.length && !blank(out[out.length - 1]) ? [""] : [], row.group, "");
|
|
1193
|
+
g = row.group;
|
|
1194
|
+
t = null;
|
|
1195
|
+
}
|
|
1196
|
+
const rt = row.table ? row.table.join("\n") : null;
|
|
1197
|
+
if (row.table && rt !== t) add.push(...row.table);
|
|
1198
|
+
t = rt;
|
|
1199
|
+
add.push(...row.lines);
|
|
1200
|
+
const size = add.join("\n").length + 1;
|
|
1201
|
+
if (r > off && len + size > budget - RESERVE) break;
|
|
1202
|
+
out.push(...add);
|
|
1203
|
+
len += size;
|
|
1204
|
+
group = g;
|
|
1205
|
+
table2 = t;
|
|
1206
|
+
b = r + 1;
|
|
1207
|
+
}
|
|
1208
|
+
const rows = `Rows ${off + 1}-${b} of ${n}.`;
|
|
1209
|
+
if (b < n) return nl([...out, "", `${rows} Next: cat("${path}", section: "${s.key}", offset: ${b})`]);
|
|
1210
|
+
if (!tail.length) return nl([...out, "", rows]);
|
|
1211
|
+
const withTail = nl([...out, "", ...tail, "", rows]);
|
|
1212
|
+
if (limit === void 0 && withTail.length <= budget) return withTail;
|
|
1213
|
+
return nl([...out, "", `${rows} The section's closing text: cat("${path}", section: "${s.key}", offset: ${n})`]);
|
|
1214
|
+
}
|
|
1215
|
+
var ANCHOR_SECTION = { field: ["fields", "field"], value: ["values", "value"], event: ["events", "event"], proc: ["procedures", "procedure"] };
|
|
1216
|
+
function anchorView(md, path, anchor, site) {
|
|
1217
|
+
const a = anchor.replace(/^#/, ""), m = /^(field|value|event|proc)-(.+)$/.exec(a);
|
|
1218
|
+
if (!m) return null;
|
|
1219
|
+
const [key, label] = ANCHOR_SECTION[m[1]];
|
|
1220
|
+
const p = parsePage(md);
|
|
1221
|
+
const s = p.sections.find((x) => x.key === key);
|
|
1222
|
+
if (!s) return null;
|
|
1223
|
+
const match = m[1] === "field" || m[1] === "value" ? (l) => l.startsWith(`| ${m[2]} |`) : (l) => l.startsWith(`- \`${m[2]}(`);
|
|
1224
|
+
const rows = s.rows.filter((r) => match(r.lines[0]));
|
|
1225
|
+
if (!rows.length) return null;
|
|
1226
|
+
return `${[`${titleOf(p, path)}, ${label} ${m[2]}: ${site}${path}/#${a}`, "", `## ${s.heading}`, ...rows[0].table ?? [], ...rows.flatMap((r) => r.lines)].join("\n")}
|
|
1227
|
+
`;
|
|
1228
|
+
}
|
|
1229
|
+
function pageView(md, path, a, budget, site) {
|
|
1230
|
+
const hash = path.indexOf("#");
|
|
1231
|
+
if (hash >= 0) {
|
|
1232
|
+
const clean = path.slice(0, hash), anchor = path.slice(hash);
|
|
1233
|
+
return anchorView(md, clean, anchor, site) ?? `(No row for ${anchor} on this page.)
|
|
1234
|
+
|
|
1235
|
+
${pageView(md, clean, {}, budget, site)}`;
|
|
1236
|
+
}
|
|
1237
|
+
if (a.section !== void 0 && a.section.trim() !== "") return sectionView(md, path, a.section, a.offset, a.limit, budget, site);
|
|
1238
|
+
if (a.offset !== void 0 || a.limit !== void 0) {
|
|
1239
|
+
return `offset and limit read one section: cat("${path}", section: "<key>", offset: ${a.offset ?? 0}). Sections: ${keyList(parsePage(md))}.`;
|
|
1240
|
+
}
|
|
1241
|
+
if (md.length <= budget) return md;
|
|
1242
|
+
return compactView(md, path, budget);
|
|
1243
|
+
}
|
|
1244
|
+
|
|
894
1245
|
// src/server.ts
|
|
895
|
-
var VERSION = "0.
|
|
1246
|
+
var VERSION = "0.4.0";
|
|
896
1247
|
var SITE = (process.env.BC_OBSERVATORY_SITE ?? "https://waldo1001.github.io/waldo.BCObservatory/").replace(/\/?$/, "/");
|
|
897
1248
|
var LOCAL = process.env.BC_OBSERVATORY_LOCAL ? resolve(process.env.BC_OBSERVATORY_LOCAL) : null;
|
|
1249
|
+
var BUDGET = budgetOf(process.env.BC_OBSERVATORY_MAX_CHARS);
|
|
1250
|
+
var B = (Number.isFinite(BUDGET) ? BUDGET : DEFAULT_BUDGET).toLocaleString("en-US");
|
|
898
1251
|
var REPO = "https://github.com/waldo1001/waldo.BCObservatory";
|
|
899
1252
|
var DAY = 864e5;
|
|
900
1253
|
var cacheDir = join(process.env.XDG_CACHE_HOME ?? join(homedir(), ".cache"), "bc-observatory", createHash("sha256").update(SITE).digest("hex").slice(0, 12));
|
|
@@ -1082,7 +1435,9 @@ async function toolLs(a) {
|
|
|
1082
1435
|
... and ${files.length - MAX} more pages; use search() to narrow down` : ""}`;
|
|
1083
1436
|
}
|
|
1084
1437
|
async function toolCat(a) {
|
|
1085
|
-
|
|
1438
|
+
const [base, anchor] = a.path.split(/(?=#)/);
|
|
1439
|
+
const clean = `${base.replace(/^\/+|\/+$/g, "").replace(/\.md$/, "")}${anchor ?? ""}`;
|
|
1440
|
+
return pageView(await pageMarkdown(clean.replace(/#.*$/, "")), clean, a, BUDGET, SITE);
|
|
1086
1441
|
}
|
|
1087
1442
|
async function toolGetObject(a) {
|
|
1088
1443
|
await loadIndex();
|
|
@@ -1123,7 +1478,7 @@ async function toolGetObject(a) {
|
|
|
1123
1478
|
const also = !country && twins.length ? `
|
|
1124
1479
|
|
|
1125
1480
|
(Also in ${twins.join(", ")}: get_object("${type}", "${pick[0].objectId ?? pick[0].name}", country).)` : "";
|
|
1126
|
-
return `${note}${await pageMarkdown(pick[0].id)}${also}`;
|
|
1481
|
+
return `${note}${pageView(await pageMarkdown(pick[0].id), pick[0].id, a, BUDGET, SITE)}${a.section ? "" : also}`;
|
|
1127
1482
|
}
|
|
1128
1483
|
if (pick.length > 1) return `${pick.length} ${type} objects match "${a.idOrName}"; read one with cat(path):
|
|
1129
1484
|
${pick.map((r) => `- ${r.title} path=${r.id}`).join("\n")}`;
|
|
@@ -1144,7 +1499,8 @@ async function toolDiffObject(a) {
|
|
|
1144
1499
|
${JSON.stringify(o, null, 1)}`;
|
|
1145
1500
|
}
|
|
1146
1501
|
async function toolLocalization(a) {
|
|
1147
|
-
|
|
1502
|
+
const p = `localizations/${a.country.toLowerCase()}`;
|
|
1503
|
+
return pageView(await pageMarkdown(p), p, a, BUDGET, SITE);
|
|
1148
1504
|
}
|
|
1149
1505
|
async function toolWhatsNew(a, today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)) {
|
|
1150
1506
|
await loadIndex();
|
|
@@ -1176,6 +1532,7 @@ ${a.message}
|
|
|
1176
1532
|
(sent from the bc-observatory MCP server ${VERSION})`;
|
|
1177
1533
|
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
1534
|
}
|
|
1535
|
+
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
1536
|
function createServer() {
|
|
1180
1537
|
const server = new McpServer({ name: "bc-observatory", version: VERSION }, {
|
|
1181
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)."
|
|
@@ -1184,7 +1541,7 @@ function createServer() {
|
|
|
1184
1541
|
"search",
|
|
1185
1542
|
{
|
|
1186
1543
|
title: "Search the knowledge base",
|
|
1187
|
-
description: "Search every page
|
|
1544
|
+
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. 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, 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
1545
|
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
1546
|
},
|
|
1190
1547
|
async (a) => text(await toolSearch(a))
|
|
@@ -1196,13 +1553,13 @@ function createServer() {
|
|
|
1196
1553
|
);
|
|
1197
1554
|
server.registerTool(
|
|
1198
1555
|
"cat",
|
|
1199
|
-
{ title: "Read a page", description:
|
|
1556
|
+
{ 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
1557
|
async (a) => text(await toolCat(a))
|
|
1201
1558
|
);
|
|
1202
1559
|
server.registerTool("get_object", {
|
|
1203
1560
|
title: "Look up an AL object",
|
|
1204
|
-
description:
|
|
1205
|
-
inputSchema: { type: z.string(), idOrName: z.string(), country: z.string().length(2).optional() }
|
|
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.`,
|
|
1562
|
+
inputSchema: { type: z.string(), idOrName: z.string(), country: z.string().length(2).optional(), ...VIEW }
|
|
1206
1563
|
}, async (a) => text(await toolGetObject(a)));
|
|
1207
1564
|
server.registerTool("diff_object", {
|
|
1208
1565
|
title: "What changed in an object",
|
|
@@ -1211,12 +1568,12 @@ function createServer() {
|
|
|
1211
1568
|
}, async (a) => text(await toolDiffObject(a)));
|
|
1212
1569
|
server.registerTool(
|
|
1213
1570
|
"localization",
|
|
1214
|
-
{ title: "A country localization", description:
|
|
1571
|
+
{ 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
1572
|
async (a) => text(await toolLocalization(a))
|
|
1216
1573
|
);
|
|
1217
1574
|
server.registerTool("whats_new", {
|
|
1218
1575
|
title: "What is new",
|
|
1219
|
-
description: "Videos, posts, roadmap features, AL extension releases (type 'release') and
|
|
1576
|
+
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
1577
|
inputSchema: { since: z.string(), type: z.string().optional(), limit: z.number().int().min(1).max(100).optional(), include_future: z.boolean().optional() }
|
|
1221
1578
|
}, async (a) => text(await toolWhatsNew(a)));
|
|
1222
1579
|
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.4.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": {
|