Skip to content

veridtools/dmn-diff

Repository files navigation

@veridtools/dmn-diff

Veridtools Logo

npm license ci docs dependencies

Semantic diff for DMN files. Compares two .dmn files and produces a structured representation of their differences — ID-based identity, severity classification, multiple output formats, and native CI/CD integration.

DMN 1.0 → 1.5 · MIT licensed · 1 runtime dependency (@veridtools/dmn-parser)

Why

DMN files are XML, but diffing them as text is noisy. Reordering decision table rules, reformatting whitespace, moving DMNDI diagram coordinates — all of these produce textual diffs that carry zero semantic meaning. Code review tools can't tell which changes actually affect runtime behaviour and which are cosmetic edits.

dmn-diff understands the structure of DMN. It matches elements by their @id attribute, not position, so reordering rules produces zero diff. It classifies every field change by severity — breaking, non-breaking, or cosmetic — so reviewers can focus on what matters. It strips DMNDI, normalises encoding artefacts (BOM, CDATA, hitPolicy casing), and exposes a clean JSON result for tooling.

The result is a diff you can act on: block PRs on breaking changes, flag non-breaking changes for review, and auto-approve cosmetic-only commits.

At a glance

  • ID-based identity — elements matched by @id, not position; reordering rules produces zero diff
  • Severity classification — every field change is breaking, non-breaking, or cosmetic
  • Four output modes — semantic prose, row-diff table, JSON, raw XML diff
  • CI/CD native — exit code 1 on changes, 0 when clean; pipe-friendly output
  • Full normalization — CDATA vs entities, empty wildcards, hitPolicy defaults, BOM, DMNDI exclusion

Installation

pnpm add @veridtools/dmn-diff
# or
npm install @veridtools/dmn-diff

For global CLI use:

npm install -g @veridtools/dmn-diff

CLI

dmn-diff before.dmn after.dmn                       # semantic output (default)
dmn-diff before.dmn after.dmn --rows    # or -r     # row-diff table with +/-
dmn-diff before.dmn after.dmn --json    # or -j     # structured JSON
dmn-diff before.dmn after.dmn --xml     # or -x     # raw XML line diff (colored)
dmn-diff before.dmn after.dmn --no-color   # or -n  # disable ANSI colors
dmn-diff before.dmn after.dmn --skip-breaking # or -s  # show diff, always exit 0
dmn-diff --help                             # or -h     # show banner, version, options

Exit code 1 when changes are detected — works as a CI gate out of the box:

Try it locally

Clone the repo and run against the bundled examples:

git clone https://github.com/veridtools/dmn-diff
cd dmn-diff
npm install
npx tsx bin/dmn-diff.ts src/examples/a.dmn src/examples/b.dmn
npx tsx bin/dmn-diff.ts src/examples/a.dmn src/examples/b.dmn --rows
npx tsx bin/dmn-diff.ts src/examples/a.dmn src/examples/b.dmn --json
npx tsx bin/dmn-diff.ts src/examples/a.dmn src/examples/b.dmn --xml
npx tsx bin/dmn-diff.ts src/examples/a.dmn src/examples/b.dmn --skip-breaking
dmn-diff main.dmn feature.dmn || echo "DMN changes detected"

GitHub Actions example

- name: Check DMN changes
  run: |
    dmn-diff main.dmn feature.dmn --json > diff.json
    node -e "
      const r = require('./diff.json');
      const breaking = r.changes.flatMap(c => c.fieldChanges ?? []).filter(f => f.severity === 'breaking');
      if (breaking.length) { console.error('Breaking DMN changes detected'); process.exit(1); }
    "

Example output

before.dmn → after.dmn

⚠  1 breaking change(s) detected

~ outputClause "riskLevel" (oc1) modified
    @name: level → riskLevel  ⚠ BREAKING

~ inputEntry "r1ie1" (r1ie1) modified
    text: > 700 → >= 700

0 added · 2 modified · 0 removed · 12 unchanged

Programmatic API

import { diff, renderSemantic } from '@veridtools/dmn-diff'
import { readFileSync } from 'fs'

const from = readFileSync('before.dmn', 'utf-8')
const to   = readFileSync('after.dmn',  'utf-8')

const result = diff(from, to, { fromFile: 'before.dmn', toFile: 'after.dmn' })

// Check for breaking changes
const hasBreaking = result.changes.some(
  (c) => c.type === 'modified' && c.fieldChanges.some((f) => f.severity === 'breaking')
)

