d365fo-mcp 1.10.1 → 1.12.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/dist/bridge/bridgeAdapter.js +37 -2
- package/dist/cli/commands/bpCatalog.d.ts +25 -0
- package/dist/cli/commands/bpCatalog.js +287 -0
- package/dist/cli/commands/doctor.d.ts +52 -1
- package/dist/cli/commands/doctor.js +156 -11
- package/dist/cli/commands/indexCmd.js +4 -0
- package/dist/cli/context.d.ts +8 -0
- package/dist/cli/context.js +8 -0
- package/dist/cli/settingsStore.d.ts +7 -0
- package/dist/cli/settingsStore.js +20 -0
- package/dist/cli/xppConfig.d.ts +34 -0
- package/dist/cli/xppConfig.js +48 -0
- package/dist/config/configFile.d.ts +6 -0
- package/dist/config/configFile.js +6 -0
- package/dist/config/settings.d.ts +18 -9
- package/dist/config/settings.js +44 -11
- package/dist/index.js +20 -2
- package/dist/knowledge/bpMonikers/catalog.generated.d.ts +31 -0
- package/dist/knowledge/bpMonikers/catalog.generated.js +600 -0
- package/dist/knowledge/bpMonikers/index.d.ts +151 -0
- package/dist/knowledge/bpMonikers/index.js +279 -0
- package/dist/knowledge/tableDataMethods.d.ts +47 -0
- package/dist/knowledge/tableDataMethods.js +159 -0
- package/dist/metadata/labelParser.js +1 -1
- package/dist/metadata/symbolIndex.d.ts +68 -3
- package/dist/metadata/symbolIndex.js +123 -28
- package/dist/metadata/types.d.ts +21 -3
- package/dist/metadata/xmlParser.d.ts +13 -0
- package/dist/metadata/xmlParser.js +32 -7
- package/dist/metadata/xppDeclaration.d.ts +18 -0
- package/dist/metadata/xppDeclaration.js +14 -0
- package/dist/middleware/apiKeyAuth.d.ts +37 -0
- package/dist/middleware/apiKeyAuth.js +85 -2
- package/dist/middleware/rateLimiter.js +6 -0
- package/dist/prompts/codeReview.js +1 -1
- package/dist/prompts/systemInstructions.js +1 -1
- package/dist/scripts/build-database.js +147 -27
- package/dist/scripts/build-fts.js +147 -27
- package/dist/scripts/extract-metadata.js +60 -13
- package/dist/server/toolSchemas/getKnowledge.d.ts +17 -0
- package/dist/server/toolSchemas/getKnowledge.js +25 -3
- package/dist/server/toolSchemas/index.d.ts +17 -0
- package/dist/tools/analysis/labelSearchHistory.d.ts +27 -1
- package/dist/tools/analysis/labelSearchHistory.js +58 -1
- package/dist/tools/analysis/prefixDiagnostics.js +1 -1
- package/dist/tools/analysis/searchLabels.js +8 -2
- package/dist/tools/analysis/validateFormPattern.d.ts +13 -0
- package/dist/tools/analysis/validateFormPattern.js +49 -16
- package/dist/tools/analysis/validateObjectNaming.js +24 -12
- package/dist/tools/analysis/validateXpp.d.ts +2 -0
- package/dist/tools/analysis/validateXpp.js +178 -0
- package/dist/tools/knowledge/bpMonikerHelp.d.ts +32 -0
- package/dist/tools/knowledge/bpMonikerHelp.js +149 -0
- package/dist/tools/knowledge/d365foErrorHelp.js +2 -1
- package/dist/tools/knowledge/extensionStrategyAdvisor.js +4 -3
- package/dist/tools/knowledge/getKnowledge.d.ts +9 -7
- package/dist/tools/knowledge/getKnowledge.js +21 -8
- package/dist/tools/knowledge/methodSignature.js +27 -1
- package/dist/tools/knowledge/xppKnowledge.d.ts +2 -0
- package/dist/tools/knowledge/xppKnowledge.js +177 -11
- package/dist/tools/labels.js +24 -4
- package/dist/tools/prepare/prepareChange.js +16 -4
- package/dist/tools/prepare/prepareCreate.js +8 -0
- package/dist/tools/readers/classInfo.js +34 -5
- package/dist/tools/readers/getMethod.js +8 -3
- package/dist/tools/readers/getObjectInfo.js +17 -1
- package/dist/tools/readers/getWorkspaceInfo.js +9 -4
- package/dist/tools/sdlc/buildProject.d.ts +9 -0
- package/dist/tools/sdlc/buildProject.js +124 -20
- package/dist/tools/sdlc/runBpCheck.d.ts +33 -1
- package/dist/tools/sdlc/runBpCheck.js +167 -18
- package/dist/tools/sdlc/undoLastModification.js +26 -8
- package/dist/tools/sdlc/updateSymbolIndex.d.ts +0 -13
- package/dist/tools/sdlc/updateSymbolIndex.js +87 -2
- package/dist/tools/smart/codeGen.js +5 -4
- package/dist/tools/smart/generateSmartForm.js +13 -2
- package/dist/tools/specs/d365foFileOpSpecs.js +15 -3
- package/dist/tools/specs/opSpecs.js +1 -1
- package/dist/tools/write/createD365File.js +13 -4
- package/dist/tools/write/modifyD365File.d.ts +16 -0
- package/dist/tools/write/modifyD365File.js +186 -72
- package/dist/tools/write/resolveReferences.js +1 -1
- package/dist/tools/xml/generateMetadata.d.ts +6 -2
- package/dist/tools/xml/generateMetadata.js +13 -4
- package/dist/utils/buildMarker.d.ts +17 -2
- package/dist/utils/buildMarker.js +27 -12
- package/dist/utils/effectivePrefix.js +1 -1
- package/dist/utils/formExtensionControlXml.d.ts +147 -0
- package/dist/utils/formExtensionControlXml.js +706 -0
- package/dist/utils/formExtensionShapeValidator.js +15 -0
- package/dist/utils/loadEnv.d.ts +4 -3
- package/dist/utils/loadEnv.js +5 -4
- package/dist/utils/methodBodyHint.d.ts +50 -0
- package/dist/utils/methodBodyHint.js +54 -0
- package/dist/utils/modelClassifier.js +10 -3
- package/dist/utils/modelToken.d.ts +30 -0
- package/dist/utils/modelToken.js +32 -0
- package/dist/utils/objectNaming.js +46 -10
- package/dist/utils/packagesRoot.d.ts +31 -0
- package/dist/utils/packagesRoot.js +33 -0
- package/dist/workspace/contextRanker.js +7 -2
- package/package.json +3 -2
- package/scripts/extract-bp-catalog.ps1 +321 -0
|
@@ -25,8 +25,9 @@ import * as debouncedRefresh from './debouncedRefresh.js';
|
|
|
25
25
|
import { debugLog } from '../utils/logger.js';
|
|
26
26
|
import { xppMethodSourceForXml } from '../utils/xppFormat.js';
|
|
27
27
|
import { ensureXppDocComment, ensureBlankLineBeforeClosingBrace } from '../utils/xppDocGen.js';
|
|
28
|
-
import { parseXppDeclaration } from '../metadata/xppDeclaration.js';
|
|
28
|
+
import { parseXppDeclaration, parseXppClassHeader } from '../metadata/xppDeclaration.js';
|
|
29
29
|
import { rankCustomFirst, isExactNameMatch } from '../utils/exactMatchRanking.js';
|
|
30
|
+
import { COMPACT_METHODS_HINT, fullBodyHint } from '../utils/methodBodyHint.js';
|
|
30
31
|
import { pageFields, fieldsHeading, fieldsFooter, createControlBudget, chargeControl, chargeSkippedSubtree, controlsFooter, } from '../utils/payloadBudget.js';
|
|
31
32
|
// TABLE
|
|
32
33
|
const TABLE_METHOD_PAGE_SIZE = 25;
|
|
@@ -176,11 +177,23 @@ function formatClass(cls, compact, methodOffset) {
|
|
|
176
177
|
if (cls.isAbstract)
|
|
177
178
|
modifiers.push('abstract');
|
|
178
179
|
const modStr = modifiers.length > 0 ? ` (${modifiers.join(', ')})` : '';
|
|
180
|
+
// The bridge sends no access modifier of its own, but it does send the
|
|
181
|
+
// declaration verbatim — and that is where the modifier lives, since AxClass
|
|
182
|
+
// XML has no element for it either. Parsed rather than regexed so a modifier
|
|
183
|
+
// named in an attribute or a doc comment cannot be mistaken for the real one.
|
|
184
|
+
const visibility = cls.declaration
|
|
185
|
+
? parseXppClassHeader(cls.declaration)?.visibility
|
|
186
|
+
: undefined;
|
|
179
187
|
let out = `# Class: ${cls.name}${modStr}\n\n`;
|
|
180
188
|
if (cls.extends)
|
|
181
189
|
out += `**Extends:** ${cls.extends}\n`;
|
|
182
190
|
if (cls.model)
|
|
183
191
|
out += `**Model:** ${cls.model}\n`;
|
|
192
|
+
// Printed on every path, so a reader comparing two classes is not left to infer
|
|
193
|
+
// package scope from a line's absence. Silent only when there was no
|
|
194
|
+
// declaration to read at all (#902).
|
|
195
|
+
if (cls.declaration)
|
|
196
|
+
out += `**Access:** ${visibility ?? 'public'}\n`;
|
|
184
197
|
out += `**Abstract:** ${cls.isAbstract ? 'Yes' : 'No'}\n`;
|
|
185
198
|
out += `**Final:** ${cls.isFinal ? 'Yes' : 'No'}\n`;
|
|
186
199
|
out += `_Source: C# bridge (IMetadataProvider)_\n\n`;
|
|
@@ -203,13 +216,35 @@ function formatClass(cls, compact, methodOffset) {
|
|
|
203
216
|
out += `### ${m.name}\n\n`;
|
|
204
217
|
if (m.source) {
|
|
205
218
|
const preview = m.source.substring(0, 500);
|
|
206
|
-
|
|
219
|
+
// Same call syntax as COMPACT_METHODS_HINT below — two different ways to
|
|
220
|
+
// ask for one method body, fifteen lines apart, is the inconsistency
|
|
221
|
+
// this hint exists to remove.
|
|
222
|
+
out += `\`\`\`xpp\n${preview}${m.source.length > 500 ? `\n// ... (${fullBodyHint(m.name)})` : ''}\n\`\`\`\n\n`;
|
|
207
223
|
}
|
|
208
224
|
}
|
|
209
225
|
}
|
|
210
226
|
if (hasMore) {
|
|
211
227
|
out += `> ⚠️ **${total - methodOffset - CLASS_METHOD_PAGE_SIZE} more methods.** Call again with \`methodOffset: ${methodOffset + CLASS_METHOD_PAGE_SIZE}\`.\n\n`;
|
|
212
228
|
}
|
|
229
|
+
// compact=true is the default, and this list is the only place a caller who
|
|
230
|
+
// didn't read the tool schema learns that bodies exist but were withheld —
|
|
231
|
+
// the DB-only fallback (classInfo.ts buildDbOnlyResponse) already says this;
|
|
232
|
+
// the bridge path (the primary, "always available on VM" path) silently gave
|
|
233
|
+
// signatures with no pointer to the escape hatch, which read as "no source
|
|
234
|
+
// available for this class" when it was really "not requested".
|
|
235
|
+
//
|
|
236
|
+
// Gated on the RENDERED page carrying source, not on `total`. Bridge method
|
|
237
|
+
// source is Safe(() => method.Source) and BridgeMethodInfo.source is
|
|
238
|
+
// optional, so a class can come back with none: the compact:false branch
|
|
239
|
+
// above then prints `### name` and no code block at all (`if (m.source)`),
|
|
240
|
+
// which is strictly LESS than the signature line compact just gave — a round
|
|
241
|
+
// trip spent to lose information, on exactly the classes where the caller was
|
|
242
|
+
// already unsure whether source existed. The same guard covers an out-of-range
|
|
243
|
+
// methodOffset, where `visible` is empty and there is no signature on the page
|
|
244
|
+
// for "signatures only" to be describing.
|
|
245
|
+
if (compact && visible.some(m => m.source)) {
|
|
246
|
+
out += `${COMPACT_METHODS_HINT}\n\n`;
|
|
247
|
+
}
|
|
213
248
|
return out;
|
|
214
249
|
}
|
|
215
250
|
// METHOD SOURCE
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { commandExists, runExe } from '../exec.js';
|
|
2
|
+
import { Target } from '../target.js';
|
|
3
|
+
/** Injectable for tests — real implementations by default. */
|
|
4
|
+
export interface BpCatalogDeps {
|
|
5
|
+
commandExists: typeof commandExists;
|
|
6
|
+
runExe: typeof runExe;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Regenerate this target's BP-moniker catalog when its resolved D365FO
|
|
10
|
+
* version has moved since the catalog file was last stamped (including
|
|
11
|
+
* "never generated" — covers first-time instance creation). A no-op on
|
|
12
|
+
* every other rebuild. Never throws and never fails the caller's reindex —
|
|
13
|
+
* a missing/stale BP catalog degrades one knowledge tool, not the server.
|
|
14
|
+
*
|
|
15
|
+
* "Never throws" is enforced here rather than left to the callee getting every
|
|
16
|
+
* path right. rebuildIndex() awaits this as its last step, unwrapped, *after*
|
|
17
|
+
* the multi-minute extract and database build have already succeeded and
|
|
18
|
+
* logged "Index rebuilt" — so anything escaping would turn a finished rebuild
|
|
19
|
+
* into a crashed command. And plenty can escape: runExe rejects on the child
|
|
20
|
+
* process's own 'error' event (commandExists only rules out ENOENT, not an
|
|
21
|
+
* EACCES on the interpreter), and saveStore writes two files that a locked or
|
|
22
|
+
* read-only instance config will refuse.
|
|
23
|
+
*/
|
|
24
|
+
export declare function ensureBpCatalogFresh(target: Target, deps?: BpCatalogDeps): Promise<void>;
|
|
25
|
+
//# sourceMappingURL=bpCatalog.d.ts.map
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeps one target's BP-moniker catalog (src/knowledge/bpMonikers/) matching
|
|
3
|
+
* its own pinned D365FO version, instead of every instance sharing the one
|
|
4
|
+
* snapshot committed in catalog.generated.ts (see that module's docblock).
|
|
5
|
+
*
|
|
6
|
+
* Called as a step of rebuildIndex() (indexCmd.ts) — the one place already
|
|
7
|
+
* reached by instance creation, upgrade, routine rebuild, `update`, and the
|
|
8
|
+
* first-time setup wizard. Regeneration only actually runs when the stamped
|
|
9
|
+
* version in the target's existing catalog file differs from what this
|
|
10
|
+
* target resolves to now (or the file does not exist yet); every other call
|
|
11
|
+
* is a cheap read-and-compare with no subprocess spawned.
|
|
12
|
+
*
|
|
13
|
+
* Two things this module will not do, both because the result would be
|
|
14
|
+
* self-perpetuating — whatever it stamps with the current version key is what
|
|
15
|
+
* every later rebuild treats as up to date: extract from an install that is
|
|
16
|
+
* not this target's (resolveSource), and accept an extraction that only
|
|
17
|
+
* partially read the one that is (verifyExtraction).
|
|
18
|
+
*/
|
|
19
|
+
import { createHash } from 'node:crypto';
|
|
20
|
+
import { existsSync, readFileSync, readdirSync, renameSync, rmSync, statSync } from 'node:fs';
|
|
21
|
+
import { basename, join, resolve } from 'node:path';
|
|
22
|
+
import { settingByPath } from '../../config/settings.js';
|
|
23
|
+
import { findPackagesRoot } from '../../utils/packagesRoot.js';
|
|
24
|
+
import { commandExists, runExe } from '../exec.js';
|
|
25
|
+
import { paths } from '../context.js';
|
|
26
|
+
import { readPath, readSetting, saveStore, writeSetting } from '../settingsStore.js';
|
|
27
|
+
import { p } from '../ui.js';
|
|
28
|
+
import { isUdeTarget, resolvePinnedXppConfig } from '../xppConfig.js';
|
|
29
|
+
const bpCatalogPathSetting = settingByPath('index.bpCatalogPath');
|
|
30
|
+
const packagePathSetting = settingByPath('environment.packagePath');
|
|
31
|
+
/**
|
|
32
|
+
* Relative on purpose — instance path settings stay portable so an instance
|
|
33
|
+
* folder can be renamed or moved (see configBaseDir in config/configFile.ts).
|
|
34
|
+
*/
|
|
35
|
+
const DEFAULT_CATALOG_RELATIVE = './data/bp-moniker-catalog.json';
|
|
36
|
+
/** Where this target's real D365FO version + packages root come from. */
|
|
37
|
+
function resolveSource(target) {
|
|
38
|
+
const xppConfig = resolvePinnedXppConfig(target.store);
|
|
39
|
+
if (xppConfig) {
|
|
40
|
+
// No frameworkDirectory means the config JSON could not be read —
|
|
41
|
+
// listXppConfigs() keeps those entries deliberately. Passing '' would not
|
|
42
|
+
// be "use the default", it would hand the script an empty -PackagesPath,
|
|
43
|
+
// whose own fallback auto-detects the NEWEST PackagesLocalDirectory on the
|
|
44
|
+
// box; the result then gets stamped with THIS target's version and treated
|
|
45
|
+
// as current. A catalog from the wrong install is worse than no catalog.
|
|
46
|
+
if (!xppConfig.frameworkDirectory)
|
|
47
|
+
return null;
|
|
48
|
+
return { versionKey: xppConfig.version, packagesPath: xppConfig.frameworkDirectory };
|
|
49
|
+
}
|
|
50
|
+
// A null from resolvePinnedXppConfig does NOT mean "traditional". It also
|
|
51
|
+
// covers a UDE target whose pin names a config that is gone — the
|
|
52
|
+
// stale-after-UDE-upgrade state `instance upgrade` exists to fix — and that
|
|
53
|
+
// function returns null there *deliberately*, rather than substituting a
|
|
54
|
+
// different environment. Falling through to the box-wide scan below would
|
|
55
|
+
// undo exactly that: findPackagesRoot() ranks every <drive>:\AosService\
|
|
56
|
+
// PackagesLocalDirectory on the machine and hands back the most plausible
|
|
57
|
+
// one, which on a mixed box is some other install entirely. Its catalog
|
|
58
|
+
// would then be stamped with this target's key and treated as current —
|
|
59
|
+
// the same "catalog from the wrong install" the branch above refuses.
|
|
60
|
+
if (isUdeTarget(target.store))
|
|
61
|
+
return null;
|
|
62
|
+
// Traditional environment: no version string on disk anywhere, so the state
|
|
63
|
+
// of the files the catalog is extracted FROM stands in for "has this install
|
|
64
|
+
// changed since we last extracted".
|
|
65
|
+
const packagesPath = readSetting(target.store, packagePathSetting)
|
|
66
|
+
|| findPackagesRoot()
|
|
67
|
+
|| undefined;
|
|
68
|
+
if (!packagesPath)
|
|
69
|
+
return null;
|
|
70
|
+
const versionKey = ruleSourceKey(packagesPath);
|
|
71
|
+
return versionKey ? { versionKey, packagesPath } : null;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* A key that moves when the rule DLLs move — the actual input to half the
|
|
75
|
+
* catalog, and the half that cannot be reconstructed from names alone.
|
|
76
|
+
*
|
|
77
|
+
* The obvious cheap key, `statSync(packagesPath/bin).mtimeMs`, does not track
|
|
78
|
+
* that: a directory's mtime changes when its *direct* children are added,
|
|
79
|
+
* removed or renamed, so a platform hotfix that rewrites DLLs inside
|
|
80
|
+
* bin\BPExtensions\ in place leaves bin\ untouched, the key matches, and the
|
|
81
|
+
* catalog is never regenerated — the exact staleness the per-instance catalog
|
|
82
|
+
* exists to fix. Size and mtime of each rule DLL move whenever its content
|
|
83
|
+
* does.
|
|
84
|
+
*
|
|
85
|
+
* The file set mirrors what extract-bp-catalog.ps1 itself reflects over:
|
|
86
|
+
* bin\*.dll whose name contains 'BestPractice', plus every DLL in
|
|
87
|
+
* bin\BPExtensions. Measured on a real install that is 25 files, reached by
|
|
88
|
+
* two readdirs and 25 stats in ~5 ms — this runs on every rebuild, so it has
|
|
89
|
+
* to stay far cheaper than the multi-minute scan it decides against.
|
|
90
|
+
*
|
|
91
|
+
* What it deliberately does NOT cover: each model's AxRuleSet\BPRules.xml, the
|
|
92
|
+
* source of the canonical names. Finding those means recursing the whole
|
|
93
|
+
* packages root, which is most of the cost of the extraction itself. Neither
|
|
94
|
+
* did the bin\ mtime it replaces, and in practice a platform update that
|
|
95
|
+
* changes the rule set ships new rule DLLs with it.
|
|
96
|
+
*/
|
|
97
|
+
function ruleSourceKey(packagesPath) {
|
|
98
|
+
// join(), not a hardcoded '\bin' — this path is only ever real on Windows in
|
|
99
|
+
// production, but the test suite (and CI) exercises it on Linux too, where a
|
|
100
|
+
// literal backslash is just another filename character, not a separator.
|
|
101
|
+
const binDir = join(packagesPath, 'bin');
|
|
102
|
+
const extensionsDir = join(binDir, 'BPExtensions');
|
|
103
|
+
const parts = [];
|
|
104
|
+
for (const dir of [binDir, extensionsDir]) {
|
|
105
|
+
let names;
|
|
106
|
+
try {
|
|
107
|
+
names = readdirSync(dir);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
for (const name of names.sort()) {
|
|
113
|
+
if (!/\.dll$/i.test(name))
|
|
114
|
+
continue;
|
|
115
|
+
if (dir === binDir && !/BestPractice/i.test(name))
|
|
116
|
+
continue;
|
|
117
|
+
try {
|
|
118
|
+
const stat = statSync(join(dir, name));
|
|
119
|
+
parts.push(`${name}:${stat.size}:${stat.mtimeMs}`);
|
|
120
|
+
}
|
|
121
|
+
catch { /* vanished between readdir and stat — just leave it out */ }
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// No rule DLL anywhere means this is not a usable packages root, so there is
|
|
125
|
+
// nothing to extract and nothing to key on. Returning a key for the empty set
|
|
126
|
+
// would make every such path agree with every other one.
|
|
127
|
+
if (parts.length === 0)
|
|
128
|
+
return null;
|
|
129
|
+
return `rules:${createHash('sha1').update(parts.join('|')).digest('hex').slice(0, 16)}`;
|
|
130
|
+
}
|
|
131
|
+
/** The version this target's *existing* catalog file was stamped with, if any. */
|
|
132
|
+
function existingVersionKey(catalogPath) {
|
|
133
|
+
if (!existsSync(catalogPath))
|
|
134
|
+
return null;
|
|
135
|
+
try {
|
|
136
|
+
// Strip a leading BOM before parsing: the script writes BOM-less UTF-8, but
|
|
137
|
+
// a catalog left behind by an older copy of it (or hand-edited in a Windows
|
|
138
|
+
// editor) still carries one, and JSON.parse throws on it. Throwing here is
|
|
139
|
+
// not visible — it just reads as "no catalog yet" and re-runs the whole scan.
|
|
140
|
+
const parsed = JSON.parse(readFileSync(catalogPath, 'utf-8').replace(/^\uFEFF/, ''));
|
|
141
|
+
return typeof parsed.version === 'string' ? parsed.version : null;
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Why a freshly extracted catalog must not be trusted as this target's current
|
|
149
|
+
* one — or null when it looks like a complete extraction.
|
|
150
|
+
*
|
|
151
|
+
* Exit code 0 does not mean "complete". Both of the script's scans are
|
|
152
|
+
* best-effort on purpose (`-ErrorAction SilentlyContinue`, a per-file catch on
|
|
153
|
+
* BPRules.xml, a per-DLL catch on assembly load), so a run that could read half
|
|
154
|
+
* the models, or none of the rule DLLs, still finishes and exits 0. Stamping
|
|
155
|
+
* that result is worse than not regenerating at all: the stamp matches on every
|
|
156
|
+
* later rebuild, so it is never retried, and loadCatalog() only rejects an
|
|
157
|
+
* exactly-empty override — a merely truncated one REPLACES the compiled
|
|
158
|
+
* snapshot, and bp_moniker starts answering "not in the extracted catalog" for
|
|
159
|
+
* monikers that are real.
|
|
160
|
+
*
|
|
161
|
+
* The failure counts are the script's own (`sources`), not a guess: measured
|
|
162
|
+
* against a real 214-model PackagesLocalDirectory, a healthy box skips zero of
|
|
163
|
+
* its 144 BPRules.xml files and zero of its 25 rule DLLs, so any skip at all is
|
|
164
|
+
* signal. The content checks below stand in for that on a catalog produced by
|
|
165
|
+
* an older copy of the script, which carries no `sources` block.
|
|
166
|
+
*/
|
|
167
|
+
function verifyExtraction(catalogPath) {
|
|
168
|
+
let parsed;
|
|
169
|
+
try {
|
|
170
|
+
// Same BOM strip as existingVersionKey — a catalog an older copy of the
|
|
171
|
+
// script produced under PowerShell 5.1 carries one, and this must reject a
|
|
172
|
+
// partial extraction, not a readable-but-BOM'd one.
|
|
173
|
+
parsed = JSON.parse(readFileSync(catalogPath, 'utf-8').replace(/^\uFEFF/, ''));
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
return `it could not be read back (${err.message})`;
|
|
177
|
+
}
|
|
178
|
+
if (!Array.isArray(parsed.entries))
|
|
179
|
+
return 'it carries no entries array';
|
|
180
|
+
const { ruleSetFailures = 0, dllFailures = 0, ruleSetFiles = 0, ruleDlls = 0 } = parsed.sources ?? {};
|
|
181
|
+
if (ruleSetFailures > 0 || dllFailures > 0) {
|
|
182
|
+
return `the extraction skipped ${ruleSetFailures} of ${ruleSetFiles} BPRules.xml files and ${dllFailures} of ${ruleDlls} rule DLLs`;
|
|
183
|
+
}
|
|
184
|
+
const entries = parsed.entries;
|
|
185
|
+
if (entries.length === 0)
|
|
186
|
+
return 'it contains no monikers at all';
|
|
187
|
+
// Either source failing wholesale leaves a catalog that still looks populated
|
|
188
|
+
// but has lost the half that answers a question: no canonical name means no
|
|
189
|
+
// AxRuleSet was read, so `validate` cannot confirm anything; no rule text
|
|
190
|
+
// means no DLL yielded resources, so `search` matches nothing.
|
|
191
|
+
if (!entries.some(e => e.canonical))
|
|
192
|
+
return 'not one of its monikers came from an AxRuleSet/BPRules.xml';
|
|
193
|
+
if (!entries.some(e => e.message || e.description))
|
|
194
|
+
return 'not one of its monikers carries rule text';
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
const defaultDeps = { commandExists, runExe };
|
|
198
|
+
/**
|
|
199
|
+
* Regenerate this target's BP-moniker catalog when its resolved D365FO
|
|
200
|
+
* version has moved since the catalog file was last stamped (including
|
|
201
|
+
* "never generated" — covers first-time instance creation). A no-op on
|
|
202
|
+
* every other rebuild. Never throws and never fails the caller's reindex —
|
|
203
|
+
* a missing/stale BP catalog degrades one knowledge tool, not the server.
|
|
204
|
+
*
|
|
205
|
+
* "Never throws" is enforced here rather than left to the callee getting every
|
|
206
|
+
* path right. rebuildIndex() awaits this as its last step, unwrapped, *after*
|
|
207
|
+
* the multi-minute extract and database build have already succeeded and
|
|
208
|
+
* logged "Index rebuilt" — so anything escaping would turn a finished rebuild
|
|
209
|
+
* into a crashed command. And plenty can escape: runExe rejects on the child
|
|
210
|
+
* process's own 'error' event (commandExists only rules out ENOENT, not an
|
|
211
|
+
* EACCES on the interpreter), and saveStore writes two files that a locked or
|
|
212
|
+
* read-only instance config will refuse.
|
|
213
|
+
*/
|
|
214
|
+
export async function ensureBpCatalogFresh(target, deps = defaultDeps) {
|
|
215
|
+
try {
|
|
216
|
+
await refreshCatalog(target, deps);
|
|
217
|
+
}
|
|
218
|
+
catch (err) {
|
|
219
|
+
p.log.warn(`BP catalog: skipped for ${target.label} (${err.message}) — the index rebuild itself is unaffected.`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
async function refreshCatalog(target, deps) {
|
|
223
|
+
const source = resolveSource(target);
|
|
224
|
+
if (!source) {
|
|
225
|
+
p.log.warn(isUdeTarget(target.store)
|
|
226
|
+
? `BP catalog: could not resolve ${target.label}'s own D365FO install (its XPP config is missing or unreadable) — skipped rather than extracting from another install on this box. \`d365fo-mcp instance upgrade\` repoints a pin left behind by a UDE upgrade.`
|
|
227
|
+
: `BP catalog: could not resolve a packages path for ${target.label} — skipped.`);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
// The setting carries no registry default (see settings.ts for why), so this
|
|
231
|
+
// fallback is the normal path until the first successful extraction writes
|
|
232
|
+
// the value below.
|
|
233
|
+
const catalogPath = readPath(target.store, bpCatalogPathSetting, resolve(target.store.baseDir, DEFAULT_CATALOG_RELATIVE));
|
|
234
|
+
if (existingVersionKey(catalogPath) === source.versionKey) {
|
|
235
|
+
p.log.info(`BP catalog up to date (${target.label}).`);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
let shell = 'pwsh';
|
|
239
|
+
if (!await deps.commandExists(shell)) {
|
|
240
|
+
shell = 'powershell';
|
|
241
|
+
if (!await deps.commandExists(shell)) {
|
|
242
|
+
p.log.warn(`BP catalog: neither pwsh nor powershell is on PATH for ${target.label} — skipped, will retry next rebuild.`);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
p.log.step(`Refreshing BP moniker catalog (${target.label}, ${basename(source.packagesPath)})…`);
|
|
247
|
+
// Extract beside the real catalog, not over it. The script writes its output
|
|
248
|
+
// in one go with no staging of its own, so pointing it straight at the live
|
|
249
|
+
// file means a partial run destroys a good catalog before anything can judge
|
|
250
|
+
// it — and the caller cannot put it back. The move below is the only thing
|
|
251
|
+
// that makes this target's catalog change.
|
|
252
|
+
const stagedPath = `${catalogPath}.new`;
|
|
253
|
+
const exitCode = await deps.runExe(shell, [
|
|
254
|
+
'-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', paths.extractBpCatalogScript,
|
|
255
|
+
'-PackagesPath', source.packagesPath,
|
|
256
|
+
'-OutFile', stagedPath,
|
|
257
|
+
'-Version', source.versionKey,
|
|
258
|
+
]);
|
|
259
|
+
if (exitCode !== 0) {
|
|
260
|
+
rmSync(stagedPath, { force: true });
|
|
261
|
+
p.log.warn(`BP catalog: extraction failed for ${target.label} (exit ${exitCode}) — keeping the previous catalog.`);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const problem = verifyExtraction(stagedPath);
|
|
265
|
+
if (problem) {
|
|
266
|
+
// Discarded rather than stamped: the file carries this target's current
|
|
267
|
+
// version key, so keeping it would make every later rebuild a no-op and
|
|
268
|
+
// freeze a half-read catalog in place. Dropping it leaves the previous
|
|
269
|
+
// catalog (or the compiled snapshot) in service and the version key stale,
|
|
270
|
+
// which is exactly what makes the next rebuild try again.
|
|
271
|
+
rmSync(stagedPath, { force: true });
|
|
272
|
+
p.log.warn(`BP catalog: discarded the extraction for ${target.label} — ${problem}. Keeping the previous catalog; the next rebuild will retry.`);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
renameSync(stagedPath, catalogPath);
|
|
276
|
+
// Write the RELATIVE literal, not the resolved catalogPath: an absolute path
|
|
277
|
+
// baked into the instance config survives neither a folder rename nor a move,
|
|
278
|
+
// and toEnvRecord() resolves a relative one against store.baseDir on its way
|
|
279
|
+
// into BP_CATALOG_PATH anyway. This write is also what turns the override on
|
|
280
|
+
// at all — the setting has no registry default.
|
|
281
|
+
if (readSetting(target.store, bpCatalogPathSetting) === undefined) {
|
|
282
|
+
writeSetting(target.store, bpCatalogPathSetting, DEFAULT_CATALOG_RELATIVE);
|
|
283
|
+
saveStore(target.store);
|
|
284
|
+
}
|
|
285
|
+
p.log.success(`BP catalog refreshed (${target.label}).`);
|
|
286
|
+
}
|
|
287
|
+
//# sourceMappingURL=bpCatalog.js.map
|
|
@@ -4,6 +4,51 @@ interface CheckResult {
|
|
|
4
4
|
message: string;
|
|
5
5
|
fix?: string;
|
|
6
6
|
}
|
|
7
|
+
/** Classic AOSService VM, or Unified Developer Experience. */
|
|
8
|
+
export type EnvKind = 'traditional' | 'ude';
|
|
9
|
+
/**
|
|
10
|
+
* What a path setting IS, so that "this folder is missing" can name the right
|
|
11
|
+
* cause and the right cure for it.
|
|
12
|
+
*
|
|
13
|
+
* Without this the three settings shared one message, and it fitted only one of
|
|
14
|
+
* them: `packagePath` on UDE. It told a traditional VM that its packages root
|
|
15
|
+
* is "normally auto-detected from the active XPP config" (it is not — the drive
|
|
16
|
+
* scan finds it), and it offered `<drive>:\AosService\PackagesLocalDirectory`,
|
|
17
|
+
* a traditional-VM artifact, as the value to give the UDE ModelStoreFolder and
|
|
18
|
+
* FrameworkDirectory.
|
|
19
|
+
*
|
|
20
|
+
* `autoSource` is the load-bearing one: a stale pin is only a *stale pin* where
|
|
21
|
+
* something would otherwise resolve the value live. Where nothing would, the
|
|
22
|
+
* pin is the configuration, and telling the user to delete it is telling them
|
|
23
|
+
* to break their install — `customPackagesPath` on a traditional VM is exactly
|
|
24
|
+
* that case, the documented fix for junction layouts (docs/MCP_CONFIG.md).
|
|
25
|
+
*/
|
|
26
|
+
interface PathFacts {
|
|
27
|
+
humanName: string;
|
|
28
|
+
/** How the value resolves when nothing pins it, or null when nothing does. */
|
|
29
|
+
autoSource(kind: EnvKind): string | null;
|
|
30
|
+
/** Values doctor can actually propose — empty when it has no way to know one. */
|
|
31
|
+
candidates(kind: EnvKind): string[];
|
|
32
|
+
/** What the setting ought to point at, in words. */
|
|
33
|
+
target(kind: EnvKind): string;
|
|
34
|
+
/** Extra context line when there is no candidate to name, or null. */
|
|
35
|
+
note(kind: EnvKind): string | null;
|
|
36
|
+
}
|
|
37
|
+
export declare const PATH_FACTS: Record<string, PathFacts>;
|
|
38
|
+
/**
|
|
39
|
+
* "Path setting points at a folder that isn't there" — with the cause named.
|
|
40
|
+
*
|
|
41
|
+
* A value pinned by the legacy .env instead of the JSON config is the likely bug
|
|
42
|
+
* whenever something else would have resolved it live: the .env copy goes stale
|
|
43
|
+
* the moment a platform update moves the folder, and it keeps outranking the
|
|
44
|
+
* now-correct detection at every startup (see configManager's envContext, which
|
|
45
|
+
* reads these env vars before consulting the XPP config). The server then dies
|
|
46
|
+
* with "C# bridge unavailable (ude)" and nothing points at the .env.
|
|
47
|
+
*
|
|
48
|
+
* Pure — every input is passed in — so the messages can be tested without a
|
|
49
|
+
* Windows box, an XPP config or a real .env.
|
|
50
|
+
*/
|
|
51
|
+
export declare function missingPathFix(setting: import('../../config/settings.js').Setting, facts: PathFacts, label: string, configured: string, kind: EnvKind, pinnedByEnv: boolean): Omit<CheckResult, 'severity'>;
|
|
7
52
|
/**
|
|
8
53
|
* The configured prefix against the one the model's own objects use.
|
|
9
54
|
*
|
|
@@ -11,8 +56,14 @@ interface CheckResult {
|
|
|
11
56
|
* for every model — but the server resolves the model's own naming ABOVE the
|
|
12
57
|
* configuration, so a user reading only their config has the wrong answer. State
|
|
13
58
|
* both, and how to pin the configured one.
|
|
59
|
+
*
|
|
60
|
+
* `pinned` is naming.prefixSource=config. This check used to call the inference
|
|
61
|
+
* directly and so never saw it — modelPrefixInference reads it in
|
|
62
|
+
* getInferredModelPrefix, one level above inferPrefixFromObjectNames — which
|
|
63
|
+
* meant a user who had already pinned the prefix was still told their model's
|
|
64
|
+
* naming wins (it does not) and offered the fix they had already applied (#893).
|
|
14
65
|
*/
|
|
15
|
-
export declare function checkPrefixResolution(configuredPrefix: string, modelName: string | null, modelObjectNames: string[], label: string): CheckResult[];
|
|
66
|
+
export declare function checkPrefixResolution(configuredPrefix: string, modelName: string | null, modelObjectNames: string[], label: string, pinned?: boolean): CheckResult[];
|
|
16
67
|
export declare function doctorCommand(): Promise<void>;
|
|
17
68
|
export {};
|
|
18
69
|
//# sourceMappingURL=doctor.d.ts.map
|