Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Compare two revisions

This guide compares two revisions of a model before a change is published, reads which differences break the apps that use the device, and records the ones you intend. It assumes Esdieff is installed.

Build a before and after pair

Save the earlier revision as v1/thermostat.sdf.json. It is the thermostat’s target temperature on its own, with a version in its info block:

{
  "info": { "title": "Room thermostat", "version": "1.0.0" },
  "sdfObject": {
    "Thermostat": {
      "sdfProperty": {
        "targetTemperature": {
          "description": "Requested room temperature in half-degree increments",
          "type": "number", "nullable": false, "minimum": 5, "maximum": 30,
          "multipleOf": 0.5, "unit": "Cel", "readable": true, "writable": true
        }
      }
    }
  }
}

Save the later revision as v2/thermostat.sdf.json. It raises the maximum to 35, drops the half-degree step, shortens the description and leaves the version unchanged:

{
  "info": { "title": "Room thermostat", "version": "1.0.0" },
  "sdfObject": {
    "Thermostat": {
      "sdfProperty": {
        "targetTemperature": {
          "description": "Requested room temperature",
          "type": "number", "nullable": false, "minimum": 5, "maximum": 35,
          "unit": "Cel", "readable": true, "writable": true
        }
      }
    }
  }
}

Compare them

$ esdieff diff --old v1/thermostat.sdf.json --new v2/thermostat.sdf.json
Revision comparison esdieff.revision-comparison/2: breaking
Documents: 1 compared, 0 removed, 0 added
Differences: 1 breaking, 1 undetermined, 1 compatible
Warnings:
  comparison.version_not_increased in document: later version "1.0.0" does not compare higher than earlier version "1.0.0", after 1 breaking difference
Breaking differences:
  changed quality maximum (client-supplied and thing-supplied data)
    witness 35 for thing-supplied data: earlier rejected, later admitted
    earlier document /sdfObject/Thermostat/sdfProperty/targetTemperature/maximum
    later document /sdfObject/Thermostat/sdfProperty/targetTemperature/maximum
Undetermined differences:
  changed quality multipleOf (client-supplied and thing-supplied data): no breaking witness or compatibility proof
    earlier document /sdfObject/Thermostat/sdfProperty/targetTemperature/multipleOf
    later document /sdfObject/Thermostat/sdfProperty/targetTemperature/multipleOf (absent)
Compatible differences:
  changed quality description (client-supplied and thing-supplied data)
    earlier document /sdfObject/Thermostat/sdfProperty/targetTemperature/description
    later document /sdfObject/Thermostat/sdfProperty/targetTemperature/description

Esdieff takes the side of the client, the app or controller that talks to the device. Each difference is classified by what it means for a client built against the earlier revision:

  • Breaking means such a client can be surprised. Raising the maximum looks harmless for values the client writes, but the property is also readable, and a device may now report 35 where the client expects at most 30. The witness line shows the concrete value that proves it: 35 was rejected before and is admitted now, in the direction the device supplies.
  • Compatible means no client can tell the difference, as with a changed description.
  • Undetermined means Esdieff found neither a breaking witness nor a proof of compatibility, so the change needs a human look. Removing the step is such a case. An undetermined difference is never presented as safe.

The warning says that a breaking change shipped under an unchanged version, which RFC 9880 advises against. Warnings do not change the result. The command exits 1 for a breaking comparison, 3 for an undetermined one and 0 for a compatible one, so a CI job can stop a breaking change from merging.

Changes that reach other documents through a shared definition

A changed shared definition appears in every device model that reuses it, at the place where that device presents it. Compare two directories to see this. Put a types.sdf.json with a shared Celsius type and a thermostat that references it in v1/, then copy both to v2/ and lower the shared minimum from -40 to -50:

{
  "info": { "title": "Shared types" },
  "namespace": { "types": "https://example.com/types" },
  "defaultNamespace": "types",
  "sdfData": { "Celsius": { "type": "number", "minimum": -40, "maximum": 80, "unit": "Cel" } }
}
{
  "info": { "title": "Room thermostat" },
  "namespace": { "types": "https://example.com/types" },
  "sdfObject": {
    "Thermostat": {
      "sdfProperty": {
        "currentTemperature": { "sdfRef": "types:#/sdfData/Celsius", "writable": false },
        "targetTemperature": { "sdfRef": "types:#/sdfData/Celsius", "minimum": 5, "maximum": 30 }
      }
    }
  }
}
$ esdieff diff --old v1 --new v2
Revision comparison esdieff.revision-comparison/2: breaking
Documents: 2 compared, 0 removed, 0 added
Differences: 1 breaking, 0 undetermined, 0 compatible
Breaking differences:
  changed quality minimum (thing-supplied data)
    witness -50 for thing-supplied data: earlier rejected, later admitted
    earlier thermostat.sdf.json /sdfObject/Thermostat/sdfProperty/currentTemperature/minimum
      from types.sdf.json /sdfData/Celsius/minimum
    later thermostat.sdf.json /sdfObject/Thermostat/sdfProperty/currentTemperature/minimum
      from types.sdf.json /sdfData/Celsius/minimum

