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.
Files changed (57) hide show
  1. package/dist/bridge/bridgeAdapter.d.ts +33 -1
  2. package/dist/bridge/bridgeAdapter.js +50 -3
  3. package/dist/cli/commands/bpCatalog.js +5 -2
  4. package/dist/knowledge/kernelEnums.js +27 -0
  5. package/dist/scripts/buildIndexWorker.js +149 -0
  6. package/dist/scripts/indexWarmupWorker.js +201 -0
  7. package/dist/server/toolSchemas/prepare.js +9 -3
  8. package/dist/server/toolSchemas/validateCode.js +2 -2
  9. package/dist/tools/analysis/validateXpp.d.ts +2 -1
  10. package/dist/tools/analysis/validateXpp.js +352 -6
  11. package/dist/tools/knowledge/bpMonikerHelp.js +22 -5
  12. package/dist/tools/knowledge/xppKnowledge.js +231 -6
  13. package/dist/tools/prepare/prepareCreate.d.ts +5 -0
  14. package/dist/tools/prepare/prepareCreate.js +196 -10
  15. package/dist/tools/prepare/prepareTest.js +58 -10
  16. package/dist/tools/readers/formInfo.js +130 -7
  17. package/dist/tools/smart/codeGen.js +166 -7
  18. package/dist/tools/smart/generateSmartForm.js +85 -15
  19. package/dist/tools/smart/generateSmartReport.js +18 -2
  20. package/dist/tools/specs/d365foFileOpSpecs.js +20 -6
  21. package/dist/tools/specs/generateObjectOpSpecs.js +23 -4
  22. package/dist/tools/specs/getFormPatternSpec.js +1 -0
  23. package/dist/tools/write/createD365File.js +76 -21
  24. package/dist/tools/write/createLabel.js +4 -1
  25. package/dist/tools/write/directXmlWriters.d.ts +28 -5
  26. package/dist/tools/write/directXmlWriters.js +56 -10
  27. package/dist/tools/write/inlineWriteVerification.d.ts +19 -0
  28. package/dist/tools/write/inlineWriteVerification.js +37 -0
  29. package/dist/tools/write/modifyD365File.d.ts +1 -0
  30. package/dist/tools/write/modifyD365File.js +44 -3
  31. package/dist/tools/write/objectIdentityGate.d.ts +71 -0
  32. package/dist/tools/write/objectIdentityGate.js +126 -0
  33. package/dist/tools/write/preserveMetadataElements.d.ts +81 -0
  34. package/dist/tools/write/preserveMetadataElements.js +217 -0
  35. package/dist/tools/write/resolveReferences.js +28 -0
  36. package/dist/tools/xml/createTablePropertyHonesty.d.ts +2 -0
  37. package/dist/tools/xml/createTablePropertyHonesty.js +24 -1
  38. package/dist/tools/xml/xmlTemplateGenerator.js +33 -4
  39. package/dist/utils/axEnumProperties.d.ts +31 -21
  40. package/dist/utils/axEnumProperties.js +48 -46
  41. package/dist/utils/formPatternTemplates.d.ts +27 -0
  42. package/dist/utils/formPatternTemplates.js +40 -14
  43. package/dist/utils/modelClassifier.js +19 -0
  44. package/dist/utils/objectNamingRules.d.ts +12 -0
  45. package/dist/utils/objectNamingRules.js +74 -17
  46. package/dist/utils/symbolLookup.d.ts +18 -0
  47. package/dist/utils/symbolLookup.js +41 -0
  48. package/dist/utils/xmlScan.d.ts +50 -0
  49. package/dist/utils/xmlScan.js +88 -0
  50. package/dist/utils/xppLexer.d.ts +3 -1
  51. package/dist/utils/xppLexer.js +3 -1
  52. package/dist/validation/formControlElementOrder.d.ts +86 -0
  53. package/dist/validation/formControlElementOrder.generated.d.ts +13 -0
  54. package/dist/validation/formControlElementOrder.generated.js +1705 -0
  55. package/dist/validation/formControlElementOrder.js +227 -0
  56. package/package.json +8 -3
  57. 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
- const bridgeHits = sr.results ?? [];
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
- const body = m.signature ? `\`${m.signature}\`` : m.name;
2462
- out += m.inheritedFrom ? `- ${body} _(inherited from ${m.inheritedFrom})_\n` : `- ${body}\n`;
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 { basename, join, resolve } from 'node:path';
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
- p.log.step(`Refreshing BP moniker catalog (${target.label}, ${basename(source.packagesPath)})…`);
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 (same value you would pass to d365fo_file create).',
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] D365FO object type — auto-detected when omitted. [create] REQUIRED — type of the new object.',
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, so no separate op-spec call. Defaults to add-method when methodName is given.',
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++ source (default), "xml-table" AxTable, "xml-report" AxReport, "xml-any" other XML.',
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>;