Selaa lähdekoodia

WIP: Semantics demo

Joeri Exelmans 2 vuotta sitten
vanhempi
commit
406a99f865

+ 2 - 0
package.json

@@ -1,4 +1,5 @@
 {
+	"type": "module",
 	"devDependencies": {
 		"@emotion/react": "^11.10.6",
 		"@mantine/core": "^5.10.5",
@@ -31,6 +32,7 @@
 		"d3-force": "^3.0.0",
 		"d3-scale": "^4.0.2",
 		"d3-selection": "^3.0.0",
+		"graphviz-react": "^1.2.5",
 		"lodash": "^4.17.21",
 		"react": "^18.2.0",
 		"react-dom": "^18.2.0",

+ 93 - 0
pnpm-lock.yaml

@@ -23,6 +23,7 @@ specifiers:
   d3-scale: ^4.0.2
   d3-selection: ^3.0.0
   fork-ts-checker-webpack-plugin: ^7.3.0
+  graphviz-react: ^1.2.5
   lodash: ^4.17.21
   mocha: ^10.2.0
   nyc: ^15.1.0
@@ -50,6 +51,7 @@ dependencies:
   d3-force: 3.0.0
   d3-scale: 4.0.2
   d3-selection: 3.0.0
+  graphviz-react: 1.2.5_react@18.2.0
   lodash: 4.17.21
   react: 18.2.0
   react-dom: 18.2.0_react@18.2.0
@@ -1806,6 +1808,10 @@ packages:
       d3-path: 3.0.1
     dev: false
 
+  /d3-color/1.4.1:
+    resolution: {integrity: sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q==}
+    dev: false
+
   /d3-color/3.1.0:
     resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
     engines: {node: '>=12'}
@@ -1825,11 +1831,22 @@ packages:
       delaunator: 5.0.0
     dev: false
 
+  /d3-dispatch/1.0.6:
+    resolution: {integrity: sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==}
+    dev: false
+
   /d3-dispatch/3.0.1:
     resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==}
     engines: {node: '>=12'}
     dev: false
 
+  /d3-drag/1.2.5:
+    resolution: {integrity: sha512-rD1ohlkKQwMZYkQlYVCrSFxsWPzI97+W+PaEIBNTMxRuxz9RF0Hi5nJWHGVJ3Om9d2fRTe1yOBINJyy/ahV95w==}
+    dependencies:
+      d3-dispatch: 1.0.6
+      d3-selection: 1.4.2
+    dev: false
+
   /d3-drag/3.0.0:
     resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==}
     engines: {node: '>=12'}
@@ -1848,6 +1865,10 @@ packages:
       rw: 1.3.3
     dev: false
 
+  /d3-ease/1.0.7:
+    resolution: {integrity: sha512-lx14ZPYkhNx0s/2HX5sLFUI3mbasHjSSpwO/KaaNACweVwxUruKyWVcb293wMv1RqTPZyZ8kSZ2NogUZNcLOFQ==}
+    dev: false
+
   /d3-ease/3.0.1:
     resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
     engines: {node: '>=12'}
@@ -1869,6 +1890,10 @@ packages:
       d3-timer: 3.0.1
     dev: false
 
+  /d3-format/1.4.5:
+    resolution: {integrity: sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==}
+    dev: false
+
   /d3-format/3.1.0:
     resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==}
     engines: {node: '>=12'}
@@ -1881,11 +1906,31 @@ packages:
       d3-array: 3.2.0
     dev: false
 
+  /d3-graphviz/2.6.1:
+    resolution: {integrity: sha512-878AFSagQyr5tTOrM7YiVYeUC2/NoFcOB3/oew+LAML0xekyJSw9j3WOCUMBsc95KYe9XBYZ+SKKuObVya1tJQ==}
+    dependencies:
+      d3-dispatch: 1.0.6
+      d3-format: 1.4.5
+      d3-interpolate: 1.4.0
+      d3-path: 1.0.9
+      d3-selection: 1.4.2
+      d3-timer: 1.0.10
+      d3-transition: 1.3.2
+      d3-zoom: 1.8.3
+      viz.js: 1.8.2
+    dev: false
+
   /d3-hierarchy/3.1.2:
     resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==}
     engines: {node: '>=12'}
     dev: false
 
+  /d3-interpolate/1.4.0:
+    resolution: {integrity: sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==}
+    dependencies:
+      d3-color: 1.4.1
+    dev: false
+
   /d3-interpolate/3.0.1:
     resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
     engines: {node: '>=12'}
@@ -1893,6 +1938,10 @@ packages:
       d3-color: 3.1.0
     dev: false
 
+  /d3-path/1.0.9:
+    resolution: {integrity: sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==}
+    dev: false
+
   /d3-path/3.0.1:
     resolution: {integrity: sha512-gq6gZom9AFZby0YLduxT1qmrp4xpBA1YZr19OI717WIdKE2OM5ETq5qrHLb301IgxhLwcuxvGZVLeeWc/k1I6w==}
     engines: {node: '>=12'}
@@ -1932,6 +1981,10 @@ packages:
       d3-time-format: 4.1.0
     dev: false
 
+  /d3-selection/1.4.2:
+    resolution: {integrity: sha512-SJ0BqYihzOjDnnlfyeHT0e30k0K1+5sR3d5fNueCNeuhZTnGw4M4o8mqJchSwgKMXCNFo+e2VTChiSJ0vYtXkg==}
+    dev: false
+
   /d3-selection/3.0.0:
     resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==}
     engines: {node: '>=12'}
@@ -1958,11 +2011,26 @@ packages:
       d3-array: 3.2.0
     dev: false
 
+  /d3-timer/1.0.10:
+    resolution: {integrity: sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==}
+    dev: false
+
   /d3-timer/3.0.1:
     resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
     engines: {node: '>=12'}
     dev: false
 
+  /d3-transition/1.3.2:
+    resolution: {integrity: sha512-sc0gRU4PFqZ47lPVHloMn9tlPcv8jxgOQg+0zjhfZXMQuvppjG6YuwdMBE0TuqCZjeJkLecku/l9R0JPcRhaDA==}
+    dependencies:
+      d3-color: 1.4.1
+      d3-dispatch: 1.0.6
+      d3-ease: 1.0.7
+      d3-interpolate: 1.4.0
+      d3-selection: 1.4.2
+      d3-timer: 1.0.10
+    dev: false
+
   /d3-transition/3.0.1_d3-selection@3.0.0:
     resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==}
     engines: {node: '>=12'}
