Skip to content

Diff Documents

Fresh

MD-LD has built-in diff authoring through its polarity system. The remove parameter in both parse() and generate() enables CRDT-style workflows, state management, and collaborative editing with human-readable diffs.

The Polarity System

Every predicate, type, and reverse link in MD-LD can be prefixed with - to retract it:

markdown
[Alice] {my:author}       ← assert: Alice is an author
[Alice] {-my:author}      ← retract: Alice is no longer an author

This turns MD-LD into an append-only semantic ledger: you never delete text, you write a new entry that cancels a previous assertion.

Intra-Document Cancellation

When a retraction matches a positive triple in the same document, both cancel out — neither appears in the final graph:

markdown
[my] <tag:you@example.com,2026:>

## Article {=my:article}

[Alice] {my:author}       ← added
[Alice] {-my:author}      ← cancel — both disappear
[Bob] {my:author}         ← this survives

Result: only my:article my:author "Bob" in quads. The remove array is empty.

External Retractions

When a retraction finds no matching positive in the current document, it becomes an external retraction stored in remove:

markdown
[my] <tag:you@example.com,2026:>

## Article {=my:article}

[Alice] {-my:author}      ← external retract: targets a prior document
[Bob] {my:author}

Result:

  • quads: contains my:article my:author "Bob"
  • remove: contains my:article my:author "Alice" (targets the prior document state)

Merge Resolution

The merge() function resolves external retractions against earlier documents:

javascript
const v1 = `# Article {=ex:article .ex:Article}
[Alice] {author}
[Draft] {status}`;

const v2 = `# Article {=ex:article}
[Alice] {-author}
[Bob] {author}
[Draft] {-status}
[Published] {status}`;

const final = merge([v1, v2]);
// quads: author=Bob, status=Published
// remove: [] — all retractions resolved

The hard invariant always holds: quads ∩ remove = empty set — a triple is never simultaneously present and retracted.

Polarity on All Annotation Forms

Retractions work on every annotation type:

markdown
## Alice {=my:alice .ex:Employee -.ex:Contractor}   ← add Employee, retract Contractor

[project-x] {+my:project-x -?my:assignedTo}         ← retract an object link
[my:team] {+my:team -!my:hasMember}                  ← retract a reverse link
{-.my:Draft .my:Published}                            ← change document state

The Ledger Pattern

The real power is in append-only files — a journal, a log, a knowledge base — where every change is a new entry:

markdown
[my] <tag:you@example.com,2026:>
[j]  <my:journal/>

# 2026-05-01 — started project {=j:2026-05-01 .j:Entry j:date ^^xsd:date}

Project status: [planning] {+my:project ?my:documents ?j:status}

# 2026-05-10 — project kicked off {=j:2026-05-10 .j:Entry j:date ^^xsd:date}

{=my:project}
[planning] {-my:status}
[active] {my:status}

Merging these two entries produces my:project my:status "active" only. The history of the change lives in the text; the graph holds only the current truth.

Generating Diff Documents

Use generate() with a remove parameter to produce a diff document:

javascript
import { generate, DataFactory } from 'mdld-parse';

const { namedNode, literal } = DataFactory;

// Current state
const quads = [
  DataFactory.quad(
    namedNode('http://example.org/doc'),
    namedNode('http://example.org/author'),
    literal('Bob')
  )
];

// What to retract
const remove = [
  DataFactory.quad(
    namedNode('http://example.org/doc'),
    namedNode('http://example.org/author'),
    literal('Alice')
  )
];

const { text } = generate({
  quads,
  remove,
  context: { ex: 'http://example.org/' }
});

console.log(text);
// [ex] <http://example.org/>
//
// # doc {=ex:doc}
// [Bob] {ex:author}
// [Alice] {-ex:author}

Version Control Workflow

Full version control workflow using MD-LD's diff system:

javascript
import { parse, generate, merge } from 'mdld-parse';

// Version 1
const v1Text = `
[ex] <http://example.org/>
# Document {=ex:doc .ex:Article}
[Alice] {ex:author}
[Draft] {ex:status}
`;

// Version 2 — corrections
const v2Text = `
[ex] <http://example.org/>
# Document {=ex:doc}
[Alice] {-ex:author}
[Bob] {ex:author}
[Draft] {-ex:status}
[Published] {ex:status}
`;

// Merge to get final state
const merged = merge([v1Text, v2Text]);
console.log(merged.quads);
// Final state: author=Bob, status=Published
// All v1 assertions that were retracted in v2 are resolved

Collaborative Editing

CRDT-style collaborative editing:

javascript
// User A's document
const userA = `[ex] <http://example.org/>
# Task {=ex:task-1 .ex:Task label}
[Alice] {+ex:alice ?ex:assignee}
[high] {ex:priority}`;

// User B's corrections (applied after User A)
const userB = `[ex] <http://example.org/>
# Task {=ex:task-1}
[Alice] {+ex:alice -?ex:assignee}
[Bob] {+ex:bob ?ex:assignee}
[low] {-ex:priority}
[medium] {ex:priority}`;

const final = merge([userA, userB]);
// Result: task assigned to Bob, priority=medium

Inline Correction Pattern

Corrections can appear in narrative prose:

markdown
[ex] <tag:carol@example.org,2026:>

New student [Alice] {=ex:new-student .prov:Person ex:name} is in our [class] {+ex:my-class !member}.

**Correction:** [Her] {=ex:new-student} name is not [Alice] {-ex:name}, it's [Ellie] {ex:name}.

**Further correction:** I asked her directly — she does not know [him] {+ex:bob -?ex:knows}.

After generate(parse({text})):

markdown
[ex] <tag:carol@example.org,2026:>

# Ellie {=ex:Ellie .prov:Person label}
[Ellie] {ex:name}

The generated document contains only the final resolved state — corrections applied, retractions removed.

Type Migration

Use retractions to change types:

markdown
## Alice {=my:alice .ex:Employee -.ex:Contractor}

This adds ex:Employee and retracts ex:Contractor in a single annotation. If ex:Contractor was asserted in a prior document, it will be placed in remove. If it was asserted earlier in the same document, both cancel and neither appears in the output.

Querying Retractions

javascript
const result = parse({ text: myDocument });

// Current state
console.log(result.quads);

// What this document retracts from prior state
console.log(result.remove);

// Apply current retractions against a prior state
const priorQuads = [...];
const currentState = priorQuads.filter(priorQuad => {
  return !result.remove.some(retractedQuad =>
    retractedQuad.subject.value === priorQuad.subject.value &&
    retractedQuad.predicate.value === priorQuad.predicate.value &&
    retractedQuad.object.value === priorQuad.object.value
  );
});