/**
* Modify D365FO File Tool
* Edit existing D365FO XML files (AxClass, AxTable, AxForm, etc.)
* Supports atomic operations: add method, add field, modify property
*/
import type { CallToolRequest } from '@modelcontextprotocol/sdk/types.js';
import { z } from 'zod';
import type { XppServerContext } from '../../types/context.js';
import { resetRepeatedNoteMemory } from '../../utils/repeatedNotes.js';
import { findBaseObjectXml, findBaseFormXml } from '../../utils/baseObjectXml.js';
/**
* Reject an X++ source payload that smuggles XML/CDATA structure.
*
* Method source written through the bridge is handed verbatim to the D365FO SDK
* serializer, which wraps it in `` and emits the surrounding
* `…` markup itself. If the caller's source already contains
* the CDATA terminator `]]>` or closing metadata tags, the serializer writes
* them inside the CDATA block unchanged — producing structurally invalid XML:
* a premature/doubled `]]>` and a stray `` that drops the enclosing
* `` (exactly the corruption D365FO refuses to deserialize). The
* direct-XML replace fallback has the same exposure: a literal string replace
* that injects `]]>` into an existing CDATA block corrupts it too.
*
* This always means the AI passed a slice of the .xml file where clean X++ was
* expected. Reject it here — before it reaches disk — with an actionable
* message, rather than silently escaping and hiding the mistake.
*
* X++ legitimately uses `<`/`>` (generics, comparisons, doc comments), so we
* only flag the CDATA terminator and the specific opening/closing metadata
* tokens, never bare angle brackets.
*/
export declare function assertCleanXppSource(source: string | undefined, paramName: string): void;
/** Count top-level X++ method bodies: a `{` opened at brace-depth 0 immediately
* after a `)` (a method signature). Nested blocks (if/for/switch) are inside the
* body (depth > 0) and a class wrapper opens after an identifier, so neither is
* miscounted. */
export declare function countTopLevelMethodBodies(source: string): number;
/** Split a source string containing one or more top-level X++ methods into the
* individual method sources (each including any leading doc comments / attributes
* and its full body). Mirrors countTopLevelMethodBodies' brace/comment/string
* handling. Used to let add-method accept several methods in one call and add them
* one at a time. */
export declare function splitTopLevelMethodBodies(source: string): string[];
/**
* Reject an add-method payload that contains more than one method. Each add-method
* call emits a single ; passing two methods drops the second outside the
* class scope and yields invalid X++ ("Unexpected token 'public' specified outside
* the scope of any class or model element"). Splitting into separate calls is the fix.
*/
export declare function assertSingleMethodSource(source: string | undefined): void;
/**
* Derive the method name from a full X++ method source: the identifier immediately
* before the first '(' of the signature, after stripping comments, strings and
* attribute blocks (e.g. [ExtensionOf(...)]). Lets add-method callers omit methodName
* when they already pass the complete source (e.g. "public static X find(...)").
* Returns null when no signature can be found.
*/
export declare function extractMethodNameFromSource(source: string | undefined): string | null;
/**
* Why the bridge path did not apply, in the caller's own words.
*
* The direct-XML fallbacks used to announce "bridge was unavailable" unconditionally,
* which is the true reason in only one of the four ways the bridge path can decline:
* it can also be a type outside BRIDGE_MODIFY_TYPES, an SDK that cannot reach the
* member (form control overrides), or a thrown error. Reporting the wrong cause sent
* agents off to restart a bridge that was healthy — and hid genuine outages behind a
* green result, since the write still succeeded (see the #4 sweep finding: two calls
* "fell back" while a third seconds later went through Update just fine).
*/
export declare function describeBridgeFallbackReason(bridge: {
isReady?: boolean;
metadataAvailable?: boolean;
} | undefined, objectType: string, operation: string, bridgeResult: {
success: boolean;
message: string;
} | null): string;
/**
* Refuse a replace-code that cannot mean what the caller intends.
*
* The bridge edits with .NET `String.Replace` (MetadataWriteService.ReplaceIn
* Methods) — replace-ALL, no count, no echo. Two failure modes are decidable
* from the file first: oldCode matching more than once, and an edit whose
* newCode is already present and contains oldCode (oldCode="checkFailed",
* newCode="this.checkFailed" over `this.checkFailed` yields
* `this.this.checkFailed`).
*
* Returns null to proceed, `noop` when the file is already in the requested
* state, `refuse` when the call is ambiguous.
*/
export interface ReplaceCodeVerdict {
kind: 'noop' | 'refuse';
message: string;
}
export declare function preflightReplaceCode(content: string, oldCode: string, newCode: string): ReplaceCodeVerdict | null;
/**
* The lines a write changed, with context, so seeing the result costs no
* read_file round trip.
*/
export declare function renderChangedLines(before: string, after: string, context?: number): string;
/**
* Heuristic: does a bridge failure message indicate the C# provider could not
* resolve the target object (vs. a genuine operation error like "index already
* exists")? An unresolved object is the one failure worth a refresh+retry,
* because an object created this session may not be in the provider's
* startup-fixed metadata roots yet.
*/
export declare function isUnresolvedObjectError(message: string | undefined): boolean;
export declare const ModifyD365FileArgsSchema: z.ZodObject<{
fieldAllowEdit: z.ZodOptional;
fieldIgnoreEdtRelation: z.ZodOptional;
joinField: z.ZodOptional;
relatedField: z.ZodOptional;
dataSourceReadOnly: z.ZodOptional;
objectType: z.ZodEnum<{
class: "class";
"class-extension": "class-extension";
"data-entity": "data-entity";
"data-entity-extension": "data-entity-extension";
edt: "edt";
"edt-extension": "edt-extension";
enum: "enum";
"enum-extension": "enum-extension";
form: "form";
"form-extension": "form-extension";
"ignore-diagnostic-list": "ignore-diagnostic-list";
menu: "menu";
"menu-extension": "menu-extension";
"menu-item-action": "menu-item-action";
"menu-item-action-extension": "menu-item-action-extension";
"menu-item-display": "menu-item-display";
"menu-item-display-extension": "menu-item-display-extension";
"menu-item-output": "menu-item-output";
"menu-item-output-extension": "menu-item-output-extension";
"model-descriptor": "model-descriptor";
query: "query";
report: "report";
"security-duty": "security-duty";
"security-privilege": "security-privilege";
"security-role": "security-role";
table: "table";
"table-extension": "table-extension";
view: "view";
}>;
objectName: z.ZodOptional;
operation: z.ZodEnum<{
"add-control": "add-control";
"add-data-source": "add-data-source";
"add-delete-action": "add-delete-action";
"add-diagnostic-suppression": "add-diagnostic-suppression";
"add-display-method": "add-display-method";
"add-entry-point": "add-entry-point";
"add-enum-value": "add-enum-value";
"add-field": "add-field";
"add-field-group": "add-field-group";
"add-field-modification": "add-field-modification";
"add-field-to-field-group": "add-field-to-field-group";
"add-full-text-index": "add-full-text-index";
"add-index": "add-index";
"add-menu-item-to-menu": "add-menu-item-to-menu";
"add-method": "add-method";
"add-module-reference": "add-module-reference";
"add-query-range": "add-query-range";
"add-relation": "add-relation";
"add-table-mapping": "add-table-mapping";
"add-table-method": "add-table-method";
"modify-enum-value": "modify-enum-value";
"modify-field": "modify-field";
"modify-property": "modify-property";
"remove-control": "remove-control";
"remove-delete-action": "remove-delete-action";
"remove-diagnostic-suppression": "remove-diagnostic-suppression";
"remove-entry-point": "remove-entry-point";
"remove-enum-value": "remove-enum-value";
"remove-field": "remove-field";
"remove-field-group": "remove-field-group";
"remove-full-text-index": "remove-full-text-index";
"remove-index": "remove-index";
"remove-method": "remove-method";
"remove-module-reference": "remove-module-reference";
"remove-query-range": "remove-query-range";
"remove-relation": "remove-relation";
"remove-table-mapping": "remove-table-mapping";
"rename-field": "rename-field";
"replace-all-fields": "replace-all-fields";
"replace-code": "replace-code";
"report-design": "report-design";
}>;
enumValueName: z.ZodOptional;
enumValueNewName: z.ZodOptional;
enumValueLabel: z.ZodOptional;
enumValueHelpText: z.ZodOptional;
enumValueInt: z.ZodOptional;
enumValueCountryRegionCodes: z.ZodOptional;
displayMethodReturnEdt: z.ZodOptional;
tableMethodType: z.ZodOptional>;
tableKeyField: z.ZodOptional;
menuItemToAdd: z.ZodOptional;
menuItemToAddType: z.ZodOptional>;
controlName: z.ZodOptional;
parentControl: z.ZodOptional;
controlDataSource: z.ZodOptional;
controlDataField: z.ZodOptional;
controlType: z.ZodOptional;
controlLabel: z.ZodOptional;
positionType: z.ZodOptional;
previousSibling: z.ZodOptional;
baseFormName: z.ZodOptional;
methodName: z.ZodOptional;
methodCode: z.ZodOptional;
sourceCode: z.ZodOptional;
oldCode: z.ZodOptional;
newCode: z.ZodOptional;
methodModifiers: z.ZodOptional;
methodReturnType: z.ZodOptional;
methodParameters: z.ZodOptional;
fieldName: z.ZodOptional;
fieldNewName: z.ZodOptional;
fieldType: z.ZodOptional;
fieldBaseType: z.ZodOptional;
fieldMandatory: z.ZodOptional;
fieldLabel: z.ZodOptional;
fieldHelpText: z.ZodOptional;
fieldEnumType: z.ZodOptional;
fieldStringSize: z.ZodOptional;
fields: z.ZodOptional;
type: z.ZodOptional;
mandatory: z.ZodOptional;
label: z.ZodOptional;
}, z.core.$strip>>>;
indexName: z.ZodOptional;
indexFields: z.ZodOptional>;
}, z.core.$strip>>>;
indexAllowDuplicates: z.ZodOptional>;
indexAlternateKey: z.ZodOptional>;
indexValidTimeStateKey: z.ZodOptional>;
indexValidTimeStateMode: z.ZodOptional;
indexEnabled: z.ZodOptional>;
mapName: z.ZodOptional;
mappingTable: z.ZodOptional;
mappingConnections: z.ZodOptional>>;
relationName: z.ZodOptional;
relatedTable: z.ZodOptional;
relationConstraints: z.ZodOptional>>;
relationCardinality: z.ZodOptional;
relatedTableCardinality: z.ZodOptional;
relationshipType: z.ZodOptional;
deleteActionTable: z.ZodOptional;
deleteActionName: z.ZodOptional;
deleteActionType: z.ZodOptional>;
deleteActionRelation: z.ZodOptional;
fieldGroupName: z.ZodOptional;
fieldGroupFields: z.ZodOptional>;
fieldGroupLabel: z.ZodOptional;
extendBaseFieldGroup: z.ZodOptional;
removeSeparator: z.ZodOptional;
entryPointName: z.ZodOptional;
entryPointObjectName: z.ZodOptional;
entryPointObjectType: z.ZodOptional;
accessLevel: z.ZodOptional;
diagnosticPath: z.ZodOptional;
diagnosticMoniker: z.ZodOptional;
diagnosticElementType: z.ZodOptional;
diagnosticElementName: z.ZodOptional;
diagnosticJustification: z.ZodOptional;
diagnosticMessage: z.ZodOptional;
diagnosticSeverity: z.ZodOptional>;
diagnosticItemSpecific: z.ZodOptional;
moduleReference: z.ZodOptional;
dataSourceName: z.ZodOptional;
dataSourceTable: z.ZodOptional;
rangeField: z.ZodOptional;
rangeName: z.ZodOptional;
rangeValue: z.ZodOptional;
reportAction: z.ZodOptional>;
tableName: z.ZodOptional;
datasetName: z.ZodOptional;
parameterName: z.ZodOptional;
parameterDataType: z.ZodOptional;
parameterHidden: z.ZodOptional;
promptString: z.ZodOptional;
joinSource: z.ZodOptional;
linkType: z.ZodOptional;
insertIfEmpty: z.ZodOptional;
dataField: z.ZodOptional;
dataSource: z.ZodOptional;
propertyPath: z.ZodOptional;
propertyValue: z.ZodOptional;
autoCorrect: z.ZodDefault>;
createBackup: z.ZodDefault>;
modelName: z.ZodOptional;
packageName: z.ZodOptional;
packagePath: z.ZodOptional;
workspacePath: z.ZodOptional;
filePath: z.ZodOptional;
addToProject: z.ZodDefault>;
projectPath: z.ZodOptional;
solutionPath: z.ZodOptional;
groundingToken: z.ZodOptional;
peerOperations: z.ZodOptional>;
batchAdvice: z.ZodOptional;
fieldGroupNoteFields: z.ZodOptional>;
}, z.core.$strip>>;
}, z.core.$strip>;
export interface ModifyArgNormalization {
/** Arguments to validate, with aliases resolved and corrections applied. */
args: Record;
/** Corrections to report as "Note:" lines in the successful result. */
notes: string[];
}
/**
* Resolve aliases and apply the corrections the server has already computed.
*
* Order matters and is not arbitrary: a key is renamed to the parameter it means
* BEFORE its value is reshaped, because the target parameter is what decides the
* shape — `add-field-group {fields:["A","B"]}` becomes `fieldGroupFields`
* (array of string, nothing to reshape), while `add-index {indexFields:["A"]}`
* keeps its name and gains the objects.
*
* autoCorrect=false keeps every correction off, so the eval harness and
* deterministic callers see exactly the errors they see today. Alias resolution
* is NOT a correction — it is the published contract (OP_PARAM_ALIASES, rendered
* in every op spec) — so it applies either way and is not reported.
*/
export declare function normalizeModifyArgs(raw: Record): ModifyArgNormalization;
/**
* What the caller needs to know about a write that has already been reported.
*
* runModifyBatch uses it to do the per-FILE work once for the whole batch
* instead of once per operation — see the trailer suppression at the end of this
* function. Mirrors CreateOutcome, which exists for the same reason.
*/
export interface ModifyOutcome {
/** Absolute path of the file the operation wrote. */
filePath?: string;
/** Identity AFTER name resolution, for the shared verification. */
objectType?: string;
objectName?: string;
modelName?: string;
/**
* The extension this modify names does not exist yet, its BASE object does,
* and creating it is the one action that turns the call into a success.
*
* Reported rather than performed: createD365File must not be imported here
* (layer direction is pinned by tests/utils/layering.test.ts, because the two
* largest files in the codebase importing each other is what made four
* read-only tools load the write path), and the dispatcher in d365foFile.ts
* already composes create -> operations[]. Going through the ordinary create
* path is the point — path containment, prefixing, .rnrproj registration, the
* model guards and the direct-XML fallbacks all still apply.
*/
createExtensionFirst?: {
objectType: string;
objectName: string;
};
/**
* The bridge declined this operation and wrote nothing (an element of that
* name is already there). The call did not fail, so it is not counted as a
* failure — but it must not be counted as applied either, which is what
* "3/3 operation(s) applied" did for three writes that never landed.
*/
skipped?: boolean;
}
export declare function modifyD365FileTool(request: CallToolRequest, context: XppServerContext, outcome?: ModifyOutcome): Promise<{
content: {
type: string;
text: string;
}[];
isError: boolean;
} | {
isError?: undefined;
content: {
type: string;
text: string;
}[];
}>;
/**
* Base-object names an extension name can refer to, best guess first.
*
* Dot notation (`CustTable.FmExtension`) says it outright. The element-style
* class form (`CustTable_Extension`, `CustTableFm_Extension`) carries the model
* infix in front of the word, so the plain strip is tried first and the
* infix-stripped form after it - both are real spellings this repo produces.
*/
export declare function baseObjectNameCandidates(objectName: string, modelName?: string): string[];
/**
* Prefix the new member name this operation carries, when writing into an
* extension. Mutates `args` in place and returns a note for the response (empty
* when nothing changed), so the agent learns the real name and addresses the
* member correctly in its next call.
*
* Idempotent: a name that already carries the prefix — in either the underscore
* or the bare form, case-insensitively — is left untouched, so an agent that
* prefixes by hand does not end up with DEMO_DEMO_Foo.
*/
export declare function applyExtensionMemberPrefix(args: Record, objectType: string, operation: string, modelName: string): string;
/**
* Point `add-field-to-field-group` at the field that actually exists.
*
* add-field on an extension renames what it adds — `QualityTier` becomes
* `CtsoSK_QualityTier`, because a member added to someone else's table has to
* carry your prefix — and says so in the response. The group entry that follows
* it names the SAME field, and it was left exactly as the caller spelled it:
* applyExtensionMemberPrefix mints names for new members and this operation
* mints none, so it is correctly absent from EXTENSION_MEMBER_NAME_ARG.
*
* The result was a dangling reference. The bridge validates no DataField, so
* `QualityTier` was written against a field named
* `CtsoSK_QualityTier`, reported as applied, and the group silently pointed at
* nothing. Sending both operations in ONE call — which every add-field response
* tells the agent to do — hit it every time.
*
* Blind prefixing is the wrong repair: a group extension may perfectly well
* carry a BASE-table field, which has no prefix. So this corrects only the case
* with one reading — the name as given is not a field of the extension, the
* prefixed name is, and the base table has no field by the given name either.
* Anything else is left untouched.
*
* Advisory, like every other auto-correction: it runs before a write that is
* perfectly capable of succeeding without it, so anything unreadable — the
* extension file, the base table, the prefix rules — means "no correction", not
* a failed modify.
*/
export declare function resolveFieldNameForFieldGroup(args: Record, objectType: string, operation: string, modelName: string, actualFilePath: string, symbolIndex: any): Promise;
/**
* Note for a successful add-field-to-field-group: is this field group already
* rendered on a form through a container's ?
*
* If it is, the compiler puts the new field on that form by itself and any
* hand-added control for it collides with the generated one. The add-control
* guard says the same thing, but only once a form extension exists and a control
* is being added to it — a create that then has to be undone. Said here it costs
* one indexed lookup.
*
* Returns '' on any miss — no form on the table, no container with that
* DataGroup, an unreadable form, a database that cannot answer — which leaves
* the reactive guard exactly as it was.
*/
export declare function describeFieldGroupRendering(baseTableName: string, groupName: string, fieldName: string, symbolIndex: any): Promise;
/**
* The other half of {@link describeFieldGroupRendering}: the field group reaches
* no form, and these are the groups on this table that do.
*
* A field group a form does not name in `` generates no controls, so a
* field parked in a brand-new group on a table extension is on no form at all.
* Nothing said so, and the two paths look identical from the outside: put the
* field in a rendered base group and it appears for free, or invent a group and
* then need a form extension, an add-control, the duplicate-name guard and an
* undo. Run 81803f01 spent ~24 AIU discovering the difference.
*
* Returns '' whenever the answer would be a guess — no form on the table, no
* `` anywhere, an unreadable form, an index that cannot answer — and
* also when the group IS rendered, which is the sibling's sentence to say.
*/
export declare function describeUnrenderedFieldGroup(baseTableName: string, groupName: string, symbolIndex: any): Promise;
/**
* The form control type to bind to a table field: ComboBox for an enum, the
* EDT's base type otherwise.
*
* This used to be `heuristicEdtBaseType(fieldName)` — a guess from the field's
* NAME, with a comment saying an index lookup was out of scope because it would
* mean resolving the data source back to its table. It costs one query:
* resolveFieldEdt() already answers "what type is field F of table T", and a
* form data source is conventionally named after its table, which is the case
* the fallbacks below cover when it is not.
*
* The gap it left was enums. A name heuristic has nothing to match on, so an
* enum field got the String default and became an AxFormStringControl — a text
* box over an enum. Recovering from that took an undo, a re-create and a
* re-modify, because a control's type cannot be changed in place.
*/
export declare function resolveControlTypeForField(dataSource: string | undefined, dataField: string | undefined, db: any): string | undefined;
/**
* Generate idiomatic X++ source for a standard table method from a high-level
* `tableMethodType`. The method name is implied by the type (find/exist/…), so
* callers need not pass methodName or sourceCode — only tableMethodType (plus
* tableKeyField for find/exist).
*
* Returns the generated method name, source, and an optional advisory note
* (e.g. when the key field's EDT could not be resolved from the index).
*/
export declare function generateTableMethodSource(tableName: string, methodType: 'find' | 'exist' | 'findByRecId' | 'validateWrite' | 'validateDelete' | 'initValue', keyField: string | undefined, db: any): {
methodName: string;
source: string;
note?: string;
};
/**
* Generate an X++ display method stub returning the given EDT/type.
* Used when add-display-method is called with displayMethodReturnEdt but no
* explicit sourceCode/methodCode.
*/
export declare function generateDisplayMethodSource(methodName: string, returnEdt: string): string;
export { resetRepeatedNoteMemory };
export { findBaseObjectXml, findBaseFormXml };
//# sourceMappingURL=modifyD365File.d.ts.map