Stored merge conflicts (feature merge/conflicts)
On this page4 sections
1.0.0, frozen 2026-10). Backward compatible within 1.x (§16.4). Supersedes the draft v0.1, kept as a historical record; 0.1 files open through the 0.1 → 1.0 migration (§16.2).https://getkimo.design/schema/kimo-1.0.json (JSON Schema draft 2020-12), generated into packages/core-schema/schema/kimo-1.0.schema.json . The 0.1 schema stays published at …/kimo-0.1.json (schema/kimo-0.1.schema.json, frozen).@kimo/core-schema (packages/core-schema). Where this document and the implementation disagree, that is a bug in one of them; file an issue.docs/teardown.md §10. Every place where this spec resolves an ambiguity in, or deviates from, that section is listed in §20.kimo merge (the git merge driver, *.kimo merge=kimo, and the editor's merge of external changes) is semantic and three-way. It never drops a side silently: every disagreement it cannot settle becomes a stored conflict, and the file stays valid JSON that validates, renders and round-trips byte-stably. The editor loads it, lists the conflicts (board 18), applies sides live, and saving with open conflicts keeps them.
21.1 Root members
merge(struct MergeInfo, optional):base,ours,theirs— display labels (≤ 200 chars; git's%S/%X/%Y, e.g."4e1a9c2","main","feat/pricing-v2") — andchanges(integer ≥ 0: changes applied without conflict). Present only together withconflicts.conflicts(record Id → Conflict, at least one entry): conflict ids use the node-id grammar. Serialized as a table (§15.1 rule 3) betweenmergeandnodes: root order is…, assets, components, merge, conflicts, nodes.- Both require feature
merge/conflicts.merge,conflictsand the feature are removed together when the merge is finished ("Mark resolved & save").
21.2 Conflict entries
Every entry has kind, applied and optional resolved, plus base / ours / theirs. A missing side means the value is absent on that side (property unset, node or variable deleted, themed entry missing): JSON has no undefined, so absence is encoded by omitting the key.
kind | Target fields | Sides hold | Board 18 row |
|---|---|---|---|
prop | node (Id), path (JSON Pointer inside the node, ≥ 1 token: /fill, /layout/gap, /args/label, /component/props/size) | the property value | "fill · both changed" |
node | node (Id), restored (optional record Id → Node) | the whole node (Node schema) | "node · deleted vs edited" |
move | node (Id) | Placement { parent, order, at? } — all three sides required | "parent · moved twice" |
variable | variable (local name), when (optional ThemeSelection) | without when: the whole Variable; with when: the value of the themed entry whose when equals it exactly | "$brand · light · both changed" |
header | path (JSON Pointer from the root into meta, themes, imports, assets, components or pages) | the value at path | — |
propnode (Id), path (JSON Pointer inside the node, ≥ 1 token: /fill, /layout/gap, /args/label, /component/props/size)nodenode (Id), restored (optional record Id → Node)movenode (Id){ parent, order, at? } — all three sides requiredvariablevariable (local name), when (optional ThemeSelection)when: the whole Variable; with when: the value of the themed entry whose when equals it exactlyheaderpath (JSON Pointer from the root into meta, themes, imports, assets, components or pages)pathapplied(required): which side the document body currently holds —"ours","theirs","base", or"edited"when the user changed the target by hand after the merge.resolved(boolean, defaultfalse): the user picked a side (or edited the target). Open conflicts are those not resolved.- A
propconflict MUST NOT addresstype(a type change is anodeconflict) orparent/order/at(amoveconflict). nodeconflicts cover delete vs edit (one side absent) and type changes.restoredholds snapshots of the other nodes that are in the body only because the target is kept — its descendants and any ancestors the deleting side removed — so that applying a side is reversible.- Entries are structs:
kindis ranked (Appendix A); the other keys are not, so they follow by code unit and their subtrees are ordered purely by code unit (§15.2). This is deterministic and needs no change to Appendix A.
21.3 Validation
Structure: the Conflict union and MergeInfo are part of the JSON Schema ($defs/Conflict, $defs/MergeInfo, …). Semantic rules (whole-document runs only):
FEATURE_NOT_DECLAREDwhenmergeorconflictsis present withoutmerge/conflicts;MERGE_WITHOUT_CONFLICTS(error);CONFLICT_PATH_INVALID(error);CONFLICT_DUPLICATE(error: two entries on the same target, anodeconflict and aprop/moveconflict on the same node, a whole-variable and a themed-entry conflict on the same variable, or nested pointers);- whole-variable sides MUST be Variables (
SCHEMA); CONFLICT_OPEN(warning) for each unresolved entry —kimo validatein CI flags unfinished merges;CONFLICT_STALE(warning) when the body no longer holdsapplied(including aprop/movetarget whose node is gone). This is deliberately a warning: a hand edit must never make the file open read-only.
21.4 Writing and resolving (normative)
- Body. The merged body holds
oursfor every conflict, except delete vs edit, where it holds the edited side (andrestored);appliedsays so. The result MUST validate (no errors). - Applying a side (
applyConflictSide):propsets or unsets the pointer (creating intermediate objects);nodeputs the side's node and everyrestorednode back, or removes the node, itsrestorednodes and all their descendants;movesetsparent/order/at;variablesets/deletes the variable, or sets/adds/removes the matching themed entry (never the last one);headersets/unsets the pointer. Thenapplied= the side andresolved= true. Applying another side afterwards restores that side exactly. - Take all ours/theirs applies to unresolved entries only.
- Finishing (
finishMerge) is refused while any entry is open; it removesmerge,conflictsand the feature. The body already holds every choice. - Saving with open conflicts writes them as they are. Editors keep autosave paused while any conflict is open; ⌘S writes.
- The three-way merge (contract
ThreeWayMergein@kimo/core-schema): inputs are the base, ours and theirs documents at the reader's format. Header members, variables (per themed entry when both sides kept the variable themed) and nodes (per property, by JSON Pointer into structs; records such asoverrides,args,props,importsmerge per key; arrays are atomic values) merge independently. A change on one side wins; equal changes on both sides are not conflicts; different changes become entries per 21.2. Placement changes (parent,order,at) merge as one unit (move). After merging, cycles are repaired per teardown §9.2 (the node whose move is later — ties: larger id — returns to its previous parent, or the root), and orphans whose parent was deleted on the other side becomenodeconflicts on the parent with the orphans inrestored. Asset manifests merge add-only. - Git driver exit codes (
MERGE_EXIT): 0 clean, 1 merged with stored conflicts (git marks the path conflicted), 2 not mergeable semantically (an input unparsable, invalid or newer): the driver leaves git's textual result, and the editor's failed-load banner explains the markers.
Fixtures: lint/ (board 18: 5 conflicts, 3 resolved), lint/, lint/, valid/, and one invalid/ file per error code.