d365fo-mcp 1.8.4 → 1.9.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/.github/copilot-instructions.md +1 -1
- package/dist/cli/copilotFiles.js +9 -7
- package/dist/prompts/systemInstructions.d.ts +1 -1
- package/dist/prompts/systemInstructions.js +2 -2
- package/dist/scripts/build-database.js +16 -1
- package/dist/scripts/build-fts.js +16 -1
- package/dist/scripts/extract-metadata.js +16 -1
- package/dist/server/toolSchemas/getWorkspaceInfo.js +3 -3
- package/dist/tools/codeGen.js +23 -21
- package/dist/tools/createD365File.js +8 -10
- package/dist/tools/createLabel.js +2 -1
- package/dist/tools/d365foFileOpSpecs.js +11 -4
- package/dist/tools/extensionStrategyAdvisor.js +26 -26
- package/dist/tools/generateSmartForm.js +10 -0
- package/dist/tools/generateSmartReport.js +11 -0
- package/dist/tools/generateSmartTable.js +11 -0
- package/dist/tools/getLabelInfo.js +31 -0
- package/dist/tools/modifyD365File.d.ts +1 -1
- package/dist/tools/modifyD365File.js +90 -8
- package/dist/tools/prefixDiagnostics.d.ts +45 -0
- package/dist/tools/prefixDiagnostics.js +105 -0
- package/dist/tools/search.js +3 -1
- package/dist/tools/toolHandler.js +128 -60
- package/dist/tools/validateFormPattern.d.ts +1 -1
- package/dist/tools/validateFormPattern.js +1 -1
- package/dist/tools/writeAnchorGuard.d.ts +41 -0
- package/dist/tools/writeAnchorGuard.js +44 -0
- package/dist/tools/xppKnowledge.js +4 -1
- package/dist/utils/configManager.d.ts +30 -0
- package/dist/utils/configManager.js +81 -0
- package/dist/utils/crossModelWriteGuard.d.ts +20 -3
- package/dist/utils/crossModelWriteGuard.js +27 -10
- package/dist/utils/indexStaleness.d.ts +7 -0
- package/dist/utils/indexStaleness.js +28 -5
- package/dist/utils/labelDiskCheck.d.ts +24 -0
- package/dist/utils/labelDiskCheck.js +88 -0
- package/dist/utils/loadEnv.d.ts +15 -0
- package/dist/utils/loadEnv.js +76 -1
- package/dist/utils/modelClassifier.d.ts +7 -4
- package/dist/utils/modelClassifier.js +12 -9
- package/dist/utils/modelPrefixInference.d.ts +25 -10
- package/dist/utils/modelPrefixInference.js +123 -21
- package/dist/workspace/contextSnapshot.d.ts +5 -3
- package/dist/workspace/contextSnapshot.js +22 -0
- package/package.json +1 -1
|
@@ -18,7 +18,7 @@ Call `get_workspace_info()` before doing anything with D365FO objects.
|
|
|
18
18
|
|----------|--------|
|
|
19
19
|
| Call fails | STOP. MCP server not connected. Ask user to start it. |
|
|
20
20
|
| `⛔ CONFIGURATION PROBLEM` | STOP. Relay message. Wait for user. |
|
|
21
|
-
|
|
|
21
|
+
| No `⛔` in the response | Note the `Model` / `Prefix` lines. Proceed. |
|
|
22
22
|
|
|
23
23
|
## Terminal Prohibition
|
|
24
24
|
|
package/dist/cli/copilotFiles.js
CHANGED
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
* folder, or, when that is unknown, into a staging folder with a README
|
|
10
10
|
* naming the two destinations.
|
|
11
11
|
*
|
|
12
|
+
* The README belongs to the staging folder only. The solutions folder is the
|
|
13
|
+
* user's own projects directory, so the direct copy leaves nothing there but
|
|
14
|
+
* `.github\copilot-instructions.md` and says the rest on the console.
|
|
15
|
+
*
|
|
12
16
|
* The README points at the one .mcp.json `mcpJsonNote()` writes in the data
|
|
13
17
|
* root rather than copying it: the staging folder sits in that same data
|
|
14
18
|
* root, so a second copy is only one more file to keep in sync.
|
|
@@ -22,7 +26,7 @@ import { dataRoot, paths, repoRoot } from './context.js';
|
|
|
22
26
|
import { askConfirm, p } from './ui.js';
|
|
23
27
|
/** The copy shipped with this installation — package root in both install modes. */
|
|
24
28
|
const copilotSource = () => resolve(repoRoot, '.github', 'copilot-instructions.md');
|
|
25
|
-
function writeCopilotSetupReadme(targetDir
|
|
29
|
+
function writeCopilotSetupReadme(targetDir) {
|
|
26
30
|
const readmePath = resolve(targetDir, 'README.md');
|
|
27
31
|
const lines = [
|
|
28
32
|
'# VS setup quick guide',
|
|
@@ -35,9 +39,7 @@ function writeCopilotSetupReadme(targetDir, opts) {
|
|
|
35
39
|
` - Generated copy: ${paths.mcpSuggestion}`,
|
|
36
40
|
'',
|
|
37
41
|
'2. Copy .github/copilot-instructions.md',
|
|
38
|
-
|
|
39
|
-
? ' - Already placed here: .github\\copilot-instructions.md'
|
|
40
|
-
: ' - Destination: a parent folder of your solution directories',
|
|
42
|
+
' - Destination: a parent folder of your solution directories',
|
|
41
43
|
' - Why: provides mandatory D365FO tool-routing and safety rules for Copilot',
|
|
42
44
|
'',
|
|
43
45
|
'3. Restart Visual Studio after copying files.',
|
|
@@ -64,16 +66,16 @@ export async function maybePrepareCopilotInstructions(solutionsPath) {
|
|
|
64
66
|
const targetDir = resolve(target, '.github');
|
|
65
67
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
66
68
|
fs.copyFileSync(source, resolve(targetDir, 'copilot-instructions.md'));
|
|
67
|
-
writeCopilotSetupReadme(target, { copilotAlreadyPlaced: true });
|
|
68
69
|
p.log.success(`Prepared: ${resolve(targetDir, 'copilot-instructions.md')}`);
|
|
69
|
-
p.log.
|
|
70
|
+
p.log.info('Still to do: copy .mcp.json to %USERPROFILE%\\.mcp.json (or next to your .sln), then restart Visual Studio.\n' +
|
|
71
|
+
` Generated copy: ${paths.mcpSuggestion}`);
|
|
70
72
|
return;
|
|
71
73
|
}
|
|
72
74
|
const stageDir = resolve(dataRoot(), 'copilot-setup');
|
|
73
75
|
const stageGitHubDir = resolve(stageDir, '.github');
|
|
74
76
|
fs.mkdirSync(stageGitHubDir, { recursive: true });
|
|
75
77
|
fs.copyFileSync(source, resolve(stageGitHubDir, 'copilot-instructions.md'));
|
|
76
|
-
writeCopilotSetupReadme(stageDir
|
|
78
|
+
writeCopilotSetupReadme(stageDir);
|
|
77
79
|
if (wantsDirectCopy && !target) {
|
|
78
80
|
p.log.warn('Solutions folder is empty, so files were prepared in the local staging folder instead.');
|
|
79
81
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Kept deliberately under 200 lines: the prompt holds only the tool decision
|
|
10
10
|
* tree and hard prohibitions. Everything that is a rule about CODE lives in
|
|
11
|
-
* the queryable knowledge base —
|
|
11
|
+
* the queryable knowledge base — get_knowledge (see the ID table below).
|
|
12
12
|
*/
|
|
13
13
|
/**
|
|
14
14
|
* Get the system instructions prompt definition
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Kept deliberately under 200 lines: the prompt holds only the tool decision
|
|
10
10
|
* tree and hard prohibitions. Everything that is a rule about CODE lives in
|
|
11
|
-
* the queryable knowledge base —
|
|
11
|
+
* the queryable knowledge base — get_knowledge (see the ID table below).
|
|
12
12
|
*/
|
|
13
13
|
/**
|
|
14
14
|
* Get the system instructions prompt definition
|
|
@@ -117,7 +117,7 @@ You are an AI assistant with access to D365FO MCP tools, assisting with Dynamics
|
|
|
117
117
|
- No literal strings in \`Info()\`/\`error()\`/labels — use \`@Model:LabelId\` (reuse via \`labels(action="search")\` first)
|
|
118
118
|
- Every public/protected member needs a meaningful \`/// <summary>\` (not "MyClass class.")
|
|
119
119
|
|
|
120
|
-
**For full rules and examples call \`
|
|
120
|
+
**For full rules and examples call \`get_knowledge(kind="knowledge", topic=<id>)\` BEFORE generating code:**
|
|
121
121
|
|
|
122
122
|
| Knowledge ID | Covers |
|
|
123
123
|
|---|---|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/utils/loadEnv.ts
|
|
2
2
|
import dotenv from "dotenv";
|
|
3
|
-
import { existsSync as existsSync2 } from "fs";
|
|
3
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, statSync } from "fs";
|
|
4
4
|
import { dirname as dirname2, isAbsolute as isAbsolute2, join as join2, resolve as resolve2 } from "path";
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
6
|
|
|
@@ -606,6 +606,12 @@ function defaultPathEnv(baseDir) {
|
|
|
606
606
|
|
|
607
607
|
// src/utils/loadEnv.ts
|
|
608
608
|
var PATH_VARS = ["DB_PATH", "LABELS_DB_PATH", "METADATA_PATH"];
|
|
609
|
+
var WRITE_POLICY_VARS = [
|
|
610
|
+
"D365FO_ALLOW_CROSS_MODEL_WRITE",
|
|
611
|
+
"D365FO_CROSS_MODEL_WRITE_MODELS"
|
|
612
|
+
];
|
|
613
|
+
var writePolicySource = null;
|
|
614
|
+
var writePolicyStamp = "";
|
|
609
615
|
function installRootFrom(callerDir) {
|
|
610
616
|
let dir = resolve2(callerDir, "..");
|
|
611
617
|
for (let up = 0; up < 3; up++) {
|
|
@@ -621,6 +627,15 @@ function loadEnv(callerImportMetaUrl) {
|
|
|
621
627
|
const callerDir = dirname2(fileURLToPath(callerImportMetaUrl));
|
|
622
628
|
const envPath = process.env.ENV_FILE ? resolve2(process.env.ENV_FILE) : resolve2(installRootFrom(callerDir), ".env");
|
|
623
629
|
const fromRealEnv = new Set(Object.keys(process.env));
|
|
630
|
+
writePolicySource = {
|
|
631
|
+
envPath,
|
|
632
|
+
pinned: new Set(WRITE_POLICY_VARS.filter((k) => fromRealEnv.has(k)))
|
|
633
|
+
};
|
|
634
|
+
try {
|
|
635
|
+
writePolicyStamp = String(statSync(envPath).mtimeMs);
|
|
636
|
+
} catch {
|
|
637
|
+
writePolicyStamp = "-";
|
|
638
|
+
}
|
|
624
639
|
const result = dotenv.config({ path: envPath, quiet: true });
|
|
625
640
|
if (result.error && !process.env.ENV_FILE) {
|
|
626
641
|
dotenv.config({ quiet: true });
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/utils/loadEnv.ts
|
|
2
2
|
import dotenv from "dotenv";
|
|
3
|
-
import { existsSync as existsSync2 } from "fs";
|
|
3
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, statSync } from "fs";
|
|
4
4
|
import { dirname as dirname2, isAbsolute as isAbsolute2, join as join2, resolve as resolve2 } from "path";
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
6
|
|
|
@@ -606,6 +606,12 @@ function defaultPathEnv(baseDir) {
|
|
|
606
606
|
|
|
607
607
|
// src/utils/loadEnv.ts
|
|
608
608
|
var PATH_VARS = ["DB_PATH", "LABELS_DB_PATH", "METADATA_PATH"];
|
|
609
|
+
var WRITE_POLICY_VARS = [
|
|
610
|
+
"D365FO_ALLOW_CROSS_MODEL_WRITE",
|
|
611
|
+
"D365FO_CROSS_MODEL_WRITE_MODELS"
|
|
612
|
+
];
|
|
613
|
+
var writePolicySource = null;
|
|
614
|
+
var writePolicyStamp = "";
|
|
609
615
|
function installRootFrom(callerDir) {
|
|
610
616
|
let dir = resolve2(callerDir, "..");
|
|
611
617
|
for (let up = 0; up < 3; up++) {
|
|
@@ -621,6 +627,15 @@ function loadEnv(callerImportMetaUrl) {
|
|
|
621
627
|
const callerDir = dirname2(fileURLToPath(callerImportMetaUrl));
|
|
622
628
|
const envPath = process.env.ENV_FILE ? resolve2(process.env.ENV_FILE) : resolve2(installRootFrom(callerDir), ".env");
|
|
623
629
|
const fromRealEnv = new Set(Object.keys(process.env));
|
|
630
|
+
writePolicySource = {
|
|
631
|
+
envPath,
|
|
632
|
+
pinned: new Set(WRITE_POLICY_VARS.filter((k) => fromRealEnv.has(k)))
|
|
633
|
+
};
|
|
634
|
+
try {
|
|
635
|
+
writePolicyStamp = String(statSync(envPath).mtimeMs);
|
|
636
|
+
} catch {
|
|
637
|
+
writePolicyStamp = "-";
|
|
638
|
+
}
|
|
624
639
|
const result = dotenv.config({ path: envPath, quiet: true });
|
|
625
640
|
if (result.error && !process.env.ENV_FILE) {
|
|
626
641
|
dotenv.config({ quiet: true });
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/utils/loadEnv.ts
|
|
2
2
|
import dotenv from "dotenv";
|
|
3
|
-
import { existsSync as existsSync2 } from "fs";
|
|
3
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, statSync } from "fs";
|
|
4
4
|
import { dirname as dirname2, isAbsolute as isAbsolute2, join as join2, resolve as resolve2 } from "path";
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
6
|
|
|
@@ -606,6 +606,12 @@ function defaultPathEnv(baseDir) {
|
|
|
606
606
|
|
|
607
607
|
// src/utils/loadEnv.ts
|
|
608
608
|
var PATH_VARS = ["DB_PATH", "LABELS_DB_PATH", "METADATA_PATH"];
|
|
609
|
+
var WRITE_POLICY_VARS = [
|
|
610
|
+
"D365FO_ALLOW_CROSS_MODEL_WRITE",
|
|
611
|
+
"D365FO_CROSS_MODEL_WRITE_MODELS"
|
|
612
|
+
];
|
|
613
|
+
var writePolicySource = null;
|
|
614
|
+
var writePolicyStamp = "";
|
|
609
615
|
function installRootFrom(callerDir) {
|
|
610
616
|
let dir = resolve2(callerDir, "..");
|
|
611
617
|
for (let up = 0; up < 3; up++) {
|
|
@@ -621,6 +627,15 @@ function loadEnv(callerImportMetaUrl) {
|
|
|
621
627
|
const callerDir = dirname2(fileURLToPath(callerImportMetaUrl));
|
|
622
628
|
const envPath = process.env.ENV_FILE ? resolve2(process.env.ENV_FILE) : resolve2(installRootFrom(callerDir), ".env");
|
|
623
629
|
const fromRealEnv = new Set(Object.keys(process.env));
|
|
630
|
+
writePolicySource = {
|
|
631
|
+
envPath,
|
|
632
|
+
pinned: new Set(WRITE_POLICY_VARS.filter((k) => fromRealEnv.has(k)))
|
|
633
|
+
};
|
|
634
|
+
try {
|
|
635
|
+
writePolicyStamp = String(statSync(envPath).mtimeMs);
|
|
636
|
+
} catch {
|
|
637
|
+
writePolicyStamp = "-";
|
|
638
|
+
}
|
|
624
639
|
const result = dotenv.config({ path: envPath, quiet: true });
|
|
625
640
|
if (result.error && !process.env.ENV_FILE) {
|
|
626
641
|
dotenv.config({ quiet: true });
|
|
@@ -5,13 +5,13 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export const getWorkspaceInfoTool = {
|
|
7
7
|
name: 'get_workspace_info',
|
|
8
|
-
description: `ALWAYS call FIRST at session start. Returns model name, package path, framework directory, project path, environment type, and EXTENSION_PREFIX. Flags placeholder model names and missing prefix.
|
|
8
|
+
description: `ALWAYS call FIRST at session start. Returns model name, package path, framework directory, project path, environment type, and EXTENSION_PREFIX. Flags placeholder model names and missing prefix. projectName/projectPath ONLY when the USER changed project. This is the authoritative source for target model — not search results.`,
|
|
9
9
|
inputSchema: {
|
|
10
10
|
type: 'object',
|
|
11
11
|
properties: {
|
|
12
12
|
projectName: {
|
|
13
13
|
type: 'string',
|
|
14
|
-
description: '
|
|
14
|
+
description: 'Only when the USER says "switch to <project>". Just the model name, e.g. "ContosoEDS"; path resolved from D365FO_SOLUTIONS_PATH. NOT a way to reach another model — reads span every model already, writes stay in the workspace model.',
|
|
15
15
|
},
|
|
16
16
|
projectPath: {
|
|
17
17
|
type: 'string',
|
|
@@ -20,7 +20,7 @@ export const getWorkspaceInfoTool = {
|
|
|
20
20
|
diagnostics: {
|
|
21
21
|
type: 'boolean',
|
|
22
22
|
default: false,
|
|
23
|
-
description: 'Include verbose
|
|
23
|
+
description: 'Include verbose sections (config sources, suffix, project paths, index scan, stdio handshake). Use when debugging config or connectivity.',
|
|
24
24
|
},
|
|
25
25
|
},
|
|
26
26
|
required: [],
|
package/dist/tools/codeGen.js
CHANGED
|
@@ -84,7 +84,7 @@ internal final class ${name}
|
|
|
84
84
|
// D365FO Data Entity: ${name}Entity
|
|
85
85
|
// ══════════════════════════════════════════════════════════════════
|
|
86
86
|
// Data entities are AxDataEntityView XML objects (NOT X++ classes).
|
|
87
|
-
// Use
|
|
87
|
+
// Use d365fo_file(action="create", objectType="view") or the VS designer.
|
|
88
88
|
//
|
|
89
89
|
// Key properties to set in XML:
|
|
90
90
|
// PublicEntityName: "${name}" (OData singular name)
|
|
@@ -107,8 +107,8 @@ internal final class ${name}
|
|
|
107
107
|
//
|
|
108
108
|
// Workflow:
|
|
109
109
|
// 1. get_object_info(objectType="data-entity", name="similar entity") → study structure
|
|
110
|
-
// 2.
|
|
111
|
-
// 3.
|
|
110
|
+
// 2. generate_object(mode="scaffold", objectType="data-entity", ...) → preview XML
|
|
111
|
+
// 3. d365fo_file(action="create", objectType="view", ...) → create file
|
|
112
112
|
// 4. After deployment: refresh entity list in Data Management workspace
|
|
113
113
|
`,
|
|
114
114
|
'batch-job': (name) => `
|
|
@@ -646,7 +646,7 @@ function menuItemXmlTemplate(name, itemType, targetObject) {
|
|
|
646
646
|
function ssrsReportFullTemplate(name) {
|
|
647
647
|
return `// ══════════════════════════════════════════════════════════════════
|
|
648
648
|
// SSRS Report: ${name}
|
|
649
|
-
// 5 objects required (use
|
|
649
|
+
// 5 objects required (use d365fo_file(action="create") for each):
|
|
650
650
|
// 1. ${name}TmpTable — TempDB table (objectType="table", tableType="TempDB")
|
|
651
651
|
// 2. ${name}Contract — DataContract class (below)
|
|
652
652
|
// 3. ${name}DP — Data Provider class (below)
|
|
@@ -1053,14 +1053,14 @@ public class ${name}Controller extends MenuFunction
|
|
|
1053
1053
|
function dataEntityStagingTemplate(name) {
|
|
1054
1054
|
return `// ══════════════════════════════════════════════════════════════════════════
|
|
1055
1055
|
// Data Entity with Staging Table: ${name}
|
|
1056
|
-
// 3 objects required (use
|
|
1056
|
+
// 3 objects required (use d365fo_file(action="create") for each):
|
|
1057
1057
|
// 1. ${name}StagingTable — TempDB staging table
|
|
1058
1058
|
// 2. ${name}Entity — Data entity (AxDataEntityView)
|
|
1059
1059
|
// 3. ${name}EntityService — Optional: AIF service class
|
|
1060
1060
|
// ══════════════════════════════════════════════════════════════════════════
|
|
1061
1061
|
|
|
1062
1062
|
// ── Object 1: Staging table ${name}Staging ─────────────────────────────────
|
|
1063
|
-
//
|
|
1063
|
+
// d365fo_file(action="create", objectType="table", objectName="${name}Staging", xmlContent=...)
|
|
1064
1064
|
// Set: TableType=TempDB (NOT RegularTable), TableGroup=Main
|
|
1065
1065
|
// Fields mirror the entity's public fields exactly (same names, same EDTs)
|
|
1066
1066
|
|
|
@@ -1661,7 +1661,7 @@ public class ${name}Service
|
|
|
1661
1661
|
}
|
|
1662
1662
|
}
|
|
1663
1663
|
|
|
1664
|
-
// ── 3. AOT objects (create via
|
|
1664
|
+
// ── 3. AOT objects (create via d365fo_file(action="create")) ────────────
|
|
1665
1665
|
// Verify the result afterwards with get_object_info(objectType="service", name="${name}Service").
|
|
1666
1666
|
// a) AxService XML (real schema: ServiceOperations / AxServiceOperation / Method):
|
|
1667
1667
|
// <AxService xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
|
|
@@ -1822,7 +1822,11 @@ export async function codeGenTool(request) {
|
|
|
1822
1822
|
// Extension pattern — args.name is the BASE element; prefix becomes the infix
|
|
1823
1823
|
// Grounding enforcement: extension patterns require proof that the AI looked at the
|
|
1824
1824
|
// real codebase (via prepare_change) before generating extension code.
|
|
1825
|
-
const groundingError = enforceGrounding(args.groundingToken,
|
|
1825
|
+
const groundingError = enforceGrounding(args.groundingToken,
|
|
1826
|
+
// The MCP-visible name, not the internal one: this string is echoed back
|
|
1827
|
+
// as the call the agent must repeat with a token, and `generate_code`
|
|
1828
|
+
// has not been a registered tool since the mega-tool consolidation.
|
|
1829
|
+
`generate_object(mode="pattern", pattern="${args.pattern}", name="${args.name}")`, args.name);
|
|
1826
1830
|
if (groundingError)
|
|
1827
1831
|
return groundingError;
|
|
1828
1832
|
// ── form-datasource-extension / form-control-extension (3-param templates) ──
|
|
@@ -1924,20 +1928,18 @@ export async function codeGenTool(request) {
|
|
|
1924
1928
|
text: `Generated ${args.pattern} template for "${displayName}":\n\n` +
|
|
1925
1929
|
`\`\`\`xpp${code}\n\`\`\`\n\n` +
|
|
1926
1930
|
`---\n\n` +
|
|
1927
|
-
|
|
1931
|
+
namingNote +
|
|
1932
|
+
// Only the CoC signature rule earns a line here: guessing static vs
|
|
1933
|
+
// instance produces code that fails to compile. The rest was a menu
|
|
1934
|
+
// of optional follow-ups — and it named tools that do not exist
|
|
1935
|
+
// (`find_coc_extensions`) with argument forms analyze_code rejects
|
|
1936
|
+
// (positional, not `className`/`methodName`), so following it cost a
|
|
1937
|
+
// failed call before the agent could get anything useful.
|
|
1928
1938
|
(args.pattern === 'class-extension'
|
|
1929
|
-
?
|
|
1930
|
-
`
|
|
1931
|
-
`
|
|
1932
|
-
|
|
1933
|
-
`4. ✅ Use \`analyze_code(mode="api-usage", "<ClassName>")\` - See how to use D365FO APIs correctly\n\n` +
|
|
1934
|
-
`⚠️ Never guess static vs instance — always use get_method(include="signature") first.`
|
|
1935
|
-
: `💡 **Next Steps for Better Code Quality:**\n\n` +
|
|
1936
|
-
`1. ✅ Use \`analyze_code(mode="patterns", "<scenario>")\` - Learn what D365FO classes are commonly used together\n` +
|
|
1937
|
-
`2. ✅ Use \`analyze_code(mode="implementations", "${displayName}", "<methodName>")\` - Get real implementation examples\n` +
|
|
1938
|
-
`3. ✅ Use \`analyze_code(mode="completeness", "${displayName}")\` - Check for missing common methods\n` +
|
|
1939
|
-
`4. ✅ Use \`analyze_code(mode="api-usage", "<ClassName>")\` - See how to use D365FO APIs correctly\n\n` +
|
|
1940
|
-
`These tools provide patterns from the actual codebase, not generic templates.`),
|
|
1939
|
+
? `\n\n⚠️ Before writing any CoC method call \`get_method(include="signature", className="${displayName}", methodName="<methodName>")\` — ` +
|
|
1940
|
+
`never guess static vs instance, the return type or the parameter list. ` +
|
|
1941
|
+
`Existing wrappers: \`extension_info(mode="coc", target="${displayName}")\`.`
|
|
1942
|
+
: ``),
|
|
1941
1943
|
},
|
|
1942
1944
|
],
|
|
1943
1945
|
};
|
|
@@ -3806,7 +3806,8 @@ export async function handleCreateD365File(request, context) {
|
|
|
3806
3806
|
objectName: args.objectName,
|
|
3807
3807
|
objectType: args.objectType,
|
|
3808
3808
|
owningModel: actualModelName,
|
|
3809
|
-
activeModel: getConfigManager().
|
|
3809
|
+
activeModel: getConfigManager().getWriteAnchorModel() ?? '',
|
|
3810
|
+
toolSwitchedModel: getConfigManager().getToolProjectSwitch()?.forcedModel ?? null,
|
|
3810
3811
|
action: 'create',
|
|
3811
3812
|
});
|
|
3812
3813
|
if (crossModelCreateRefusal) {
|
|
@@ -4711,16 +4712,13 @@ export async function handleCreateD365File(request, context) {
|
|
|
4711
4712
|
`\nUntil resolved, add the file manually in Visual Studio: right-click project → Add Existing Item → ${normalizedFullPath}\n`;
|
|
4712
4713
|
}
|
|
4713
4714
|
}
|
|
4714
|
-
//
|
|
4715
|
+
// Only the step the AGENT can take. "Reload the project in VS / refresh the
|
|
4716
|
+
// AOT" is a human's UI chore, repeated on every object of a feature; when it
|
|
4717
|
+
// matters (addToProject failed, no projectPath) `projectMessage` above
|
|
4718
|
+
// already says so, in that specific case.
|
|
4715
4719
|
const nextSteps = args.addToProject
|
|
4716
|
-
? `Next
|
|
4717
|
-
|
|
4718
|
-
`2. Build the project to synchronize the object\n` +
|
|
4719
|
-
`3. Refresh AOT in Visual Studio to see the new object\n`
|
|
4720
|
-
: `Next steps:\n` +
|
|
4721
|
-
`1. Add the file to your Visual Studio project (.rnrproj)\n` +
|
|
4722
|
-
`2. Build the project to synchronize the object\n` +
|
|
4723
|
-
`3. Refresh AOT in Visual Studio to see the new object\n`;
|
|
4720
|
+
? `Next: build_d365fo_project to synchronize the object.\n`
|
|
4721
|
+
: `Next: add the file to your .rnrproj, then build_d365fo_project to synchronize the object.\n`;
|
|
4724
4722
|
// Record the freshly-created file for non-git undo (see the bridge paths above).
|
|
4725
4723
|
if (!fileExisted) {
|
|
4726
4724
|
recordCreatedArtifact({
|
|
@@ -456,7 +456,8 @@ export async function createLabelTool(request, context) {
|
|
|
456
456
|
objectType: 'label',
|
|
457
457
|
owningModel: model,
|
|
458
458
|
owningPackage: resolvedPackageName,
|
|
459
|
-
activeModel: configManager.
|
|
459
|
+
activeModel: configManager.getWriteAnchorModel() ?? '',
|
|
460
|
+
toolSwitchedModel: configManager.getToolProjectSwitch()?.forcedModel ?? null,
|
|
460
461
|
action: 'create',
|
|
461
462
|
});
|
|
462
463
|
if (crossModelLabelRefusal) {
|
|
@@ -73,7 +73,11 @@ export const D365FO_FILE_PARAM_SPECS = {
|
|
|
73
73
|
fieldMandatory: { type: 'boolean', description: 'Mark the field Mandatory=Yes.' },
|
|
74
74
|
fieldLabel: { type: 'string', description: 'Field label.' },
|
|
75
75
|
fieldHelpText: { type: 'string', description: 'Field help text.' },
|
|
76
|
-
fieldEnumType: {
|
|
76
|
+
fieldEnumType: {
|
|
77
|
+
type: 'string',
|
|
78
|
+
description: 'Enum name for an enum-typed field. On add-field this is all an enum field needs — ' +
|
|
79
|
+
'it writes AxTableFieldEnum + EnumType, no EDT.',
|
|
80
|
+
},
|
|
77
81
|
fieldStringSize: { type: 'string', description: 'String size to set on a string-typed field.' },
|
|
78
82
|
dataField: {
|
|
79
83
|
type: 'string',
|
|
@@ -267,9 +271,12 @@ export const D365FO_FILE_OP_SPECS = {
|
|
|
267
271
|
},
|
|
268
272
|
'add-field': {
|
|
269
273
|
required: ['fieldName'],
|
|
270
|
-
optional: ['fieldType', 'fieldBaseType', 'fieldMandatory', 'fieldLabel', 'dataField', 'dataSource', 'fieldGroupName'],
|
|
271
|
-
mutationOneOf: ['fieldType', 'dataField'],
|
|
272
|
-
note: '
|
|
274
|
+
optional: ['fieldType', 'fieldBaseType', 'fieldEnumType', 'fieldMandatory', 'fieldLabel', 'dataField', 'dataSource', 'fieldGroupName'],
|
|
275
|
+
mutationOneOf: ['fieldType', 'fieldEnumType', 'dataField'],
|
|
276
|
+
note: 'Enum field: pass fieldEnumType="<enum name>" and NO fieldType — an enum-typed table field ' +
|
|
277
|
+
'is an AxTableFieldEnum with an EnumType and needs no EDT. (fieldType is the EDT name here, ' +
|
|
278
|
+
'never an XML element name like "AxTableFieldEnum".) ' +
|
|
279
|
+
'Table/table-extension: otherwise fieldType (EDT) is REQUIRED. data-entity-extension: pass dataField AND ' +
|
|
273
280
|
'dataSource instead — BOTH, or nothing is written; a mapped field has no EDT of its own, it points ' +
|
|
274
281
|
'at dataField on the entity data source dataSource. fieldGroupName is optional and only applies to ' +
|
|
275
282
|
'a data-entity-extension: it appends the field to that BASE-entity field group (shipped extensions ' +
|
|
@@ -46,9 +46,9 @@ const STRATEGY_RULES = [
|
|
|
46
46
|
{ mechanism: 'Form data source validateWrite()', when: 'Validation is UI-specific and should NOT apply to service/entity imports' },
|
|
47
47
|
],
|
|
48
48
|
nextSteps: [
|
|
49
|
-
'
|
|
50
|
-
'
|
|
51
|
-
'
|
|
49
|
+
'extension_info(mode="points", target="TableName") — available events and existing extensions',
|
|
50
|
+
'extension_info(mode="events", target="TableName") — see if someone already handles the same event',
|
|
51
|
+
'generate_object(mode="pattern", pattern="event-handler", name="TableName") — handler skeleton',
|
|
52
52
|
],
|
|
53
53
|
antiPatterns: [
|
|
54
54
|
{ wrong: 'Business Event', why: 'Business Events are for outbound notifications, not validation logic' },
|
|
@@ -73,8 +73,8 @@ const STRATEGY_RULES = [
|
|
|
73
73
|
{ mechanism: 'Form data source initValue()', when: 'Default depends on form context (e.g. header record of a lines form)' },
|
|
74
74
|
],
|
|
75
75
|
nextSteps: [
|
|
76
|
-
'
|
|
77
|
-
'get_method(include="signature", "TableName", "initValue"
|
|
76
|
+
'extension_info(mode="points", target="TableName") — check initValue availability',
|
|
77
|
+
'get_method(include="signature", className="TableName", methodName="initValue") — exact signature for the CoC wrapper',
|
|
78
78
|
],
|
|
79
79
|
antiPatterns: [
|
|
80
80
|
{ wrong: 'Overriding insert()', why: 'insert() is for persistence — defaults belong in initValue()' },
|
|
@@ -110,9 +110,9 @@ const STRATEGY_RULES = [
|
|
|
110
110
|
{ mechanism: 'CoC on table.modifiedFieldValue()', when: 'You also need the previous value to decide what to do' },
|
|
111
111
|
],
|
|
112
112
|
nextSteps: [
|
|
113
|
-
'
|
|
114
|
-
'get_method(include="signature", "TableName", "modifiedField"
|
|
115
|
-
'
|
|
113
|
+
'extension_info(mode="points", target="TableName") — confirm modifiedField is CoC-eligible and see existing extensions',
|
|
114
|
+
'get_method(include="signature", className="TableName", methodName="modifiedField") — exact signature for the CoC wrapper',
|
|
115
|
+
'extension_info(mode="coc", target="TableName", method="modifiedField") — check for existing wrappers',
|
|
116
116
|
],
|
|
117
117
|
antiPatterns: [
|
|
118
118
|
{ wrong: 'CoC on initValue()', why: 'initValue fires only at record creation — it will NOT run when the user later changes the field' },
|
|
@@ -141,9 +141,9 @@ const STRATEGY_RULES = [
|
|
|
141
141
|
{ mechanism: 'Replaceable method', when: 'Method is marked [Replaceable] — you can fully replace it (rare in standard app)' },
|
|
142
142
|
],
|
|
143
143
|
nextSteps: [
|
|
144
|
-
'
|
|
145
|
-
'get_method(include="signature", "ClassName", "methodName"
|
|
146
|
-
'
|
|
144
|
+
'extension_info(mode="points", target="ClassName") — see CoC-eligible methods and delegates',
|
|
145
|
+
'get_method(include="signature", className="ClassName", methodName="methodName") — exact signature for the CoC wrapper',
|
|
146
|
+
'extension_info(mode="coc", target="ClassName", method="methodName") — check for existing CoC wrappers',
|
|
147
147
|
],
|
|
148
148
|
antiPatterns: [
|
|
149
149
|
{ wrong: 'Copy-paste the entire class', why: 'Over-layering defeats the purpose of extensions and blocks upgrades' },
|
|
@@ -172,8 +172,8 @@ const STRATEGY_RULES = [
|
|
|
172
172
|
{ mechanism: 'Dual-write', when: 'Real-time bidirectional sync with Dataverse is needed' },
|
|
173
173
|
],
|
|
174
174
|
nextSteps: [
|
|
175
|
-
'get_knowledge(kind="knowledge", "business events") — learn the pattern',
|
|
176
|
-
'
|
|
175
|
+
'get_knowledge(kind="knowledge", topic="business events") — learn the pattern',
|
|
176
|
+
'generate_object(mode="pattern", pattern="business-event", name="MyEvent") — generate skeleton',
|
|
177
177
|
],
|
|
178
178
|
antiPatterns: [
|
|
179
179
|
{ wrong: 'CoC calling HttpClient', why: 'Synchronous HTTP in a transaction blocks the user and risks timeout/rollback' },
|
|
@@ -203,9 +203,9 @@ const STRATEGY_RULES = [
|
|
|
203
203
|
{ mechanism: 'Composite entity', when: 'Header + lines structure needs to be imported as a document' },
|
|
204
204
|
],
|
|
205
205
|
nextSteps: [
|
|
206
|
-
'get_knowledge(kind="knowledge", "data-management-framework") — learn DMF patterns',
|
|
207
|
-
'search("MyTable", "data-entity") — check if an entity already exists',
|
|
208
|
-
'
|
|
206
|
+
'get_knowledge(kind="knowledge", topic="data-management-framework") — learn DMF patterns',
|
|
207
|
+
'search(query="MyTable", type="data-entity") — check if an entity already exists',
|
|
208
|
+
'generate_object(mode="pattern", pattern="data-entity", name="MyEntity") — generate entity skeleton',
|
|
209
209
|
],
|
|
210
210
|
antiPatterns: [
|
|
211
211
|
{ wrong: 'Direct table insert via custom endpoint', why: 'Bypasses validation, number sequences, and event handlers' },
|
|
@@ -234,7 +234,7 @@ const STRATEGY_RULES = [
|
|
|
234
234
|
],
|
|
235
235
|
nextSteps: [
|
|
236
236
|
'get_object_info(objectType="form", name="FormName", options={searchControl:"General"}) — find exact control names and hierarchy',
|
|
237
|
-
'
|
|
237
|
+
'extension_info(mode="points", target="FormName", objectType="form") — check form extension points',
|
|
238
238
|
'd365fo_file(action="create", objectType="form-extension") — create the extension',
|
|
239
239
|
],
|
|
240
240
|
antiPatterns: [
|
|
@@ -265,8 +265,8 @@ const STRATEGY_RULES = [
|
|
|
265
265
|
],
|
|
266
266
|
nextSteps: [
|
|
267
267
|
'get_object_info(objectType="report", name="ReportName") — inspect existing report structure',
|
|
268
|
-
'get_knowledge(kind="knowledge", "ssrs-reports") — patterns for SSRS',
|
|
269
|
-
'
|
|
268
|
+
'get_knowledge(kind="knowledge", topic="ssrs-reports") — patterns for SSRS',
|
|
269
|
+
'generate_object(mode="scaffold", objectType="report", name="MyReport") — generate full SSRS stack',
|
|
270
270
|
],
|
|
271
271
|
antiPatterns: [
|
|
272
272
|
{ wrong: 'Business Event for document delivery', why: 'Business Events send notifications, not formatted documents' },
|
|
@@ -290,8 +290,8 @@ const STRATEGY_RULES = [
|
|
|
290
290
|
{ mechanism: 'Custom counter table', when: 'Simple auto-increment without legal entity scope or configurable format (rare — prefer the framework)' },
|
|
291
291
|
],
|
|
292
292
|
nextSteps: [
|
|
293
|
-
'get_knowledge(kind="knowledge", "number-sequences") — full pattern reference',
|
|
294
|
-
'
|
|
293
|
+
'get_knowledge(kind="knowledge", topic="number-sequences") — full pattern reference',
|
|
294
|
+
'generate_object(mode="pattern", pattern="number-seq-handler", name="MyModule") — generate skeleton',
|
|
295
295
|
],
|
|
296
296
|
antiPatterns: [
|
|
297
297
|
{ wrong: 'Identity column / RecId as business number', why: 'RecId is internal — users need formatted, gapless (or configurable) business numbers' },
|
|
@@ -318,8 +318,8 @@ const STRATEGY_RULES = [
|
|
|
318
318
|
{ mechanism: 'Table permission framework override', when: 'Granting DML access without a menu item entry point (rare)' },
|
|
319
319
|
],
|
|
320
320
|
nextSteps: [
|
|
321
|
-
'get_knowledge(kind="knowledge", "security-privileges-duties") — security pattern reference',
|
|
322
|
-
'security_info(mode="coverage", "ObjectName") — check existing security chain',
|
|
321
|
+
'get_knowledge(kind="knowledge", topic="security-privileges-duties") — security pattern reference',
|
|
322
|
+
'security_info(mode="coverage", objectName="ObjectName") — check existing security chain',
|
|
323
323
|
'd365fo_file(action="create", objectType="security-privilege") — create privilege',
|
|
324
324
|
],
|
|
325
325
|
antiPatterns: [
|
|
@@ -346,9 +346,9 @@ const STRATEGY_RULES = [
|
|
|
346
346
|
{ mechanism: 'Business Event + external processor', when: 'Processing should happen outside D365FO (e.g. Azure Function)' },
|
|
347
347
|
],
|
|
348
348
|
nextSteps: [
|
|
349
|
-
'get_knowledge(kind="knowledge", "sysoperation") — SysOperation patterns',
|
|
350
|
-
'
|
|
351
|
-
'
|
|
349
|
+
'get_knowledge(kind="knowledge", topic="sysoperation") — SysOperation patterns',
|
|
350
|
+
'generate_object(mode="pattern", pattern="sysoperation", name="MyProcess") — generate SysOperation skeleton',
|
|
351
|
+
'generate_object(mode="pattern", pattern="batch-job", name="MyBatch") — generate RunBaseBatch skeleton',
|
|
352
352
|
],
|
|
353
353
|
antiPatterns: [
|
|
354
354
|
{ wrong: 'Thread.Sleep / while-polling in batch', why: 'Use batch recurrence and alerts — polling wastes AOS resources' },
|
|
@@ -21,6 +21,7 @@ import { methodStubsForPattern, injectMethodStubs } from '../knowledge/formPatte
|
|
|
21
21
|
import { findBaseFormXml } from './modifyD365File.js';
|
|
22
22
|
import { getFieldControlMap, getTableTitleField } from '../utils/fieldControlTypes.js';
|
|
23
23
|
import { lookupSymbolNocase } from '../utils/symbolLookup.js';
|
|
24
|
+
import { scaffoldWriteRefusalResult } from './writeAnchorGuard.js';
|
|
24
25
|
/**
|
|
25
26
|
* Symbol types a form datasource may bind to. Views are indexed as 'view'
|
|
26
27
|
* (see symbolIndex.indexViews) and are legal in <Table> just like tables.
|
|
@@ -560,6 +561,15 @@ export async function handleGenerateSmartForm(args, symbolIndex) {
|
|
|
560
561
|
if (finalName !== name) {
|
|
561
562
|
console.log(`[generateSmartForm] Applied naming: ${name} → ${finalName}`);
|
|
562
563
|
}
|
|
564
|
+
// See generateSmartTable: the resolved model follows the ACTIVE project, the
|
|
565
|
+
// write anchor does not. Checked before the first byte is written.
|
|
566
|
+
const anchorRefusal = scaffoldWriteRefusalResult({
|
|
567
|
+
objectName: finalName,
|
|
568
|
+
objectType: 'form',
|
|
569
|
+
targetModel: resolvedModel,
|
|
570
|
+
});
|
|
571
|
+
if (anchorRefusal)
|
|
572
|
+
return anchorRefusal;
|
|
563
573
|
// Generate XML: clone an existing form (preferred) or build from a template.
|
|
564
574
|
// Without an explicit pattern, default to the majority pattern mined from
|
|
565
575
|
// standard models (property_stats), falling back to SimpleList.
|
|
@@ -32,6 +32,7 @@ import { resolveObjectPrefix, applyObjectPrefix, getObjectSuffix, applyObjectSuf
|
|
|
32
32
|
import { extractModelFromProject, findProjectInSolution } from '../utils/projectUtils.js';
|
|
33
33
|
import { normalizeD365Xml } from '../utils/d365XmlNormalizer.js';
|
|
34
34
|
import { canonicalSymbolName, lookupSymbolNocase } from '../utils/symbolLookup.js';
|
|
35
|
+
import { scaffoldWriteRefusalResult } from './writeAnchorGuard.js';
|
|
35
36
|
export const generateSmartReportTool = {
|
|
36
37
|
name: 'generate_smart_report',
|
|
37
38
|
description: `🎨 AI-driven SSRS report generation — creates up to 7 D365FO objects in one call.
|
|
@@ -221,6 +222,16 @@ export async function handleGenerateSmartReport(args, symbolIndex, bridge) {
|
|
|
221
222
|
finalName = applyObjectSuffix(finalName, objectSuffix);
|
|
222
223
|
if (finalName !== name)
|
|
223
224
|
log(`Applied naming: ${name} → ${finalName}`);
|
|
225
|
+
// See generateSmartTable: the resolved model follows the ACTIVE project, the
|
|
226
|
+
// write anchor does not. A report scaffold writes several objects at once, so
|
|
227
|
+
// this is checked before any of them exists.
|
|
228
|
+
const anchorRefusal = scaffoldWriteRefusalResult({
|
|
229
|
+
objectName: finalName,
|
|
230
|
+
objectType: 'report',
|
|
231
|
+
targetModel: resolvedModel,
|
|
232
|
+
});
|
|
233
|
+
if (anchorRefusal)
|
|
234
|
+
return anchorRefusal;
|
|
224
235
|
// Derived object names
|
|
225
236
|
const tmpTableName = `${finalName}Tmp`;
|
|
226
237
|
const contractClassName = `${finalName}Contract`;
|
|
@@ -13,6 +13,7 @@ import { ProjectFileManager } from './createD365File.js';
|
|
|
13
13
|
import { extractModelFromProject, findProjectInSolution } from '../utils/projectUtils.js';
|
|
14
14
|
import { normalizeD365Xml } from '../utils/d365XmlNormalizer.js';
|
|
15
15
|
import { lookupSymbolNocase } from '../utils/symbolLookup.js';
|
|
16
|
+
import { scaffoldWriteRefusalResult } from './writeAnchorGuard.js';
|
|
16
17
|
export const generateSmartTableTool = {
|
|
17
18
|
name: 'generate_smart_table',
|
|
18
19
|
description: 'Generate AxTable XML with AI-driven field/index/relation suggestions based on indexed patterns. Can copy structure from existing tables, analyze table group patterns, or use field hints.',
|
|
@@ -559,6 +560,16 @@ export async function handleGenerateSmartTable(args, symbolIndex, bridge) {
|
|
|
559
560
|
if (finalName !== name) {
|
|
560
561
|
console.log(`[generateSmartTable] Applied naming: ${name} → ${finalName}`);
|
|
561
562
|
}
|
|
563
|
+
// Where this scaffold would land, checked before anything is written. The
|
|
564
|
+
// model above comes from the ACTIVE project, which a get_workspace_info switch
|
|
565
|
+
// moves; writes stay anchored to the model the workspace resolved on its own.
|
|
566
|
+
const anchorRefusal = scaffoldWriteRefusalResult({
|
|
567
|
+
objectName: finalName,
|
|
568
|
+
objectType: 'table',
|
|
569
|
+
targetModel: resolvedModel,
|
|
570
|
+
});
|
|
571
|
+
if (anchorRefusal)
|
|
572
|
+
return anchorRefusal;
|
|
562
573
|
// Generate standard methods (find, exist) based on primary key fields
|
|
563
574
|
const generatedMethods = [];
|
|
564
575
|
if (requestedMethods && requestedMethods.length > 0) {
|