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