d365fo-mcp 1.15.0 → 1.16.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.d.ts +33 -1
- package/dist/bridge/bridgeAdapter.js +50 -3
- package/dist/cli/commands/bpCatalog.js +5 -2
- package/dist/knowledge/kernelEnums.js +27 -0
- package/dist/scripts/buildIndexWorker.js +149 -0
- package/dist/scripts/indexWarmupWorker.js +201 -0
- package/dist/server/toolSchemas/prepare.js +9 -3
- package/dist/server/toolSchemas/validateCode.js +2 -2
- package/dist/tools/analysis/validateXpp.d.ts +2 -1
- package/dist/tools/analysis/validateXpp.js +352 -6
- package/dist/tools/knowledge/bpMonikerHelp.js +22 -5
- package/dist/tools/knowledge/xppKnowledge.js +231 -6
- package/dist/tools/prepare/prepareCreate.d.ts +5 -0
- package/dist/tools/prepare/prepareCreate.js +196 -10
- package/dist/tools/prepare/prepareTest.js +58 -10
- package/dist/tools/readers/formInfo.js +130 -7
- package/dist/tools/smart/codeGen.js +166 -7
- package/dist/tools/smart/generateSmartForm.js +85 -15
- package/dist/tools/smart/generateSmartReport.js +18 -2
- package/dist/tools/specs/d365foFileOpSpecs.js +20 -6
- package/dist/tools/specs/generateObjectOpSpecs.js +23 -4
- package/dist/tools/specs/getFormPatternSpec.js +1 -0
- package/dist/tools/write/createD365File.js +76 -21
- package/dist/tools/write/createLabel.js +4 -1
- package/dist/tools/write/directXmlWriters.d.ts +28 -5
- package/dist/tools/write/directXmlWriters.js +56 -10
- package/dist/tools/write/inlineWriteVerification.d.ts +19 -0
- package/dist/tools/write/inlineWriteVerification.js +37 -0
- package/dist/tools/write/modifyD365File.d.ts +1 -0
- package/dist/tools/write/modifyD365File.js +44 -3
- package/dist/tools/write/objectIdentityGate.d.ts +71 -0
- package/dist/tools/write/objectIdentityGate.js +126 -0
- package/dist/tools/write/preserveMetadataElements.d.ts +81 -0
- package/dist/tools/write/preserveMetadataElements.js +217 -0
- package/dist/tools/write/resolveReferences.js +28 -0
- package/dist/tools/xml/createTablePropertyHonesty.d.ts +2 -0
- package/dist/tools/xml/createTablePropertyHonesty.js +24 -1
- package/dist/tools/xml/xmlTemplateGenerator.js +33 -4
- package/dist/utils/axEnumProperties.d.ts +31 -21
- package/dist/utils/axEnumProperties.js +48 -46
- package/dist/utils/formPatternTemplates.d.ts +27 -0
- package/dist/utils/formPatternTemplates.js +40 -14
- package/dist/utils/modelClassifier.js +19 -0
- package/dist/utils/objectNamingRules.d.ts +12 -0
- package/dist/utils/objectNamingRules.js +74 -17
- package/dist/utils/symbolLookup.d.ts +18 -0
- package/dist/utils/symbolLookup.js +41 -0
- package/dist/utils/xmlScan.d.ts +50 -0
- package/dist/utils/xmlScan.js +88 -0
- package/dist/utils/xppLexer.d.ts +3 -1
- package/dist/utils/xppLexer.js +3 -1
- package/dist/validation/formControlElementOrder.d.ts +86 -0
- package/dist/validation/formControlElementOrder.generated.d.ts +13 -0
- package/dist/validation/formControlElementOrder.generated.js +1705 -0
- package/dist/validation/formControlElementOrder.js +227 -0
- package/package.json +8 -3
- package/scripts/extract-bp-catalog.ps1 +52 -13
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
import type { BridgeClient } from './bridgeClient.js';
|
|
24
24
|
import type { BridgeAttempt } from './bridgeFailure.js';
|
|
25
|
-
import type { BridgeSmartTableResult } from './bridgeTypes.js';
|
|
25
|
+
import type { BridgeCompletionResult, BridgeSmartTableResult } from './bridgeTypes.js';
|
|
26
26
|
/** Standard MCP tool response shape */
|
|
27
27
|
export interface ToolResult {
|
|
28
28
|
content: Array<{
|
|
@@ -538,7 +538,39 @@ export declare function tryBridgeSecurityArtifact(bridge: BridgeClient | undefin
|
|
|
538
538
|
export declare function tryBridgeMenuItem(bridge: BridgeClient | undefined, name: string, itemType?: string): Promise<ToolResult | null>;
|
|
539
539
|
export declare function tryBridgeTableExtensions(bridge: BridgeClient | undefined, baseTableName: string): Promise<ToolResult | null>;
|
|
540
540
|
export declare function tryBridgeCompletion(bridge: BridgeClient | undefined, symbolName: string, prefix?: string, ancestors?: string[]): Promise<ToolResult | null>;
|
|
541
|
+
/**
|
|
542
|
+
* One line of a member list — NAME first, always.
|
|
543
|
+
*
|
|
544
|
+
* This used to print the signature INSTEAD of the name whenever a signature
|
|
545
|
+
* existed, which made a member unidentifiable the moment the signature was
|
|
546
|
+
* wrong. It is sometimes wrong: the bridge's extractor hands back the line
|
|
547
|
+
* preceding the body when a macro sits between the doc block and the signature,
|
|
548
|
+
* so `SysQuery.range` was listed as `#ISOCountryRegionCodes`. Filtering runs on
|
|
549
|
+
* the NAME, so asking for prefix "range" returned "1 member" that appeared to be
|
|
550
|
+
* something else entirely — and a member list that renders a public API as
|
|
551
|
+
* another string argues the API does not exist. An eval run concluded exactly
|
|
552
|
+
* that and worked around a method that was there all along.
|
|
553
|
+
*
|
|
554
|
+
* The signature is still shown, as detail, and only when it plausibly belongs to
|
|
555
|
+
* this member. Fixing the extractor is a separate, C#-side change; this makes the
|
|
556
|
+
* output honest either way.
|
|
557
|
+
*/
|
|
558
|
+
declare function memberLine(m: {
|
|
559
|
+
name: string;
|
|
560
|
+
signature?: string;
|
|
561
|
+
}): string;
|
|
562
|
+
declare function formatCompletion(r: BridgeCompletionResult, prefix?: string): string;
|
|
541
563
|
export declare function tryBridgeCocExtensions(bridge: BridgeClient | undefined, baseClassName: string, methodName?: string): Promise<ToolResult | null>;
|
|
542
564
|
export declare function tryBridgeEventHandlers(bridge: BridgeClient | undefined, targetName: string, eventName?: string, handlerType?: string): Promise<ToolResult | null>;
|
|
543
565
|
export declare function tryBridgeApiUsageCallers(bridge: BridgeClient | undefined, apiName: string, limit?: number): Promise<ToolResult | null>;
|
|
566
|
+
/**
|
|
567
|
+
* Internals exposed for tests only. `formatCompletion` renders a member list,
|
|
568
|
+
* and a member list that cannot be trusted to name its members is worse than no
|
|
569
|
+
* list at all — see tests/bridge/completionMemberLine.test.ts.
|
|
570
|
+
*/
|
|
571
|
+
export declare const __testing: {
|
|
572
|
+
formatCompletion: typeof formatCompletion;
|
|
573
|
+
memberLine: typeof memberLine;
|
|
574
|
+
};
|
|
575
|
+
export {};
|
|
544
576
|
//# sourceMappingURL=bridgeAdapter.d.ts.map
|
|
@@ -758,7 +758,23 @@ export async function tryBridgeSearch(bridge, query, objectType, maxResults = 50
|
|
|
758
758
|
if (!sr)
|
|
759
759
|
return null;
|
|
760
760
|
// Splice in exact matches the bridge's truncated window missed (#15).
|
|
761
|
-
|
|
761
|
+
//
|
|
762
|
+
// …but first, honour the type filter the caller asked for, because the bridge
|
|
763
|
+
// does not always honour it. Its C#-side type map has no entry for every AOT
|
|
764
|
+
// kind — `report` is one — and an unmapped type runs the query UNFILTERED, so
|
|
765
|
+
// `search(type="report")` answered with tables and queries and looked
|
|
766
|
+
// authoritative doing it ("Cust" returned six tables). A report reached the
|
|
767
|
+
// caller only when the SQLite exact-name splice happened to carry one, which
|
|
768
|
+
// is why the failure looked intermittent rather than total.
|
|
769
|
+
//
|
|
770
|
+
// Only the BRIDGE's hits are filtered. The spliced exact and custom matches
|
|
771
|
+
// are deliberately allowed to differ in type — a `table-extension` is a
|
|
772
|
+
// wanted answer to a `type="table"` search for the table it extends — and
|
|
773
|
+
// filtering those too would delete that behaviour.
|
|
774
|
+
const rawBridgeHits = sr.results ?? [];
|
|
775
|
+
const bridgeHits = objectType && objectType !== 'all'
|
|
776
|
+
? rawBridgeHits.filter(r => r.type === objectType)
|
|
777
|
+
: rawBridgeHits;
|
|
762
778
|
const known = new Set(bridgeHits.map(r => `${r.name.toLowerCase()}\0${r.type}`));
|
|
763
779
|
const spliced = [];
|
|
764
780
|
for (const cand of opts?.exactMatches ?? []) {
|
|
@@ -2437,6 +2453,30 @@ export async function tryBridgeCompletion(bridge, symbolName, prefix, ancestors)
|
|
|
2437
2453
|
return null;
|
|
2438
2454
|
}
|
|
2439
2455
|
}
|
|
2456
|
+
/**
|
|
2457
|
+
* One line of a member list — NAME first, always.
|
|
2458
|
+
*
|
|
2459
|
+
* This used to print the signature INSTEAD of the name whenever a signature
|
|
2460
|
+
* existed, which made a member unidentifiable the moment the signature was
|
|
2461
|
+
* wrong. It is sometimes wrong: the bridge's extractor hands back the line
|
|
2462
|
+
* preceding the body when a macro sits between the doc block and the signature,
|
|
2463
|
+
* so `SysQuery.range` was listed as `#ISOCountryRegionCodes`. Filtering runs on
|
|
2464
|
+
* the NAME, so asking for prefix "range" returned "1 member" that appeared to be
|
|
2465
|
+
* something else entirely — and a member list that renders a public API as
|
|
2466
|
+
* another string argues the API does not exist. An eval run concluded exactly
|
|
2467
|
+
* that and worked around a method that was there all along.
|
|
2468
|
+
*
|
|
2469
|
+
* The signature is still shown, as detail, and only when it plausibly belongs to
|
|
2470
|
+
* this member. Fixing the extractor is a separate, C#-side change; this makes the
|
|
2471
|
+
* output honest either way.
|
|
2472
|
+
*/
|
|
2473
|
+
function memberLine(m) {
|
|
2474
|
+
const sig = m.signature?.trim();
|
|
2475
|
+
const belongs = sig && new RegExp(`\\b${m.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*\\(`, 'i').test(sig);
|
|
2476
|
+
if (belongs)
|
|
2477
|
+
return `\`${sig}\``;
|
|
2478
|
+
return sig ? `**${m.name}** _(signature unavailable)_` : `**${m.name}**`;
|
|
2479
|
+
}
|
|
2440
2480
|
function formatCompletion(r, prefix) {
|
|
2441
2481
|
let members = r.members;
|
|
2442
2482
|
if (prefix) {
|
|
@@ -2458,8 +2498,9 @@ function formatCompletion(r, prefix) {
|
|
|
2458
2498
|
const inheritedCount = methodMembers.filter(m => m.inheritedFrom).length;
|
|
2459
2499
|
out += `## Methods (${methodMembers.length})\n`;
|
|
2460
2500
|
for (const m of methodMembers) {
|
|
2461
|
-
|
|
2462
|
-
|
|
2501
|
+
out += m.inheritedFrom
|
|
2502
|
+
? `- ${memberLine(m)} _(inherited from ${m.inheritedFrom})_\n`
|
|
2503
|
+
: `- ${memberLine(m)}\n`;
|
|
2463
2504
|
}
|
|
2464
2505
|
if (inheritedCount > 0) {
|
|
2465
2506
|
out += `\n> ${inheritedCount} of these are inherited — callable on ${r.symbolName}, but ` +
|
|
@@ -2602,4 +2643,10 @@ function formatApiUsageCallers(r) {
|
|
|
2602
2643
|
}
|
|
2603
2644
|
return out;
|
|
2604
2645
|
}
|
|
2646
|
+
/**
|
|
2647
|
+
* Internals exposed for tests only. `formatCompletion` renders a member list,
|
|
2648
|
+
* and a member list that cannot be trusted to name its members is worse than no
|
|
2649
|
+
* list at all — see tests/bridge/completionMemberLine.test.ts.
|
|
2650
|
+
*/
|
|
2651
|
+
export const __testing = { formatCompletion, memberLine };
|
|
2605
2652
|
//# sourceMappingURL=bridgeAdapter.js.map
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { createHash } from 'node:crypto';
|
|
20
20
|
import { existsSync, readFileSync, readdirSync, renameSync, rmSync, statSync } from 'node:fs';
|
|
21
|
-
import {
|
|
21
|
+
import { join, resolve } from 'node:path';
|
|
22
22
|
import { settingByPath } from '../../config/settings.js';
|
|
23
23
|
import { findPackagesRoot } from '../../utils/packagesRoot.js';
|
|
24
24
|
import { commandExists, runExe } from '../exec.js';
|
|
@@ -243,7 +243,10 @@ async function refreshCatalog(target, deps) {
|
|
|
243
243
|
return;
|
|
244
244
|
}
|
|
245
245
|
}
|
|
246
|
-
|
|
246
|
+
// No packages path in this line: the script's own report opens with the
|
|
247
|
+
// full path, and the tail of it ('PackagesLocalDirectory') identified
|
|
248
|
+
// nothing on a box with several installs.
|
|
249
|
+
p.log.step(`Refreshing BP moniker catalog (${target.label})…`);
|
|
247
250
|
// Extract beside the real catalog, not over it. The script writes its output
|
|
248
251
|
// in one go with no staging of its own, so pointing it straight at the live
|
|
249
252
|
// file means a partial run destroys a good catalog before anything can judge
|
|
@@ -50,6 +50,33 @@ const ENTRIES = [
|
|
|
50
50
|
'Integer', 'List', 'RString', 'Real', 'Record', 'String', 'Time',
|
|
51
51
|
'UserType', 'UtcDateTime', 'VarArg', 'VarString', 'Void',
|
|
52
52
|
] },
|
|
53
|
+
// What Box::yesNo/confirm/okCancel answer with. Found by the knowledge audit,
|
|
54
|
+
// which reported "DialogButton does not exist in the symbol index" for a topic
|
|
55
|
+
// whose call shape had already compiled in an xppc probe — the same
|
|
56
|
+
// absent-from-the-AOT-is-not-absent trap this module exists for. Values are the
|
|
57
|
+
// ones shipped code actually uses, counted over 30,000 AxClass/AxForm files:
|
|
58
|
+
// Yes 602, No 561, Cancel 154, Ok 129, YesToAll 7, NoToAll 6.
|
|
59
|
+
{ name: 'DialogButton', values: ['Cancel', 'No', 'NoToAll', 'Ok', 'Yes', 'YesToAll'] },
|
|
60
|
+
// Added 2026-08-31 after an eval run tripped over AccessType and MenuItemType:
|
|
61
|
+
// validate_code(mode="references") called both hard ERRORS on code that
|
|
62
|
+
// compiles, and with GROUNDING_ENFORCE on it would have REFUSED the write —
|
|
63
|
+
// the exact failure this module was created to prevent. That both were absent
|
|
64
|
+
// was the signal that the list had never been derived from anything, so it is
|
|
65
|
+
// now checked by `npm run oracle:kernel-enums`, which finds every `Name::Value`
|
|
66
|
+
// in shipped X++ with no AOT element behind it. Values below are what the
|
|
67
|
+
// product itself writes, counted by that audit, not recalled.
|
|
68
|
+
{ name: 'MenuItemType', values: ['Action', 'Display', 'Output'] },
|
|
69
|
+
{ name: 'AccessType', values: ['Add', 'Correction', 'Delete', 'Edit', 'NoAccess', 'View'] },
|
|
70
|
+
{ name: 'AccessRight', values: ['Add', 'Correction', 'Delete', 'Edit', 'NoAccess', 'View'] },
|
|
71
|
+
{ name: 'SortOrder', values: ['Ascending', 'Descending'] },
|
|
72
|
+
// QueryBuildDataSource.joinMode(). Reported by a second eval run, through the
|
|
73
|
+
// static-member rule rather than the declared-type one — the two rules did not
|
|
74
|
+
// share the kernel-name exemption, so the same fact was a warning in one place
|
|
75
|
+
// and an error in the other.
|
|
76
|
+
{ name: 'JoinMode', values: ['ExistsJoin', 'InnerJoin', 'NoExistsJoin', 'OuterJoin'] },
|
|
77
|
+
// The argument to an `unchecked(...)` block. Only these two members exist —
|
|
78
|
+
// DBSchemaDrift and Deadlock read plausibly and are not real (xppc-verified).
|
|
79
|
+
{ name: 'Uncheck', values: ['TableSecurityPermission', 'XDS'] },
|
|
53
80
|
{ name: 'TableScope', values: ['CurrentTableOnly', 'IncludeBaseTables', 'IncludeDerivedTables'] },
|
|
54
81
|
{ name: 'ConcurrencyModel', values: ['Auto', 'Optimistic', 'Pessimistic'] },
|
|
55
82
|
{ name: 'StatementType', values: ['Delete', 'Insert', 'Select', 'Update'] },
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// src/metadata/buildIndexWorker.ts
|
|
2
|
+
import { parentPort, workerData } from "node:worker_threads";
|
|
3
|
+
|
|
4
|
+
// src/database/nodeSqlite.ts
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
function load() {
|
|
7
|
+
const original = process.emitWarning.bind(process);
|
|
8
|
+
process.emitWarning = ((warning, ...rest) => {
|
|
9
|
+
const message = typeof warning === "string" ? warning : warning?.message ?? "";
|
|
10
|
+
const first = rest[0];
|
|
11
|
+
const type = typeof first === "string" ? first : first?.type ?? "";
|
|
12
|
+
if (type === "ExperimentalWarning" && /\bSQLite\b/.test(message)) return;
|
|
13
|
+
original(warning, ...rest);
|
|
14
|
+
});
|
|
15
|
+
try {
|
|
16
|
+
return createRequire(import.meta.url)("node:sqlite");
|
|
17
|
+
} finally {
|
|
18
|
+
process.emitWarning = original;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
var { DatabaseSync: DatabaseSyncClass } = load();
|
|
22
|
+
|
|
23
|
+
// src/database/sqlite.ts
|
|
24
|
+
var OBJECT_PROTO = Object.prototype;
|
|
25
|
+
function reproto(row) {
|
|
26
|
+
if (row !== null && typeof row === "object") Object.setPrototypeOf(row, OBJECT_PROTO);
|
|
27
|
+
return row;
|
|
28
|
+
}
|
|
29
|
+
var Statement = class {
|
|
30
|
+
constructor(stmt) {
|
|
31
|
+
this.stmt = stmt;
|
|
32
|
+
}
|
|
33
|
+
stmt;
|
|
34
|
+
run(...params) {
|
|
35
|
+
return this.stmt.run(...params);
|
|
36
|
+
}
|
|
37
|
+
get(...params) {
|
|
38
|
+
const row = this.stmt.get(...params);
|
|
39
|
+
return row === void 0 ? void 0 : reproto(row);
|
|
40
|
+
}
|
|
41
|
+
all(...params) {
|
|
42
|
+
const rows = this.stmt.all(...params);
|
|
43
|
+
for (let i = 0; i < rows.length; i++) reproto(rows[i]);
|
|
44
|
+
return rows;
|
|
45
|
+
}
|
|
46
|
+
*iterate(...params) {
|
|
47
|
+
for (const row of this.stmt.iterate(...params)) {
|
|
48
|
+
yield reproto(row);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
var Database = class {
|
|
53
|
+
handle;
|
|
54
|
+
/**
|
|
55
|
+
* Depth of nested transaction() calls. better-sqlite3 nests via SAVEPOINT;
|
|
56
|
+
* plain BEGIN would throw "cannot start a transaction within a transaction".
|
|
57
|
+
*/
|
|
58
|
+
txDepth = 0;
|
|
59
|
+
name;
|
|
60
|
+
constructor(filename, options = {}) {
|
|
61
|
+
this.name = filename;
|
|
62
|
+
this.handle = new DatabaseSyncClass(filename, {
|
|
63
|
+
readOnly: options.readonly === true,
|
|
64
|
+
// better-sqlite3 leaves foreign_keys at SQLite's OFF default; node:sqlite
|
|
65
|
+
// turns them on. Our schema has FK columns that are intentionally not
|
|
66
|
+
// enforced (rows are inserted out of order during a build).
|
|
67
|
+
enableForeignKeyConstraints: false
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
get open() {
|
|
71
|
+
return this.handle.isOpen;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* `PRAGMA <source>`. With `{ simple: true }` returns the first column of the
|
|
75
|
+
* first row (better-sqlite3's shorthand for scalar pragmas such as
|
|
76
|
+
* journal_mode); otherwise the full row array. Assignment pragmas
|
|
77
|
+
* ("cache_size = -64000") return an empty array.
|
|
78
|
+
*/
|
|
79
|
+
pragma(source, options) {
|
|
80
|
+
let rows;
|
|
81
|
+
try {
|
|
82
|
+
rows = this.handle.prepare(`PRAGMA ${source}`).all();
|
|
83
|
+
} catch {
|
|
84
|
+
this.handle.exec(`PRAGMA ${source}`);
|
|
85
|
+
rows = [];
|
|
86
|
+
}
|
|
87
|
+
if (options?.simple) {
|
|
88
|
+
const first = rows[0];
|
|
89
|
+
return first === void 0 ? void 0 : Object.values(first)[0];
|
|
90
|
+
}
|
|
91
|
+
for (const row of rows) reproto(row);
|
|
92
|
+
return rows;
|
|
93
|
+
}
|
|
94
|
+
prepare(sql2) {
|
|
95
|
+
return new Statement(this.handle.prepare(sql2));
|
|
96
|
+
}
|
|
97
|
+
exec(sql2) {
|
|
98
|
+
this.handle.exec(sql2);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Wraps `fn` so that calling it runs inside a transaction, rolling back if it
|
|
102
|
+
* throws. Matches better-sqlite3's `db.transaction(fn)`: arguments passed to
|
|
103
|
+
* the returned function are forwarded to `fn`, and the return value is
|
|
104
|
+
* `fn`'s.
|
|
105
|
+
*/
|
|
106
|
+
transaction(fn) {
|
|
107
|
+
return (...args) => {
|
|
108
|
+
const depth = this.txDepth++;
|
|
109
|
+
const nested = depth > 0;
|
|
110
|
+
const savepoint = `d365fo_sp_${depth}`;
|
|
111
|
+
this.handle.exec(nested ? `SAVEPOINT ${savepoint}` : "BEGIN");
|
|
112
|
+
try {
|
|
113
|
+
const result = fn(...args);
|
|
114
|
+
this.handle.exec(nested ? `RELEASE ${savepoint}` : "COMMIT");
|
|
115
|
+
return result;
|
|
116
|
+
} catch (err) {
|
|
117
|
+
try {
|
|
118
|
+
this.handle.exec(
|
|
119
|
+
nested ? `ROLLBACK TO ${savepoint}; RELEASE ${savepoint}` : "ROLLBACK"
|
|
120
|
+
);
|
|
121
|
+
} catch {
|
|
122
|
+
}
|
|
123
|
+
throw err;
|
|
124
|
+
} finally {
|
|
125
|
+
this.txDepth--;
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
close() {
|
|
130
|
+
if (this.handle.isOpen) this.handle.close();
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
var sqlite_default = Database;
|
|
134
|
+
|
|
135
|
+
// src/metadata/buildIndexWorker.ts
|
|
136
|
+
var { dbPath, sql } = workerData;
|
|
137
|
+
try {
|
|
138
|
+
const db = new sqlite_default(dbPath);
|
|
139
|
+
try {
|
|
140
|
+
db.pragma("busy_timeout = 120000");
|
|
141
|
+
const started = Date.now();
|
|
142
|
+
db.exec(sql);
|
|
143
|
+
parentPort.postMessage({ ok: true, elapsedMs: Date.now() - started });
|
|
144
|
+
} finally {
|
|
145
|
+
db.close();
|
|
146
|
+
}
|
|
147
|
+
} catch (e) {
|
|
148
|
+
parentPort.postMessage({ ok: false, error: String(e) });
|
|
149
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// src/metadata/indexWarmupWorker.ts
|
|
2
|
+
import { parentPort, workerData } from "node:worker_threads";
|
|
3
|
+
|
|
4
|
+
// src/database/nodeSqlite.ts
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
function load() {
|
|
7
|
+
const original = process.emitWarning.bind(process);
|
|
8
|
+
process.emitWarning = ((warning, ...rest) => {
|
|
9
|
+
const message = typeof warning === "string" ? warning : warning?.message ?? "";
|
|
10
|
+
const first = rest[0];
|
|
11
|
+
const type = typeof first === "string" ? first : first?.type ?? "";
|
|
12
|
+
if (type === "ExperimentalWarning" && /\bSQLite\b/.test(message)) return;
|
|
13
|
+
original(warning, ...rest);
|
|
14
|
+
});
|
|
15
|
+
try {
|
|
16
|
+
return createRequire(import.meta.url)("node:sqlite");
|
|
17
|
+
} finally {
|
|
18
|
+
process.emitWarning = original;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
var { DatabaseSync: DatabaseSyncClass } = load();
|
|
22
|
+
|
|
23
|
+
// src/database/sqlite.ts
|
|
24
|
+
var OBJECT_PROTO = Object.prototype;
|
|
25
|
+
function reproto(row) {
|
|
26
|
+
if (row !== null && typeof row === "object") Object.setPrototypeOf(row, OBJECT_PROTO);
|
|
27
|
+
return row;
|
|
28
|
+
}
|
|
29
|
+
var Statement = class {
|
|
30
|
+
constructor(stmt) {
|
|
31
|
+
this.stmt = stmt;
|
|
32
|
+
}
|
|
33
|
+
stmt;
|
|
34
|
+
run(...params) {
|
|
35
|
+
return this.stmt.run(...params);
|
|
36
|
+
}
|
|
37
|
+
get(...params) {
|
|
38
|
+
const row = this.stmt.get(...params);
|
|
39
|
+
return row === void 0 ? void 0 : reproto(row);
|
|
40
|
+
}
|
|
41
|
+
all(...params) {
|
|
42
|
+
const rows = this.stmt.all(...params);
|
|
43
|
+
for (let i = 0; i < rows.length; i++) reproto(rows[i]);
|
|
44
|
+
return rows;
|
|
45
|
+
}
|
|
46
|
+
*iterate(...params) {
|
|
47
|
+
for (const row of this.stmt.iterate(...params)) {
|
|
48
|
+
yield reproto(row);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
var Database = class {
|
|
53
|
+
handle;
|
|
54
|
+
/**
|
|
55
|
+
* Depth of nested transaction() calls. better-sqlite3 nests via SAVEPOINT;
|
|
56
|
+
* plain BEGIN would throw "cannot start a transaction within a transaction".
|
|
57
|
+
*/
|
|
58
|
+
txDepth = 0;
|
|
59
|
+
name;
|
|
60
|
+
constructor(filename, options = {}) {
|
|
61
|
+
this.name = filename;
|
|
62
|
+
this.handle = new DatabaseSyncClass(filename, {
|
|
63
|
+
readOnly: options.readonly === true,
|
|
64
|
+
// better-sqlite3 leaves foreign_keys at SQLite's OFF default; node:sqlite
|
|
65
|
+
// turns them on. Our schema has FK columns that are intentionally not
|
|
66
|
+
// enforced (rows are inserted out of order during a build).
|
|
67
|
+
enableForeignKeyConstraints: false
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
get open() {
|
|
71
|
+
return this.handle.isOpen;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* `PRAGMA <source>`. With `{ simple: true }` returns the first column of the
|
|
75
|
+
* first row (better-sqlite3's shorthand for scalar pragmas such as
|
|
76
|
+
* journal_mode); otherwise the full row array. Assignment pragmas
|
|
77
|
+
* ("cache_size = -64000") return an empty array.
|
|
78
|
+
*/
|
|
79
|
+
pragma(source, options) {
|
|
80
|
+
let rows;
|
|
81
|
+
try {
|
|
82
|
+
rows = this.handle.prepare(`PRAGMA ${source}`).all();
|
|
83
|
+
} catch {
|
|
84
|
+
this.handle.exec(`PRAGMA ${source}`);
|
|
85
|
+
rows = [];
|
|
86
|
+
}
|
|
87
|
+
if (options?.simple) {
|
|
88
|
+
const first = rows[0];
|
|
89
|
+
return first === void 0 ? void 0 : Object.values(first)[0];
|
|
90
|
+
}
|
|
91
|
+
for (const row of rows) reproto(row);
|
|
92
|
+
return rows;
|
|
93
|
+
}
|
|
94
|
+
prepare(sql) {
|
|
95
|
+
return new Statement(this.handle.prepare(sql));
|
|
96
|
+
}
|
|
97
|
+
exec(sql) {
|
|
98
|
+
this.handle.exec(sql);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Wraps `fn` so that calling it runs inside a transaction, rolling back if it
|
|
102
|
+
* throws. Matches better-sqlite3's `db.transaction(fn)`: arguments passed to
|
|
103
|
+
* the returned function are forwarded to `fn`, and the return value is
|
|
104
|
+
* `fn`'s.
|
|
105
|
+
*/
|
|
106
|
+
transaction(fn) {
|
|
107
|
+
return (...args) => {
|
|
108
|
+
const depth = this.txDepth++;
|
|
109
|
+
const nested = depth > 0;
|
|
110
|
+
const savepoint = `d365fo_sp_${depth}`;
|
|
111
|
+
this.handle.exec(nested ? `SAVEPOINT ${savepoint}` : "BEGIN");
|
|
112
|
+
try {
|
|
113
|
+
const result = fn(...args);
|
|
114
|
+
this.handle.exec(nested ? `RELEASE ${savepoint}` : "COMMIT");
|
|
115
|
+
return result;
|
|
116
|
+
} catch (err) {
|
|
117
|
+
try {
|
|
118
|
+
this.handle.exec(
|
|
119
|
+
nested ? `ROLLBACK TO ${savepoint}; RELEASE ${savepoint}` : "ROLLBACK"
|
|
120
|
+
);
|
|
121
|
+
} catch {
|
|
122
|
+
}
|
|
123
|
+
throw err;
|
|
124
|
+
} finally {
|
|
125
|
+
this.txDepth--;
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
close() {
|
|
130
|
+
if (this.handle.isOpen) this.handle.close();
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
var sqlite_default = Database;
|
|
134
|
+
|
|
135
|
+
// src/metadata/indexWarmupWorker.ts
|
|
136
|
+
var WARMUP_STEPS = [
|
|
137
|
+
{
|
|
138
|
+
name: "symbols.idx_symbols_name",
|
|
139
|
+
db: "symbols",
|
|
140
|
+
sql: `SELECT COUNT(*) AS n FROM symbols INDEXED BY idx_symbols_name WHERE name > ''`
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
name: "labels join label_files",
|
|
144
|
+
db: "labels",
|
|
145
|
+
sql: `SELECT COUNT(*) AS n FROM labels l JOIN label_files lf ON lf.id = l.file_path_id
|
|
146
|
+
WHERE LOWER(l.language) = 'en-us'`
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
name: "symbols.idx_type_name",
|
|
150
|
+
db: "symbols",
|
|
151
|
+
sql: `SELECT COUNT(*) AS n FROM symbols INDEXED BY idx_type_name WHERE type > ''`
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
name: "labels_fts",
|
|
155
|
+
db: "labels",
|
|
156
|
+
sql: `SELECT COUNT(*) AS n FROM labels_fts WHERE labels_fts MATCH 'cannot OR value OR not'`
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: "symbols_fts",
|
|
160
|
+
db: "symbols",
|
|
161
|
+
sql: `SELECT COUNT(*) AS n FROM symbols_fts WHERE symbols_fts MATCH '"table"*'`
|
|
162
|
+
}
|
|
163
|
+
];
|
|
164
|
+
if (parentPort) {
|
|
165
|
+
const { dbPath, labelsDbPath, budgetMs } = workerData;
|
|
166
|
+
const started = Date.now();
|
|
167
|
+
const open = (p) => {
|
|
168
|
+
const db = new sqlite_default(p, { readonly: true });
|
|
169
|
+
db.pragma("busy_timeout = 5000");
|
|
170
|
+
return db;
|
|
171
|
+
};
|
|
172
|
+
let symbols = null;
|
|
173
|
+
let labels = null;
|
|
174
|
+
try {
|
|
175
|
+
for (const step of WARMUP_STEPS) {
|
|
176
|
+
if (Date.now() - started >= budgetMs) break;
|
|
177
|
+
try {
|
|
178
|
+
if (step.db === "symbols") symbols ??= open(dbPath);
|
|
179
|
+
else labels ??= open(labelsDbPath);
|
|
180
|
+
const db = step.db === "symbols" ? symbols : labels;
|
|
181
|
+
const t0 = Date.now();
|
|
182
|
+
const row = db.prepare(step.sql).get();
|
|
183
|
+
parentPort.postMessage({
|
|
184
|
+
type: "step",
|
|
185
|
+
name: step.name,
|
|
186
|
+
ms: Date.now() - t0,
|
|
187
|
+
rows: row?.n ?? 0
|
|
188
|
+
});
|
|
189
|
+
} catch (e) {
|
|
190
|
+
parentPort.postMessage({ type: "error", name: step.name, error: String(e) });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
parentPort.postMessage({ type: "done", totalMs: Date.now() - started });
|
|
194
|
+
} finally {
|
|
195
|
+
symbols?.close();
|
|
196
|
+
labels?.close();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
export {
|
|
200
|
+
WARMUP_STEPS
|
|
201
|
+
};
|
|
@@ -29,7 +29,7 @@ export const prepareTool = {
|
|
|
29
29
|
},
|
|
30
30
|
objectName: {
|
|
31
31
|
type: 'string',
|
|
32
|
-
description: '[change] Name of the object to extend/modify (e.g. "CustTable"). [create] Proposed BASE name WITHOUT model prefix
|
|
32
|
+
description: '[change] Name of the object to extend/modify (e.g. "CustTable"). [create] Proposed BASE name WITHOUT model prefix.',
|
|
33
33
|
},
|
|
34
34
|
objectType: {
|
|
35
35
|
type: 'string',
|
|
@@ -39,8 +39,14 @@ export const prepareTool = {
|
|
|
39
39
|
'menu-item-output', 'menu', 'security-privilege', 'security-duty', 'security-role',
|
|
40
40
|
'business-event', 'tile', 'kpi', 'service', 'service-group',
|
|
41
41
|
'macro', 'configuration-key', 'security-policy', 'aggregate-measurement', 'license-code',
|
|
42
|
+
// Extension artefacts, published here as well as in prepareCreate's zod
|
|
43
|
+
// schema. #983 added them to the zod schema only — the handler accepted
|
|
44
|
+
// them and the tool list did not offer them, so an agent still could not
|
|
45
|
+
// pick one. That is the very disagreement #983 was filed about, one level
|
|
46
|
+
// up. tests/server/toolSchemaAgreement.test.ts now pins the two together.
|
|
47
|
+
'table-extension', 'class-extension', 'form-extension', 'enum-extension', 'edt-extension',
|
|
42
48
|
],
|
|
43
|
-
description: '[change]
|
|
49
|
+
description: '[change] type — auto-detected when omitted. [create] REQUIRED; an extension is Base.Suffix.',
|
|
44
50
|
},
|
|
45
51
|
methodName: {
|
|
46
52
|
type: 'string',
|
|
@@ -51,7 +57,7 @@ export const prepareTool = {
|
|
|
51
57
|
// Comma-separated rather than a second array parameter: a table change
|
|
52
58
|
// is normally add-field AND add-index AND add-field-to-field-group, and
|
|
53
59
|
// one clause here is far cheaper per session than another schema block.
|
|
54
|
-
description: '[change] The modify operation(s) you intend to run — comma-separated for several ("add-field,add-index"). Their full parameter contracts come back in THIS response
|
|
60
|
+
description: '[change] The modify operation(s) you intend to run — comma-separated for several ("add-field,add-index"). Their full parameter contracts come back in THIS response. Defaults to add-method when methodName is given.',
|
|
55
61
|
},
|
|
56
62
|
proposedName: {
|
|
57
63
|
type: 'string',
|
|
@@ -23,9 +23,9 @@ export const validateCodeTool = {
|
|
|
23
23
|
},
|
|
24
24
|
codeType: {
|
|
25
25
|
type: 'string',
|
|
26
|
-
enum: ['xpp', 'xml-table', 'xml-any', 'xml-report'],
|
|
26
|
+
enum: ['xpp', 'xml-table', 'xml-form', 'xml-any', 'xml-report'],
|
|
27
27
|
default: 'xpp',
|
|
28
|
-
description: '[syntax] "xpp" X++
|
|
28
|
+
description: '[syntax] "xpp" X++ (default), "xml-table" AxTable, "xml-form" AxForm(+Extension), "xml-report" AxReport, "xml-any" other.',
|
|
29
29
|
},
|
|
30
30
|
context: {
|
|
31
31
|
type: 'string',
|
|
@@ -64,6 +64,7 @@ export declare const validateXppArgsSchema: z.ZodObject<{
|
|
|
64
64
|
code: z.ZodString;
|
|
65
65
|
codeType: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
|
|
66
66
|
"xml-any": "xml-any";
|
|
67
|
+
"xml-form": "xml-form";
|
|
67
68
|
"xml-report": "xml-report";
|
|
68
69
|
"xml-table": "xml-table";
|
|
69
70
|
xpp: "xpp";
|
|
@@ -100,7 +101,7 @@ export interface PropertyStatsProvider {
|
|
|
100
101
|
count: number;
|
|
101
102
|
}>;
|
|
102
103
|
}
|
|
103
|
-
export declare function runRules(code: string, codeType: 'xpp' | 'xml-table' | 'xml-any' | 'xml-report', stats?: PropertyStatsProvider): ValidationViolation[];
|
|
104
|
+
export declare function runRules(code: string, codeType: 'xpp' | 'xml-table' | 'xml-form' | 'xml-any' | 'xml-report', stats?: PropertyStatsProvider): ValidationViolation[];
|
|
104
105
|
export declare function validateXppTool(request: any, serverContext?: {
|
|
105
106
|
symbolIndex?: PropertyStatsProvider;
|
|
106
107
|
}): Promise<any>;
|