Documents in the two directories pair up by relative path, and files that end in .sdf.json are selected as for esdieff check --directory. The difference is reported on the thermostat’s currentTemperature, because that is what a client sees, and the from lines name the shared definition that caused it. targetTemperature overrides the minimum, so it is unaffected. Add --recursive and --exclude to select nested files, as Check a local model collection describes.

Acknowledge the changes you intend

When a breaking change is deliberate, record it so the comparison passes while the report still shows the break. First export the eligible differences for review:

esdieff diff --old v1/thermostat.sdf.json --new v2/thermostat.sdf.json --acknowledgements-output reviewed.json

The comparison report and exit status stay as before, and reviewed.json lists every breaking and undetermined difference with its exact identity:

$ jq . reviewed.json
{
  "differences": [
    {
      "classification": "breaking",
      "earlier": {
        "document": "document",
        "pointer": "/sdfObject/Thermostat/sdfProperty/targetTemperature/maximum"
      },
      "kind": "changed",
      "later": {
        "document": "document",
        "pointer": "/sdfObject/Thermostat/sdfProperty/targetTemperature/maximum"
      },
      "quality": "maximum",
      "subject": "quality"
    },
    {
      "classification": "undetermined",
      ...
    }
  ],
  "schema": "esdieff.acknowledged-differences",
  "version": "1"
}

Review the file, delete any entry you do not intend, add a note to each if you like, and keep it with the model. Then supply it:

$ esdieff diff --old v1/thermostat.sdf.json --new v2/thermostat.sdf.json --acknowledged reviewed.json
Revision comparison esdieff.revision-comparison/2: breaking
Disposition: passed, with 1 breaking and 1 undetermined differences acknowledged
Documents: 1 compared, 0 removed, 0 added
Differences: 1 breaking, 1 undetermined, 1 compatible
Warnings:
  comparison.version_not_increased in document: later version "1.0.0" does not compare higher than earlier version "1.0.0", after 1 breaking difference
Acknowledged differences:
  changed quality maximum (client-supplied and thing-supplied data), breaking
    witness 35 for thing-supplied data: earlier rejected, later admitted
    earlier document /sdfObject/Thermostat/sdfProperty/targetTemperature/maximum
    later document /sdfObject/Thermostat/sdfProperty/targetTemperature/maximum
  changed quality multipleOf (client-supplied and thing-supplied data), undetermined: no breaking witness or compatibility proof
    earlier document /sdfObject/Thermostat/sdfProperty/targetTemperature/multipleOf
    later document /sdfObject/Thermostat/sdfProperty/targetTemperature/multipleOf (absent)
Compatible differences:
  changed quality description (client-supplied and thing-supplied data)
    earlier document /sdfObject/Thermostat/sdfProperty/targetTemperature/description
    later document /sdfObject/Thermostat/sdfProperty/targetTemperature/description

The classification is still breaking, the disposition is passed and the exit status is 0. Only unacknowledged differences decide the result. An entry stops matching if the difference changes, for example when a later release can classify a former undetermined change as breaking, so a stale acknowledgement never hides a new problem. The version warning stays, so bump the version before publishing.

Share the comparison

--format markdown writes the comparison as a changelog for release notes, grouped by breaking, undetermined and compatible changes:

$ esdieff diff --old v1/thermostat.sdf.json --new v2/thermostat.sdf.json --format markdown
# Model changes

Disposition: failed, with 1 breaking, 1 undetermined and 1 compatible differences.

## Warnings

- Version `1.0.0` in `document` at `/info/version` does not compare higher than earlier version `1.0.0`, after 1 breaking difference (`comparison.version_not_increased`)

## Breaking differences

- Changed quality `maximum` in `document` at `/sdfObject/Thermostat/sdfProperty/targetTemperature/maximum` (witness `35` for thing-supplied data, earlier rejected, later admitted)

## Undetermined differences

- Changed quality `multipleOf` in `document` at `/sdfObject/Thermostat/sdfProperty/targetTemperature/multipleOf`: no breaking witness or compatibility proof (`comparison.unproven`)

## Compatible differences

- Changed quality `description` in `document` at `/sdfObject/Thermostat/sdfProperty/targetTemperature/description`

--format sarif writes a SARIF log that code-scanning tools such as GitHub’s show as annotations on the model file. Acknowledged differences are marked as suppressed, so reviewers still see them while scanning treats them as intended:

$ esdieff diff --old v1/thermostat.sdf.json --new v2/thermostat.sdf.json --acknowledged reviewed.json --format sarif | jq -c '.runs[0].results[] | {ruleId, level, suppressed: (.suppressions | length > 0)}'
{"ruleId":"comparison.version_not_increased","level":"warning","suppressed":false}
{"ruleId":"comparison.breaking","level":"error","suppressed":true}
{"ruleId":"comparison.unproven","level":"warning","suppressed":true}
{"ruleId":"comparison.compatible","level":"note","suppressed":false}

--format json gives the full report for scripts. A comparison can also run as a case in a check plan beside the model checks, as Repeat checks and share reports shows. The revision comparison contract explains how each kind of quality is classified and what a witness proves.