// Filter changes in a specific decision
const decisionChanges = result.changes.filter((c) => c.path.includes('decision[d1]'))

// Render
console.log(renderSemantic(result))

DiffResult shape

interface DiffResult {
  meta: {
    fromFile: string
    toFile: string
    timestamp: string    // ISO 8601
    hasChanges: boolean
  }
  summary: {
    added: number
    removed: number
    modified: number
    unchanged: number
  }
  changes: Change[]      // AddedChange | RemovedChange | ModifiedChange
}

interface ModifiedChange {
  type: 'modified'
  id: string
  name: string           // element name, falls back to id when the element has no name
  elementType: ElementType
  parentId?: string
  path: string           // e.g. "decision[d1].decisionTable[dt1].rule[r1].inputEntry[r1ie1]"
  fieldChanges: FieldChange[]
}

interface FieldChange {
  field: string          // e.g. "text", "@name", "inputExpression.@typeRef"
  from: unknown
  to: unknown
  severity: 'breaking' | 'non-breaking' | 'cosmetic'
}

Renderers

import { renderSemantic, renderRows, renderJson, renderXml, tokenize } from '@veridtools/dmn-diff'

renderSemantic(result)         // human-readable prose (default CLI output)
renderRows(result)             // row-diff table with +/- per field
renderJson(result)             // JSON.stringify of DiffResult
renderXml(fromXml, toXml)     // raw line diff of XML source (includes DMNDI)
tokenize(result)               // DiffToken[] — structured intermediate representation
detectFormat(output)           // 'json' | 'semantic' | 'rows' — identify a rendered string's format

tokenize accepts either a DiffResult or the string output of any renderer, and returns a flat DiffToken[] stream. Use it to build custom formatters (HTML, React, Monaco) without re-implementing diff logic, or to parse dmn-diff CLI output back into structured tokens for downstream tooling. JSON output round-trips losslessly; semantic and rows formats recover element types, ids, field changes, and severities, but not path or parentId.

Severity reference

Severity is assigned per field, not per element. A single modification can carry changes of mixed severity — the summary calls out the worst one.

Severity Meaning Examples
breaking Changes runtime evaluation — expression, type, name used as reference decision.@name, outputClause.@name, itemDefinition.@typeRef, functionDefinition.@kind
non-breaking Changes behaviour but not in a way that breaks existing consumers inputEntry.text, outputEntry.text, decisionTable.@hitPolicy, itemDefinition.allowedValues
cosmetic No runtime impact *.@label, *.@description, textAnnotation.text, decisionTable.@preferredOrientation

What gets normalized away

The following differences produce a zero diff result — they are encoding or tooling artefacts, not semantic changes:

Source difference Normalized to
hitPolicy absent "UNIQUE"
hitPolicy="unique" "UNIQUE" (uppercase)
preferredOrientation absent "Rule-as-Row"
isCollection="false" treated same as absent
isCollection="true" true (boolean)
Empty <inputEntry> "-" (DMN wildcard)
<![CDATA[> 5]]> > 5 (via entity escaping)
UTF-8 BOM removed
<DMNDI> section removed entirely
<extensionElements> removed entirely
<typeRef>string</typeRef> (child element) @typeRef="string" (attribute form)
<description>…</description> (child element) @description="…" (attribute form)

DMN version support

Version Namespace
1.1 http://www.omg.org/spec/DMN/20151101/dmn.xsd
1.2 http://www.omg.org/spec/DMN/20180521/MODEL/
1.3 https://www.omg.org/spec/DMN/20191111/MODEL/
1.4 https://www.omg.org/spec/DMN/20211108/MODEL/
1.5 https://www.omg.org/spec/DMN/20230324/MODEL/

DMN 1.4 boxed expressions (conditional, filter, iterator) and DMN 1.5 specifics (typeConstraint, optional import.namespace, DMN15-74 BKM variable.typeRef removal) are fully handled.

Testing

The test suite is backed by @veridtools/dmn-fixtures — a curated set of 300+ real-world DMN files covering every element type, all five DMN versions, and edge cases (BOM, CDATA, DMNDI-only changes, empty wildcards, duplicate namespace declarations). Fixture-driven tests verify both that known-equivalent variants produce zero diff and that known-different variants produce the expected change type and severity.

Contributing

See CONTRIBUTING.md.

License

MIT

About

Semantic diff for DMN files

Resources

License

Contributing

Stars

Watchers

Forks

Contributors