/** * Direct-XML writers — the fallback that lands a change the bridge could not. * * Every one of these reads the object's XML, patches the string and writes it * back, and each is wrapped in `serializedOnFile` because two that overlap on * one file would both read the same original and the second write would silently * drop the first one's element. * * They lived inside modifyD365File.ts, which the 2026-08-25 audit found at 5,645 * lines and growing 44% between audits — a 62-arm operation switch, the argument * schema, the disk locators and these fifteen writers in one file. This is a pure * move: the block needed exactly one symbol from its old home * (`serializedOnFile`, used nowhere else in src, so it came too) and exports the * nineteen the switch consumes. */ import { type PlaceFormControlSpec } from '../../utils/formControlPlacement.js'; import { type EntityDataSourceSpec, type EntityMappedFieldSpec } from '../xml/dataEntityViewEdit.js'; /** * Direct XML file-level replace-code fallback, used when the C# bridge can't * reach a method (e.g. form/form-extension control overrides not exposed via * the Methods API). Last resort — the bridge is always preferred. * * `reason` comes from describeBridgeFallbackReason so the success message names the * cause that actually applied instead of asserting an outage that may not exist. */ export declare const directXmlReplaceCode: (filePath: string, oldCode: string, newCode: string, reason: string) => Promise<{ success: boolean; message: string; } | null>; /** * Direct XML fallback for modify-property, used when the bridge rejects * modify-property for an object type (e.g. AxForm) even though the property * is a plain text element (e.g. ) editable by string replacement. * Only used when the bridge itself reports failure. * * `propertyPath` is a bare element name (no XPath/dotted-path support). * Refuses to act if the element is missing (returns null so the caller * surfaces the original bridge error), ambiguous (appears more than once), * or is not a leaf — string replacement can only express a text value, and * writing one over an element that has children produces malformed XML. */ export declare const directXmlModifyProperty: (filePath: string, propertyPath: string, propertyValue: string, reason: string) => Promise<{ success: boolean; message: string; } | null>; /** * Direct XML fallback for add-menu-item-to-menu. * The C# bridge can only modify menus it has loaded from its startup roots; * newly created menus trigger a NullRef because they aren't in the bridge's * in-memory model yet (even after update_symbol_index). This function edits * the XML file directly as a last-resort fallback. */ export declare const directXmlAddMenuItemToMenu: (filePath: string, menuItemToAdd: string, menuItemToAddType: string) => Promise<{ success: boolean; message: string; } | null>; /** * Mark a write result as having come from this server's XML writer rather than * the bridge, so the success banner can name the path that actually did the work. * * The headline used to read "applied via IMetadataProvider.Update()" * unconditionally, while the line below it said "via direct XML fallback" — the * reply named an API that never ran. That matters beyond tidiness: a reader who * believes the metadata API performed the write also believes the write was * validated by it, which is exactly the assumption that let a silently-discarded * control read as a success. */ export declare function viaXmlFallback(result: T): T; /** * Direct XML fallback for add-control on a form-extension. * * The C# bridge's AddControl resolves its target via _provider.Forms.Read(name), * which can NEVER find a form EXTENSION (named "BaseForm.Suffix") — it always * reports 'Form "" not found'. add-control on a form-extension therefore has * no working bridge path at all (independent of metadata-root freshness). This * edits the file on disk, so it is unaffected by what the bridge has loaded. * * The shape and the insertion point both come from where `parentControl` lives: * an envelope in the extension's ROOT for a * base-form parent, a bare in the parent's NESTED * when the extension defines that parent itself. See formExtensionControlXml.ts * — the placement logic lives there, pure and unit-tested, because getting it * wrong writes structurally invalid metadata under a ✅. */ export declare const directXmlAddControl: (filePath: string, controlName: string, parentControl: string, controlType: string, dataSource?: string | undefined, dataField?: string | undefined, label?: string | undefined, previousSibling?: string | undefined, positionType?: string | undefined, asked?: ControlCreationArgs | undefined) => Promise<{ success: boolean; message: string; skipped?: boolean; } | null>; /** What add-control CREATES a control with; none of it applies to a control that already exists. */ export interface ControlCreationArgs { controlType?: string; controlDataSource?: string; controlDataField?: string; controlLabel?: string; } /** * Whether this add-control MOVES a control the form (or extension) file already * defines: a position is given and the control exists. Such a call creates nothing * (addBaseFormControl / directXmlAddControl), so the DataGroup and sub-pattern * pre-flights — which judge a NEW control by its controlType/controlDataField — * must not refuse it over arguments it never applies. One read, and only for a * positioned add-control. */ export declare function addControlMovesExisting(args: { controlName?: string; previousSibling?: string; positionType?: string; }, objectType: string, objectName: string, explicitFilePath: string | undefined, symbolIndex: unknown): Promise; /** Move a control already in the file to previousSibling / positionType. */ export declare const directXmlPlaceControl: (filePath: string, spec: PlaceFormControlSpec) => Promise<{ success: boolean; message: string; skipped?: boolean; }>; /** * add-control on a plain form: the bridge creates the control, the XML puts it in place. * * The bridge's AddControl has no position (it appends to the parent) and no duplicate * check, so a control the form already defines is not added again: it is moved when a * position was asked for, and reported as present otherwise. A control the bridge did * add is then moved to previousSibling / positionType; a failure there leaves it, added, * at the end of its parent, and says so. A position that cannot work is refused before * the bridge writes anything. * * Every outcome the bridge took no part in is marked viaXmlFallback (and skipped when * nothing was written), so the reply neither credits nor blames IMetadataProvider. */ export declare function addBaseFormControl(filePath: string, spec: PlaceFormControlSpec & { parentControl: string; } & ControlCreationArgs, addViaBridge: () => Promise<{ success: boolean; message: string; } | null>): Promise<{ success: boolean; message: string; viaXmlFallback?: boolean; skipped?: boolean; } | null>; /** * Normalises a NoYes-shaped flag to a boolean. * * The wire type is boolean, but the value these params end up as in AxTable XML * is `No`/`Yes`, so callers legitimately pass the string spelling (corpus #27: * indexAllowDuplicates="No" was rejected with a bare "expected boolean"). * Anything unrecognised returns undefined so the op's own default applies. */ export declare function coerceNoYesFlag(value: unknown): boolean | undefined; /** * Direct XML fallback for add-index on a TABLE. * * The C# bridge's AddIndex resolves its target via _provider.Tables.Read(name), * whose DiskProvider metadata roots are fixed at bridge startup. A table CREATED * THIS SESSION is therefore reported "Table '' not found" — even after * update_symbol_index and even when an explicit filePath was supplied (filePath * only steers the TS-side file lookup, never the bridge's own name resolution). * Corpus evidence: 2026-07-21T19__L2-error-handling-infolog (add-index on * ConDemoTicket failed 3×) and 2026-07-21T20__L3-workflow-document-submit. * With no working grounded path the only way to land the index was * d365fo_file(action="create", overwrite=true) — the whole-file escape hatch the * eval loop forbids. * * This writes an element straight into the table's * collection, in the shape the D365FO SDK serialises: , , * optional , then of . * is a collection sibling, NOT part of the order-sensitive top-level * property block, so appending to it is safe. Unlike the bridge it edits the file * on disk, so it is unaffected by what the provider loaded at startup — and it * carries allowDuplicates/alternateKey, which the whole-file overwrite workaround * dropped (cluster #35). */ export declare const directXmlAddIndex: (filePath: string, indexName: string, fields: string[] | undefined, allowDuplicates: boolean | undefined, alternateKey: boolean | undefined) => Promise<{ success: boolean; message: string; } | null>; /** * Direct XML writer for add-query-range on an AxDataEntityView. * * Inserts an into the the named data * source OWNS, inside . There is no bridge API for this operation — * the C# bridge does not expose AddQueryRange on entities — so this direct writer * is the only path. * * Idempotent: a RANGE (not merely some element) with the same Name already in that * collection is left unchanged and success is returned. */ export declare const directXmlAddQueryRange: (filePath: string, dataSourceName: string, rangeField: string, rangeName: string, rangeValue: string) => Promise<{ success: boolean; message: string; } | null>; /** * Direct XML writer for remove-query-range on an AxDataEntityView. * * Removes the whose equals `rangeName` from * the the named data source OWNS — a same-named range on a joined data * source is left alone. Collapses … to when the * collection becomes empty. Idempotent: not-found is reported as success. */ export declare const directXmlRemoveQueryRange: (filePath: string, dataSourceName: string, rangeName: string) => Promise<{ success: boolean; message: string; } | null>; /** * Locates ONE top-level collection element inside a root element's body, e.g. * `` directly under ``. * * A plain `content.replace('', …)` is wrong here and silently so: an * AxDataEntityViewExtension carries `` BEFORE ``, and * each `` inside it has a nested `` of its own. * The first `` in the file therefore closes the field GROUP, and the new * element lands in a collection the deserializer will not read it from. * * Depth-counts from the root's opening tag so only a DIRECT child matches. * Returns the insertion offset (just before the collection's closing tag), or a * `selfClosingAt` range when the collection is `` and must be expanded. */ export declare function findTopLevelCollection(content: string, rootElement: string, collection: string): { insertAt: number; } | { selfClosingAt: [number, number]; } | null; /** * Direct XML fallback for add-field on a DATA-ENTITY-EXTENSION. * * The bridge handles this via IMetaDataEntityViewExtensionProvider; this is the * same-session escape hatch that add-index and add-control already have — the * provider resolves against metadata roots fixed at startup, so an extension * created THIS session is invisible to it. * * The element itself comes from the shared builder (dataEntityViewExtensionXml.ts) * so the modify path cannot drift from the create path: sub-element ORDER is not * cosmetic, the deserializer drops children it meets out of order, and Label must * precede the DataField/DataSource binding pair. */ export declare const directXmlAddDataEntityExtensionField: (filePath: string, fieldName: string, dataField: string, dataSource: string, label?: string | undefined, fieldGroupName?: string | undefined) => Promise<{ success: boolean; message: string; } | null>; /** * add-field on a VIEW: appends an `AxViewFieldBound` to the view's own . * * A view field has no EDT of its own — it names a field (`dataField`) of one of the * view's data sources (`dataSource`). The bridge has no view path (its AddField * resolves tables and table extensions only), so this is the only writer. * * Shape measured on the 2,987 shipped views (source XML, not XppMetadata): 28,493 * bound fields, every one with a ; 25,946 are exactly Name, DataField, * DataSource, and an optional Label (1,325 + 100) sits between Name and DataField. * * `dataSource` is checked against the data sources the view declares in its own * and takes their casing. A view built on an AxQuery declares none * of its own, so there the name cannot be checked here — the build checks it. */ export declare const directXmlAddViewField: (filePath: string, fieldName: string, dataField: string, dataSource: string, label?: string | undefined) => Promise<{ success: boolean; message: string; skipped?: boolean; }>; /** * Sets InsertIfEmpty on the form data source `dataSourceName` — after the bridge has * added it, since its AddDataSource takes no such property. * * The 12,798 shipped data sources that carry the element all say No: Yes is the * default and the serializer omits it. So `false` writes `No` * before the first element that follows it (AFTER_INSERT_IF_EMPTY), and `true` * removes the element — the file then reads exactly as a defaulted one. */ export declare const directXmlSetFormDataSourceInsertIfEmpty: (filePath: string, dataSourceName: string, insertIfEmpty: boolean) => Promise<{ success: boolean; message: string; changed?: boolean; }>; /** DeleteAction values accepted by the AxTable serialiser. */ export declare const DELETE_ACTION_TYPES: readonly ['None', 'Restricted', 'Cascade', 'CascadeRestricted']; /** * add-delete-action / remove-delete-action on a TABLE, written straight to the XML. * * There is no bridge operation for DeleteActions at all (finding #36), so a * cascading delete action was inexpressible through the modify surface — the only * route was the forbidden whole-file overwrite. * * ORDER INSIDE THE ENTRY IS LOAD-BEARING, and this writer had it wrong. The * comment here used to say "shape matches MetadataWriteService.cs: Name, Table, * DeleteAction" — that is the order of a C# object initialiser, which says * nothing about the order the DataContract serialiser reads. Microsoft's own * metadata is the oracle: all 126 entries shipped in * ApplicationSuite/Foundation/Common/Platform are * * Name → DeleteAction → Relation → Table (26 of them then add Tags) * * with no exceptions. Writing AFTER put it out of * declaration order, and the deserializer drops a misordered element in * silence — the same failure mode the top-level property block already had * (axTablePropertyOrder.ts, finding #13). The file on disk said Cascade, the * provider read the entry with DeleteAction at its default, and the next * bridge-backed Update() serialised the object back WITHOUT the element. The * 2026-08-31 capture run of L4-headerlines-document-slice recorded that as * "the bridge destroyed the delete action"; the bridge only wrote back what * the deserializer had been able to read. * * is emitted when the caller names one: a delete action without an * explicit relation is legal but draws BPUpgradeMetadataDeleteAction. Which side * DECLARES that relation is not fixed — of the resolvable delete actions in the * four big packages, 5 name a relation declared on the related table and 4 name * one declared on the table that owns the delete action — so the op-spec tells * the caller to read it rather than derive it. */ export declare const directXmlDeleteAction: (filePath: string, mode: "add" | "remove", name: string, table: string | undefined, deleteAction: string | undefined, relation?: string | undefined) => Promise<{ success: boolean; message: string; } | null>; /** * remove-control on a FORM or FORM-EXTENSION, written straight to the XML. * * add-control has a bridge path for a plain form (AddControl) and a direct-XML * path for an extension; removal has neither — MetadataWriteService exposes no * RemoveControl at all — so this is the only grounded route, exactly as with * directXmlDeleteAction. Without it, taking a button off a form meant * d365fo_file(action="create", overwrite=true), the whole-file escape hatch the * eval loop forbids. * * The tree walk, the two form-extension shapes and the separator rule all live * in formControlRemoval.ts — pure and unit-tested, because a `` substring * replace here would cut the wrong element and report a ✅ for it. */ /** * modify-property on a control the FILE ITSELF defines: a control of the agent's * own form, or one a form extension ADDS. The bridge has no modify-property for * forms and the Design writer stops at , so without this a button's * MenuItemName or NeedsRecord could only be set by rewriting the whole form. The * property becomes a child of that control, in the order shipped metadata uses * for its type — see formControlPropertyXml.ts. * * Returns null when the request is not for such a control, so the caller carries * on: a form extension then means a BASE-form control (), * and a form with a dotted path that names no control ("Design.Caption") goes to * the Design writer, as before. A form with an explicit controlName it does not * define is refused instead — writing a Design property there would be a ✅ on * the wrong element. So is a dotted path whose first segment is neither Design * nor a control of the form ("PostButon.Caption"): the Design writer keeps only * the last segment, and would set the form's caption. */ export declare const directXmlModifyControlProperty: (filePath: string, objectType: string, objectName: string, propertyPath: string, propertyValue: string, controlName: string | undefined) => Promise<{ success: boolean; message: string; skipped?: boolean; } | null>; export declare const directXmlRemoveControl: (filePath: string, controlName: string, removeSeparator: boolean | undefined) => Promise<{ success: boolean; message: string; } | null>; /** * add-entry-point on an AxSecurityPrivilege, written straight to the XML. * * The missing half of the pair. `create` takes ONE entry point, as the scalar * `properties.targetObject`, so a privilege granting two menu items could only * be produced by `create(overwrite=true, xmlContent=…)` — hand-authored XML that * bypasses the generator. That also left remove-entry-point's ambiguity refusal * unreachable through supported parameters and therefore untested against a real * privilege (eval case L2-object-delete-and-entry-point-cleanup, 2026-08-23). * * Same structural reason as its inverse for being XML-only: security objects * have no bridge write path — the generic `properties: Dictionary` * channel cannot carry , which is why security-privilege is * excluded from BRIDGE_CREATE_TYPES. */ export declare const directXmlAddEntryPoint: (filePath: string, spec: { objectName: string; objectType: string; name?: string; accessLevel?: string; }) => Promise<{ success: boolean; message: string; } | null>; /** * remove-entry-point on an AxSecurityPrivilege, written straight to the XML. * * Security objects have no bridge write path at all: the generic * `properties: Dictionary` channel cannot carry , * which is why security-privilege is excluded from BRIDGE_CREATE_TYPES. So this * mirrors remove-relation / remove-field-group in shape while being, like the * delete-action pair, XML-only by necessity. * * The matching and the collapse of an emptied live in * securityPrivilegeXml.ts, next to the builder that writes the element — the two * halves of one shape drifting apart is how an entry point becomes unremovable. */ export declare const directXmlRemoveEntryPoint: (filePath: string, criteria: { name?: string; objectName?: string; objectType?: string; }) => Promise<{ success: boolean; message: string; } | null>; /** * remove-diagnostic-suppression on an AxIgnoreDiagnosticList, written straight * to the XML. * * Not an AOT object at all — MetadataWriteService has no notion of a * suppression list — so this is XML-only for the same structural reason as * remove-entry-point. The matching and the collapse of an emptied live * in ignoreDiagnosticListXml.ts, next to the bulk-removal helper delete uses * to strip suppressions for an object it just deleted. */ export declare const directXmlRemoveDiagnosticSuppression: (filePath: string, criteria: { path: string; moniker?: string; }) => Promise<{ success: boolean; message: string; } | null>; /** * add-diagnostic-suppression on an AxIgnoreDiagnosticList, written straight to * the XML. * * The block itself is built by buildSuppressionXml * (bpMonikers/index.ts) — the SAME function get_knowledge(kind="bp-moniker", * action="suppress") uses to render one for a human to paste by hand, so the * two paths cannot describe two different shapes of suppression. This is only * the "place it in the file" half. * * When the model's suppression file does not exist yet, a fresh one is created * — skeleton and all, measured against a shipped PackagesLocalDirectory (see * emptySuppressionListXml). Creating it means creating its FOLDER too: * AxIgnoreDiagnosticList exists only in models that have suppressed something, * which is by definition not the model this branch runs for, and writeFileAtomic * does no mkdir — so without this the one path that advertises "creates the file * for you" failed with ENOENT on the directory, and the caller was told the C# * bridge could not resolve the object. */ export declare const directXmlAddDiagnosticSuppression: (filePath: string, input: { moniker: string; path?: string; elementType?: string; elementName?: string; justification?: string; message?: string; severity?: 'Error' | 'Warning'; itemSpecific?: boolean; }) => Promise<{ success: boolean; message: string; } | null>; /** * Writes the relation properties the bridge drops into an . * * add-relation documents relationCardinality / relatedTableCardinality / * relationshipType WITH defaults, but neither bridgeClient.addRelation nor the * C# MetadataWriteService.AddRelation carries them: AddRelation only sets Name, * RelatedTable and the constraints. The result was a relation that reports * "✅ Relation 'X' added" and then fails BP with * BPErrorTableRelationshipPropertiesCompleteness naming exactly those three * properties — with no repair path, because modify-property rejects * Relations//RelationshipType (corpus findings #5 / #35). * * The C# side cannot be fixed or tested without the VM's metadata assemblies, so * the properties are written on disk after the relation lands. Element order is * the one both in-repo generators emit (createD365File.ts / generateTableRelation.ts, * matching the SDK serialiser): Name, Cardinality, RelatedTable, * RelatedTableCardinality, RelationshipType, Constraints. Order matters — AxTable * XML silently drops misordered properties (#13) — so nothing is guessed here: * each element is anchored to the sibling it must follow, and the function is a * no-op if the anchor is absent or the property is already present. */ export declare const directXmlEnsureRelationProperties: (filePath: string, relationName: string, cardinality: string, relatedTableCardinality: string, relationshipType: string) => Promise<{ applied: string[]; } | null>; /** * Stamp `` / `` onto an existing ``. * * Neither the bridge's AddIndex nor its SetAxTableProperty knows these two index * properties, so a date-effective table (ValidTimeStateFieldType = Date/UtcDateTime) * could be created but never completed through the tool path — its valid-time-state * key index is exactly what xppc demands (Phase F, L2-date-effective-table). The * SDK serialises index properties alphabetically before the collection * (Name, AllowDuplicates, AlternateKey, …, ValidTimeStateKey, ValidTimeStateMode, * Fields — see PersonnelCore/AxTable/HcmPositionDetail.xml), so both land right * before . Idempotent: an already-present element is rewritten in place. * * ValidTimeStateMode: NoGap is the SDK DEFAULT and the serializer omits it — a * bridge round-trip of a table carrying NoGap dropped the * element, and every shipped index that spells the mode out says Gap * (HcmEmployment, HcmJobDuration, HcmPositionHierarchy…). So NoGap REMOVES the * element and Gap writes it; either way the request is honoured. */ export declare const directXmlSetIndexValidTimeState: (filePath: string, indexName: string, validTimeStateKey: boolean | undefined, validTimeStateMode: string | undefined) => Promise<{ success: boolean; message: string; } | null>; /** * Remove an EMPTY top-level property element (`` or * ``) from an AOT XML file. * * modify-property with propertyValue="" is how a caller says "back to the default", * and the bridge's SetProperty obliges by writing the empty element the SDK * serialiser produces for an empty string — which no shipped file carries: the * default is expressed by ABSENCE (a table with no has the * surrogate-key primary index, the shape of every date-effective table). Only a * top-level, empty element is touched; a populated one is left alone. */ export declare const directXmlClearEmptyProperty: (filePath: string, property: string) => Promise<{ success: boolean; message: string; } | null>; /** * add-module-reference on a model descriptor, written straight to the XML. * * A descriptor is not an AOT object — MetadataWriteService has no concept of one * — so this is XML-only for the same structural reason as * add-diagnostic-suppression, and the transform itself lives next to the readers * that already parse this element (src/metadata/modelDescriptor.ts) rather than * being a second, drifting copy of the same parse. * * Nothing is created implicitly. Unlike add-diagnostic-suppression, which writes * a first {Model}_BPSuppressions.xml for a model that has never suppressed * anything, a model with NO descriptor is not a model — it is a wrong path or a * wrong model name, and inventing `` would hide that behind a ✅. * `unknownModuleNote` carries the same principle to the reference itself. */ export declare const directXmlAddModuleReference: (filePath: string, moduleName: string, unknownModuleNote: string) => Promise<{ success: boolean; message: string; } | null>; /** * remove-module-reference on a model descriptor, written straight to the XML. * * Removing a reference is the one direction that can break a build that * currently passes — every type the model resolves through that module goes * invisible to xppc — so the success message says so rather than leaving it to * be discovered at compile time. */ export declare const directXmlRemoveModuleReference: (filePath: string, moduleName: string) => Promise<{ success: boolean; message: string; } | null>; /** add-data-source on a data-entity: an embedded, joined query datasource (no bridge operation exists). */ export declare const directXmlAddDataEntityDataSource: (filePath: string, spec: EntityDataSourceSpec) => Promise<{ success: boolean; message: string; } | null>; /** add-field on a data-entity: one mapped field bound to an existing datasource of its query. */ export declare const directXmlAddDataEntityField: (filePath: string, spec: EntityMappedFieldSpec) => Promise<{ success: boolean; message: string; } | null>; /** modify-field AllowEdit / IgnoreEDTRelation, written to the XML; writes nothing when the field is absent. */ export declare const directXmlSetTableFieldProperties: (filePath: string, fieldName: string, props: Record) => Promise<{ success: boolean; message: string; } | null>; //# sourceMappingURL=directXmlWriters.d.ts.map