d365fo-mcp 1.6.0 → 1.7.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 (47) hide show
  1. package/README.md +1 -1
  2. package/bridge/D365MetadataBridge/Services/MetadataWriteService.cs +54 -1
  3. package/dist/bridge/bridgeAdapter.js +1 -1
  4. package/dist/metadata/modelDescriptor.d.ts +53 -0
  5. package/dist/metadata/modelDescriptor.js +129 -0
  6. package/dist/metadata/symbolIndex.js +4 -6
  7. package/dist/metadata/types.d.ts +8 -0
  8. package/dist/metadata/xmlParser.d.ts +5 -1
  9. package/dist/metadata/xmlParser.js +10 -2
  10. package/dist/metadata/xppDeclaration.d.ts +22 -0
  11. package/dist/metadata/xppDeclaration.js +36 -2
  12. package/dist/scripts/build-database.js +10 -6
  13. package/dist/scripts/build-fts.js +10 -6
  14. package/dist/scripts/extract-metadata.js +17 -4
  15. package/dist/server/toolSchemas/d365foFile.js +5 -5
  16. package/dist/tools/buildProject.d.ts +10 -0
  17. package/dist/tools/buildProject.js +49 -38
  18. package/dist/tools/compileLabels.d.ts +62 -0
  19. package/dist/tools/compileLabels.js +239 -0
  20. package/dist/tools/createD365File.js +25 -3
  21. package/dist/tools/dataEntityInfo.d.ts +9 -12
  22. package/dist/tools/dataEntityInfo.js +12 -3
  23. package/dist/tools/dataEntityXml.d.ts +60 -0
  24. package/dist/tools/dataEntityXml.js +165 -21
  25. package/dist/tools/enumInfo.d.ts +10 -3
  26. package/dist/tools/enumInfo.js +69 -10
  27. package/dist/tools/formInfo.js +11 -0
  28. package/dist/tools/generateD365Xml.d.ts +157 -0
  29. package/dist/tools/generateD365Xml.js +30 -5
  30. package/dist/tools/queryInfo.d.ts +10 -3
  31. package/dist/tools/queryInfo.js +123 -10
  32. package/dist/tools/reportInfo.js +42 -14
  33. package/dist/tools/resolveReferences.d.ts +14 -1
  34. package/dist/tools/resolveReferences.js +160 -14
  35. package/dist/tools/runBpCheck.js +18 -0
  36. package/dist/tools/updateSymbolIndex.js +3 -4
  37. package/dist/tools/validateCode.js +2 -1
  38. package/dist/tools/viewInfo.d.ts +15 -4
  39. package/dist/tools/viewInfo.js +162 -9
  40. package/dist/tools/xppKnowledge.js +53 -9
  41. package/dist/utils/axTablePropertyOrder.d.ts +1 -1
  42. package/dist/utils/axTablePropertyOrder.js +5 -0
  43. package/dist/utils/frameworkBin.d.ts +19 -0
  44. package/dist/utils/frameworkBin.js +48 -0
  45. package/dist/utils/indexedXmlLookup.d.ts +54 -0
  46. package/dist/utils/indexedXmlLookup.js +103 -0
  47. package/package.json +1 -1
