Feature Specification: Difference from a Baseline

Created: 2026-09-22 Status: Implemented Input: Reconstructed from the code at d225ebe (src/diff.ts, src/diff-highlight.ts, src/index.ts: the baseline, onDiffChange and recomputeDiffAgainst parts; src/diff.test.ts, src/diff-highlight.dom-test.js), docs/usage.md, demo/value-diff/, the history of the former panel/forms-island directory in the fediversity monorepo, and the Claude Code logs of the maintainers.

User Scenarios & Testing (mandatory)

User Story 1 - See which fields differ from the applied value (Priority: P1)

The embedder gives the widget a baseline: the value that is now in effect (for example the value of the last deployment). While the user edits, each field that differs from the baseline gets a colored border: green when the user set a field that was unset, yellow when the user changed a set field, red when the user set a field back to unset.

Why this priority: in a form with many options, the user must see what they are about to change before they apply it.

Independent Test: open the value-diff page of the demo, which mounts a value with a baseline, and check the three colors.

Acceptance Scenarios:

  1. Given a baseline where a field is unset, When the user enters a value, Then the field gets the "added" class.
  2. Given a baseline where a field has a value, When the user changes it, Then the field gets the "changed" class.
  3. Given a baseline where a field has a value, When the user clears it, Then the field gets the "removed" class.
  4. Given a field with a changed value, When the user enters the baseline value again, Then the field has no class.

User Story 2 - Know if there is anything to apply (Priority: P1)

The embedder has a button that applies the value. It enables the button only when the value differs from the baseline.

Why this priority: an apply without a change costs time and can hide mistakes.

Independent Test: mount with onDiffChange, edit a field, and set it back; check that the callback reports true and then false.

Acceptance Scenarios:

  1. Given a value equal to the baseline, When the form mounts, Then onDiffChange receives false.
  2. Given a mounted form, When the user changes a field, Then onDiffChange receives true.
  3. Given a value that a script wrote outside the form, When the embedder calls recomputeDiffAgainst(value), Then onDiffChange receives the result for that value.

User Story 3 - A default is not a change (Priority: P2)

A section that the user turns on shows its fields at their defaults. These fields do not show as changed.

Why this priority: markers on fields that the user did not touch hide the real changes.

Independent Test: the unit tests of changeKind with a schema default.

Acceptance Scenarios:

  1. Given a field that is absent in the baseline, When it appears with a value equal to its default, Then it has no class.
  2. Given a field at its default, When the user changes it to another value, Then it gets the "added" class.

Edge Cases

  • No baseline is given: the baseline is the empty value {}, so each field that the value sets shows as added.
  • null, false, "" and an absent key all count as unset. 0 is a value.
  • A list changes: the widget compares the list as one value and does not mark single items.
  • A container (object, map or list) changes: only the scalar leaves inside it get a class, not the container.
  • A nullable leaf counts as scalar when its inner schema is scalar.
  • The border shows only that a field differs, not the old value. This keeps secret values out of the page.

Requirements (mandatory)

Functional Requirements

  • FR-001: The widget MUST accept an optional baseline value at mount. Without it, the baseline MUST be {}.
  • FR-002: The widget MUST NOT change the baseline during the life of the form.
  • FR-003: For each scalar leaf, the widget MUST classify the difference between the baseline and the current value as added, changed, removed or none, and MUST put the matching class (nmf-diff-added, nmf-diff-changed, nmf-diff-removed) on the control.
  • FR-004: The widget MUST treat an absent key, null, false, "" and a value equal to the schema default of the field as unset.
  • FR-005: The widget MUST NOT mark containers, and MUST compare lists as whole values.
  • FR-006: The widget MUST call onDiffChange at mount and after each edit with true when the value differs from the baseline, and false when not. Synthetic fields do not count.
  • FR-007: The handle MUST offer recomputeDiffAgainst(value) for a value that changed outside the form.
  • FR-008: The difference markers MUST NOT show the baseline values.

Key Entities

  • Baseline: the value that is now in effect, given by the embedder. It is the reference for all difference markers.
  • Change kind: added, changed, removed or none, for one field.

Success Criteria (mandatory)

Measurable Outcomes

  • SC-001: A form mounted with a value equal to its baseline shows no difference marker and reports false.
  • SC-002: Each edit of a scalar leaf changes the marker of that leaf only.
  • SC-003: The unit tests of the classification and the DOM tests of the markers pass in CI.

Assumptions

  • The embedder knows which value is in effect. The widget does not store or fetch it.
  • The embedder gives the baseline in the current schema. When options were renamed, it migrates the baseline first (see 004-schema-changes-and-migration).

Known Deviations

None.