er-guard
by D365Wale
README.md
# ER Guard
**Static analysis for Dynamics 365 Finance & Operations Electronic Reporting (ER) configurations.**
Lint, diff and rebase-check ER exports *before* the bank rejects the payment file or the tax
authority rejects the e-invoice.
CLI · MCP server · Microsoft Copilot harness skill · SARIF for GitHub/Azure DevOps code scanning
---
## Why
ER runs every payment file (ISO 20022), e-invoice (PEPPOL, XRechnung, PINT-AE, ZATCA, FatturaPA)
and statutory report in D365 F&O. Yet ER changes today are reviewed by clicking through the
designer, and the problems show up in production:
- a customised `<Ustrd>` binding without an empty-value guard → `<Ustrd/>` in the file → **pain.002 RJCT**
- a remittance text that is not truncated to 140 characters → **schema rejection**
- `FIRST()` on an empty invoice list → **the whole payment export fails at runtime**
- a rebase onto Microsoft's new version silently overwrites (or is overwritten by) your customisation
- a production BIC or endpoint URL hard-coded in a formula
There are many MCP servers for D365 F&O (OData, metadata, X++). **None of them understands ER.**
ER Guard fills that gap.
## What it does
| Command / MCP tool | Purpose |
|---|---|
| `lint` / `er_lint` | 10 rules with business impact and a ready-to-paste fix |
| `diff` / `er_diff` | Structural + binding diff. **Breaking changes first**, including *removed safety nets* (dropped `ISEMPTY` guard, dropped `LEFT(…,140)`, removed `enabled` condition) |
| `rebase` / `er_rebase_check` | Three-way check: old Microsoft base × new base × your derived version → conflicts *before* you click Rebase |
| `summary` / `er_summary` | What is in this export, which standard it targets |
| `inspect` / `er_inspect` | Tag/attribute census to tune recognition for unusual exports |
Knowledge packs (auto-detected):
- **ISO 20022 pain.001** (credit transfer): Max35Text/Max140Text limits, ISODate/ISODateTime, IBAN/BIC patterns, mandatory GrpHdr/PmtInf/CdtTrfTxInf children
- **ISO 20022 pain.008** (direct debit, SEPA SDD): mandate fields (MndtId, DtOfSgntr), SeqTp, ReqdColltnDt
- **UBL 2.1 / EN 16931**: PEPPOL BIS 3, XRechnung (UBL), PINT-AE: mandatory invoice structure, BT references
- **ZATCA Fatoora (KSA)**: UUID, IssueTime, TaxCurrencyCode = SAR, AdditionalDocumentReference (on top of UBL)
Generic rules (ER001, ER003, ER004, ER006, ER007, ER010) apply to **every** ER configuration: JSON e-invoices, text formats, Excel/Word reports and model mappings.
### Rules
| Rule | Catches |
|---|---|
| ER001 | Leaf element with no binding and no value → emitted empty |
| ER002 | Value can be blank, standard needs ≥1 char, no `ISEMPTY`/`IF` guard or `enabled` condition (the `<Ustrd/>` RJCT) |
| ER003 | Dangling binding (target removed/renamed, typical after rebase) |
| ER004 | Expression uses an undeclared data source |
| ER005 | Value not truncated to the standard's max length |
| ER006 | `FIRST()` instead of `FIRSTORNULL()` |
| ER007 | Hard-coded IBAN, BIC, URL, GUID, e-mail, registration number |
| ER008 | Mandatory element missing or disabled |
| ER009 | Date/time without explicit ISO format |
| ER010 | Data source declared but never used |
## Example
```
$ er-guard rebase base-v9.1.xml base-v9.2.xml derived-dh-v9.1.1.xml
## ER Guard rebase check
Base changed 3 things; your derived version changed 8.
1 conflicts, 2 base changes merge cleanly.
🔴 …/RmtInf/Ustrd: Microsoft changed the expression you overrode:
base now LEFT(IF(ISEMPTY(@.PaymentNote), @.InvoiceList, @.PaymentNote), 140),
yours CONCATENATE(@.PaymentNote, " ", FIRST(@.Invoices).Number)
```
```
$ er-guard lint derived-dh-v9.1.1.xml
❌ ER008 Mandatory element missing …/GrpHdr/InitgPty
ISO 20022 pain.001: <GrpHdr> requires <InitgPty>, which is missing from the format.
Impact: Rejected by the bank (schema validation, pain.002 status RJCT).
⚠️ ER002 No empty-value guard …/RmtInf/Ustrd
If the source is blank the file contains <Ustrd/> (minimum length 1).
Fix: set the element's 'enabled' binding to NOT(ISEMPTY(@.PaymentNote)) …
⚠️ ER006 FIRST() on possibly empty list …/RmtInf/Ustrd
```
## Install
```bash
# from GitHub (builds on install, Node 20+)
npm i -g github:D365Wale/er-guard
er-guard summary "ISO20022 Credit transfer.version.9.xml" # check it recognised your export
er-guard lint "ISO20022 Credit transfer.version.9.xml"
```
Export a configuration in F&O: **Electronic reporting → Configurations → Exchange → Export as XML file**.
### As an MCP server
Any MCP host (Copilot CLI, VS Code, Claude, …):
```json
{
"mcpServers": {
"er-guard": { "command": "er-guard-mcp" }
}
}
```
All tools are **read-only**. They analyse files and never connect to an F&O environment.
### On the Microsoft Copilot harness
[`examples/copilot-sdk/review-er.ts`](examples/copilot-sdk/review-er.ts) builds a rebase-review
agent with `@github/copilot-sdk`: the harness runs the loop and model routing, ER Guard brings the
MCP tools and the [`er-guard` skill](skills/er-guard/SKILL.md), and a permission handler blocks
everything except ER Guard tools and file reads.
```bash
npx tsx examples/copilot-sdk/review-er.ts old-base.xml new-base.xml derived.xml
```
### In CI (ER configs in git)
[`.github/workflows/er-guard-example.yml`](.github/workflows/er-guard-example.yml) lints every
changed ER export on pull requests and uploads SARIF, so findings appear as code-scanning alerts.
`--fail-on error|warning|none` controls the exit code.
## How recognition works (and its limits)
ER exports have no published schema, so ER Guard does not depend on exact tag names:
1. **Format components**: any `ER…Format…` element/attribute/item/component with a `Name` (plus the patterns in `src/profile.ts`).
2. **Expressions**: every `ExpressionAsString` (including nested `<ERModelExpressionItem … SyntaxVersion="1"/>` items) is linked to the component whose **ID/GUID** appears on the nearest enclosing node. Wrappers named `Enabled.` mark condition bindings.
3. **Model mappings**: exports without format components are treated as mappings, so model paths aren't reported as dangling bindings.
4. **Diff identity**: components match by GUID first and path second, so a rename is reported as a rename.
If `er-guard summary` reports 0 elements for one of your exports, run `er-guard inspect` and pass a
profile override:
```json
{ "formatElementTag": "^ER.*(XMLElement|MyComponent)", "bindingTargetAttrs": ["ComponentId"] }
```
```bash
er-guard lint my.xml --profile my-profile.json
```
**Status: v0.1, early.** The fixtures are *synthetic* ER-style exports. They follow the
structure seen in real exports (GUID component IDs, nested `ERModelExpressionItem`) and reproduce
real-world defects, but they are not Microsoft files. If `er-guard summary` shows 0 elements for
one of your exports, please open an issue with the `er-guard inspect` output. It contains tag and
attribute names only, no customer data.
Performance: ~6 MB / 30,000 components parse in under 2 s; lint, diff and rebase each take under 1 s.
What ER Guard does **not** do: run the format, validate against each bank's usage rules
(CGI-MP, EPC, local), or check SAF-T/Intrastat/FatturaPA/camt specifics yet. The generic
rules still apply to every format.
## Roadmap
- Knowledge packs: camt.053/054 (import formats), FatturaPA, SAF-T, Slovakia/Germany CIUS specifics
- Model-mapping analysis (unused data sources, expensive `ALLITEMS`/`FILTER` patterns)
- Optional live mode via [d365fo-mcp-server](https://github.com/dynamics365ninja/d365fo-mcp-server): pull configs straight from an environment
- VS Code extension view
## Develop
```bash
npm i && npm run build && npm test
node scripts/make-fixtures.mjs # regenerate fixtures
```
MIT © Dhruv Khatri
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo recent community issues