/** * 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