d365fo-mcp 1.11.0 → 1.13.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 +60 -2
- package/dist/cli/commands/bpCatalog.d.ts +25 -0
- package/dist/cli/commands/bpCatalog.js +287 -0
- 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/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.js +19 -0
- 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/metadata/symbolIndex.d.ts +39 -3
- package/dist/metadata/symbolIndex.js +77 -25
- 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 +78 -19
- package/dist/scripts/build-fts.js +78 -19
- package/dist/scripts/extract-metadata.js +18 -1
- package/dist/server/serverMode.d.ts +2 -2
- package/dist/server/serverMode.js +2 -2
- package/dist/server/toolAnnotations.js +1 -1
- package/dist/server/toolSchemas/d365foFile.js +11 -7
- 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/d365foFile.d.ts +3 -2
- package/dist/tools/d365foFile.js +10 -4
- package/dist/tools/knowledge/bpMonikerHelp.d.ts +32 -0
- package/dist/tools/knowledge/bpMonikerHelp.js +155 -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 +2 -1
- package/dist/tools/knowledge/xppKnowledge.js +6 -5
- package/dist/tools/readers/classInfo.js +24 -5
- package/dist/tools/readers/getMethod.js +8 -3
- package/dist/tools/readers/getObjectInfo.js +17 -1
- 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 +132 -12
- package/dist/tools/smart/codeGen.js +5 -4
- package/dist/tools/specs/d365foFileOpSpecs.js +184 -4
- package/dist/tools/specs/opSpecs.js +39 -0
- package/dist/tools/write/createD365File.js +3 -2
- package/dist/tools/write/deleteD365File.d.ts +49 -0
- package/dist/tools/write/deleteD365File.js +357 -0
- package/dist/tools/write/modifyD365File.js +862 -77
- 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/tools/xml/securityPrivilegeXml.d.ts +54 -0
- package/dist/tools/xml/securityPrivilegeXml.js +86 -0
- package/dist/utils/crossModelWriteGuard.d.ts +8 -16
- package/dist/utils/crossModelWriteGuard.js +18 -2
- package/dist/utils/formControlRemoval.d.ts +70 -0
- package/dist/utils/formControlRemoval.js +216 -0
- package/dist/utils/formExtensionControlXml.d.ts +159 -0
- package/dist/utils/formExtensionControlXml.js +592 -0
- package/dist/utils/formExtensionShapeValidator.js +15 -0
- package/dist/utils/ignoreDiagnosticListXml.d.ts +186 -0
- package/dist/utils/ignoreDiagnosticListXml.js +267 -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/objectFileLookup.d.ts +19 -0
- package/dist/utils/objectFileLookup.js +85 -29
- package/dist/utils/xmlNodeTree.d.ts +54 -0
- package/dist/utils/xmlNodeTree.js +144 -0
- package/dist/workspace/contextRanker.js +7 -2
- package/dist/workspace/projectFile.js +57 -14
- package/dist/workspace/projectMembership.d.ts +17 -3
- package/dist/workspace/projectMembership.js +22 -7
- package/package.json +3 -2
- package/scripts/extract-bp-catalog.ps1 +321 -0
|
@@ -27,6 +27,7 @@ import { xppMethodSourceForXml } from '../utils/xppFormat.js';
|
|
|
27
27
|
import { ensureXppDocComment, ensureBlankLineBeforeClosingBrace } from '../utils/xppDocGen.js';
|
|
28
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;
|
|
@@ -215,13 +216,35 @@ function formatClass(cls, compact, methodOffset) {
|
|
|
215
216
|
out += `### ${m.name}\n\n`;
|
|
216
217
|
if (m.source) {
|
|
217
218
|
const preview = m.source.substring(0, 500);
|
|
218
|
-
|
|
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`;
|
|
219
223
|
}
|
|
220
224
|
}
|
|
221
225
|
}
|
|
222
226
|
if (hasMore) {
|
|
223
227
|
out += `> ⚠️ **${total - methodOffset - CLASS_METHOD_PAGE_SIZE} more methods.** Call again with \`methodOffset: ${methodOffset + CLASS_METHOD_PAGE_SIZE}\`.\n\n`;
|
|
224
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
|
+
}
|
|
225
248
|
return out;
|
|
226
249
|
}
|
|
227
250
|
// METHOD SOURCE
|
|
@@ -1117,8 +1140,20 @@ const BRIDGE_MODIFY_OPS = new Set([
|
|
|
1117
1140
|
'modify-property',
|
|
1118
1141
|
'add-enum-value', 'modify-enum-value', 'remove-enum-value',
|
|
1119
1142
|
'add-control', 'add-data-source',
|
|
1143
|
+
// Removal of a control, and of a privilege's entry point, have no C# op either
|
|
1144
|
+
// (MetadataWriteService exposes no RemoveControl, and security objects have no
|
|
1145
|
+
// bridge write path at all) — both are served by a direct-XML writer and still
|
|
1146
|
+
// pass through this gate.
|
|
1147
|
+
'remove-control', 'remove-entry-point',
|
|
1148
|
+
// AxIgnoreDiagnosticList is not an AOT object at all — MetadataWriteService has
|
|
1149
|
+
// no concept of it — so this is XML-only for the same structural reason as the
|
|
1150
|
+
// two above.
|
|
1151
|
+
'remove-diagnostic-suppression', 'add-diagnostic-suppression',
|
|
1120
1152
|
'add-display-method', 'add-table-method',
|
|
1121
1153
|
'add-field-modification', 'add-menu-item-to-menu',
|
|
1154
|
+
// No C# op exists for query ranges on entities — served entirely by a
|
|
1155
|
+
// direct-XML writer (data-entity is already in BRIDGE_MODIFY_TYPES).
|
|
1156
|
+
'add-query-range', 'remove-query-range',
|
|
1122
1157
|
]);
|
|
1123
1158
|
/**
|
|
1124
1159
|
* Supported object types for bridge-based modification.
|
|
@@ -1133,6 +1168,25 @@ const BRIDGE_MODIFY_TYPES = new Set([
|
|
|
1133
1168
|
'menu-item-action', 'menu-item-display', 'menu-item-output',
|
|
1134
1169
|
'menu',
|
|
1135
1170
|
]);
|
|
1171
|
+
/**
|
|
1172
|
+
* Operation → the object types it may target that BRIDGE_MODIFY_TYPES does not
|
|
1173
|
+
* cover, because a direct-XML writer serves the pair and the bridge never will.
|
|
1174
|
+
*
|
|
1175
|
+
* This gate is what EVERY modify operation clears before dispatch, whether or not
|
|
1176
|
+
* a C# op backs it, so an XML-only writer for an XML-only type has nowhere else to
|
|
1177
|
+
* be admitted. The pairing is per-operation rather than per-type on purpose:
|
|
1178
|
+
* dropping 'security-privilege' into BRIDGE_MODIFY_TYPES would also claim
|
|
1179
|
+
* add-method, replace-code and modify-property work on a privilege, which they do
|
|
1180
|
+
* not — the bridge has no write path for security objects at all (the same reason
|
|
1181
|
+
* they are absent from BRIDGE_CREATE_TYPES: a generic
|
|
1182
|
+
* Dictionary<string,string> cannot carry <EntryPoints>). The caller would then get
|
|
1183
|
+
* a bridge resolution failure instead of "not supported for this object type".
|
|
1184
|
+
*/
|
|
1185
|
+
const XML_ONLY_MODIFY_PAIRS = {
|
|
1186
|
+
'remove-entry-point': new Set(['security-privilege']),
|
|
1187
|
+
'remove-diagnostic-suppression': new Set(['ignore-diagnostic-list']),
|
|
1188
|
+
'add-diagnostic-suppression': new Set(['ignore-diagnostic-list']),
|
|
1189
|
+
};
|
|
1136
1190
|
/**
|
|
1137
1191
|
* Names the properties the bridge could not write, for appending to a success message.
|
|
1138
1192
|
*
|
|
@@ -1157,7 +1211,11 @@ export function canBridgeCreate(objectType) {
|
|
|
1157
1211
|
* Checks if bridge can handle this modify operation.
|
|
1158
1212
|
*/
|
|
1159
1213
|
export function canBridgeModify(objectType, operation) {
|
|
1160
|
-
|
|
1214
|
+
const type = objectType.toLowerCase();
|
|
1215
|
+
const op = operation.toLowerCase();
|
|
1216
|
+
if (!BRIDGE_MODIFY_OPS.has(op))
|
|
1217
|
+
return false;
|
|
1218
|
+
return BRIDGE_MODIFY_TYPES.has(type) || (XML_ONLY_MODIFY_PAIRS[op]?.has(type) ?? false);
|
|
1161
1219
|
}
|
|
1162
1220
|
/**
|
|
1163
1221
|
* Give a create's X++ the same doc comments and indentation the XML fallback gives it.
|
|
@@ -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,7 @@
|
|
|
4
4
|
* instances/rebuild-instance.ps1 minus the git-pull step (that lives in
|
|
5
5
|
* `d365fo-mcp update`).
|
|
6
6
|
*/
|
|
7
|
+
import { ensureBpCatalogFresh } from './bpCatalog.js';
|
|
7
8
|
import { dataRoot, installMode, isWindows, paths } from '../context.js';
|
|
8
9
|
import { runNode } from '../exec.js';
|
|
9
10
|
import { listInstances } from '../instances.js';
|
|
@@ -47,6 +48,9 @@ export async function rebuildIndex(target) {
|
|
|
47
48
|
return false;
|
|
48
49
|
}
|
|
49
50
|
p.log.success(`Index rebuilt: ${target.label}`);
|
|
51
|
+
// Best-effort: a stale/missing BP catalog degrades one knowledge tool, not
|
|
52
|
+
// the metadata index rebuild that just succeeded above.
|
|
53
|
+
await ensureBpCatalogFresh(target);
|
|
50
54
|
return true;
|
|
51
55
|
}
|
|
52
56
|
export async function indexCommand(instanceName, opts) {
|
package/dist/cli/context.d.ts
CHANGED
|
@@ -84,6 +84,14 @@ export declare const paths: {
|
|
|
84
84
|
/** esbuild bundles of the two scripts above — what an npm install ships instead of the sources. */
|
|
85
85
|
extractScriptDist: string;
|
|
86
86
|
buildDbScriptDist: string;
|
|
87
|
+
/**
|
|
88
|
+
* Not compiled, not bundled — a raw PowerShell script that reflects over the
|
|
89
|
+
* .NET-authored BP-rule DLLs to pull real message/description text, which
|
|
90
|
+
* Node cannot do on its own. Same path for a git checkout and an npm
|
|
91
|
+
* install; ships as a listed file in package.json rather than through the
|
|
92
|
+
* dist/ pipeline the .ts scripts above use.
|
|
93
|
+
*/
|
|
94
|
+
extractBpCatalogScript: string;
|
|
87
95
|
};
|
|
88
96
|
/**
|
|
89
97
|
* The exact command that builds the bridge for this installation, for every
|
package/dist/cli/context.js
CHANGED
|
@@ -153,6 +153,14 @@ export const paths = {
|
|
|
153
153
|
/** esbuild bundles of the two scripts above — what an npm install ships instead of the sources. */
|
|
154
154
|
extractScriptDist: resolve(repoRoot, 'dist', 'scripts', 'extract-metadata.js'),
|
|
155
155
|
buildDbScriptDist: resolve(repoRoot, 'dist', 'scripts', 'build-database.js'),
|
|
156
|
+
/**
|
|
157
|
+
* Not compiled, not bundled — a raw PowerShell script that reflects over the
|
|
158
|
+
* .NET-authored BP-rule DLLs to pull real message/description text, which
|
|
159
|
+
* Node cannot do on its own. Same path for a git checkout and an npm
|
|
160
|
+
* install; ships as a listed file in package.json rather than through the
|
|
161
|
+
* dist/ pipeline the .ts scripts above use.
|
|
162
|
+
*/
|
|
163
|
+
extractBpCatalogScript: resolve(repoRoot, 'scripts', 'extract-bp-catalog.ps1'),
|
|
156
164
|
};
|
|
157
165
|
/**
|
|
158
166
|
* The exact command that builds the bridge for this installation, for every
|
package/dist/cli/xppConfig.d.ts
CHANGED
|
@@ -13,6 +13,40 @@ export interface XppConfig {
|
|
|
13
13
|
export declare function xppConfigDir(): string | null;
|
|
14
14
|
/** All versioned configs, newest first. Empty when the directory is absent. */
|
|
15
15
|
export declare function listXppConfigs(): XppConfig[];
|
|
16
|
+
/**
|
|
17
|
+
* The full {@link XppConfig} a UDE target resolves to: the pinned one, or the
|
|
18
|
+
* newest available when nothing is pinned. Null for a traditional target, when
|
|
19
|
+
* no config exists at all, and — deliberately — when the pin names a config
|
|
20
|
+
* that is gone.
|
|
21
|
+
*
|
|
22
|
+
* Mirrors XppConfigProvider.getActiveConfig exactly, including that last case:
|
|
23
|
+
* it matches either form of the name and returns null rather than substituting
|
|
24
|
+
* a different environment. A pin that no longer resolves is the
|
|
25
|
+
* stale-after-UDE-upgrade state isXppConfigStale() flags and `instance upgrade`
|
|
26
|
+
* exists to fix; answering it with the newest config would have a caller
|
|
27
|
+
* extract from 10.0.2500 and stamp the result as this target's, while its index
|
|
28
|
+
* and server still reference the old pin.
|
|
29
|
+
*/
|
|
30
|
+
export declare function resolvePinnedXppConfig(store: SettingsStore): XppConfig | null;
|
|
31
|
+
/**
|
|
32
|
+
* Is this target a UDE target at all — i.e. does a null from
|
|
33
|
+
* {@link resolvePinnedXppConfig} mean "its pin does not resolve" rather than
|
|
34
|
+
* "this is a traditional install"?
|
|
35
|
+
*
|
|
36
|
+
* That function returns null for three different reasons and a caller cannot
|
|
37
|
+
* tell them apart, which matters because the two answers call for opposite
|
|
38
|
+
* behaviour: a traditional target legitimately falls back to detecting the
|
|
39
|
+
* packages root on this box, while for a UDE target that fallback throws away
|
|
40
|
+
* the very guarantee the null was there to provide (see resolveSource in
|
|
41
|
+
* commands/bpCatalog.ts, and the docblock above).
|
|
42
|
+
*
|
|
43
|
+
* Mirrors the detection settings.ts documents for an unset environment.type:
|
|
44
|
+
* UDE when XPP config files exist in %LOCALAPPDATA%\Microsoft\Dynamics365\
|
|
45
|
+
* XPPConfig. So a box with no configs at all is traditional — the one case
|
|
46
|
+
* where falling through is correct — and anything else with an explicit
|
|
47
|
+
* `traditional` type is taken at its word.
|
|
48
|
+
*/
|
|
49
|
+
export declare function isUdeTarget(store: SettingsStore): boolean;
|
|
16
50
|
/**
|
|
17
51
|
* Expand a short config name (e.g. "myenv-dev") to the newest full versioned
|
|
18
52
|
* name ("myenv-dev___10.0.2345.153") so a later staleness check is a plain
|
package/dist/cli/xppConfig.js
CHANGED
|
@@ -54,6 +54,54 @@ function pinnedConfigName(store) {
|
|
|
54
54
|
const value = readSetting(store, xppConfigNameSetting);
|
|
55
55
|
return typeof value === 'string' && value ? value : null;
|
|
56
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* The full {@link XppConfig} a UDE target resolves to: the pinned one, or the
|
|
59
|
+
* newest available when nothing is pinned. Null for a traditional target, when
|
|
60
|
+
* no config exists at all, and — deliberately — when the pin names a config
|
|
61
|
+
* that is gone.
|
|
62
|
+
*
|
|
63
|
+
* Mirrors XppConfigProvider.getActiveConfig exactly, including that last case:
|
|
64
|
+
* it matches either form of the name and returns null rather than substituting
|
|
65
|
+
* a different environment. A pin that no longer resolves is the
|
|
66
|
+
* stale-after-UDE-upgrade state isXppConfigStale() flags and `instance upgrade`
|
|
67
|
+
* exists to fix; answering it with the newest config would have a caller
|
|
68
|
+
* extract from 10.0.2500 and stamp the result as this target's, while its index
|
|
69
|
+
* and server still reference the old pin.
|
|
70
|
+
*/
|
|
71
|
+
export function resolvePinnedXppConfig(store) {
|
|
72
|
+
if (readSetting(store, envTypeSetting) === 'traditional')
|
|
73
|
+
return null;
|
|
74
|
+
const configs = listXppConfigs();
|
|
75
|
+
if (configs.length === 0)
|
|
76
|
+
return null;
|
|
77
|
+
const configName = pinnedConfigName(store);
|
|
78
|
+
if (!configName)
|
|
79
|
+
return configs[0];
|
|
80
|
+
return configs.find(c => c.fullName === configName || c.name === configName) ?? null;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Is this target a UDE target at all — i.e. does a null from
|
|
84
|
+
* {@link resolvePinnedXppConfig} mean "its pin does not resolve" rather than
|
|
85
|
+
* "this is a traditional install"?
|
|
86
|
+
*
|
|
87
|
+
* That function returns null for three different reasons and a caller cannot
|
|
88
|
+
* tell them apart, which matters because the two answers call for opposite
|
|
89
|
+
* behaviour: a traditional target legitimately falls back to detecting the
|
|
90
|
+
* packages root on this box, while for a UDE target that fallback throws away
|
|
91
|
+
* the very guarantee the null was there to provide (see resolveSource in
|
|
92
|
+
* commands/bpCatalog.ts, and the docblock above).
|
|
93
|
+
*
|
|
94
|
+
* Mirrors the detection settings.ts documents for an unset environment.type:
|
|
95
|
+
* UDE when XPP config files exist in %LOCALAPPDATA%\Microsoft\Dynamics365\
|
|
96
|
+
* XPPConfig. So a box with no configs at all is traditional — the one case
|
|
97
|
+
* where falling through is correct — and anything else with an explicit
|
|
98
|
+
* `traditional` type is taken at its word.
|
|
99
|
+
*/
|
|
100
|
+
export function isUdeTarget(store) {
|
|
101
|
+
if (readSetting(store, envTypeSetting) === 'traditional')
|
|
102
|
+
return false;
|
|
103
|
+
return listXppConfigs().length > 0;
|
|
104
|
+
}
|
|
57
105
|
/**
|
|
58
106
|
* Expand a short config name (e.g. "myenv-dev") to the newest full versioned
|
|
59
107
|
* name ("myenv-dev___10.0.2345.153") so a later staleness check is a plain
|
|
@@ -62,6 +62,12 @@ export declare function toEnvRecord(files: Pick<ResolvedConfigFiles, 'baseDir' |
|
|
|
62
62
|
*
|
|
63
63
|
* Emitting the defaults here pins them to the installation directory instead.
|
|
64
64
|
* A checkout is its own data directory, so its paths do not move.
|
|
65
|
+
*
|
|
66
|
+
* BP_CATALOG_PATH is deliberately NOT one of them: index.bpCatalogPath carries
|
|
67
|
+
* no registry default precisely so that "unset" keeps meaning "use the
|
|
68
|
+
* compiled-in catalog". Giving it one here would make the variable permanently
|
|
69
|
+
* set and every install that has never regenerated a catalog warn about a
|
|
70
|
+
* missing file on each start.
|
|
65
71
|
*/
|
|
66
72
|
export declare function defaultPathEnv(baseDir: string): Record<string, string>;
|
|
67
73
|
/** Write the config file, creating its directory. Keys are emitted in registry order. */
|
|
@@ -142,6 +142,12 @@ export function toEnvRecord(files) {
|
|
|
142
142
|
*
|
|
143
143
|
* Emitting the defaults here pins them to the installation directory instead.
|
|
144
144
|
* A checkout is its own data directory, so its paths do not move.
|
|
145
|
+
*
|
|
146
|
+
* BP_CATALOG_PATH is deliberately NOT one of them: index.bpCatalogPath carries
|
|
147
|
+
* no registry default precisely so that "unset" keeps meaning "use the
|
|
148
|
+
* compiled-in catalog". Giving it one here would make the variable permanently
|
|
149
|
+
* set and every install that has never regenerated a catalog warn about a
|
|
150
|
+
* missing file on each start.
|
|
145
151
|
*/
|
|
146
152
|
export function defaultPathEnv(baseDir) {
|
|
147
153
|
const out = {};
|
package/dist/config/settings.js
CHANGED
|
@@ -331,6 +331,25 @@ export const SETTINGS = [
|
|
|
331
331
|
description: 'Working folder for the XML dumped during extraction, before it is loaded into the database.',
|
|
332
332
|
default: './extracted-metadata',
|
|
333
333
|
},
|
|
334
|
+
{
|
|
335
|
+
path: 'index.bpCatalogPath',
|
|
336
|
+
env: 'BP_CATALOG_PATH',
|
|
337
|
+
section: 'index',
|
|
338
|
+
tier: 'advanced',
|
|
339
|
+
type: 'path',
|
|
340
|
+
label: 'BP moniker catalog file',
|
|
341
|
+
description: 'Per-instance JSON catalog of real BP-check monikers, extracted from this instance\'s own D365FO version ' +
|
|
342
|
+
'(scripts/extract-bp-catalog.ps1). Falls back to the compiled-in snapshot when absent — this setting is only ' +
|
|
343
|
+
'written once an instance has regenerated its own catalog.',
|
|
344
|
+
// Deliberately no `default`. defaultPathEnv() projects every path setting
|
|
345
|
+
// that has a string default onto process.env, which would make
|
|
346
|
+
// BP_CATALOG_PATH permanently set — the "unset → compiled-in catalog"
|
|
347
|
+
// branch in bpMonikers/loadCatalog() would be unreachable and every
|
|
348
|
+
// install that has never regenerated a catalog (all existing ones, every
|
|
349
|
+
// Linux deployment, every checkout) would warn about a missing file on
|
|
350
|
+
// each start. ensureBpCatalogFresh writes the value after a successful
|
|
351
|
+
// extraction instead, which is what the description above promises.
|
|
352
|
+
},
|
|
334
353
|
{
|
|
335
354
|
path: 'index.labelSortOrder',
|
|
336
355
|
env: 'LABEL_SORT_ORDER',
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GENERATED FILE — do not hand-edit. Regenerate with:
|
|
3
|
+
* pwsh scripts/extract-bp-catalog.ps1
|
|
4
|
+
*
|
|
5
|
+
* BP-rule moniker catalog, extracted from a local D365FO install:
|
|
6
|
+
* - `canonical: true` monikers come from the union of every model's
|
|
7
|
+
* <Model>/<Model>/AxRuleSet/BPRules.xml — the authoritative name list.
|
|
8
|
+
* - `message`/`description` come from the .NET-authored rule DLLs'
|
|
9
|
+
* resx-backed resource classes (bin/BPExtensions/*.dll and a couple of
|
|
10
|
+
* core bin/*.dll) where the rule author provided one. A `null` there
|
|
11
|
+
* means "not found in a resource class", NOT "not a real rule".
|
|
12
|
+
* - Presence in this file does NOT by itself mean "BP rule". The resource
|
|
13
|
+
* dump also yields upgrade- and form-conversion-tool messages, which come
|
|
14
|
+
* out with `canonical: false`. `canonical` is the field that answers
|
|
15
|
+
* "is this a BP rule".
|
|
16
|
+
*
|
|
17
|
+
* Extracted from: C:\Users\laeliand\AppData\Local\Microsoft\Dynamics365\10.0.2527.174\PackagesLocalDirectory
|
|
18
|
+
* Generated at: (stamp with the actual date when regenerating — omitted
|
|
19
|
+
* here so re-running with no real change produces no diff)
|
|
20
|
+
*/
|
|
21
|
+
export interface BpMonikerEntry {
|
|
22
|
+
moniker: string;
|
|
23
|
+
/** Message template (often with '{0}'-style placeholders), or null if not found in a resource class. */
|
|
24
|
+
message: string | null;
|
|
25
|
+
/** What the rule checks, or null if not found in a resource class. */
|
|
26
|
+
description: string | null;
|
|
27
|
+
/** True if this moniker appears in at least one model's AxRuleSet/BPRules.xml. */
|
|
28
|
+
canonical: boolean;
|
|
29
|
+
}
|
|
30
|
+
export declare const BP_MONIKER_CATALOG: BpMonikerEntry[];
|
|
31
|
+
//# sourceMappingURL=catalog.generated.d.ts.map
|