package/README.md CHANGED
@@ -10,7 +10,7 @@
10
10
  [![TypeScript](https://img.shields.io/badge/TypeScript-6.0-blue.svg)](https://www.typescriptlang.org/)
11
11
  [![Tests](https://img.shields.io/badge/tests-1400%2B-brightgreen.svg)](docs/TESTING.md)
12
12
  <!-- coverage-badge:start -->
13
- [![Core coverage](https://img.shields.io/badge/core_coverage-100%25-brightgreen.svg)](eval/COVERAGE.md) [![Total coverage](https://img.shields.io/badge/total_coverage-85.9%25-lightgrey.svg)](eval/COVERAGE.md)
13
+ [![Core coverage](https://img.shields.io/badge/core_coverage-100%25-brightgreen.svg)](eval/COVERAGE.md) [![Total coverage](https://img.shields.io/badge/total_coverage-91%25-lightgrey.svg)](eval/COVERAGE.md)
14
14
  <!-- coverage-badge:end -->
15
15
 
16
16
  *Grounded AI development for Dynamics 365 Finance & Operations — works with GitHub Copilot and Claude Code*
@@ -1504,7 +1504,7 @@ namespace D365MetadataBridge.Services
1504
1504
  ?? throw new ArgumentException($"Table '{objectName}' not found");
1505
1505
  var msi = GetModelSaveInfoForObject(_provider.Tables, objectName);
1506
1506
  if (!SetAxTableProperty(obj, propertyPath, propertyValue))
1507
- throw new ArgumentException($"Unknown AxTable property '{propertyPath}' — nothing was written. Supported: label, developerDocumentation, configurationKey, formRef, tableGroup, cacheLookup, clusteredIndex, primaryIndex, replacementKey, saveDataPerCompany, tableType, supportInheritance, instanceRelationType, extends, titleField1, titleField2.");
1507
+ throw new ArgumentException($"Unknown AxTable property '{propertyPath}' — nothing was written. Supported: label, developerDocumentation, configurationKey, formRef, tableGroup, cacheLookup, clusteredIndex, primaryIndex, replacementKey, saveDataPerCompany, allowRowVersionChangeTracking, createdBy, createdDateTime, createdTransactionId, modifiedBy, modifiedDateTime, modifiedTransactionId, tableType, supportInheritance, instanceRelationType, extends, titleField1, titleField2.");
1508
1508
  ((IMetaTableProvider)_provider.Tables).Update(obj, msi);
1509
1509
  return new { success = true, operation = "modify-property", objectType, objectName, propertyPath, propertyValue, api = "Update" };
1510
1510
  }
@@ -1548,6 +1548,16 @@ namespace D365MetadataBridge.Services
1548
1548
  ((IMetaViewProvider)_provider.Views).Update(obj, msi);
1549
1549
  return new { success = true, operation = "modify-property", objectType, objectName, propertyPath, propertyValue, api = "Update" };
1550
1550
  }
1551
+ case "data-entity":
1552
+ {
1553
+ var obj = _provider.DataEntityViews.Read(objectName)
1554
+ ?? throw new ArgumentException($"Data entity '{objectName}' not found");
1555
+ var msi = GetModelSaveInfoForObject(_provider.DataEntityViews, objectName);
1556
+ if (!SetAxDataEntityViewProperty(obj, propertyPath, propertyValue))
1557
+ throw new ArgumentException($"Unknown AxDataEntityView property '{propertyPath}' — nothing was written. Supported: label, developerDocumentation, primaryKey, isPublic, publicEntityName, publicCollectionName, dataManagementEnabled, dataManagementStagingTable, entityCategory, allowRowVersionChangeTracking, allowRetention.");
1558
+ ((IMetaDataEntityViewProvider)_provider.DataEntityViews).Update(obj, msi);
1559
+ return new { success = true, operation = "modify-property", objectType, objectName, propertyPath, propertyValue, api = "Update" };
1560
+ }
1551
1561
  case "menu-item-action":
1552
1562
  {
1553
1563
  var obj = _provider.MenuItemActions.Read(objectName)
@@ -2642,6 +2652,16 @@ namespace D365MetadataBridge.Services
2642
2652
  case "savedatapercompany":
2643
2653
  tbl.SaveDataPerCompany = ParseNoYes(value);
2644
2654
  break;
2655
+ // Dual-write's table-side change-tracking prerequisite.
2656
+ case "allowrowversionchangetracking":
2657
+ tbl.AllowRowVersionChangeTracking = ParseNoYes(value);
2658
+ break;
2659
+ case "createdby": tbl.CreatedBy = ParseNoYes(value); break;
2660
+ case "createddatetime": tbl.CreatedDateTime = ParseNoYes(value); break;
2661
+ case "createdtransactionid": tbl.CreatedTransactionId = ParseNoYes(value); break;
2662
+ case "modifiedby": tbl.ModifiedBy = ParseNoYes(value); break;
2663
+ case "modifieddatetime": tbl.ModifiedDateTime = ParseNoYes(value); break;
2664
+ case "modifiedtransactionid": tbl.ModifiedTransactionId = ParseNoYes(value); break;
2645
2665
  case "tabletype":
2646
2666
  if (!Enum.TryParse<Microsoft.Dynamics.AX.Metadata.Core.MetaModel.TableType>(value, true, out var tt))
2647
2667
  throw new ArgumentException(
@@ -2725,6 +2745,39 @@ namespace D365MetadataBridge.Services
2725
2745
  return true;
2726
2746
  }
2727
2747
 
2748
+ /// <summary>
2749
+ /// AxDataEntityView property setter. AllowRowVersionChangeTracking is
2750
+ /// dual-write's change-tracking switch and is also required on every
2751
+ /// source table.
2752
+ /// </summary>
2753
+ private bool SetAxDataEntityViewProperty(AxDataEntityView e, string prop, string value)
2754
+ {
2755
+ switch (prop.ToLowerInvariant())
2756
+ {
2757
+ case "label": e.Label = value; break;
2758
+ case "developerdocumentation": e.DeveloperDocumentation = value; break;
2759
+ case "primarykey": e.PrimaryKey = value; break;
2760
+ case "publicentityname": e.PublicEntityName = value; break;
2761
+ case "publiccollectionname": e.PublicCollectionName = value; break;
2762
+ case "datamanagementstagingtable": e.DataManagementStagingTable = value; break;
2763
+ case "ispublic": e.IsPublic = ParseNoYes(value); break;
2764
+ case "datamanagementenabled": e.DataManagementEnabled = ParseNoYes(value); break;
2765
+ case "allowrowversionchangetracking": e.AllowRowVersionChangeTracking = ParseNoYes(value); break;
2766
+ case "allowretention": e.AllowRetention = ParseNoYes(value); break;
2767
+ case "entitycategory":
2768
+ if (!Enum.TryParse<Microsoft.Dynamics.AX.Metadata.Core.MetaModel.EntityCategory>(value, true, out var ec))
2769
+ throw new ArgumentException(
2770
+ $"'{value}' is not a valid EntityCategory. Valid values: " +
2771
+ string.Join(", ", Enum.GetNames(typeof(Microsoft.Dynamics.AX.Metadata.Core.MetaModel.EntityCategory))) + ".");
2772
+ e.EntityCategory = ec;
2773
+ break;
2774
+ default:
2775
+ Console.Error.WriteLine($"[WriteService] Unknown AxDataEntityView property: {prop}");
2776
+ return false;
2777
+ }
2778
+ return true;
2779
+ }
2780
+
2728
2781
  private bool SetAxViewProperty(AxView v, string prop, string value)
2729
2782
  {
2730
2783
  switch (prop.ToLowerInvariant())
@@ -1077,7 +1077,7 @@ const BRIDGE_MODIFY_OPS = new Set([
1077
1077
  */
1078
1078
  const BRIDGE_MODIFY_TYPES = new Set([
1079
1079
  'class', 'table', 'enum', 'edt',
1080
- 'form', 'query', 'view',
1080
+ 'form', 'query', 'view', 'data-entity',
1081
1081
  'class-extension', 'table-extension', 'form-extension', 'enum-extension',
1082
1082
  'menu-item-action', 'menu-item-display', 'menu-item-output',
1083
1083
  'menu',
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Model descriptor reader — `<PackagesLocalDirectory>/<Package>/Descriptor/<Model>.xml`.
3
+ *
4
+ * `<ModuleReferences>` is the only statement of what a model may see: xppc
5
+ * resolves types against the referenced packages, not against everything
6
+ * installed, so an indexed type can still be invisible to the model being
7
+ * compiled. The visible set is the model's own package plus its DIRECT
8
+ * references — walking the closure would mark such a type visible again and
9
+ * hide the defect this exists to catch.
10
+ *
11
+ * Not a compiler: a table in a referenced package can still need a further
12
+ * reference because a third package contributes a table extension to it.
13
+ *
14
+ * build_d365fo_project reads the same element to order its build queue and
15
+ * calls straight into `parseModuleReferences`.
16
+ */
17
+ /**
18
+ * Extract every `<d2p1:string>` entry of a descriptor's `<ModuleReferences>`.
19
+ * The sibling `<ModelReferences>` is `i:nil` in every descriptor observed on a
20
+ * real box, so the flat scan cannot pick up entries from it.
21
+ */
22
+ export declare function parseModuleReferences(descriptorXml: string): string[];
23
+ /**
24
+ * Read a model's direct module references, or null when it has no readable
25
+ * descriptor — which callers must treat as unknown, never as "references nothing".
26
+ */
27
+ export declare function readModuleReferences(packagesPath: string, modelName: string): Promise<string[] | null>;
28
+ /** The PackagesLocalDirectory root contained in a package or workspace path. */
29
+ export declare function packagesRootFromPath(candidate: string | undefined | null): string | null;
30
+ export interface ModelVisibility {
31
+ /** Target model, for diagnostics. */
32
+ model: string;
33
+ /** PackagesLocalDirectory root the indexed file paths are relative to. */
34
+ packagesRoot: string;
35
+ /** Lower-cased package folder names the model may reference (incl. its own). */
36
+ visiblePackages: ReadonlySet<string>;
37
+ /**
38
+ * Package folder owning an indexed file, or null when the path is not under
39
+ * this packages root. Null means "cannot tell" and must silence the check.
40
+ */
41
+ packageOf(filePath: string): string | null;
42
+ }
43
+ /**
44
+ * Build (and memoise) the visibility oracle for `modelName`; null whenever the
45
+ * answer would be a guess. Callers must then skip the check — a missing
46
+ * descriptor turning into a wall of errors is worse than the gap it closes.
47
+ */
48
+ export declare function getModelVisibility(packagesRoot: string | null | undefined, modelName: string | null | undefined): ModelVisibility | null;
49
+ /** Uncached form — exported for tests, which need a fresh fixture each time. */
50
+ export declare function buildModelVisibility(packagesRoot: string | null | undefined, modelName: string | null | undefined): ModelVisibility | null;
51
+ /** Test seam — drops the memoised visibility oracles. */
52
+ export declare function clearModelVisibilityCache(): void;
53
+ //# sourceMappingURL=modelDescriptor.d.ts.map
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Model descriptor reader — `<PackagesLocalDirectory>/<Package>/Descriptor/<Model>.xml`.
3
+ *
4
+ * `<ModuleReferences>` is the only statement of what a model may see: xppc
5
+ * resolves types against the referenced packages, not against everything
6
+ * installed, so an indexed type can still be invisible to the model being
7
+ * compiled. The visible set is the model's own package plus its DIRECT
8
+ * references — walking the closure would mark such a type visible again and
9
+ * hide the defect this exists to catch.
10
+ *
11
+ * Not a compiler: a table in a referenced package can still need a further
12
+ * reference because a third package contributes a table extension to it.
13
+ *
14
+ * build_d365fo_project reads the same element to order its build queue and
15
+ * calls straight into `parseModuleReferences`.
16
+ */
17
+ import { readFile } from 'fs/promises';
18
+ import { existsSync, readFileSync, readdirSync } from 'fs';
19
+ import * as path from 'path';
20
+ /**
21
+ * Extract every `<d2p1:string>` entry of a descriptor's `<ModuleReferences>`.
22
+ * The sibling `<ModelReferences>` is `i:nil` in every descriptor observed on a
23
+ * real box, so the flat scan cannot pick up entries from it.
24
+ */
25
+ export function parseModuleReferences(descriptorXml) {
26
+ return Array.from(descriptorXml.matchAll(/<d2p1:string>\s*([^<\s]+)\s*<\/d2p1:string>/g))
27
+ .map(m => m[1].trim())
28
+ .filter(Boolean);
29
+ }
30
+ /**
31
+ * Read a model's direct module references, or null when it has no readable
32
+ * descriptor — which callers must treat as unknown, never as "references nothing".
33
+ */
34
+ export async function readModuleReferences(packagesPath, modelName) {
35
+ try {
36
+ return parseModuleReferences(await readFile(path.join(packagesPath, modelName, 'Descriptor', `${modelName}.xml`), 'utf-8'));
37
+ }
38
+ catch {
39
+ return null;
40
+ }
41
+ }
42
+ /** The PackagesLocalDirectory root contained in a package or workspace path. */
43
+ export function packagesRootFromPath(candidate) {
44
+ if (!candidate)
45
+ return null;
46
+ const m = /^(.+[\\/]PackagesLocalDirectory)(?:[\\/]|$)/i.exec(candidate.replace(/\//g, '\\'));
47
+ return m ? m[1] : null;
48
+ }
49
+ /**
50
+ * Locate `<Model>.xml`. `<root>/<Model>/Descriptor/<Model>.xml` is the common
51
+ * layout and costs one stat; ISV models inside a differently-named package fall
52
+ * back to a single bounded sweep of the root's package folders.
53
+ */
54
+ function findDescriptor(packagesRoot, modelName) {
55
+ const direct = path.join(packagesRoot, modelName, 'Descriptor', `${modelName}.xml`);
56
+ if (existsSync(direct))
57
+ return { file: direct, pkg: modelName };
58
+ let entries;
59
+ try {
60
+ entries = readdirSync(packagesRoot);
61
+ }
62
+ catch {
63
+ return null;
64
+ }
65
+ for (const pkg of entries) {
66
+ const file = path.join(packagesRoot, pkg, 'Descriptor', `${modelName}.xml`);
67
+ if (existsSync(file))
68
+ return { file, pkg };
69
+ }
70
+ return null;
71
+ }
72
+ /** Cache key → resolved visibility (or null when it could not be resolved). */
73
+ const visibilityCache = new Map();
74
+ /**
75
+ * Build (and memoise) the visibility oracle for `modelName`; null whenever the
76
+ * answer would be a guess. Callers must then skip the check — a missing
77
+ * descriptor turning into a wall of errors is worse than the gap it closes.
78
+ */
79
+ export function getModelVisibility(packagesRoot, modelName) {
80
+ if (!packagesRoot || !modelName)
81
+ return null;
82
+ const key = `${packagesRoot.toLowerCase()}|${modelName.toLowerCase()}`;
83
+ const cached = visibilityCache.get(key);
84
+ if (cached !== undefined)
85
+ return cached;
86
+ const built = buildModelVisibility(packagesRoot, modelName);
87
+ visibilityCache.set(key, built);
88
+ return built;
89
+ }
90
+ /** Uncached form — exported for tests, which need a fresh fixture each time. */
91
+ export function buildModelVisibility(packagesRoot, modelName) {
92
+ if (!packagesRoot || !modelName)
93
+ return null;
94
+ const found = findDescriptor(packagesRoot, modelName);
95
+ if (!found)
96
+ return null;
97
+ let refs;
98
+ try {
99
+ refs = parseModuleReferences(readFileSync(found.file, 'utf-8'));
100
+ }
101
+ catch {
102
+ return null;
103
+ }
104
+ const visiblePackages = new Set([found.pkg.toLowerCase()]);
105
+ for (const ref of refs)
106
+ visiblePackages.add(ref.toLowerCase());
107
+ const rootPrefix = packagesRoot.replace(/\//g, '\\').replace(/\\+$/, '').toLowerCase() + '\\';
108
+ return {
109
+ model: modelName,
110
+ packagesRoot,
111
+ visiblePackages,
112
+ packageOf(filePath) {
113
+ if (!filePath)
114
+ return null;
115
+ const norm = filePath.replace(/\//g, '\\');
116
+ // Case-insensitive: index and config disagree on the root's casing
117
+ // (`K:\AOSService\…` vs `K:\AosService\…`) for the same directory.
118
+ if (!norm.toLowerCase().startsWith(rootPrefix))
119
+ return null;
120
+ const segment = norm.slice(rootPrefix.length).split('\\')[0];
121
+ return segment || null;
122
+ },
123
+ };
124
+ }
125
+ /** Test seam — drops the memoised visibility oracles. */
126
+ export function clearModelVisibilityCache() {
127
+ visibilityCache.clear();
128
+ }
129
+ //# sourceMappingURL=modelDescriptor.js.map
@@ -6,6 +6,7 @@ import Database from '../database/sqlite.js';
6
6
  import { Worker } from 'node:worker_threads';
7
7
  import * as fs from 'fs';
8
8
  import * as path from 'path';
9
+ import { renderMethodSignature } from './xppDeclaration.js';
9
10
  import { isStandardModel } from '../utils/modelClassifier.js';
10
11
  import { c, log } from '../utils/terminalUi.js';
11
12
  /**
@@ -1531,12 +1532,11 @@ export class XppSymbolIndex {
1531
1532
  // Add method symbols with enhanced metadata
1532
1533
  if (classData.methods && Array.isArray(classData.methods)) {
1533
1534
  for (const method of classData.methods) {
1534
- const params = method.parameters?.map((p) => `${p.type} ${p.name}`).join(', ') || '';
1535
1535
  this.addSymbol({
1536
1536
  name: method.name,
1537
1537
  type: 'method',
1538
1538
  parentName: classData.name,
1539
- signature: `${method.returnType} ${method.name}(${params})`,
1539
+ signature: renderMethodSignature(method),
1540
1540
  filePath: sourceFilePath,
1541
1541
  model,
1542
1542
  description: method.documentation,
@@ -1595,12 +1595,11 @@ export class XppSymbolIndex {
1595
1595
  // Add method symbols (parallel to indexClasses)
1596
1596
  if (tableData.methods && Array.isArray(tableData.methods)) {
1597
1597
  for (const method of tableData.methods) {
1598
- const params = method.parameters?.map((p) => `${p.type} ${p.name}`).join(', ') || '';
1599
1598
  this.addSymbol({
1600
1599
  name: method.name,
1601
1600
  type: 'method',
1602
1601
  parentName: tableData.name,
1603
- signature: `${method.returnType} ${method.name}(${params})`,
1602
+ signature: renderMethodSignature(method),
1604
1603
  filePath: sourceFilePath,
1605
1604
  model,
1606
1605
  description: method.documentation,
@@ -1848,12 +1847,11 @@ export class XppSymbolIndex {
1848
1847
  // Add method symbols (views and data-entities can have display/computed methods)
1849
1848
  if (viewData.methods && Array.isArray(viewData.methods)) {
1850
1849
  for (const method of viewData.methods) {
1851
- const params = method.parameters?.map((p) => `${p.type} ${p.name}`).join(', ') || '';
1852
1850
  this.addSymbol({
1853
1851
  name: method.name,
1854
1852
  type: 'method',
1855
1853
  parentName: viewName,
1856
- signature: `${method.returnType} ${method.name}(${params})`,
1854
+ signature: renderMethodSignature(method),
1857
1855
  filePath: sourceFilePath,
1858
1856
  model,
1859
1857
  description: method.documentation,
@@ -34,6 +34,12 @@ export interface XppMethodInfo {
34
34
  visibility: 'public' | 'private' | 'protected';
35
35
  returnType: string;
36
36
  parameters: XppParameterInfo[];
37
+ /**
38
+ * Declaration unparseable, so `parameters` is empty for lack of evidence
39
+ * rather than because the method takes none. Arity checks must skip, not
40
+ * assume zero.
41
+ */
42
+ parametersUnknown?: boolean;
37
43
  isStatic: boolean;
38
44
  source: string;
39
45
  documentation?: string;
@@ -48,6 +54,8 @@ export interface XppMethodInfo {
48
54
  export interface XppParameterInfo {
49
55
  name: string;
50
56
  type: string;
57
+ /** Default as declared; a dropped default makes an optional parameter look required. */
58
+ defaultValue?: string;
51
59
  }
52
60
  export interface XppTableInfo {
53
61
  name: string;
@@ -93,7 +93,11 @@ export declare class XppMetadataParser {
93
93
  private parseViewRelationFields;
94
94
  private extractLabelId;
95
95
  private ensureArray;
96
- /** Declaration parameters narrowed to the shape XppMethodInfo carries. */
96
+ /**
97
+ * Declaration parameters narrowed to the shape XppMethodInfo carries.
98
+ * A null decl yields `[]`; the `parametersUnknown` flag set alongside is what
99
+ * tells that apart from a genuinely empty list, so don't read this alone.
100
+ */
97
101
  private toParameterInfo;
98
102
  /**
99
103
  * Parse Form XML file (AxForm)
@@ -262,6 +262,7 @@ export class XppMetadataParser {
262
262
  visibility: this.parseVisibility(method.Visibility),
263
263
  returnType: decl?.returnType || method.ReturnType || 'void',
264
264
  parameters: this.toParameterInfo(decl),
265
+ parametersUnknown: decl === null,
265
266
  isStatic: decl?.modifiers.includes('static') ?? false,
266
267
  source: source,
267
268
  documentation: method.DeveloperDocumentation || undefined,
@@ -441,9 +442,15 @@ export class XppMetadataParser {
441
442
  return [];
442
443
  return Array.isArray(value) ? value : [value];
443
444
  }
444
- /** Declaration parameters narrowed to the shape XppMethodInfo carries. */
445
+ /**
446
+ * Declaration parameters narrowed to the shape XppMethodInfo carries.
447
+ * A null decl yields `[]`; the `parametersUnknown` flag set alongside is what
448
+ * tells that apart from a genuinely empty list, so don't read this alone.
449
+ */
445
450
  toParameterInfo(decl) {
446
- return decl?.parameters.map(p => ({ type: p.type, name: p.name })) ?? [];
451
+ return decl?.parameters.map(p => (p.defaultValue
452
+ ? { type: p.type, name: p.name, defaultValue: p.defaultValue }
453
+ : { type: p.type, name: p.name })) ?? [];
447
454
  }
448
455
  /**
449
456
  * Parse Form XML file (AxForm)
@@ -561,6 +568,7 @@ export class XppMetadataParser {
561
568
  visibility: 'public', // Forms typically have public methods
562
569
  returnType: decl?.returnType || 'void',
563
570
  parameters: this.toParameterInfo(decl),
571
+ parametersUnknown: decl === null,
564
572
  isStatic: decl?.modifiers.includes('static') ?? false,
565
573
  source,
566
574
  sourceSnippet: source.split('\n').slice(0, 10).join('\n'),
@@ -26,6 +26,28 @@ export interface XppDeclaration {
26
26
  returnType: string;
27
27
  parameters: XppDeclarationParameter[];
28
28
  }
29
+ /** Rendered for a parameter list that could not be read; not legal X++, so it can't collide with a real one. */
30
+ export declare const UNKNOWN_PARAMETER_LIST = "...";
31
+ /** The subset of a method that `renderMethodSignature` needs. */
32
+ export interface RenderableMethod {
33
+ name: string;
34
+ returnType?: string;
35
+ parameters?: ReadonlyArray<{
36
+ type: string;
37
+ name: string;
38
+ defaultValue?: string;
39
+ }> | null;
40
+ /** See XppMethodInfo.parametersUnknown. */
41
+ parametersUnknown?: boolean;
42
+ }
43
+ /**
44
+ * Render `ReturnType name(Type _a, Type _b = default)` for the symbol index.
45
+ *
46
+ * Defaults are kept and an unreadable list renders as `(...)`, never `()` —
47
+ * resolve_references reads this string back and gates writes on it, so `()` is
48
+ * a positive claim of zero parameters that consumers act on.
49
+ */
50
+ export declare function renderMethodSignature(method: RenderableMethod): string;
29
51
  export interface XppClassHeader {
30
52
  kind: 'class' | 'interface';
31
53
  name: string;
@@ -15,6 +15,23 @@
15
15
  * A parse that cannot be trusted returns null so callers fall through to their
16
16
  * own fallbacks, rather than emitting a confidently wrong signature.
17
17
  */
18
+ /** Rendered for a parameter list that could not be read; not legal X++, so it can't collide with a real one. */
19
+ export const UNKNOWN_PARAMETER_LIST = '...';
20
+ /**
21
+ * Render `ReturnType name(Type _a, Type _b = default)` for the symbol index.
22
+ *
23
+ * Defaults are kept and an unreadable list renders as `(...)`, never `()` —
24
+ * resolve_references reads this string back and gates writes on it, so `()` is
25
+ * a positive claim of zero parameters that consumers act on.
26
+ */
27
+ export function renderMethodSignature(method) {
28
+ const params = method.parametersUnknown
29
+ ? UNKNOWN_PARAMETER_LIST
30
+ : (method.parameters ?? [])
31
+ .map(p => `${p.type} ${p.name}${p.defaultValue ? ` = ${p.defaultValue}` : ''}`)
32
+ .join(', ');
33
+ return `${method.returnType ?? 'void'} ${method.name}(${params})`;
34
+ }
18
35
  export const MODIFIER_KEYWORDS = [
19
36
  'public', 'private', 'protected', 'internal', 'static', 'final', 'abstract', 'display', 'edit',
20
37
  ];
@@ -126,6 +143,20 @@ function isTypeToken(t) {
126
143
  function isNameToken(t) {
127
144
  return /^[A-Za-z_]\w*$/.test(t);
128
145
  }
146
+ /**
147
+ * Slice `raw` from `from`, trimmed to the extent that still carries content in
148
+ * the blanked twin — comments became whitespace there and so drop out, while
149
+ * string literals survive (their quotes are not blanked).
150
+ */
151
+ function sliceByBlankedExtent(raw, blanked, from) {
152
+ let start = from;
153
+ while (start < blanked.length && /\s/.test(blanked[start]))
154
+ start++;
155
+ let end = blanked.length;
156
+ while (end > start && /\s/.test(blanked[end - 1]))
157
+ end--;
158
+ return raw.slice(start, end).replace(/\s+/g, ' ');
159
+ }
129
160
  /**
130
161
  * Parse one "Type _name [= default]" parameter; null when it doesn't look like
131
162
  * a declared parameter (which is how a call's arguments are told apart from a
@@ -146,8 +177,11 @@ function parseParameter(raw, blanked) {
146
177
  break;
147
178
  }
148
179
  }
149
- const left = (eq >= 0 ? raw.slice(0, eq) : raw).trim().replace(/\s+/g, ' ');
150
- const defaultValue = eq >= 0 ? raw.slice(eq + 1).trim().replace(/\s+/g, ' ') : undefined;
180
+ // Type and name come off the blanked twin: the standard models interleave
181
+ // pragma comments inside wrapped parameter lists, and read raw those look
182
+ // like a leading type token, failing the parameter and discarding the lot.
183
+ const left = (eq >= 0 ? blanked.slice(0, eq) : blanked).trim().replace(/\s+/g, ' ');
184
+ const defaultValue = eq >= 0 ? sliceByBlankedExtent(raw, blanked, eq + 1) : undefined;
151
185
  const tokens = left.split(' ').filter(Boolean);
152
186
  if (tokens.length < 2)
153
187
  return null;
@@ -785,6 +785,13 @@ import { Worker } from "node:worker_threads";
785
785
  import * as fs2 from "fs";
786
786
  import * as path from "path";
787
787
 
788
+ // src/metadata/xppDeclaration.ts
789
+ var UNKNOWN_PARAMETER_LIST = "...";
790
+ function renderMethodSignature(method) {
791
+ const params = method.parametersUnknown ? UNKNOWN_PARAMETER_LIST : (method.parameters ?? []).map((p) => `${p.type} ${p.name}${p.defaultValue ? ` = ${p.defaultValue}` : ""}`).join(", ");
792
+ return `${method.returnType ?? "void"} ${method.name}(${params})`;
793
+ }
794
+
788
795
  // src/utils/modelClassifier.ts
789
796
  var autoDetectedCustomModels = /* @__PURE__ */ new Set();
790
797
  function isAutoDetectedCustomModel(modelName) {
@@ -2342,12 +2349,11 @@ Point the installation at a drive with room (a full index needs several GB): re-
2342
2349
  });
2343
2350
  if (classData.methods && Array.isArray(classData.methods)) {
2344
2351
  for (const method of classData.methods) {
2345
- const params = method.parameters?.map((p) => `${p.type} ${p.name}`).join(", ") || "";
2346
2352
  this.addSymbol({
2347
2353
  name: method.name,
2348
2354
  type: "method",
2349
2355
  parentName: classData.name,
2350
- signature: `${method.returnType} ${method.name}(${params})`,
2356
+ signature: renderMethodSignature(method),
2351
2357
  filePath: sourceFilePath,
2352
2358
  model,
2353
2359
  description: method.documentation,
@@ -2401,12 +2407,11 @@ Point the installation at a drive with room (a full index needs several GB): re-
2401
2407
  }
2402
2408
  if (tableData.methods && Array.isArray(tableData.methods)) {
2403
2409
  for (const method of tableData.methods) {
2404
- const params = method.parameters?.map((p) => `${p.type} ${p.name}`).join(", ") || "";
2405
2410
  this.addSymbol({
2406
2411
  name: method.name,
2407
2412
  type: "method",
2408
2413
  parentName: tableData.name,
2409
- signature: `${method.returnType} ${method.name}(${params})`,
2414
+ signature: renderMethodSignature(method),
2410
2415
  filePath: sourceFilePath,
2411
2416
  model,
2412
2417
  description: method.documentation,
@@ -2659,12 +2664,11 @@ Point the installation at a drive with room (a full index needs several GB): re-
2659
2664
  }
2660
2665
  if (viewData.methods && Array.isArray(viewData.methods)) {
2661
2666
  for (const method of viewData.methods) {
2662
- const params = method.parameters?.map((p) => `${p.type} ${p.name}`).join(", ") || "";
2663
2667
  this.addSymbol({
2664
2668
  name: method.name,
2665
2669
  type: "method",
2666
2670
  parentName: viewName,
2667
- signature: `${method.returnType} ${method.name}(${params})`,
2671
+ signature: renderMethodSignature(method),
2668
2672
  filePath: sourceFilePath,
2669
2673
  model,
2670
2674
  description: method.documentation,
@@ -784,6 +784,13 @@ import { Worker } from "node:worker_threads";
784
784
  import * as fs2 from "fs";
785
785
  import * as path from "path";
786
786
 
787
+ // src/metadata/xppDeclaration.ts
788
+ var UNKNOWN_PARAMETER_LIST = "...";
789
+ function renderMethodSignature(method) {
790
+ const params = method.parametersUnknown ? UNKNOWN_PARAMETER_LIST : (method.parameters ?? []).map((p) => `${p.type} ${p.name}${p.defaultValue ? ` = ${p.defaultValue}` : ""}`).join(", ");
791
+ return `${method.returnType ?? "void"} ${method.name}(${params})`;
792
+ }
793
+
787
794
  // src/utils/modelClassifier.ts
788
795
  var autoDetectedCustomModels = /* @__PURE__ */ new Set();
789
796
  function isAutoDetectedCustomModel(modelName) {
@@ -2281,12 +2288,11 @@ Point the installation at a drive with room (a full index needs several GB): re-
2281
2288
  });
2282
2289
  if (classData.methods && Array.isArray(classData.methods)) {
2283
2290
  for (const method of classData.methods) {
2284
- const params = method.parameters?.map((p) => `${p.type} ${p.name}`).join(", ") || "";
2285
2291
  this.addSymbol({
2286
2292
  name: method.name,
2287
2293
  type: "method",
2288
2294
  parentName: classData.name,
2289
- signature: `${method.returnType} ${method.name}(${params})`,
2295
+ signature: renderMethodSignature(method),
2290
2296
  filePath: sourceFilePath,
2291
2297
  model,
2292
2298
  description: method.documentation,
@@ -2340,12 +2346,11 @@ Point the installation at a drive with room (a full index needs several GB): re-
2340
2346
  }
2341
2347
  if (tableData.methods && Array.isArray(tableData.methods)) {
2342
2348
  for (const method of tableData.methods) {
2343
- const params = method.parameters?.map((p) => `${p.type} ${p.name}`).join(", ") || "";
2344
2349
  this.addSymbol({
2345
2350
  name: method.name,
2346
2351
  type: "method",
2347
2352
  parentName: tableData.name,
2348
- signature: `${method.returnType} ${method.name}(${params})`,
2353
+ signature: renderMethodSignature(method),
2349
2354
  filePath: sourceFilePath,
2350
2355
  model,
2351
2356
  description: method.documentation,
@@ -2598,12 +2603,11 @@ Point the installation at a drive with room (a full index needs several GB): re-
2598
2603
  }
2599
2604
  if (viewData.methods && Array.isArray(viewData.methods)) {
2600
2605
  for (const method of viewData.methods) {
2601
- const params = method.parameters?.map((p) => `${p.type} ${p.name}`).join(", ") || "";
2602
2606
  this.addSymbol({
2603
2607
  name: method.name,
2604
2608
  type: "method",
2605
2609
  parentName: viewName,
2606
- signature: `${method.returnType} ${method.name}(${params})`,
2610
+ signature: renderMethodSignature(method),
2607
2611
  filePath: sourceFilePath,
2608
2612
  model,
2609
2613
  description: method.documentation,