@@ -1977,6 +2045,16 @@ packages:
       d3-timer: 3.0.1
     dev: false
 
+  /d3-zoom/1.8.3:
+    resolution: {integrity: sha512-VoLXTK4wvy1a0JpH2Il+F2CiOhVu7VRXWF5M/LroMIh3/zBAC3WAt7QoIvPibOavVo20hN6/37vwAsdBejLyKQ==}
+    dependencies:
+      d3-dispatch: 1.0.6
+      d3-drag: 1.2.5
+      d3-interpolate: 1.4.0
+      d3-selection: 1.4.2
+      d3-transition: 1.3.2
+    dev: false
+
   /d3-zoom/3.0.0:
     resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==}
     engines: {node: '>=12'}
@@ -2530,6 +2608,16 @@ packages:
   /graceful-fs/4.2.10:
     resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==}
 
+  /graphviz-react/1.2.5_react@18.2.0:
+    resolution: {integrity: sha512-IRFDzEt09hRzfqrrvAW1PAPBqG4t8hykArcoxq7UhEqO5RUKCBN6126D6rjiL2QAwAznbUSg0Fba2RnSH2V4sA==}
+    engines: {npm: '>= 8.3'}
+    peerDependencies:
+      react: '>= 16.13.1'
+    dependencies:
+      d3-graphviz: 2.6.1
+      react: 18.2.0
+    dev: false
+
   /handle-thing/2.0.1:
     resolution: {integrity: sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==}
 
@@ -4266,6 +4354,11 @@ packages:
     resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
     engines: {node: '>= 0.8'}
 
+  /viz.js/1.8.2:
+    resolution: {integrity: sha512-W+1+N/hdzLpQZEcvz79n2IgUE9pfx6JLdHh3Kh8RGvLL8P1LdJVQmi2OsDcLdY4QVID4OUy+FPelyerX0nJxIQ==}
+    deprecated: 2.x is no longer supported, 3.x published as @viz-js/viz
+    dev: false
+
   /watchpack/2.4.0:
     resolution: {integrity: sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==}
     engines: {node: '>=10.13.0'}

+ 18 - 6
src/frontend/app.tsx

@@ -13,6 +13,7 @@ import {demo_BM_description, getDemoBM} from "./demos/demo_bm";
 import {demo_Editor_description, getDemoEditor} from "./demos/demo_editor";
 import {demo_Welcome_description, Welcome} from "./demos/demo_welcome";
 import {demo_LE_description, getDemoLE} from "./demos/demo_le";
