d365fo-mcp 1.13.0 → 1.14.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/bridge/D365MetadataBridge/Program.cs +128 -20
- package/bridge/D365MetadataBridge/Protocol/RequestDispatcher.cs +37 -0
- package/bridge/D365MetadataBridge/Services/MetadataReadService.cs +56 -59
- package/dist/bridge/bridgeAdapter.js +7 -1
- package/dist/cli/session/analyze.js +6 -1
- package/dist/knowledge/kernelEnums.d.ts +64 -0
- package/dist/knowledge/kernelEnums.js +104 -0
- package/dist/server/toolSchemas/buildD365foProject.d.ts +1 -1
- package/dist/server/toolSchemas/buildD365foProject.js +2 -2
- package/dist/server/toolSchemas/d365foFile.js +7 -6
- package/dist/server/toolSchemas/extensionInfo.js +2 -2
- package/dist/server/toolSchemas/findReferences.js +1 -1
- package/dist/server/toolSchemas/getKnowledge.d.ts +8 -0
- package/dist/server/toolSchemas/getKnowledge.js +6 -0
- package/dist/server/toolSchemas/getObjectInfo.js +3 -3
- package/dist/server/toolSchemas/index.d.ts +10 -60
- package/dist/server/toolSchemas/labels.d.ts +1 -19
- package/dist/server/toolSchemas/labels.js +7 -15
- package/dist/server/toolSchemas/objectPatterns.js +3 -3
- package/dist/server/toolSchemas/runBpCheck.js +3 -3
- package/dist/server/toolSchemas/runSystestClass.js +2 -2
- package/dist/server/toolSchemas/search.d.ts +0 -36
- package/dist/server/toolSchemas/search.js +3 -42
- package/dist/server/toolSchemas/triggerDbSync.d.ts +0 -4
- package/dist/server/toolSchemas/triggerDbSync.js +4 -5
- package/dist/server/toolSchemas/undoLastModification.js +1 -1
- package/dist/server/toolSchemas/validateCode.js +3 -3
- package/dist/server/toolSchemas/verifyD365foProject.js +1 -1
- package/dist/tools/analysis/search.js +4 -2
- package/dist/tools/analysis/validateCode.d.ts +1 -1
- package/dist/tools/analysis/validateCode.js +237 -42
- package/dist/tools/analysis/validateObjectNaming.d.ts +4 -3
- package/dist/tools/analysis/validateObjectNaming.js +12 -355
- package/dist/tools/d365foFile.js +91 -2
- package/dist/tools/knowledge/getKnowledge.d.ts +2 -0
- package/dist/tools/knowledge/getKnowledge.js +71 -0
- package/dist/tools/prepare/prepare.d.ts +3 -1
- package/dist/tools/prepare/prepare.js +53 -1
- package/dist/tools/prepare/prepareChange.js +45 -7
- package/dist/tools/prepare/prepareCreate.js +45 -5
- package/dist/tools/readers/enumInfo.d.ts +1 -6
- package/dist/tools/readers/enumInfo.js +9 -0
- package/dist/tools/sdlc/buildProject.d.ts +13 -0
- package/dist/tools/sdlc/buildProject.js +89 -7
- package/dist/tools/sdlc/runBpCheck.d.ts +13 -0
- package/dist/tools/sdlc/runBpCheck.js +82 -7
- package/dist/tools/smart/codeGen.js +3 -2
- package/dist/tools/specs/d365foFileOpSpecs.js +45 -12
- package/dist/tools/specs/generateObjectOpSpecs.js +3 -4
- package/dist/tools/toolHandler.js +54 -14
- package/dist/tools/write/createD365File.d.ts +15 -1
- package/dist/tools/write/createD365File.js +31 -25
- package/dist/tools/write/deleteD365File.js +8 -6
- package/dist/tools/write/inlineWriteVerification.d.ts +15 -0
- package/dist/tools/write/inlineWriteVerification.js +20 -0
- package/dist/tools/write/modifyD365File.d.ts +205 -0
- package/dist/tools/write/modifyD365File.js +199 -25
- package/dist/tools/write/resolveReferences.js +5 -6
- package/dist/tools/xml/securityPrivilegeXml.d.ts +56 -0
- package/dist/tools/xml/securityPrivilegeXml.js +121 -7
- package/dist/utils/callDedup.d.ts +31 -2
- package/dist/utils/callDedup.js +73 -4
- package/dist/utils/formExtensionControlModifications.d.ts +92 -0
- package/dist/utils/formExtensionControlModifications.js +289 -0
- package/dist/utils/objectNamingRules.d.ts +60 -0
- package/dist/utils/objectNamingRules.js +398 -0
- package/dist/utils/toolMetrics.d.ts +18 -1
- package/dist/utils/toolMetrics.js +23 -2
- package/dist/validation/formPatternValidator.js +15 -0
- package/package.json +1 -1
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
using System;
|
|
2
|
+
using System.Collections.Generic;
|
|
2
3
|
using System.Diagnostics;
|
|
3
4
|
using System.IO;
|
|
4
5
|
using System.Reflection;
|
|
5
6
|
using System.Runtime.CompilerServices;
|
|
6
7
|
using System.Text.Json;
|
|
8
|
+
using System.Threading;
|
|
7
9
|
using System.Threading.Tasks;
|
|
8
10
|
using D365MetadataBridge.Protocol;
|
|
9
11
|
|
|
@@ -219,9 +221,33 @@ namespace D365MetadataBridge
|
|
|
219
221
|
}
|
|
220
222
|
}
|
|
221
223
|
|
|
224
|
+
/// <summary>
|
|
225
|
+
/// How many reads may be inside the provider at once. Bounded on purpose: the
|
|
226
|
+
/// win is in overlapping a handful of fan-out reads, not in handing an unbounded
|
|
227
|
+
/// number of threads to a metadata provider whose thread-safety is undocumented.
|
|
228
|
+
/// </summary>
|
|
229
|
+
private const int ReadSlots = 6;
|
|
230
|
+
|
|
231
|
+
/// <summary>
|
|
232
|
+
/// A read takes one slot; anything else takes ALL of them, which is what makes a
|
|
233
|
+
/// write or a provider refresh exclusive against every in-flight read. Simple
|
|
234
|
+
/// enough to reason about without an async reader/writer lock.
|
|
235
|
+
///
|
|
236
|
+
/// A writer waits for the slots, so a continuous stream of reads could delay it.
|
|
237
|
+
/// In practice requests come from one MCP client that awaits its own calls, so
|
|
238
|
+
/// there is no such stream.
|
|
239
|
+
/// </summary>
|
|
240
|
+
private static readonly SemaphoreSlim _slots = new SemaphoreSlim(ReadSlots, ReadSlots);
|
|
241
|
+
|
|
242
|
+
/// <summary>stdout is one stream: concurrent handlers must not interleave lines on it.</summary>
|
|
243
|
+
private static readonly SemaphoreSlim _stdoutLock = new SemaphoreSlim(1, 1);
|
|
244
|
+
private static readonly StreamWriter _stdout =
|
|
245
|
+
new StreamWriter(Console.OpenStandardOutput()) { AutoFlush = true };
|
|
246
|
+
|
|
222
247
|
private static async Task<int> RunStdioLoop(RequestDispatcher dispatcher)
|
|
223
248
|
{
|
|
224
249
|
var reader = new StreamReader(Console.OpenStandardInput());
|
|
250
|
+
var inFlight = new List<Task>();
|
|
225
251
|
|
|
226
252
|
string? line;
|
|
227
253
|
while ((line = await reader.ReadLineAsync()) != null)
|
|
@@ -229,45 +255,127 @@ namespace D365MetadataBridge
|
|
|
229
255
|
if (string.IsNullOrWhiteSpace(line))
|
|
230
256
|
continue;
|
|
231
257
|
|
|
232
|
-
|
|
258
|
+
BridgeRequest? request = null;
|
|
233
259
|
try
|
|
234
260
|
{
|
|
235
|
-
|
|
236
|
-
if (request == null || string.IsNullOrEmpty(request.Method))
|
|
237
|
-
{
|
|
238
|
-
response = BridgeResponse.CreateError("?", -32600, "Invalid request");
|
|
239
|
-
}
|
|
240
|
-
else
|
|
241
|
-
{
|
|
242
|
-
Log.WriteLine($"[DEBUG] → {request.Method} (id={request.Id})");
|
|
243
|
-
response = await dispatcher.Dispatch(request);
|
|
244
|
-
Log.WriteLine($"[DEBUG] ← {request.Method} OK (id={request.Id})");
|
|
245
|
-
}
|
|
261
|
+
request = JsonSerializer.Deserialize<BridgeRequest>(line, JsonOptions.Default);
|
|
246
262
|
}
|
|
247
263
|
catch (JsonException ex)
|
|
248
264
|
{
|
|
249
265
|
Log.WriteLine($"[ERROR] JSON parse error: {ex.Message}");
|
|
250
|
-
|
|
266
|
+
await WriteResponse(BridgeResponse.CreateError("?", -32700, $"Parse error: {ex.Message}"));
|
|
267
|
+
continue;
|
|
251
268
|
}
|
|
252
|
-
|
|
269
|
+
|
|
270
|
+
if (request == null || string.IsNullOrEmpty(request.Method))
|
|
253
271
|
{
|
|
254
|
-
|
|
255
|
-
|
|
272
|
+
await WriteResponse(BridgeResponse.CreateError("?", -32600, "Invalid request"));
|
|
273
|
+
continue;
|
|
256
274
|
}
|
|
257
275
|
|
|
258
|
-
|
|
276
|
+
// Reads go to the thread pool so the loop can read the next line at once;
|
|
277
|
+
// everything else keeps the original behaviour of running to completion
|
|
278
|
+
// before the next request is even parsed.
|
|
279
|
+
if (RequestDispatcher.IsConcurrentSafeRead(request.Method))
|
|
280
|
+
{
|
|
281
|
+
// Touch Exception on a faulted task before dropping it, so the fault
|
|
282
|
+
// is observed rather than left for the finalizer — a task pruned here
|
|
283
|
+
// is never awaited anywhere else.
|
|
284
|
+
inFlight.RemoveAll(t =>
|
|
285
|
+
{
|
|
286
|
+
if (t.IsFaulted) { var _ = t.Exception; }
|
|
287
|
+
return t.IsCompleted;
|
|
288
|
+
});
|
|
289
|
+
inFlight.Add(HandleConcurrently(dispatcher, request));
|
|
290
|
+
}
|
|
291
|
+
else
|
|
292
|
+
{
|
|
293
|
+
// Drain the reads already running, then take every slot: a write or a
|
|
294
|
+
// provider rebuild must not overlap a read of the provider it replaces.
|
|
295
|
+
if (inFlight.Count > 0)
|
|
296
|
+
{
|
|
297
|
+
// Guarded. DispatchGuarded turns any handler escape into a
|
|
298
|
+
// response, so the only realistic thrower left is WriteResponse on
|
|
299
|
+
// a broken stdout — and an unguarded WhenAll rethrows that HERE,
|
|
300
|
+
// out of RunStdioLoop and out of Main, killing the bridge because
|
|
301
|
+
// an unrelated read could not print. The final drain below was
|
|
302
|
+
// already written this way; this one was not.
|
|
303
|
+
try { await Task.WhenAll(inFlight); }
|
|
304
|
+
catch { /* each read already answered, or failed on its own */ }
|
|
305
|
+
inFlight.Clear();
|
|
306
|
+
}
|
|
307
|
+
for (var i = 0; i < ReadSlots; i++) await _slots.WaitAsync();
|
|
308
|
+
try
|
|
309
|
+
{
|
|
310
|
+
await WriteResponse(await DispatchGuarded(dispatcher, request));
|
|
311
|
+
}
|
|
312
|
+
finally
|
|
313
|
+
{
|
|
314
|
+
_slots.Release(ReadSlots);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
259
317
|
}
|
|
260
318
|
|
|
319
|
+
if (inFlight.Count > 0)
|
|
320
|
+
{
|
|
321
|
+
try { await Task.WhenAll(inFlight); } catch { /* each response was already sent */ }
|
|
322
|
+
}
|
|
261
323
|
Log.WriteLine("[INFO] stdin closed, bridge exiting");
|
|
262
324
|
return 0;
|
|
263
325
|
}
|
|
264
326
|
|
|
327
|
+
/// <summary>One read: take a slot, dispatch, answer. Never throws back into the loop.</summary>
|
|
328
|
+
private static async Task HandleConcurrently(RequestDispatcher dispatcher, BridgeRequest request)
|
|
329
|
+
{
|
|
330
|
+
await _slots.WaitAsync();
|
|
331
|
+
try
|
|
332
|
+
{
|
|
333
|
+
// Task.Run, not a bare await: every read handler returns
|
|
334
|
+
// Task.FromResult(...), so Dispatch runs to completion SYNCHRONOUSLY on
|
|
335
|
+
// whichever thread calls it. Awaiting it here left the work on the stdio
|
|
336
|
+
// loop thread and measured 1.06x against 1.04x for the serial build —
|
|
337
|
+
// the change did nothing until the work actually left this thread.
|
|
338
|
+
var response = await Task.Run(() => DispatchGuarded(dispatcher, request));
|
|
339
|
+
await WriteResponse(response);
|
|
340
|
+
}
|
|
341
|
+
finally
|
|
342
|
+
{
|
|
343
|
+
_slots.Release();
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/// <summary>Dispatch, turning any escape into a response rather than a lost request.</summary>
|
|
348
|
+
private static async Task<BridgeResponse> DispatchGuarded(RequestDispatcher dispatcher, BridgeRequest request)
|
|
349
|
+
{
|
|
350
|
+
try
|
|
351
|
+
{
|
|
352
|
+
Log.WriteLine($"[DEBUG] → {request.Method} (id={request.Id})");
|
|
353
|
+
var response = await dispatcher.Dispatch(request);
|
|
354
|
+
Log.WriteLine($"[DEBUG] ← {request.Method} OK (id={request.Id})");
|
|
355
|
+
return response;
|
|
356
|
+
}
|
|
357
|
+
catch (Exception ex)
|
|
358
|
+
{
|
|
359
|
+
Log.WriteLine($"[ERROR] Unhandled: {ex.Message}\n{ex.StackTrace}");
|
|
360
|
+
return BridgeResponse.CreateError(request.Id, -32603, $"Internal error: {ex.Message}");
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
265
364
|
private static async Task WriteResponse(BridgeResponse response)
|
|
266
365
|
{
|
|
267
366
|
var json = JsonSerializer.Serialize(response, JsonOptions.Default);
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
await
|
|
367
|
+
// Serialized, and on ONE cached writer: concurrent handlers each opening their
|
|
368
|
+
// own StreamWriter over the same handle is how a JSON-RPC stream gets interleaved.
|
|
369
|
+
await _stdoutLock.WaitAsync();
|
|
370
|
+
try
|
|
371
|
+
{
|
|
372
|
+
await _stdout.WriteLineAsync(json);
|
|
373
|
+
await _stdout.FlushAsync();
|
|
374
|
+
}
|
|
375
|
+
finally
|
|
376
|
+
{
|
|
377
|
+
_stdoutLock.Release();
|
|
378
|
+
}
|
|
271
379
|
}
|
|
272
380
|
|
|
273
381
|
/// <summary>
|
|
@@ -813,6 +813,43 @@ namespace D365MetadataBridge.Protocol
|
|
|
813
813
|
}
|
|
814
814
|
}
|
|
815
815
|
|
|
816
|
+
/// <summary>
|
|
817
|
+
/// Methods that only READ metadata, and may therefore run concurrently with
|
|
818
|
+
/// each other.
|
|
819
|
+
///
|
|
820
|
+
/// Measured on this VM: six reads issued together took 674 ms against 689 ms
|
|
821
|
+
/// issued one at a time — a 1.02x "speedup", because the stdio loop awaited each
|
|
822
|
+
/// Dispatch before reading the next line. Every fan-out the TS side performs
|
|
823
|
+
/// (get_object_info objects[], search queries[], prepare's five probes) was
|
|
824
|
+
/// therefore pipelining into a queue of one.
|
|
825
|
+
///
|
|
826
|
+
/// The list is explicit and the DEFAULT IS FALSE: anything not named here —
|
|
827
|
+
/// including a method added later — keeps the old exclusive behaviour. A write
|
|
828
|
+
/// misfiled as a read would corrupt metadata; a read misfiled as a write is
|
|
829
|
+
/// merely as slow as it is today.
|
|
830
|
+
/// </summary>
|
|
831
|
+
private static readonly HashSet<string> ConcurrentSafeReads =
|
|
832
|
+
new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
|
833
|
+
{
|
|
834
|
+
"ping", "getcapabilities", "getinfo", "getxrefschema",
|
|
835
|
+
// Typed readers
|
|
836
|
+
"readtable", "readclass", "readenum", "readedt", "readform", "readquery",
|
|
837
|
+
"readview", "readreport", "readdataentity", "readmenuitem",
|
|
838
|
+
"readsecurityprivilege", "readsecurityduty", "readsecurityrole",
|
|
839
|
+
"readtableextensions",
|
|
840
|
+
// Short aliases of the same readers
|
|
841
|
+
"table", "class", "enum", "edt", "form", "query", "view", "menu",
|
|
842
|
+
// Lookup / search
|
|
843
|
+
"searchobjects", "listobjects", "resolveobjectinfo", "getcompletionmembers",
|
|
844
|
+
"getmethodsource", "validateobject", "discoverformpatterns",
|
|
845
|
+
// Cross-reference queries
|
|
846
|
+
"findreferences", "findeventsubscribers", "findextensionclasses",
|
|
847
|
+
"findapiusagecallers", "samplexrefrows",
|
|
848
|
+
};
|
|
849
|
+
|
|
850
|
+
/// <summary>True when <paramref name="method"/> may run alongside other reads.</summary>
|
|
851
|
+
public static bool IsConcurrentSafeRead(string? method)
|
|
852
|
+
=> !string.IsNullOrEmpty(method) && ConcurrentSafeReads.Contains(method!);
|
|
816
853
|
private Task<BridgeResponse> HandleMetadata(BridgeRequest request, Func<object?> handler)
|
|
817
854
|
{
|
|
818
855
|
if (_metadataService == null)
|
|
@@ -1150,68 +1150,65 @@ namespace D365MetadataBridge.Services
|
|
|
1150
1150
|
// seen set prevents duplicate names when the same object exists in both providers
|
|
1151
1151
|
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
1152
1152
|
|
|
1153
|
-
|
|
1153
|
+
// Takes the getter, not a materialized list: as an ARGUMENT, Keys(...) ran to
|
|
1154
|
+
// completion before this method could return, so every collection was fully
|
|
1155
|
+
// enumerated even when the result budget was already full. For the default
|
|
1156
|
+
// "all" filter that is 14 primary-key lists per search, on both providers.
|
|
1157
|
+
void Search(string objType, Func<IEnumerable<string>> getter)
|
|
1154
1158
|
{
|
|
1155
|
-
|
|
1156
|
-
|
|
1159
|
+
// Budget spent — do not touch this collection at all.
|
|
1160
|
+
if (result.Results.Count >= maxResults) return;
|
|
1161
|
+
try
|
|
1157
1162
|
{
|
|
1158
|
-
|
|
1159
|
-
if (
|
|
1160
|
-
|
|
1163
|
+
var raw = getter();
|
|
1164
|
+
if (raw == null) return;
|
|
1165
|
+
foreach (var n in raw)
|
|
1166
|
+
{
|
|
1167
|
+
// Stops ENUMERATING, not merely adding.
|
|
1168
|
+
if (result.Results.Count >= maxResults) return;
|
|
1169
|
+
if (n.IndexOf(query, StringComparison.OrdinalIgnoreCase) >= 0 && seen.Add(n))
|
|
1170
|
+
result.Results.Add(new SearchItemModel { Name = n, Type = objType });
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
catch (Exception ex)
|
|
1174
|
+
{
|
|
1175
|
+
Console.Error.WriteLine($"[WARN] Search: could not list '{objType}': {ex.GetType().Name}: {ex.Message}");
|
|
1161
1176
|
}
|
|
1162
1177
|
}
|
|
1163
1178
|
|
|
1164
|
-
//
|
|
1165
|
-
// IMPORTANT: these are accessed STRONGLY-TYPED through the public
|
|
1179
|
+
// Collections are accessed STRONGLY-TYPED through the public
|
|
1166
1180
|
// Microsoft.Dynamics.AX.Metadata.Providers.IMetadataProvider interface — NOT via
|
|
1167
1181
|
// `dynamic`. The concrete provider (DiskMetadataProvider) is an internal type, and
|
|
1168
1182
|
// the DLR cannot bind to public members of an internal type from this assembly:
|
|
1169
1183
|
// `dynamic dyn = prov; dyn.MenuItemDisplays` throws RuntimeBinderException
|
|
1170
1184
|
// ("'object' does not contain a definition for 'MenuItemDisplays'") and silently
|
|
1171
1185
|
// yields nothing — which is exactly how menu items/security stayed invisible.
|
|
1172
|
-
// Interface access binds at compile time and works.
|
|
1173
|
-
// to IList<string>) so a lazy IEnumerable<string> return is handled safely too.
|
|
1174
|
-
List<string> Keys(string label, Func<IEnumerable<string>> getter)
|
|
1175
|
-
{
|
|
1176
|
-
var list = new List<string>();
|
|
1177
|
-
try
|
|
1178
|
-
{
|
|
1179
|
-
var raw = getter();
|
|
1180
|
-
if (raw != null)
|
|
1181
|
-
foreach (var k in raw) list.Add(k);
|
|
1182
|
-
}
|
|
1183
|
-
catch (Exception ex)
|
|
1184
|
-
{
|
|
1185
|
-
Console.Error.WriteLine($"[WARN] Search: could not list '{label}': {ex.GetType().Name}: {ex.Message}");
|
|
1186
|
-
}
|
|
1187
|
-
return list;
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1186
|
+
// Interface access binds at compile time and works.
|
|
1190
1187
|
void SearchProvider(IMetadataProvider prov)
|
|
1191
1188
|
{
|
|
1192
1189
|
try
|
|
1193
1190
|
{
|
|
1194
1191
|
switch (type.ToLowerInvariant())
|
|
1195
1192
|
{
|
|
1196
|
-
case "table": Search("table",
|
|
1197
|
-
case "class": Search("class",
|
|
1198
|
-
case "enum": Search("enum",
|
|
1199
|
-
case "edt": Search("edt",
|
|
1200
|
-
case "form": Search("form",
|
|
1201
|
-
case "query": Search("query",
|
|
1202
|
-
case "view": Search("view",
|
|
1193
|
+
case "table": Search("table", () => prov.Tables.GetPrimaryKeys()); break;
|
|
1194
|
+
case "class": Search("class", () => prov.Classes.GetPrimaryKeys()); break;
|
|
1195
|
+
case "enum": Search("enum", () => prov.Enums.GetPrimaryKeys()); break;
|
|
1196
|
+
case "edt": Search("edt", () => prov.Edts.GetPrimaryKeys()); break;
|
|
1197
|
+
case "form": Search("form", () => prov.Forms.GetPrimaryKeys()); break;
|
|
1198
|
+
case "query": Search("query", () => prov.Queries.GetPrimaryKeys()); break;
|
|
1199
|
+
case "view": Search("view", () => prov.Views.GetPrimaryKeys()); break;
|
|
1203
1200
|
case "data-entity":
|
|
1204
|
-
case "dataentity": Search("data-entity",
|
|
1201
|
+
case "dataentity": Search("data-entity", () => prov.DataEntityViews.GetPrimaryKeys()); break;
|
|
1205
1202
|
|
|
1206
|
-
case "menu-item-display": Search("menu-item-display",
|
|
1207
|
-
case "menu-item-action": Search("menu-item-action",
|
|
1208
|
-
case "menu-item-output": Search("menu-item-output",
|
|
1203
|
+
case "menu-item-display": Search("menu-item-display", () => prov.MenuItemDisplays.GetPrimaryKeys()); break;
|
|
1204
|
+
case "menu-item-action": Search("menu-item-action", () => prov.MenuItemActions.GetPrimaryKeys()); break;
|
|
1205
|
+
case "menu-item-output": Search("menu-item-output", () => prov.MenuItemOutputs.GetPrimaryKeys()); break;
|
|
1209
1206
|
|
|
1210
|
-
case "security-privilege": Search("security-privilege",
|
|
1211
|
-
case "security-duty": Search("security-duty",
|
|
1212
|
-
case "security-role": Search("security-role",
|
|
1207
|
+
case "security-privilege": Search("security-privilege", () => prov.SecurityPrivileges.GetPrimaryKeys()); break;
|
|
1208
|
+
case "security-duty": Search("security-duty", () => prov.SecurityDuties.GetPrimaryKeys()); break;
|
|
1209
|
+
case "security-role": Search("security-role", () => prov.SecurityRoles.GetPrimaryKeys()); break;
|
|
1213
1210
|
|
|
1214
|
-
case "table-extension": Search("table-extension",
|
|
1211
|
+
case "table-extension": Search("table-extension", () => prov.TableExtensions.GetPrimaryKeys()); break;
|
|
1215
1212
|
// IMetadataProvider has no ClassExtensions collection — CoC/augmentation
|
|
1216
1213
|
// classes live in the regular Classes collection (named *_Extension). The
|
|
1217
1214
|
// bridge can't filter them out here, so return nothing and let the caller
|
|
@@ -1219,28 +1216,28 @@ namespace D365MetadataBridge.Services
|
|
|
1219
1216
|
case "class-extension":
|
|
1220
1217
|
Console.Error.WriteLine("[DEBUG] Search: class-extension requested — no IMetadataProvider collection; falling back to SQLite index");
|
|
1221
1218
|
break;
|
|
1222
|
-
case "form-extension": Search("form-extension",
|
|
1223
|
-
case "enum-extension": Search("enum-extension",
|
|
1224
|
-
case "edt-extension": Search("edt-extension",
|
|
1225
|
-
case "data-entity-extension": Search("data-entity-extension",
|
|
1219
|
+
case "form-extension": Search("form-extension", () => prov.FormExtensions.GetPrimaryKeys()); break;
|
|
1220
|
+
case "enum-extension": Search("enum-extension", () => prov.EnumExtensions.GetPrimaryKeys()); break;
|
|
1221
|
+
case "edt-extension": Search("edt-extension", () => prov.EdtExtensions.GetPrimaryKeys()); break;
|
|
1222
|
+
case "data-entity-extension": Search("data-entity-extension", () => prov.DataEntityViewExtensions.GetPrimaryKeys()); break;
|
|
1226
1223
|
|
|
1227
1224
|
default:
|
|
1228
1225
|
// "all" (or any unrecognized filter): enumerate every object kind so
|
|
1229
1226
|
// nothing — menu items included — is silently invisible to search.
|
|
1230
|
-
Search("table",
|
|
1231
|
-
Search("class",
|
|
1232
|
-
Search("enum",
|
|
1233
|
-
Search("edt",
|
|
1234
|
-
Search("form",
|
|
1235
|
-
Search("query",
|
|
1236
|
-
Search("view",
|
|
1237
|
-
Search("data-entity",
|
|
1238
|
-
Search("menu-item-display",
|
|
1239
|
-
Search("menu-item-action",
|
|
1240
|
-
Search("menu-item-output",
|
|
1241
|
-
Search("security-privilege",
|
|
1242
|
-
Search("security-duty",
|
|
1243
|
-
Search("security-role",
|
|
1227
|
+
Search("table", () => prov.Tables.GetPrimaryKeys());
|
|
1228
|
+
Search("class", () => prov.Classes.GetPrimaryKeys());
|
|
1229
|
+
Search("enum", () => prov.Enums.GetPrimaryKeys());
|
|
1230
|
+
Search("edt", () => prov.Edts.GetPrimaryKeys());
|
|
1231
|
+
Search("form", () => prov.Forms.GetPrimaryKeys());
|
|
1232
|
+
Search("query", () => prov.Queries.GetPrimaryKeys());
|
|
1233
|
+
Search("view", () => prov.Views.GetPrimaryKeys());
|
|
1234
|
+
Search("data-entity", () => prov.DataEntityViews.GetPrimaryKeys());
|
|
1235
|
+
Search("menu-item-display", () => prov.MenuItemDisplays.GetPrimaryKeys());
|
|
1236
|
+
Search("menu-item-action", () => prov.MenuItemActions.GetPrimaryKeys());
|
|
1237
|
+
Search("menu-item-output", () => prov.MenuItemOutputs.GetPrimaryKeys());
|
|
1238
|
+
Search("security-privilege", () => prov.SecurityPrivileges.GetPrimaryKeys());
|
|
1239
|
+
Search("security-duty", () => prov.SecurityDuties.GetPrimaryKeys());
|
|
1240
|
+
Search("security-role", () => prov.SecurityRoles.GetPrimaryKeys());
|
|
1244
1241
|
break;
|
|
1245
1242
|
}
|
|
1246
1243
|
}
|
|
@@ -1144,7 +1144,12 @@ const BRIDGE_MODIFY_OPS = new Set([
|
|
|
1144
1144
|
// (MetadataWriteService exposes no RemoveControl, and security objects have no
|
|
1145
1145
|
// bridge write path at all) — both are served by a direct-XML writer and still
|
|
1146
1146
|
// pass through this gate.
|
|
1147
|
-
|
|
1147
|
+
// add-entry-point shipped its schema entry, op-spec, dispatcher case and writer
|
|
1148
|
+
// without ever being listed here, so canBridgeModify() short-circuited and the
|
|
1149
|
+
// whole feature was unreachable from the tool surface — dead code behind a ✅
|
|
1150
|
+
// schema. Both this set AND XML_ONLY_MODIFY_PAIRS have to name it; either alone
|
|
1151
|
+
// still returns false. (eval case L2-object-delete-and-entry-point-cleanup)
|
|
1152
|
+
'remove-control', 'add-entry-point', 'remove-entry-point',
|
|
1148
1153
|
// AxIgnoreDiagnosticList is not an AOT object at all — MetadataWriteService has
|
|
1149
1154
|
// no concept of it — so this is XML-only for the same structural reason as the
|
|
1150
1155
|
// two above.
|
|
@@ -1183,6 +1188,7 @@ const BRIDGE_MODIFY_TYPES = new Set([
|
|
|
1183
1188
|
* a bridge resolution failure instead of "not supported for this object type".
|
|
1184
1189
|
*/
|
|
1185
1190
|
const XML_ONLY_MODIFY_PAIRS = {
|
|
1191
|
+
'add-entry-point': new Set(['security-privilege']),
|
|
1186
1192
|
'remove-entry-point': new Set(['security-privilege']),
|
|
1187
1193
|
'remove-diagnostic-suppression': new Set(['ignore-diagnostic-list']),
|
|
1188
1194
|
'add-diagnostic-suppression': new Set(['ignore-diagnostic-list']),
|
|
@@ -26,7 +26,12 @@ const MAX_RELATIVE_RMSE = 0.02;
|
|
|
26
26
|
* covers the tool's primary use — `labels` has a bulk create but its searches
|
|
27
27
|
* (the common case) have no batch form, and flagging those would be noise.
|
|
28
28
|
*/
|
|
29
|
-
const PLURAL_FORM_TOOLS = [
|
|
29
|
+
const PLURAL_FORM_TOOLS = [
|
|
30
|
+
'get_object_info', 'run_bp_check', 'verify_d365fo_project',
|
|
31
|
+
'd365fo_file', // operations[] applies many edits to one object in a call
|
|
32
|
+
'get_knowledge', // topics[] answers several lookups in a call
|
|
33
|
+
'search', // queries[] runs several searches in one call, in parallel
|
|
34
|
+
];
|
|
30
35
|
/**
|
|
31
36
|
* Least squares with no intercept, three unknowns, via the normal equations.
|
|
32
37
|
*
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kernel enums — enums the X++ runtime defines, with NO AOT metadata element.
|
|
3
|
+
*
|
|
4
|
+
* These are referenced by ordinary metadata (`<EnumType>NoYes</EnumType>` appears
|
|
5
|
+
* 48 times in CustTable.xml alone, and EDT NoYesId reports `Enum Type: NoYes`)
|
|
6
|
+
* but they have no `AxEnum/*.xml` anywhere, so no metadata-backed lookup can
|
|
7
|
+
* ever prove them: not the C# bridge (`IMetadataProvider.Enums` does not carry
|
|
8
|
+
* them), not the SQLite symbol index (it indexes AOT XML), not a disk probe.
|
|
9
|
+
*
|
|
10
|
+
* Treating "absent from the AOT" as "does not exist" produced two failures, both
|
|
11
|
+
* confirmed live on 2026-08-24:
|
|
12
|
+
*
|
|
13
|
+
* 1. `validate_code(mode="references", codeType="xml-table")` reported
|
|
14
|
+
* `<EnumType>NoYes</EnumType>` as a hard ERROR — "Enum NoYes not found in
|
|
15
|
+
* the symbol index" — under "Fix errors before writing — these will cause
|
|
16
|
+
* compiler failures". An agent obeying that edits correct metadata, and
|
|
17
|
+
* `search` offers it NoYesBlank / DefaultNoYes / NoYesCombo to "correct" to.
|
|
18
|
+
* Those enums are real, so the result compiles clean and means the wrong
|
|
19
|
+
* thing.
|
|
20
|
+
* 2. `get_object_info(objectType="enum", name="NoYes")` answered "not found via
|
|
21
|
+
* bridge, symbol index, or on disk" and advised searching other spellings,
|
|
22
|
+
* running update_symbol_index on `NoYes.xml`, and checking
|
|
23
|
+
* D365FO_CUSTOM_PACKAGES_PATH. None of that can ever succeed: there is no
|
|
24
|
+
* file to index. Meanwhile the EDT reader hands the agent the name `NoYes`
|
|
25
|
+
* in the first place — a loop with no exit.
|
|
26
|
+
*
|
|
27
|
+
* The X++ reference resolver already knew this (resolveReferences.ts allow-listed
|
|
28
|
+
* 'noyes' and 'exception' for exactly this reason), which is why X++ using
|
|
29
|
+
* `NoYes::Yes` validated cleanly while the XML path hard-errored on the same
|
|
30
|
+
* enum. This module is that knowledge in one place, so the next path to need it
|
|
31
|
+
* does not have to rediscover it.
|
|
32
|
+
*
|
|
33
|
+
* MEMBERSHIP IS EVIDENCE-BASED, not remembered: every name below was checked
|
|
34
|
+
* against the complete set of 8,220 `AxEnum/*.xml` basenames in
|
|
35
|
+
* K:\AosService\PackagesLocalDirectory (metamodel 7.0.7996.33) and is absent
|
|
36
|
+
* from all of them.
|
|
37
|
+
*/
|
|
38
|
+
/** A kernel enum, and the value names it is used with. */
|
|
39
|
+
export interface KernelEnum {
|
|
40
|
+
/** Canonical casing, for rendering. */
|
|
41
|
+
name: string;
|
|
42
|
+
/**
|
|
43
|
+
* Value names as OBSERVED in shipped X++/metadata under
|
|
44
|
+
* PackagesLocalDirectory. Deliberately not claimed to be exhaustive — it is
|
|
45
|
+
* what the product itself uses. Omitted where a scan found no usage, rather
|
|
46
|
+
* than filled in from memory.
|
|
47
|
+
*/
|
|
48
|
+
values?: string[];
|
|
49
|
+
}
|
|
50
|
+
/** Lowercase names, for the allow-lists that already work in lowercase. */
|
|
51
|
+
export declare const KERNEL_ENUM_NAMES: ReadonlySet<string>;
|
|
52
|
+
/** Is `name` an enum the runtime defines rather than the AOT? Case-insensitive. */
|
|
53
|
+
export declare function isKernelEnum(name: string | null | undefined): boolean;
|
|
54
|
+
/** The entry for `name`, or undefined when it is not a kernel enum. */
|
|
55
|
+
export declare function getKernelEnum(name: string | null | undefined): KernelEnum | undefined;
|
|
56
|
+
/**
|
|
57
|
+
* The answer a reader should give instead of "not found".
|
|
58
|
+
*
|
|
59
|
+
* States the one thing the caller has to know — there is nothing to index, and
|
|
60
|
+
* references to it are valid — so the reply cannot be read as "this name is
|
|
61
|
+
* wrong, go find the right one".
|
|
62
|
+
*/
|
|
63
|
+
export declare function describeKernelEnum(name: string): string | null;
|
|
64
|
+
//# sourceMappingURL=kernelEnums.d.ts.map
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kernel enums — enums the X++ runtime defines, with NO AOT metadata element.
|
|
3
|
+
*
|
|
4
|
+
* These are referenced by ordinary metadata (`<EnumType>NoYes</EnumType>` appears
|
|
5
|
+
* 48 times in CustTable.xml alone, and EDT NoYesId reports `Enum Type: NoYes`)
|
|
6
|
+
* but they have no `AxEnum/*.xml` anywhere, so no metadata-backed lookup can
|
|
7
|
+
* ever prove them: not the C# bridge (`IMetadataProvider.Enums` does not carry
|
|
8
|
+
* them), not the SQLite symbol index (it indexes AOT XML), not a disk probe.
|
|
9
|
+
*
|
|
10
|
+
* Treating "absent from the AOT" as "does not exist" produced two failures, both
|
|
11
|
+
* confirmed live on 2026-08-24:
|
|
12
|
+
*
|
|
13
|
+
* 1. `validate_code(mode="references", codeType="xml-table")` reported
|
|
14
|
+
* `<EnumType>NoYes</EnumType>` as a hard ERROR — "Enum NoYes not found in
|
|
15
|
+
* the symbol index" — under "Fix errors before writing — these will cause
|
|
16
|
+
* compiler failures". An agent obeying that edits correct metadata, and
|
|
17
|
+
* `search` offers it NoYesBlank / DefaultNoYes / NoYesCombo to "correct" to.
|
|
18
|
+
* Those enums are real, so the result compiles clean and means the wrong
|
|
19
|
+
* thing.
|
|
20
|
+
* 2. `get_object_info(objectType="enum", name="NoYes")` answered "not found via
|
|
21
|
+
* bridge, symbol index, or on disk" and advised searching other spellings,
|
|
22
|
+
* running update_symbol_index on `NoYes.xml`, and checking
|
|
23
|
+
* D365FO_CUSTOM_PACKAGES_PATH. None of that can ever succeed: there is no
|
|
24
|
+
* file to index. Meanwhile the EDT reader hands the agent the name `NoYes`
|
|
25
|
+
* in the first place — a loop with no exit.
|
|
26
|
+
*
|
|
27
|
+
* The X++ reference resolver already knew this (resolveReferences.ts allow-listed
|
|
28
|
+
* 'noyes' and 'exception' for exactly this reason), which is why X++ using
|
|
29
|
+
* `NoYes::Yes` validated cleanly while the XML path hard-errored on the same
|
|
30
|
+
* enum. This module is that knowledge in one place, so the next path to need it
|
|
31
|
+
* does not have to rediscover it.
|
|
32
|
+
*
|
|
33
|
+
* MEMBERSHIP IS EVIDENCE-BASED, not remembered: every name below was checked
|
|
34
|
+
* against the complete set of 8,220 `AxEnum/*.xml` basenames in
|
|
35
|
+
* K:\AosService\PackagesLocalDirectory (metamodel 7.0.7996.33) and is absent
|
|
36
|
+
* from all of them.
|
|
37
|
+
*/
|
|
38
|
+
const ENTRIES = [
|
|
39
|
+
// By far the most common: boolean-ish fields reach it through EDT NoYesId.
|
|
40
|
+
{ name: 'NoYes', values: ['No', 'Yes'] },
|
|
41
|
+
{ name: 'Exception', values: [
|
|
42
|
+
'Break', 'CLRError', 'CodeAccessSecurity', 'DDEerror', 'Deadlock',
|
|
43
|
+
'DuplicateKeyException', 'DuplicateKeyExceptionNotRecovered', 'Error',
|
|
44
|
+
'FunctionArgument', 'Info', 'Internal', 'Sequence', 'Timeout',
|
|
45
|
+
'TransientSqlConnectionError', 'UpdateConflict',
|
|
46
|
+
'UpdateConflictNotRecovered', 'ViewDataSourceValidation', 'Warning',
|
|
47
|
+
] },
|
|
48
|
+
{ name: 'Types', values: [
|
|
49
|
+
'AnyType', 'Blob', 'Class', 'Container', 'Date', 'Enum', 'Guid', 'Int64',
|
|
50
|
+
'Integer', 'List', 'RString', 'Real', 'Record', 'String', 'Time',
|
|
51
|
+
'UserType', 'UtcDateTime', 'VarArg', 'VarString', 'Void',
|
|
52
|
+
] },
|
|
53
|
+
{ name: 'TableScope', values: ['CurrentTableOnly', 'IncludeBaseTables', 'IncludeDerivedTables'] },
|
|
54
|
+
{ name: 'ConcurrencyModel', values: ['Auto', 'Optimistic', 'Pessimistic'] },
|
|
55
|
+
{ name: 'StatementType', values: ['Delete', 'Insert', 'Select', 'Update'] },
|
|
56
|
+
// Real kernel enums with no usage in the packages scanned — listed without
|
|
57
|
+
// values rather than with guessed ones.
|
|
58
|
+
{ name: 'IsolationLevel' },
|
|
59
|
+
{ name: 'UtcDateTimeOrder' },
|
|
60
|
+
{ name: 'DateOrder' },
|
|
61
|
+
{ name: 'DateDay' },
|
|
62
|
+
{ name: 'DateMonth' },
|
|
63
|
+
{ name: 'DateYear' },
|
|
64
|
+
];
|
|
65
|
+
const BY_LOWER = new Map(ENTRIES.map(e => [e.name.toLowerCase(), e]));
|
|
66
|
+
/** Lowercase names, for the allow-lists that already work in lowercase. */
|
|
67
|
+
export const KERNEL_ENUM_NAMES = new Set(BY_LOWER.keys());
|
|
68
|
+
/** Is `name` an enum the runtime defines rather than the AOT? Case-insensitive. */
|
|
69
|
+
export function isKernelEnum(name) {
|
|
70
|
+
return !!name && BY_LOWER.has(name.trim().toLowerCase());
|
|
71
|
+
}
|
|
72
|
+
/** The entry for `name`, or undefined when it is not a kernel enum. */
|
|
73
|
+
export function getKernelEnum(name) {
|
|
74
|
+
return name ? BY_LOWER.get(name.trim().toLowerCase()) : undefined;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The answer a reader should give instead of "not found".
|
|
78
|
+
*
|
|
79
|
+
* States the one thing the caller has to know — there is nothing to index, and
|
|
80
|
+
* references to it are valid — so the reply cannot be read as "this name is
|
|
81
|
+
* wrong, go find the right one".
|
|
82
|
+
*/
|
|
83
|
+
export function describeKernelEnum(name) {
|
|
84
|
+
const entry = getKernelEnum(name);
|
|
85
|
+
if (!entry)
|
|
86
|
+
return null;
|
|
87
|
+
const lines = [
|
|
88
|
+
`# Enum: ${entry.name} (kernel enum)`,
|
|
89
|
+
'',
|
|
90
|
+
`\`${entry.name}\` is defined by the X++ runtime, not by an AOT element. There is no ` +
|
|
91
|
+
`\`AxEnum/${entry.name}.xml\` in any package, so the bridge, the symbol index and a disk ` +
|
|
92
|
+
`probe all correctly fail to find one — and there is nothing to index.`,
|
|
93
|
+
'',
|
|
94
|
+
`**It is valid to reference.** \`<EnumType>${entry.name}</EnumType>\` in metadata and ` +
|
|
95
|
+
`\`${entry.name}::Value\` in X++ both compile. Do NOT substitute a similarly named AOT ` +
|
|
96
|
+
`enum (for ${entry.name === 'NoYes' ? '`NoYes`, search offers `NoYesBlank`, `NoYesCombo`, `DefaultNoYes`' : 'example, a prefixed variant'}) — those are different types.`,
|
|
97
|
+
];
|
|
98
|
+
if (entry.values?.length) {
|
|
99
|
+
lines.push('', '## Values', '', entry.values.map(v => `- \`${entry.name}::${v}\``).join('\n'), '', '_Value names as used by shipped X++ under PackagesLocalDirectory; the kernel is the ' +
|
|
100
|
+
'authority, so treat the list as observed rather than exhaustive._');
|
|
101
|
+
}
|
|
102
|
+
return lines.join('\n');
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=kernelEnums.js.map
|
|
@@ -27,9 +27,9 @@ export const buildD365foProjectTool = {
|
|
|
27
27
|
type: 'boolean',
|
|
28
28
|
description: 'Full recompile of the TARGET model only (deps stay incremental). Use when xppc reports stale symbol errors.',
|
|
29
29
|
},
|
|
30
|
-
|
|
30
|
+
bpCheck: {
|
|
31
31
|
type: 'boolean',
|
|
32
|
-
description: '
|
|
32
|
+
description: 'On a SUCCESSFUL build, also run the best-practice checker and append its findings — saves the usual follow-up run_bp_check call.',
|
|
33
33
|
},
|
|
34
34
|
wait: {
|
|
35
35
|
type: 'boolean',
|