Skip to content

Polarity and Retraction

Fresh

Polarity is MD-LD's built-in diff authoring system. Every assertion can have a positive (+) or negative (-) polarity, turning the format into an append-only semantic ledger.

The Core Concept

In a traditional document, you edit text — you change what's there. In an MD-LD ledger, you never delete text — you write a new entry that cancels a previous assertion. The history lives in the file; the graph holds only the current truth.

markdown
## Article {=my:article}

[Alice] {my:author}     ← add Alice as author
[Alice] {-my:author}    ← retract: Alice is no longer the author
[Bob] {my:author}       ← add Bob as author

After parsing: only my:article my:author "Bob" in quads. The Alice entries cancelled each other.

Polarity Syntax

Prefix any annotation token with - to negate it:

FormPositiveNegative
Literal property{my:name}{-my:name}
Object property{?my:knows}{-?my:knows}
Reverse property{!my:member}{-!my:member}
Type declaration{.ex:Draft}{-.ex:Draft}

Multiple polarities can appear in one annotation:

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

This adds ex:Employee and retracts ex:Contractor in a single annotation block.

Two Types of Retraction

Intra-Document Cancellation

When a retraction matches a positive triple in the same document, both cancel out. Neither appears in the final output:

markdown
[Alice] {my:author}     ← positive
[Alice] {-my:author}    ← negative, matches above → both disappear
[Bob] {my:author}       ← positive, survives

Result: quads contains only author=Bob. The remove array is empty.

External Retraction

When a retraction finds no matching positive in the current document, it becomes an external retraction. It is stored in result.remove rather than disappearing:

markdown
[Alice] {-my:author}    ← no matching positive in this document
[Bob] {my:author}       ← positive, survives

Result:

  • quads contains author=Bob
  • remove contains author=Alice (targets a prior document)

External retractions wait to cancel triples from previously merged documents.

Merge Resolution

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

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

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: quads ∩ remove = empty set — a triple is never simultaneously present and retracted.

The Ledger Pattern

The real power comes from append-only files where every change is a new entry:

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

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

Project Alpha status: [planning] {+my:alpha ?my:status}

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

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

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

{=my:alpha}
[active] {-my:status}
[done] {my:status}

merge([v1, v2, v3]) produces my:alpha my:status "done" only. Every state transition is recorded in the text. The graph holds current truth.

Inline Corrections in Prose

Corrections can appear inline in narrative text:

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 [Bob] {+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 shows only the final resolved state.

Type Migration

Change types by combining a retraction and an assertion:

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

If ex:Contractor was asserted earlier in the same document, both cancel. If it came from a prior merged document, it ends up in remove.

markdown
[project-x] {+my:project-x -?my:assignedTo}   ← remove the assignment link
[my:team] {+my:team -!my:hasMember}            ← remove reverse membership

These retract relationship triples rather than literal values.

Querying the Polarity State

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

// What this document currently asserts
console.log(result.quads);

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

// The hard invariant: never overlap
const quadKeys = new Set(result.quads.map(q =>
  `${q.subject.value}|${q.predicate.value}|${q.object.value}`
));
const noOverlap = result.remove.every(q => {
  const key = `${q.subject.value}|${q.predicate.value}|${q.object.value}`;
  return !quadKeys.has(key);
});
console.log(noOverlap); // always true

CRDT Semantics

MD-LD's polarity system implements CRDT (Conflict-free Replicated Data Type) semantics for text documents:

  • Each document is an append-only sequence of assertions and retractions
  • The merge() function resolves the combined state deterministically
  • No coordination is required between authors — merge is commutative for retractions that don't conflict
  • The final state is always well-defined

This makes MD-LD suitable for:

  • Distributed collaboration without central coordination
  • Offline-first applications where edits can be made disconnected and merged later
  • Audit trails that preserve the full history of changes
  • Version control systems for knowledge bases