Skip to content

Conflicts and concurrent editing

What OntoBoard does today

  • Unit of synchronisation is one OWL axiom change, encoded as one of 18 typed JSON operations. Not characters, not files. (OperationMapper, OntologyOperation.TYPES)
  • Transport is an append-only shared log (a Yjs array over a WebSocket bridge). The bridge relays and authenticates; it does not arbitrate.
  • Resolution is last-writer-wins by arrival order. No lock, no version check, no vector clocks. The bridge does enforce one thing since 1.125.0: a viewer token cannot write.
  • A change that cannot be applied is now reported, with a running count, to the person whose copy is behind. The sender is still not told — that needs a NACK the protocol lacks.
  • Deliberately no entity locking. A plugin learns of an edit only after Protégé applied it, so a lock it could not enforce would be a promise it could not keep. What there is is an advisory claim: publishing a change says which term you are editing, and peers show a badge. That claim never fired before 1.125.0 — it read the operation's random UUID where it expected an IRI, so no claim was ever made and the badge never appeared.
  • A conflicted pull request says so. gh is asked for mergeable now, and the listing marks a PR that cannot be merged — for an .owl that matters, because a file full of conflict markers does not parse, so GitHub's web editor is the wrong place to resolve one.
  • Git conflicts are refused, not committed. Since 1.123.0 an unmerged file (UU, AA, DD, AU, UA, DU, UD) is its own state, and a commit while one exists is declined. Before that, git add -A + commit recorded the conflict markers and reported success — which destroys the ontology, because an .owl full of markers does not parse.
  • A refused commit, and "show me what has changed", say which terms disagree. Since 1.126.0 the three versions git keeps of a conflicted file (git show :1:, :2:, :3:) are each parsed and compared as ontologies, and the refusal carries lines like "Both sides relabelled Material, differently: this branch has "Material Entity", the other branch has "Material Thing"". Measured on a real conflicted repository: all three stages parse cleanly and the merged working copy does not parse at all (Content is not allowed in prolog) — which is the whole case against a side-by-side text view of a conflicted .owl. One side deleting the file, both sides creating it, and a stage git has that will not parse are each named as what they are rather than guessed at — the shape of the conflict is read from git ls-files -u, because inferring it from which versions parse reports a file somebody committed with markers still in it as a deletion.
  • Axiom-level diff exists and is descriptive: AxiomDiff and ReleaseDiff with six typed change kinds. They render a report; they do not produce applicable changes.

Why this is harder than merging text or rows

  • No canonical serialisation. Reorder the owl:Class elements or relabel _:b0, and you get a ~100% text diff and a zero-axiom diff.
  • Same logic, different syntax. A ⊑ (B ⊓ C) versus A ⊑ B plus A ⊑ C: diff3 reports a conflict, a reasoner reports no difference.
  • Different regions, contradictory result. One side adds DisjointClasses(B, C), the other adds A ⊑ C, with A ⊑ B already present. Clean merge, and A is now unsatisfiable. This is database write skew: each side is valid against its own snapshot; the union is not.
  • Entailment has no locality. The two axioms that contradict can be 10,000 lines apart, in different files, or one of them inferred — so the hunk abstraction that makes diff3 work does not transfer.
  • Imports. One side bumps owl:imports to a version where upstream made two classes disjoint. Both diffs are one line, neither touches the axiom that breaks, and the unsatisfiable class is in a file nobody here edited.
  • Blank nodes. Graphs must be standardised apart before union; deciding isomorphism with blank nodes is GI-complete.
  • Only a reasoner can say a merge is good, and it answers "class X is unsatisfiable" rather than "line 412 conflicts" — so the verdict cannot be shown in a diff gutter.

The established methods, and what each would buy

Method Guarantee Cost here
Two-phase locking Serializability Needs a lock before the edit — impossible in a Protégé plugin
Optimistic concurrency control Serializability, no blocking Reachable: version counter + refuse a non-fast-forward publish
MVCC / snapshot isolation Readers never block writers Not serializable — write skew is exactly the ontology problem
Operational transformation (ShareDB, ot-text) Identical character sequence Says nothing about meaning; two edits to one axiom interleave into a third
CRDTs (Yjs, Automerge) Strong eventual consistency Agreement on state ≠ correctness; LWW silently drops an author
Three-way merge / diff3 Non-overlapping hunks merge "Separate regions never conflict" is formally not guaranteed
Git merge driver Hook for an OWL-aware merge The right place for one; union takes both sides with no markers
Axiom-level diff (ROBOT diff, Bubastis) Ignores serialisation noise Lower level than how curators think about a change
Structural + logical diff (OWLDiff, ECCO) Catches entailment differences Full semantic diff is tractable only for EL
SHACL / reasoner on the merged result The only "is this good?" A reasoner run per merge

Closest prior art: ContentCVS (Jiménez-Ruiz et al., DKE 2011) — a Protégé 4 plugin for concurrent OWL development built on structural and semantic difference, with debugging and repair. OntoEditor (ESWC 2024) takes the other route: character-level OT, syntax validation only, and a hand-driven side-by-side diff for git history.

What a Protégé plugin can and cannot do

  • Cannot: take a lock before an edit, veto an edit, pre-validate, or transform an incoming operation. ontologiesChanged is past tense, returns void, and has no veto.
  • Can: read every change as typed OWL API objects, keep an op log and a version counter, do an optimistic check at publish time, apply remote changes with applyChanges, run a reasoner over the result, and compensate by applying the inverse change.
  • Good news about the unit: axiom add/add is set union, which is commutative — so the transform function OT needs never arises. Only two real conflict classes remain: remove versus concurrent re-add, and two individually-fine adds whose union is inconsistent.

What is still missing, cheapest first

  1. ~~The editing claim never fires.~~ Done in 1.125.0. Each mapping now says whether it has a single subject; an edge claims nothing, because data["iri"] holds the property there and a claim on the wrong term is worse than none.
  2. ~~PR mergeability.~~ Done in 1.125.0.
  3. Tell the sender when an operation is dropped. Half done in 1.125.0: the receiver is now told, with a running count, instead of the drop being an INFO line nobody reads. The sender still is not — that needs a NACK the protocol has no message for. (1–2 days for the remaining half)
  4. ~~Role enforcement.~~ Done in 1.125.0. A viewer token receives everything, writes nothing, and its holder is told why rather than watching a change vanish.
  5. ~~A read-only conflict report.~~ Done in 1.126.0, for git conflicts. ConflictReview reads git's three stages, ConflictReport names the collisions in curator language, and agreement — the same edit on both sides — is reported apart from it, so the list somebody has to decide about holds only real disagreements.

This item's premise was wrong. It was estimated as needing a base version and applicable changes, which put it behind 6 and 7. For a git conflict git has kept a base version all along: stage 1 is the common ancestor, and no session, lock or vector clock was needed to obtain it. Rendered strings turned out to be enough to name a disagreement; they are only insufficient to resolve one. What is still unreported is the live collaboration path, which genuinely has no ancestor — that is item 7, not this one. 6. Applicable diff objects. Neither diff returns List<OWLOntologyChange>, so nothing can apply a decision once a person has made one — resolution depends on this, where naming did not. (2–3 days) 7. A base version per session, so a three-way merge is possible at all. (1–2 weeks)