+import {demo_Sem_description, getDemoSem} from "./demos/demo_sem";
 
 export function getApp() {
     const DemoEditor = getDemoEditor();
@@ -20,6 +21,7 @@ export function getApp() {
     const DemoCorr = getDemoCorr();
     const DemoBM = getDemoBM();
     const DemoLE = getDemoLE();
+    const DemoSem = getDemoSem();
 
     return function App(props) {
         React.useEffect(() => {
@@ -31,6 +33,8 @@ export function getApp() {
         // detect if user has light or dark color scheme going on :)
         const preferredColorScheme = useColorScheme();
 
+        const tabStyle = {width: '100%', border: 0};
+
         return <>
             <MantineProvider theme={{colorScheme: preferredColorScheme}} withGlobalStyles withNormalizeCSS>
                 <Styledtabs defaultValue="welcome" orientation="vertical" style={{height: '100%'}}>
@@ -39,13 +43,14 @@ export function getApp() {
                             <Stack style={{height: '100%', direction: 'column', gap: '0px'}}>
                                 <Title order={4} style={{paddingLeft: '5px'}}>Pick a demo:</Title>
                                 <Tabs.List>
-                                    <Tabs.Tab style={{width: '100%', border: 0}} value="welcome">Welcome</Tabs.Tab>
-                                    <Tabs.Tab style={{width: '100%', border: 0}} value="pd">Primitive Deltas</Tabs.Tab>
-                                    <Tabs.Tab style={{width: '100%', border: 0}} value="editor">Rountangle
+                                    <Tabs.Tab style={tabStyle} value="welcome">Welcome</Tabs.Tab>
+                                    <Tabs.Tab style={tabStyle} value="pd">Primitive Deltas</Tabs.Tab>
+                                    <Tabs.Tab style={tabStyle} value="editor">Rountangle
                                         Editor</Tabs.Tab>
-                                    <Tabs.Tab style={{width: '100%', border: 0}} value="corr">Correspondence</Tabs.Tab>
-                                    <Tabs.Tab style={{width: '100%', border: 0}} value="bm">Blended Modeling</Tabs.Tab>
-                                    <Tabs.Tab style={{width: '100%', border: 0}} value="le">List Editor</Tabs.Tab>
+                                    <Tabs.Tab style={tabStyle} value="corr">Correspondence</Tabs.Tab>
+                                    <Tabs.Tab style={tabStyle} value="bm">Blended Modeling</Tabs.Tab>
+                                    <Tabs.Tab style={tabStyle} value="le">List Editor</Tabs.Tab>
+                                    <Tabs.Tab style={tabStyle} value="sem">Semantics</Tabs.Tab>
                                 </Tabs.List>
                                 <Divider my="md"/>
                                 <div style={{overflow: 'hidden', paddingLeft: '5px'}}>
@@ -70,6 +75,9 @@ export function getApp() {
                                             <Tabs.Panel value="le" style={{height: '100%'}}>
                                                 {demo_LE_description}
                                             </Tabs.Panel>
+                                            <Tabs.Panel value="sem" style={{height: '100%'}}>
+                                                {demo_Sem_description}
+                                            </Tabs.Panel>
                                         </div>
                                     </ScrollArea>
                                 </div>
@@ -107,6 +115,10 @@ export function getApp() {
                                             {/* DemoLE comes with its own OnionContext provider */}
                                             <DemoLE/>
                                         </Tabs.Panel>
+                                        <Tabs.Panel value="sem">
+                                            {/* DemoLE comes with its own OnionContext provider */}
+                                            <DemoSem/>
+                                        </Tabs.Panel>
                                         <Anchor href="https://msdl.uantwerpen.be/git/jexelmans/onioncollab"
                                                 target="_blank"
                                                 style={{position: "absolute", bottom: 8, right: 8, fontWeight: 'bold'}}

+ 16 - 6
src/frontend/d3graph/reducers/history_graph.ts

@@ -5,9 +5,9 @@ import {D3GraphData, D3NodeData, D3LinkData} from "../d3graph";
 export type HistoryGraphState = D3GraphData<Version,Delta>;
 
 interface AppendToHistoryGraph {type:'addVersion', version: Version}
-interface SetCurrentVersion {type:'highlightVersion', version: Version, bold: boolean, overrideColor?: string}
+interface HighlightVersion {type:'highlightVersion', version: Version, bold?: boolean, overrideColor?: string, resetColor?: boolean}
 
-export type HistoryGraphAction = Readonly<SetCurrentVersion> | Readonly<AppendToHistoryGraph>;
+export type HistoryGraphAction = Readonly<HighlightVersion> | Readonly<AppendToHistoryGraph>;
 
 export function historyGraphReducer(prevState: HistoryGraphState, action: HistoryGraphAction): HistoryGraphState {
   switch (action.type) {
@@ -29,7 +29,13 @@ export function historyGraphReducer(prevState: HistoryGraphState, action: Histor
     case 'highlightVersion': {
       const nodes = prevState.nodes.map(n => {
         if (n.obj === action.version) {
-          return versionToNode(action.version, action.bold, n.x, n.y, action.overrideColor);
+          const {color, bold, ...rest} = n;
+          return {
+            color: action.overrideColor ? action.overrideColor : action.resetColor ? versionColor(n.obj) : color,
+            bold: action.bold !== undefined ? action.bold : bold,
+            ...rest,
+          }
+          // return versionToNode(action.version, action.bold, n.x, n.y, action.overrideColor);
         }
         else {
           return n;
@@ -66,13 +72,17 @@ export function fullVersionId(version: Version): string {
 
 // Helpers
 
-function versionToNode(version: Version, bold: boolean, x?: number, y?: number, overrideColor?: string): D3NodeData<Version> {
+function versionColor(version: Version): string {
+  return (version.parents.length === 0 ? "grey" : "purple");
+}
+
+function versionToNode(version: Version, bold?: boolean, x?: number, y?: number, overrideColor?: string): D3NodeData<Version> {
   return {
     id: fullVersionId(version),
     label: (version.parents.length === 0 ? "initial" : ""),
-    color: overrideColor ? overrideColor : (version.parents.length === 0 ? "grey" : "purple"),
+    color: overrideColor ? overrideColor : versionColor(version),
     obj: version,
-    bold,
+    bold: bold === true, // may be true, false or undefined
     x, y,
   }
 }

+ 5 - 5
src/frontend/demos/demo_bm.tsx

@@ -144,7 +144,7 @@ export function getDemoBM() {
         const corr1Reducer = corr1.getReducer(setCorr1State, cs1Reducer, asReducer);
         const corr2Reducer = corr2.getReducer(setCorr2State, cs2Reducer, asReducer);
 
-        const asComponents = as.getReactComponents(asState, {
+        const asComponents = as.getReactComponents(asState, setAsState, {
             onUserEdit: (deltas, description) => {
                 const newVersion = asReducer.createAndGotoNewVersion(deltas, description);
                 if (autoRender1) {
@@ -159,7 +159,7 @@ export function getDemoBM() {
             onVersionClicked: asReducer.gotoVersion,
         });
 
-        const cs1Components = cs1.getReactComponents(cs1State, {
+        const cs1Components = cs1.getReactComponents(cs1State, setCs1State, {
             onUserEdit: (deltas, description) => {
                 const newVersion = cs1Reducer.createAndGotoNewVersion(deltas, description);
                 if (autoParse1) {
@@ -170,7 +170,7 @@ export function getDemoBM() {
             onRedoClicked: cs1Reducer.redo,
             onVersionClicked: cs1Reducer.gotoVersion,
         });
-        const cs2Components = cs2.getReactComponents(cs2State, {
+        const cs2Components = cs2.getReactComponents(cs2State, setCs2State, {
             onUserEdit: (deltas, description) => {
                 const newVersion = cs2Reducer.createAndGotoNewVersion(deltas, description);
                 if (autoParse2) {
@@ -182,12 +182,12 @@ export function getDemoBM() {
             onVersionClicked: cs2Reducer.gotoVersion,
         });
 
-        const corr1Components = corr1.getReactComponents(corr1State, {
+        const corr1Components = corr1.getReactComponents(corr1State, setCorr1State, {
             onUndoClicked: corr1Reducer.gotoVersion,
             onRedoClicked: corr1Reducer.gotoVersion,
             onVersionClicked: corr1Reducer.gotoVersion,
         });
-        const corr2Components = corr2.getReactComponents(corr2State, {
+        const corr2Components = corr2.getReactComponents(corr2State, setCorr2State, {
             onUndoClicked: corr2Reducer.gotoVersion,
             onRedoClicked: corr2Reducer.gotoVersion,
             onVersionClicked: corr2Reducer.gotoVersion,

+ 3 - 3
src/frontend/demos/demo_corr.tsx

@@ -133,7 +133,7 @@ export function getDemoCorr() {
         const csReducer = cs.getReducer(setCsState);
         const corrReducer = corr.getReducer(setCorrState, csReducer, asReducer);
 
-        const csComponents = cs.getReactComponents(csState, {
+        const csComponents = cs.getReactComponents(csState, setCsState, {
             onUserEdit: (deltas, description) => {
                 const newVersion = csReducer.createAndGotoNewVersion(deltas, description);
                 if (autoParse) {
@@ -144,12 +144,12 @@ export function getDemoCorr() {
             onRedoClicked: csReducer.redo,
             onVersionClicked: csReducer.gotoVersion,
         });
-        const corrComponents = corr.getReactComponents(corrState, {
+        const corrComponents = corr.getReactComponents(corrState, setCorrState, {
             onUndoClicked: corrReducer.gotoVersion,
             onRedoClicked: corrReducer.gotoVersion,
             onVersionClicked: corrReducer.gotoVersion,
         });
-        const asComponents = as.getReactComponents(asState, {
+        const asComponents = as.getReactComponents(asState, setAsState, {
             onUserEdit: (deltas, description) => {
                 const newVersion = asReducer.createAndGotoNewVersion(deltas, description);
                 if (autoRender) {

+ 2 - 2
src/frontend/demos/demo_editor.tsx

@@ -80,7 +80,7 @@ export function getDemoEditor() {
 
         const csReducer = cs.getReducer(setCsState);
 
-        const csComponents = cs.getReactComponents(csState, {
+        const csComponents = cs.getReactComponents(csState, setCsState, {
             onUserEdit: (deltas, description) => {
                 const newVersion = csReducer.createAndGotoNewVersion(deltas, description);
             },
@@ -120,7 +120,7 @@ export function getDemoEditor() {
                     {csComponents.makeTabs("deltaL1", deltaTabs)}
                 </div>
                 <div>
-                    <Title order={4}>State Graph</Title>
+                    <Title order={4}>Graph State</Title>
                     <Space h="48px"/>
                     {csComponents.graphStateComponent}
                 </div>

+ 33 - 8
src/frontend/demos/demo_le.tsx

@@ -1,5 +1,5 @@
 import * as React from "react";
-import {SimpleGrid, Text, Title, Stack, Center, Group, Space, Image, Button, Paper, Alert, createStyles} from "@mantine/core";
+import {SimpleGrid, Text, Title, Stack, Center, Group, Space, Image, Button, Paper, Alert, createStyles, Switch} from "@mantine/core";
 import {IconTrash, IconRowInsertTop, IconRowInsertBottom, IconAlertCircle} from '@tabler/icons';
 
 import {PrimitiveRegistry, PrimitiveDelta} from "onion/primitive_delta";
@@ -23,7 +23,7 @@ export const demo_LE_description =
             This demo was not mentioned in our (submitted) paper, and was added in response to a question from one of the reviewers. The reviewer wanted to know if we can support data structures other than objects diagrams with primitive values in the objects' slots.
         </Text>
         <Text>
-            This demo shows one particular way of encoding an ordered sequence as a graph structure (namely as a doubly-linked list). The list itself is represented by a single node in the graph state. The user can insert/remove items at any point into the sequence.
+            This demo shows one particular way of encoding an ordered sequence as a graph structure (namely as a doubly-linked list). The list itself is represented by a single node in the graph state. The user can insert/remove items at any point in the sequence. Concurrent insertions can be merged, as long as the insertions are not at the same location. Deletions never give conflicts.
         </Text>
     </>;
 
@@ -40,6 +40,7 @@ export function getDemoLE() {
     // returns functional react component
     return function () {
         const [modelState, setModelState] = React.useState<VersionedModelState>(model.initialState);
+        const [pseudoDelete, setPseudoDelete] = React.useState<boolean>(true);
         const modelReducer = model.getReducer(setModelState);
 
         React.useEffect(() => {
@@ -76,7 +77,7 @@ export function getDemoLE() {
 
             const deltas = model.graphState.popState();
 
-            modelReducer.createAndGotoNewVersion(deltas, "insert"+JSON.stringify(val));            
+            modelReducer.createAndGotoNewVersion(deltas, "insert"+JSON.stringify(val));
         }
 
         function insertBefore(node: INodeState, val: PrimitiveValue) {
@@ -104,7 +105,14 @@ export function getDemoLE() {
             modelReducer.createAndGotoNewVersion(deltas, "delete"+JSON.stringify((node.getOutgoingEdges().get("value") as IValueState).value));
         }
 
-        const modelComponents = model.getReactComponents(modelState, {
+        // Alternative implementation of deletion - just add a property 'deleted' to the deleted node.
+        function deleteItemAlt(node: INodeState) {
+            const deltas = node.getDeltasForSetEdge(primitiveRegistry, "deleted", true);
+
+            modelReducer.createAndGotoNewVersion(deltas, "delete"+JSON.stringify((node.getOutgoingEdges().get("value") as IValueState).value));
+        }
+
+        const modelComponents = model.getReactComponents(modelState, setModelState, {
             onUserEdit: (deltas, description) => {
                 const newVersion = modelReducer.createAndGotoNewVersion(deltas, description);
             },
@@ -118,19 +126,23 @@ export function getDemoLE() {
 
         // Recursively renders list elements as a vertical stack
         function renderList(head: INodeState, stopAt: INodeState) {
+            const nextItem = head.getOutgoingEdges().get("next") as INodeState;
+            const nextDOM = nextItem === stopAt ? <></> : renderList(nextItem, stopAt);
+            if (head.getOutgoingEdges().get("deleted")?.asTarget() === true) {
+                return nextDOM; // skip deleted items
+            }
             const value = head.getOutgoingEdges().get("value");
             const itemText = value === undefined ? "Start of list" : "List item: " + JSON.stringify((value as IValueState).value);
-            const nextItem = head.getOutgoingEdges().get("next") as INodeState;
             return <>
                 <Paper shadow="sm" radius="md" p="md" withBorder>
                     <Group>
                         {itemText}
                         <Button onClick={() => insertBefore(head, nextVal++)} compact leftIcon={<IconRowInsertBottom/>}>Insert before</Button>
                         <Button onClick={() => insertAfter(head, nextVal++)} compact leftIcon={<IconRowInsertTop/>}>Insert after</Button>
-                        {value === undefined ? <></> : <Button compact onClick={() => deleteItem(head)} leftIcon={<IconTrash />}>Delete</Button>}
+                        {value === undefined ? <></> : <Button compact onClick={() => pseudoDelete ? deleteItemAlt(head) : deleteItem(head)} leftIcon={<IconTrash />}>Delete</Button>}
                     </Group>
                 </Paper>
-                {nextItem === stopAt ? <></> : renderList(nextItem, stopAt)}
+                {nextDOM}
             </>;
         }
 
@@ -140,6 +152,19 @@ export function getDemoLE() {
                     <div>
                         <Stack>
                             <Title order={5}>List Editor</Title>
+                            <Group>
+                                <Switch label="Enable Pseudo-Delete"
+                                    labelPosition="right"
+                                    checked={pseudoDelete}
+                                    onChange={(event) => setPseudoDelete(event.currentTarget.checked)}
+                                  />
+                                <InfoHoverCard>
+                                    <Text>
+                                        <p>Deletions of list items can be performed in two ways: Pseudo-deletion and real deletion. With pseudo-delete, deleted list items are kept in the doubly-linked list, and are only marked "deleted". "Real" deletions on the other hand, alter the "prev" and "next" edges of the neighboring nodes of deleted items.</p>
+                                        <p>Pseudo-deletions are never conflicting with any other operations, which may be desired, depending on the context. Real deletions can give conflicts with both insertions and other deletions.</p>
+                                    </Text>
+                                </InfoHoverCard>
+                            </Group>
                             {
                             listNode === undefined ?
                                 <Alert icon={<IconAlertCircle/>} title="There exists no list!" radius="md">
@@ -169,7 +194,7 @@ export function getDemoLE() {
                     </div>
                     <div>
                         <Stack>
-                            <Title order={5}>State Graph (read-only)</Title>
+                            <Title order={5}>Graph State (read-only)</Title>
                             {modelComponents.graphStateComponent}
                             <div>
                             Nodes:

+ 2 - 1
src/frontend/demos/demo_pd.tsx

@@ -120,11 +120,12 @@ export function getDemoPD() {
 
             const reducer = getReducer(setBranchState);
 
-            const components = getReactComponents(branchState, {
+            const components = getReactComponents(branchState, setBranchState, {
                 onUserEdit: reducer.createAndGotoNewVersion,
                 onUndoClicked: reducer.undo,
                 onRedoClicked: reducer.redo,
                 onVersionClicked: reducer.gotoVersion,
+                onMerge: reducer.appendVersions,
             });
 
             const unionClicked = () => {

+ 284 - 0
src/frontend/demos/demo_sem.tsx

@@ -0,0 +1,284 @@
+import * as React from 'react';
+import * as Icons from '@tabler/icons';
+import {Button, Divider, Group, Image, SimpleGrid, Space, Text, Title, TextInput, Select, Stack} from '@mantine/core';
+
+import { Graphviz } from 'graphviz-react';
+
+import {newVersionedModel, undoButtonHelpText, VersionedModelState,} from '../versioned_model/single_model';
+import {OnionContext} from "../onion_context";
+import {mockUuid} from "onion/test_helpers";
+import {PrimitiveRegistry, PrimitiveDelta} from "onion/primitive_delta";
+import {INodeState, IValueState} from "onion/graph_state";
+
+export const demo_Sem_description = <>
+  <Title order={4}>
+    Semantics
+  </Title>
+</>;
+
+export function getDemoSem() {
+  const primitiveRegistry = new PrimitiveRegistry();
+  const generateUUID = mockUuid();
+
+  const designModelId = generateUUID();
+
+  const designModel = newVersionedModel({readonly: true});
+
+  return function DemoSem() {
+
+    const [designModelState, setDesignModelState] = React.useState<VersionedModelState>(designModel.initialState);
+
+    const designModelReducer = designModel.getReducer(setDesignModelState);
+
+    // We start out with the 'createList' delta already having occurred:
+    React.useEffect(() => {
+        // idempotent:
+        const designModelCreation = primitiveRegistry.newNodeCreation(designModelId);
+        designModelReducer.createAndGotoNewVersion([
+            designModelCreation,
+            primitiveRegistry.newEdgeCreation(designModelCreation, "type", "DesignModel"),
+        ], "createDesignModel", designModel.initialState.version);
+    }, []);
+
+
+    const [dotGraph, setDotGraph] = React.useState("digraph {}");
+
+    const [initialState, setInitialState] = React.useState<string|null>(null);
+    const [currentState, setCurrentState] = React.useState<string|null>(null);
+
+    // Whenever the current version changes, we calculate a bunch of values that are needed in the UI and in its callbacks
+    const [states, transitions, designModelNode, initial,
+      runtimeModelNode, current] = React.useMemo(() => {
+      const states: Array<[string, INodeState]> = [];
+      const transitions: Array<[string, string, string, INodeState]> = [];
+      let designModelNode: INodeState|null = null;
+      let runtimeModelNode: INodeState|null = null;
+      let initialStateName : string|null = null;
+      let currentStateName : string|null = null;
+      let initial;
+      let current;
+      for (const nodeState of designModel.graphState.nodes.values()) {
+        if (nodeState.isDeleted) {
+          continue;
+        }
+        const getStateName = s => (s.getOutgoingEdges().get("name") as IValueState).value as string;
+
+        const nodeType = (nodeState.getOutgoingEdges().get("type") as IValueState)?.value as string;
+
+        if (nodeType === "State") {
+          states.push([getStateName(nodeState), nodeState]);
+        }
+
+        if (nodeType === "Transition") {
+          transitions.push([
+            getStateName(nodeState.getOutgoingEdges().get("src") as INodeState),
+            getStateName(nodeState.getOutgoingEdges().get("tgt") as INodeState),
+            (nodeState.getOutgoingEdges().get("event") as IValueState).value as string,
+            nodeState,
+          ]);
+        }
+
+        if (nodeType === "DesignModel") {
+          designModelNode = nodeState;
+          initial = nodeState.getOutgoingEdges().get("initial") as INodeState;
+          initialStateName = initial ? getStateName(initial) : null;
+          setInitialState(initialStateName);
+        }
+
+        if (nodeType === "RuntimeModel") {
+          runtimeModelNode = nodeState;
+          current = nodeState.getOutgoingEdges().get("current") as INodeState;
+          currentStateName = current ? getStateName(current) : null;
+          setCurrentState(currentStateName);
+        }
+      }
+
+      setDotGraph(`
+        digraph {
+          ${states.map(([name])=>name
+            + (name === initialStateName ? `[color=blue, style=filled, fontcolor=white]`:``)
+            + (name === currentStateName ? `[shape=doublecircle]`:`[shape=circle]`)
+          ).join(' ')}
+          ${transitions.map(([src,tgt,label])=> src + ' -> ' + tgt + ' [label='+label+']').join('\n')}
+        }`);
+
+      return [states, transitions, designModelNode, initial,
+        runtimeModelNode, current];
+    }, [designModelState.version]);
+
+    const designModelComponents = designModel.getReactComponents(designModelState, setDesignModelState, {
+        onUserEdit: (deltas, description) => {
+            const newVersion = designModelReducer.createAndGotoNewVersion(deltas, description);
+        },
+        onUndoClicked: designModelReducer.undo,
+        onRedoClicked: designModelReducer.redo,
+        onVersionClicked: designModelReducer.gotoVersion,
+        onMerge: designModelReducer.appendVersions,
+    });
+
+    const [addStateName, setAddStateName] = React.useState<string>("A");
+    const [addTransitionSrc, setAddTransitionSrc] = React.useState<string>("");
+    const [addTransitionTgt, setAddTransitionTgt] = React.useState<string>("");
+    const [addTransitionEvent, setAddTransitionEvent] = React.useState<string>("e");
+
+    function addState() {
+      if (designModelNode !== null) {
+        designModel.graphState.pushState();
+
+        const nodeId = generateUUID();
+        const nodeCreation = primitiveRegistry.newNodeCreation(nodeId);
+        designModel.graphState.exec(nodeCreation);
+        designModel.graphState.exec(primitiveRegistry.newEdgeCreation(nodeCreation, "type", "State"));
+        designModel.graphState.exec(primitiveRegistry.newEdgeCreation(nodeCreation, "name", addStateName));
+
+        designModelNode.getDeltasForSetEdge(primitiveRegistry, "contains-"+JSON.stringify(nodeId.value), nodeCreation).forEach(d => designModel.graphState.exec(d));
+
+        const deltas = designModel.graphState.popState();
+        designModelReducer.createAndGotoNewVersion(deltas, "addState:"+addStateName);
+
+        // Auto-increment state names
+        if (addStateName.match(/[A-Z]/i)) {
+          setAddStateName(String.fromCharCode(addStateName.charCodeAt(0) + 1));
+        }
+      }
+    }
+
+    const getState = name => {
+      const s = states.find(([n]) => n === name);
+      if (s !== undefined) return s[1].creation;
+    }
+
+    function addTransition() {
+      if (designModelNode !== null) {
+        designModel.graphState.pushState();
+
+        const nodeId = generateUUID();
+        const nodeCreation = primitiveRegistry.newNodeCreation(nodeId);
+        designModel.graphState.exec(nodeCreation);
+        designModel.graphState.exec(primitiveRegistry.newEdgeCreation(nodeCreation, "type", "Transition"));
+        designModel.graphState.exec(primitiveRegistry.newEdgeCreation(nodeCreation, "event", addTransitionEvent));
+        designModel.graphState.exec(primitiveRegistry.newEdgeCreation(nodeCreation, "src", getState(addTransitionSrc)!));
+        designModel.graphState.exec(primitiveRegistry.newEdgeCreation(nodeCreation, "tgt", getState(addTransitionTgt)!));
+
+        designModelNode.getDeltasForSetEdge(primitiveRegistry, "contains-"+JSON.stringify(nodeId.value), nodeCreation).forEach(d => designModel.graphState.exec(d));
+
+        const deltas = designModel.graphState.popState();
+        designModelReducer.createAndGotoNewVersion(deltas, "addTransition:"+addTransitionSrc+"--"+addTransitionEvent+"->"+addTransitionTgt);
+      }
+    }
+
+    function onDeleteState(name: string, nodeState: INodeState) {
+      if (designModelNode !== null) {
+        designModel.graphState.pushState();
+        while (true) {
+          // Delete all incoming and outgoing transitions:
+          const found = nodeState.getIncomingEdges().find(([label]) => label === "src" || label === "tgt");
+          if (found === undefined) break;
+          const [_, from] = found;
+          const deltas = from.getDeltasForDelete(primitiveRegistry);
+          console.log({deltas});
+          deltas.forEach(d => designModel.graphState.exec(d));
+        }
+        nodeState.getDeltasForDelete(primitiveRegistry).forEach(d => designModel.graphState.exec(d));
+        const deltas = designModel.graphState.popState();
+        console.log({deltas});
+        designModelReducer.createAndGotoNewVersion(deltas, "deleteState:"+name);
+      }
+    }
+
+    function onInitialStateChange(value) {
+      if (designModelNode !== null) {
+        designModel.graphState.pushState();
+        designModelNode.getDeltasForSetEdge(primitiveRegistry, "initial", getState(value) || null).forEach(d => designModel.graphState.exec(d));
+        const deltas = designModel.graphState.popState();
+        designModelReducer.createAndGotoNewVersion(deltas, "setInitial:"+value);
+        setInitialState(value);
+      }
+    }
+
+    function onInitialize() {
+      if (designModelNode !== null) {
+        designModel.graphState.pushState();
+        const runtimeId = generateUUID();
+        const nodeCreation = primitiveRegistry.newNodeCreation(runtimeId);
+        designModel.graphState.exec(nodeCreation);
+        designModel.graphState.exec(primitiveRegistry.newEdgeCreation(nodeCreation, "type", "RuntimeModel"));
+        designModel.graphState.exec(primitiveRegistry.newEdgeCreation(nodeCreation, "design", designModelNode.creation));
+        designModel.graphState.exec(primitiveRegistry.newEdgeCreation(nodeCreation, "current", initial.creation));
+        const deltas = designModel.graphState.popState();
+        designModelReducer.createAndGotoNewVersion(deltas, "initialize");
+      }
+    }
+
+    function onAbort() {
+      if (runtimeModelNode !== null) {
+        designModel.graphState.pushState();
+        runtimeModelNode.getDeltasForDelete(primitiveRegistry).forEach(d => designModel.graphState.exec(d));
+        const deltas = designModel.graphState.popState();
+        designModelReducer.createAndGotoNewVersion(deltas, "abort");
+        setCurrentState(null);
+      }
+    }
+
+    function onCurrentStateChange(value) {
+      if (runtimeModelNode !== null) {
+        designModel.graphState.pushState();
+        runtimeModelNode.getDeltasForSetEdge(primitiveRegistry, "current", getState(value) || null).forEach(d => designModel.graphState.exec(d));
+        const deltas = designModel.graphState.popState();
+        designModelReducer.createAndGotoNewVersion(deltas, "setCurrent:"+value);
+        setCurrentState(value);
+      }
+    }
+
+    return <>
+      <OnionContext.Provider value={{generateUUID, primitiveRegistry}}>
+        <SimpleGrid cols={2}>
+          <Stack>
+            <Group grow>
+              {designModelComponents.undoRedoButtons}
+            </Group>
+            <Group>
+              <TextInput value={addStateName} onChange={e => setAddStateName(e.currentTarget.value)} label="State Name" withAsterisk/>
+              <Button onClick={addState} disabled={addStateName===""||states.some(([name]) => name ===addStateName)} leftIcon={<Icons.IconPlus/>}>Add State</Button>
+            </Group>
+            <Group>
+              <Select searchable clearable label="Source" data={states.map(([stateName]) => ({value:stateName, label:stateName}))} value={addTransitionSrc} onChange={setAddTransitionSrc}/>
+              <Select searchable clearable label="Target" data={states.map(([stateName]) => ({value:stateName, label:stateName}))} value={addTransitionTgt} onChange={setAddTransitionTgt}/>
+              <TextInput value={addTransitionEvent} onChange={e => setAddTransitionEvent(e.currentTarget.value)}  label="Event" />
+              <Button disabled={addTransitionSrc === null || addTransitionTgt === null} onClick={addTransition} leftIcon={<Icons.IconPlus/>}>Add Transition</Button>
+            </Group>
+            <Group>
+              {
+                states.map(([stateName, stateNodeState]) => {
+                  return <Button key={stateName} leftIcon={<Icons.IconX/>} onClick={() => onDeleteState(stateName, stateNodeState)}>Delete State {stateName}</Button>
+                })
+              }
+            </Group>
+            <Group>
+              {
+                transitions.map(([srcName, tgtName, label, tNodeState]) => {
+                  const key = srcName+'--('+label+')-->'+tgtName;
+                  return <Button key={key} leftIcon={<Icons.IconX/>} onClick={() => onDeleteState(key, tNodeState)}>Delete Transition {key}</Button>
+                })
+              }
+            </Group>
+            <Group>
+              <Select searchable clearable label="Initial State" data={states.map(([stateName]) => ({value:stateName, label:stateName}))} value={initialState} onChange={onInitialStateChange}/>
+              <Button disabled={initialState === null || runtimeModelNode !== null} onClick={onInitialize} leftIcon={<Icons.IconPlayerPlay/>}>Initialize Execution</Button>
+            </Group>
+            <Group>
+              <Select disabled={runtimeModelNode === null} searchable clearable label="Current State" data={states.map(([stateName]) => ({value:stateName, label:stateName}))} value={currentState} onChange={onCurrentStateChange}/>
+              <Button disabled={runtimeModelNode === null} onClick={onAbort} leftIcon={<Icons.IconPlayerStop/>}>Abort Execution</Button>
+            </Group>
+            <Graphviz dot={dotGraph} options={{fit:false}} className="canvas"/>
+            <Text>Powered by GraphViz and WebAssembly.</Text>
+          </Stack>
+          <Stack>
+            {designModelComponents.makeTabs("merge", ["state", "merge", "deltaL1", "deltaL0"])}
+            {designModelComponents.makeTabs("deltaL1", ["state", "merge", "deltaL1", "deltaL0"])}
+          </Stack>
+        </SimpleGrid>
+      </OnionContext.Provider>
+    </>;
+  }
+}

+ 17 - 9
src/frontend/versioned_model/merge_view.tsx

@@ -8,6 +8,9 @@ import {D3Graph} from "../d3graph/d3graph";
 import {fullVersionId, HistoryGraphState, historyGraphReducer} from "../d3graph/reducers/history_graph";
 import {InfoHoverCardOverlay} from "../info_hover_card";
 
+const inputColor = 'seashell';
+const outputColor = 'lightblue';
+
 export const historyGraphHelpText = <>
   <Mantine.Divider label="Legend" labelPosition="center"/>
   <Mantine.Text>
@@ -24,17 +27,19 @@ export const historyGraphHelpText = <>
   </Mantine.Text>
 </>;
 
-export function MergeView({history, forces, versionRegistry, onMerge, onGoto}) {
+export function MergeView({history, setHistory, forces, versionRegistry, onMerge, onGoto}) {
   const [inputs, setInputs] = React.useState<Version[]>([]);
   const [outputs, setOutputs] = React.useState<Version[]>([]);
 
+  // TODO: better to move the style of D3Graph nodes/links to a separate (React state) data structure.
+  // An update of the style will then not trigger an update of the layout.
   const historyHighlightedInputs = inputs.reduce(
     (history, version) =>
-      historyGraphReducer(history, {type: 'highlightVersion', version, bold: false, overrideColor: 'white'}),
+      historyGraphReducer(history, {type: 'highlightVersion', version, overrideColor: inputColor}),
     history);
   const historyHighlighted = outputs.reduce(
     (history, version) =>
-      historyGraphReducer(history, {type: 'highlightVersion', version, bold: false, overrideColor: 'yellow'}),
+      historyGraphReducer(history, {type: 'highlightVersion', version, overrideColor: outputColor}),
     historyHighlightedInputs);
 
   const removeButton = version => (
@@ -56,10 +61,10 @@ export function MergeView({history, forces, versionRegistry, onMerge, onGoto}) {
             // @ts-ignore:
             if (e.button === 2) { // right mouse button
               if (inputs.includes(node.obj)) {
-                setInputs(inputs.filter(v => v !== node.obj));
+                setInputs(inputs => inputs.filter(v => v !== node.obj));
               }
               else {
-                setInputs(inputs.concat(node.obj));
+                setInputs(inputs => inputs.concat(node.obj));
               }
               setOutputs([]);
             }
@@ -74,7 +79,7 @@ export function MergeView({history, forces, versionRegistry, onMerge, onGoto}) {
     <Mantine.Group>
       { inputs.length === 0 ? <></> :
         <>
-          {inputs.map(version => <Mantine.Badge key={fullVersionId(version)} pr={3} variant="outline" color="dark" rightSection={removeButton(version)}>
+          {inputs.map(version => <Mantine.Badge key={fullVersionId(version)} pr={3} variant="outline" color="dark" style={{backgroundColor: inputColor}} rightSection={removeButton(version)}>
             {fullVersionId(version).slice(0,8)}
             </Mantine.Badge>)}
         </> }
@@ -82,15 +87,18 @@ export function MergeView({history, forces, versionRegistry, onMerge, onGoto}) {
         <>
           <Icons.IconArrowNarrowRight/>
           {/*Merge Outputs:*/}
-          {outputs.map(version => <Mantine.Badge key={fullVersionId(version)} variant="outline" color="dark" style={{backgroundColor: 'yellow'}}>
+          {outputs.map(version => <Mantine.Badge key={fullVersionId(version)} variant="outline" color="dark" style={{backgroundColor: outputColor}}>
             {fullVersionId(version).slice(0,8)}
             </Mantine.Badge>)}
         </> }
     </Mantine.Group>
     <Mantine.Group grow>
-      <Mantine.Button compact variant="light" disabled={inputs.length===0 && outputs.length===0} onClick={() => {setInputs([]); setOutputs([]);}}>Clear Selection</Mantine.Button>
+      <Mantine.Button compact variant="light" disabled={inputs.length===0 && outputs.length===0} onClick={() => {
+        setInputs([]);
+        setOutputs([]);
+      }}>Clear Selection</Mantine.Button>
       <Mantine.Button compact leftIcon={<Icons.IconArrowMerge/>} disabled={inputs.length===0} onClick={() => {
-        const outputs = versionRegistry.merge(inputs);
+        const outputs = versionRegistry.merge(inputs, d => d.getDescription());
         setOutputs(outputs);
         onMerge(outputs);
       }}>Merge</Mantine.Button>

+ 2 - 1
src/frontend/versioned_model/single_model.tsx

@@ -215,7 +215,7 @@ export function newVersionedModel({readonly}) {
     };
   }
 
-  function getReactComponents(state: VersionedModelState, callbacks: VersionedModelCallbacks) {
+  function getReactComponents(state: VersionedModelState, setState: React.Dispatch<React.SetStateAction<VersionedModelState>>, callbacks: VersionedModelCallbacks) {
     const graphStateComponent = <InfoHoverCardOverlay
       contents={readonly ? helpText.graphEditorReadonly : helpText.graphEditor}>
         {readonly ? 
@@ -244,6 +244,7 @@ export function newVersionedModel({readonly}) {
 
     const historyComponentWithMerge = <MergeView
       history={state.historyGraph}
+      setHistory={callback => setState(({historyGraph, ...rest})=>({historyGraph: callback(historyGraph), ...rest}))}
       forces={defaultGraphForces}
       versionRegistry={versionRegistry}
       onMerge={outputs => callbacks.onMerge?.(outputs)}

+ 11 - 11
src/onion/version.test.ts

@@ -96,7 +96,7 @@ describe("Version", () => {
 
   describe("Merging", () => {
     // Helper
-    function mergeAgain(registry, merged, nameMap) {
+    function mergeAgain(registry, merged, nameMap?) {
       const mergedAgain = registry.merge(merged, nameMap);
       assert(mergedAgain.length === merged.length
         && mergedAgain.every(version => merged.includes(version)),
@@ -105,10 +105,10 @@ describe("Version", () => {
 
     it("Merge empty set", () => {
       const registry = new VersionRegistry();
-      const merged = registry.merge([], new Map());
+      const merged = registry.merge([]);
       assert(merged.length === 1 && merged[0] === registry.initialVersion, "expected intial version");
 
-      mergeAgain(registry, merged, new Map());
+      mergeAgain(registry, merged);
     })
 
     it("Merge non-conflicting versions", () => {
@@ -124,7 +124,7 @@ describe("Version", () => {
 
       const nameMap = new Map([[nodeCreationA, "A"], [nodeCreationB, "B"]]);
 
-      const merged = registry.merge([versionA, versionB], nameMap);
+      const merged = registry.merge([versionA, versionB], delta => nameMap.get(delta)!);
       assert(merged.length === 1, "expected 1 merged version");
 
       const deltas = [... merged[0]];
@@ -133,7 +133,7 @@ describe("Version", () => {
         && deltas.includes(nodeCreationB),
         "expected merged version to contain nodes A and B");
 
-      mergeAgain(registry, merged, nameMap);
+      mergeAgain(registry, merged, delta => nameMap.get(delta)!);
     });
 
     it("Merge complex conflicting versions", () => {
@@ -171,13 +171,13 @@ describe("Version", () => {
       const seven = registry.quickVersion([C,X,Y,Z]);
       const five  = registry.quickVersion([D,BB,B,X,Y,Z]);
 
-      const merged = registry.merge([three, seven, five], nameMap);
+      const merged = registry.merge([three, seven, five], d => nameMap.get(d)!);
       assert(merged.length === 3, "expected three maximal versions");
       assert(merged.includes(registry.quickVersion([A,C,X,Y,Z])), "expected [X,Y,Z,A,C] to be a maximal version");
       assert(merged.includes(registry.quickVersion([BB,B,C,X,Y,Z])), "expected [X,Y,Z,B,C] to be a maximal version");
       assert(merged.includes(registry.quickVersion([D,BB,B,X,Y,Z])), "expected [X,Y,Z,B,D] to be a maximal version");
 
-      mergeAgain(registry, merged, nameMap);
+      mergeAgain(registry, merged, d => nameMap.get(d)!);
     });
 
     it("Merge many non-conflicting versions (scalability test)", () => {
@@ -196,10 +196,10 @@ describe("Version", () => {
       // Create a version for each delta, containing only that delta:
       const versions = deltas.map(d => registry.createVersion(registry.initialVersion, d));
 
-      const merged = registry.merge(versions, nameMap);
+      const merged = registry.merge(versions, d => nameMap.get(d)!);
       assert(merged.length === 1, "only one merged version should result");
 
-      const mergedAgain = registry.merge(merged, nameMap);
+      const mergedAgain = registry.merge(merged, d => nameMap.get(d)!);
       assert(mergedAgain.length === merged.length
         && mergedAgain.every(version => merged.includes(version)),
         "merging a merge result should just give the same result again.");
@@ -236,10 +236,10 @@ describe("Version", () => {
         versions.push(registry.quickVersion([edge, creation]));
       }
 
-      const merged = registry.merge(versions, nameMap);
+      const merged = registry.merge(versions, d => nameMap.get(d)!);
       assert(merged.length === Math.pow(2, HOW_MANY), HOW_MANY.toString() + " binary choices should result in " + Math.pow(2,HOW_MANY).toString() + " possible conflict resolutions and therefore merge results.");
 
-      mergeAgain(registry, merged, nameMap);
+      mergeAgain(registry, merged, d => nameMap.get(d)!);
     });
   });
 

+ 8 - 5
src/onion/version.ts

@@ -295,17 +295,20 @@ export class VersionRegistry {
 
   // Of the union of all deltas of the versions given, compute the maximal left-closed conflict-free subsets.
   // These are the subsets to which no delta can be added without introducing a conflict or missing dependency.
-  merge(versions: Array<Version>, debugNames?: Map<Delta, string> | undefined): Array<Version> {
+  merge(versions: Array<Version>, debugNames?: (Delta) => string): Array<Version> {
 
     function printDebug(...args) {
       if (debugNames !== undefined) {
         for (const [i,arg] of args.entries()) {
-          const name = debugNames.get(arg);
-          if (name !== undefined) {
-            args[i] = name;
+          try {
+            const name = debugNames(arg);
+            if (name !== undefined) {
+              args[i] = name;
+            }
           }
+          catch (e) {}
         }
-        // console.log(...args);
+        console.log(...args);
       }
     }
 

+ 1 - 1
tsconfig.json

@@ -13,6 +13,6 @@
     "strictFunctionTypes": true,
     "strictPropertyInitialization": true,
     "alwaysStrict": true,
-    "moduleResolution": "nodenext",
+    "moduleResolution": "node",
   },
 }

+ 3 - 1
webpack.config.js

@@ -1,7 +1,9 @@
 const path = require('path');
+// import * as path from "path";
 const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
+// import * as ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
 
-
+// export default { 
 module.exports = {
   entry: path.resolve(__dirname, 'src', 'frontend', 'index.tsx'),
   module: {