Alef1986 commited on
Commit
cd2a4a1
·
1 Parent(s): 0955121

Cleanup - module dependencies removed, Perspective merged, UNDER THE HOOD and QUERY fixed

Browse files
js/app.js CHANGED
@@ -1,29 +1,35 @@
1
  /**
2
  * APPLICATION ORCHESTRATOR (Vue 3)
3
  * --------------------------------
4
- * This module serves as the entry point. It binds the reactive state
5
- * to the DOM, handles user inputs, and acts as the controller that
6
- * passes data between the Event Log, Domain Actions, Ontologies, and Renderers.
7
  */
8
 
9
  import { createApp, computed, watch, onMounted, ref, reactive, nextTick } from 'vue';
10
- import { state, advanceTime, loadState } from './state.js';
11
  import { i18n } from './i18n.js';
12
- import {
13
- createRegisterPartEvent,
14
- createAssembleEvent,
15
- createReportDefectEvent,
16
- createDismantleEvent
 
17
  } from './smrm/actions.js';
18
- import { ENTITY_CLASSES, RELATION_TYPES } from './smrm/definitions.js';
19
  import * as bfoAdapter from './smim/bfo.js';
20
  import * as isoAdapter from './smim/iso15926.js';
21
  import * as naiveAdapter from './smim/naive.js';
22
  import { initVisNetwork, updateVisNetwork } from './renderers/vis-adapter.js';
23
  import { updateMermaidNetwork } from './renderers/mermaid-adapter.js';
24
- import { generateBomTree } from './urmsm/bom-view.js';
25
  import { reduceGraphForSMRM } from './urmsm/smrm-view.js';
26
- import { generateLpgStorage, generateLpgQuery } from './sms/lpg-generator.js';
 
 
 
 
 
27
  import { generateRdfStorage, generateRdfQuery } from './sms/rdf-generator.js';
28
  import { generatePhysicalGraph } from './sms/sms-graph-generator.js';
29
  import {
@@ -35,8 +41,8 @@ import {
35
  /**
36
  * RECURSIVE BOM TREE NODE COMPONENT
37
  * ---------------------------------
38
- * Pure Vue component for rendering hierarchical Bill of Materials nodes
39
- * without raw HTML strings or external DOM manipulation.
40
  */
41
  const BomNode = {
42
  name: 'BomNode',
@@ -48,7 +54,15 @@ const BomNode = {
48
  },
49
  template: `
50
  <li>
51
- <strong>{{ node.id }}</strong>
 
 
 
 
 
 
 
 
52
  <ul v-if="node.children && node.children.length > 0" class="tree">
53
  <bom-node
54
  v-for="child in node.children"
@@ -65,96 +79,13 @@ const App = {
65
  BomNode
66
  },
67
  setup() {
68
- // 1. Local UI Settings (Isolated from domain state)
69
- const settings = reactive({
70
- language: 'sv', // Available options: 'sv', 'en', 'ru'
71
- ontology: 'NAIVE_NODE', // SMIM Top-Level Ontology: 'BFO' | 'ISO15926' | 'NAIVE_NODE' | 'NAIVE_EDGE'
72
- storage: 'LPG', // SMS Storage Paradigm: 'LPG' or 'RDF'
73
- engine: 'vis', // Renderer: 'vis' (Vis.js) or 'mermaid'
74
- perspective: 'SMRM' // Perspective: 'SMRM' | 'SMIM' | 'SMS'
75
- });
76
-
77
- // User Role State ('UNI_ENGINEER')
78
- const userRole = ref('UNI_ENGINEER');
79
- watch(userRole, (newRole) => {
80
- state.userRole = newRole;
81
- });
82
- watch(() => state.userRole, (newRole) => {
83
- if (newRole && userRole.value !== newRole) {
84
- userRole.value = newRole;
85
- }
86
- });
87
-
88
- // Synchronise reactive ontology between settings and domain state
89
- watch(() => state.ontology, (newOnto) => {
90
- if (newOnto === 'ISO_15926_4D' || newOnto === 'ISO15926') {
91
- settings.ontology = 'ISO15926';
92
- } else if (newOnto === 'NAIVE_NODE') {
93
- settings.ontology = 'NAIVE_NODE';
94
- } else if (newOnto === 'NAIVE_EDGE') {
95
- settings.ontology = 'NAIVE_EDGE';
96
- } else if (newOnto === 'NAIVE') {
97
- settings.ontology = 'NAIVE_NODE';
98
- } else if (newOnto === 'BFO') {
99
- settings.ontology = 'BFO';
100
- }
101
- }, { immediate: true });
102
- watch(() => settings.ontology, (newOnto) => {
103
- if (state.ontology !== newOnto) {
104
- state.ontology = newOnto;
105
- }
106
- });
107
-
108
- // Synchronise perspective between settings and domain state
109
- watch(() => state.perspective, (newPersp) => {
110
- if (newPersp && settings.perspective !== newPersp) {
111
- settings.perspective = newPersp;
112
- }
113
- }, { immediate: true });
114
- watch(() => settings.perspective, (newPersp) => {
115
- if (state.perspective !== newPersp) {
116
- state.perspective = newPersp;
117
- }
118
- });
119
-
120
- // Synchronise engine between settings and domain state
121
- watch(() => state.graphEngine, (newEngine) => {
122
- const lower = (newEngine || 'VIS').toLowerCase();
123
- if (settings.engine !== lower) {
124
- settings.engine = lower;
125
- }
126
- }, { immediate: true });
127
- watch(() => settings.engine, (newEngine) => {
128
- const upper = (newEngine || 'vis').toUpperCase();
129
- if (state.graphEngine !== upper) {
130
- state.graphEngine = upper;
131
- }
132
- });
133
-
134
  // DOM References for renderers & file operations
135
  const visContainer = ref(null);
136
  const mermaidContainer = ref(null);
137
  const fileInput = ref(null);
138
 
139
- // Form inputs
140
- const partNameInput = ref('');
141
- const selectedParent = ref('System_A');
142
- const selectedEdgeToRemove = ref('');
143
- const showHistory = ref(true);
144
-
145
- // New Sandbox & Domain Extension Form inputs
146
- const selectedNodeClass = ref(ENTITY_CLASSES[0]);
147
- const newNodeIdInput = ref('');
148
- const selectedRelSource = ref('System_A');
149
- const selectedRelType = ref(RELATION_TYPES[0]);
150
- const selectedRelTarget = ref('');
151
- const propTargetType = ref('NODE');
152
- const propTargetId = ref('');
153
- const propKeyInput = ref('');
154
- const propValInput = ref('');
155
-
156
  // Universal Engineer Business UI inputs
157
- const bizRegType = ref('Vehicle');
158
  const bizRegId = ref('');
159
  const bizInstSource = ref('');
160
  const bizInstTarget = ref('');
@@ -173,30 +104,14 @@ const App = {
173
  const audioDuration = ref(0);
174
  const showSubtitles = ref(true);
175
  const currentSubtitleText = ref('');
176
- const executedTimelineActions = ref(new Set());
177
 
178
  // Layout State (Flex percentages for resizers)
179
- const layout = reactive({ leftWidth: 66, bomHeight: 25, storageHeight: 40 });
180
-
181
- // Active dictionary based on local settings
182
- const t = computed(() => i18n[settings.language]);
183
 
184
- /**
185
- * Resolves the domain entity class of a node by inspecting ID conventions.
186
- *
187
- * @param {string} nodeId - Identifier of the node.
188
- * @returns {string} Domain entity class ('Vehicle' | 'Slot' | 'Part' | 'Defect' | 'Warehouse').
189
- */
190
- function getNodeEntityClass(nodeId) {
191
- if (!nodeId) return 'Part';
192
- if (/^veh/i.test(nodeId) || /^sys/i.test(nodeId)) return 'Vehicle';
193
- if (/^slot/i.test(nodeId)) return 'Slot';
194
- if (/^ware|^wh/i.test(nodeId)) return 'Warehouse';
195
- if (/^def/i.test(nodeId)) return 'Defect';
196
- return 'Part';
197
- }
198
 
199
- // Dynamic entity projection: Reconstruct base entities from pure business events in eventLog
200
  const baseNodes = computed(() => {
201
  const nodeMap = new Map();
202
 
@@ -204,27 +119,30 @@ const App = {
204
  if (event.valid_from !== undefined && event.valid_from > state.currentTime) return;
205
 
206
  if (event.type === 'REGISTER_PART' && event.partId) {
 
207
  nodeMap.set(event.partId, {
208
  id: event.partId,
209
  label: event.partId,
210
- type: getNodeEntityClass(event.partId),
211
- entityClass: getNodeEntityClass(event.partId)
212
  });
213
  } else if (event.type === 'ASSEMBLE') {
214
  if (event.parentId && !nodeMap.has(event.parentId)) {
 
215
  nodeMap.set(event.parentId, {
216
  id: event.parentId,
217
  label: event.parentId,
218
- type: getNodeEntityClass(event.parentId),
219
- entityClass: getNodeEntityClass(event.parentId)
220
  });
221
  }
222
  if (event.childId && !nodeMap.has(event.childId)) {
 
223
  nodeMap.set(event.childId, {
224
  id: event.childId,
225
  label: event.childId,
226
- type: getNodeEntityClass(event.childId),
227
- entityClass: getNodeEntityClass(event.childId)
228
  });
229
  }
230
  }
@@ -233,151 +151,140 @@ const App = {
233
  return Array.from(nodeMap.values());
234
  });
235
 
236
- const availableNodes = computed(() => {
237
- return baseNodes.value.map(n => n.id);
238
- });
239
 
240
- const activeNodes = computed(() => {
241
- return availableNodes.value;
242
- });
243
 
244
- const activeEdges = computed(() => {
245
- return naiveAdapter.generateSemanticGraph(
246
- state.eventLog,
247
- baseNodes.value,
248
- state.currentTime
249
- ).edges.filter(e => e.properties?.isCurrent !== false);
250
- });
251
 
252
- const activeVehicles = computed(() => availableNodes.value.filter(id => getNodeEntityClass(id) === 'Vehicle'));
253
- const activeSlots = computed(() => availableNodes.value.filter(id => getNodeEntityClass(id) === 'Slot'));
254
- const activeParts = computed(() => availableNodes.value.filter(id => !id.startsWith('Slot_')));
255
- const activeWarehouses = computed(() => availableNodes.value.filter(id => getNodeEntityClass(id) === 'Warehouse'));
256
 
257
- const bizInstallSources = computed(() => {
258
- return availableNodes.value.filter(id => !id.startsWith('Slot_'));
259
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
 
261
- const bizInstallTargets = computed(() => {
262
- return availableNodes.value.filter(id => !id.startsWith('Slot_'));
 
 
 
 
 
263
  });
264
 
 
 
 
265
  const installedComponents = computed(() => {
266
  const comps = new Set();
267
- activeEdges.value.forEach(e => {
268
- if (e.label === 'mounted_in' || e.label === 'stored_at') {
269
- comps.add(e.childId || e.from);
 
 
 
270
  }
271
  });
272
  return Array.from(comps).filter(id => !id.startsWith('Slot_'));
273
  });
274
 
 
275
  const activeDefectsList = computed(() => {
276
- return state.eventLog
277
- .map((e, index) => ({ event: e, logIndex: index }))
278
- .filter(({ event: e }) => e.type === 'REPORT_DEFECT' && (e.valid_to === null || e.valid_to > state.currentTime) && (e.valid_from === undefined || e.valid_from <= state.currentTime))
279
- .map(({ event: e, logIndex }) => {
280
- const id = e.id || `def_${logIndex}_${e.targetId}_${e.defectName}`;
281
- return {
282
- id,
283
- logIndex,
 
 
 
284
  targetId: e.targetId,
285
  defectName: e.defectName,
286
  defectValue: e.defectValue,
287
  label: `${e.targetId} • ${e.defectName}=${e.defectValue}`
288
- };
289
- });
 
 
 
 
 
 
290
  });
291
 
 
292
  watch(bizInstallSources, (sources) => {
293
- if (!sources.includes(bizInstSource.value)) {
294
- bizInstSource.value = sources[0] || '';
295
- }
296
  }, { immediate: true });
297
 
298
  watch(bizInstallTargets, (targets) => {
299
- if (!targets.includes(bizInstTarget.value)) {
300
- bizInstTarget.value = targets[0] || '';
301
- }
302
  }, { immediate: true });
303
 
304
  watch(activeParts, (parts) => {
305
- if (!parts.includes(bizStorePart.value)) {
306
- bizStorePart.value = parts[0] || '';
307
- }
308
  }, { immediate: true });
309
 
310
  watch(activeWarehouses, (warehouses) => {
311
- if (!warehouses.includes(bizStoreTarget.value)) {
312
- bizStoreTarget.value = warehouses[0] || '';
313
- }
314
  }, { immediate: true });
315
 
316
  watch(installedComponents, (comps) => {
317
- if (!comps.includes(bizDismPart.value)) {
318
- bizDismPart.value = comps[0] || '';
319
- }
320
  }, { immediate: true });
321
 
322
  watch(activeDefectsList, (defects) => {
323
- if (!defects.some(d => d.id === bizRevDefect.value)) {
324
- bizRevDefect.value = defects[0]?.id || '';
325
- }
326
  }, { immediate: true });
327
 
328
  watch(availableNodes, (nodes) => {
329
- if (!nodes.includes(bizDefTarget.value)) {
330
- bizDefTarget.value = nodes[0] || '';
331
- }
332
  }, { immediate: true });
333
 
334
- /**
335
- * Central Event Dispatcher
336
- * ------------------------
337
- * Receives domain action events, validates consistency, and mutates
338
- * the event log strictly according to Event Sourcing principles.
339
- *
340
- * @param {Object} event - The domain event object to dispatch.
341
- */
342
- /**
343
- * Central Event Dispatcher (Pure Event Sourcing)
344
- * ------------------------------------------------
345
- * Directly appends immutable business facts to the event log.
346
- *
347
- * @param {Object} event - The domain event object to dispatch.
348
- */
349
- function dispatch(event) {
350
- if (!event) return;
351
- state.eventLog.push(event);
352
- }
353
 
354
- // The core transformation pipeline: Event Log -> Ontology -> Semantic Graph
355
- const semanticGraph = computed(() => {
356
- const eventsToProcess = state.eventLog;
357
-
358
- if (settings.ontology === 'BFO') {
359
- return bfoAdapter.generateSemanticGraph(eventsToProcess, baseNodes.value, state.currentTime);
360
- } else if (settings.ontology === 'ISO15926') {
361
- return isoAdapter.generateSemanticGraph(eventsToProcess, baseNodes.value, state.currentTime);
362
- } else {
363
- return naiveAdapter.generateSemanticGraph(eventsToProcess, baseNodes.value, state.currentTime, {
364
- ontology: settings.ontology
365
- });
366
  }
 
367
  });
368
 
369
- // Derived Views for the Sidebar (URMSM & SMS)
370
- const bomTree = computed(() => generateBomTree('System_A', activeEdges.value));
371
-
372
  const lpgRecords = computed(() => {
373
- if (settings.storage === 'LPG') {
374
  return generateLpgStorage(semanticGraph.value);
375
  }
376
  return [];
377
  });
378
 
379
  const rdfTtl = computed(() => {
380
- if (settings.storage === 'RDF') {
381
  return generateRdfStorage(semanticGraph.value);
382
  }
383
  return '';
@@ -385,7 +292,11 @@ const App = {
385
 
386
  function formatProperties(props) {
387
  if (!props) return '';
388
- const entries = Object.entries(props).filter(([key]) => key !== 'isCurrent');
 
 
 
 
389
  if (entries.length === 0) return '';
390
  return entries.map(([k, v]) => {
391
  const valStr = v === null ? 'null' : (typeof v === 'number' ? `T${v}` : `'${v}'`);
@@ -394,80 +305,102 @@ const App = {
394
  }
395
 
396
  const queryCode = computed(() => {
397
- if (settings.storage === 'LPG') {
398
- return generateLpgQuery(semanticGraph.value, state.currentTime);
399
- } else {
400
- return generateRdfQuery(semanticGraph.value, state.currentTime);
401
  }
 
402
  });
403
 
404
- // Physical projection (SMS) vs Engineer View (SMRM) vs Canonical Logical (SMIM) graph routing
 
 
 
 
405
  const activeGraph = computed(() => {
406
- if (settings.perspective === 'SMS') {
407
- return generatePhysicalGraph(semanticGraph.value, settings.storage);
 
 
 
408
  }
409
- if (settings.perspective === 'SMRM') {
410
- return reduceGraphForSMRM(semanticGraph.value);
411
  }
412
  return semanticGraph.value;
413
  });
414
 
415
- // Reactive Graph Caption
416
  const graphCaption = computed(() => {
417
  let ontoText = 'BFO (3D)';
418
- if (settings.ontology === 'ISO15926') ontoText = 'ISO 15926 (4D)';
419
- if (settings.ontology === 'NAIVE_NODE') ontoText = t.value.ontoNaiveNode || 'Naive Graph (Node)';
420
- if (settings.ontology === 'NAIVE_EDGE') ontoText = t.value.ontoNaiveEdge || 'Naive Graph (Edge)';
421
- const storageText = settings.storage;
422
  let viewText = t.value.viewLogicalCaption;
423
- if (settings.perspective === 'SMRM') viewText = t.value.viewSmrmCaption;
424
- if (settings.perspective === 'SMS') viewText = t.value.viewPhysicalCaption;
425
- return `${ontoText} | ${storageText} | ${viewText}`;
 
 
426
  });
427
 
428
- // Contextual Dynamic Legend
429
  const graphLegendItems = computed(() => {
430
  const nodes = [];
431
  const edges = [];
432
  const dict = t.value.legend;
433
 
434
- if (settings.ontology === 'NAIVE_NODE' || settings.ontology === 'NAIVE_EDGE' || settings.ontology === 'NAIVE') {
 
435
  nodes.push({ id: 'naiveVehicle', label: dict.naiveVehicle, swatch: 'swatch-box-vehicle' });
436
- nodes.push({ id: 'naiveSlot', label: dict.naiveSlot, swatch: 'swatch-box-slot' });
 
 
437
  nodes.push({ id: 'naivePart', label: dict.naivePart, swatch: 'swatch-box-part' });
438
  nodes.push({ id: 'naiveDefect', label: dict.naiveDefect, swatch: 'swatch-diamond-defect' });
439
  nodes.push({ id: 'naiveWarehouse', label: dict.naiveWarehouse, swatch: 'swatch-box-warehouse' });
440
 
441
- edges.push({ id: 'relHasSlot', label: dict.relHasSlot, swatch: 'swatch-line-slot' });
442
- edges.push({ id: 'relMountedIn', label: dict.relMountedIn, swatch: 'swatch-line-mount' });
443
- edges.push({ id: 'relStoredAt', label: dict.relStoredAt, swatch: 'swatch-line-store' });
444
- edges.push({ id: 'relHasDefect', label: dict.relHasDefect, swatch: 'swatch-line-defect' });
445
- } else if (settings.ontology === 'BFO') {
 
 
 
 
 
 
 
 
446
  nodes.push({ id: 'bfoSys', label: dict.bfoSys, swatch: 'swatch-box-sys' });
447
  nodes.push({ id: 'bfoElem', label: dict.bfoElem, swatch: 'swatch-box-elem' });
448
  nodes.push({ id: 'bfoQuality', label: dict.bfoQuality, swatch: 'swatch-diamond' });
449
 
450
- if (settings.perspective === 'SMS' && settings.storage === 'RDF') {
451
  nodes.push({ id: 'rdfStmt', label: dict.rdfStmt, swatch: 'swatch-diamond' });
452
  nodes.push({ id: 'rdfLit', label: dict.rdfLit, swatch: 'swatch-literal' });
453
  edges.push({ id: 'rdfPred', label: dict.rdfPred, swatch: 'swatch-line-past' });
454
  } else {
455
  edges.push({ id: 'bfoActive', label: dict.bfoActiveEdge, swatch: 'swatch-line-active' });
456
  edges.push({ id: 'bfoInheres', label: dict.bfoInheres, swatch: 'swatch-line-transition' });
457
- if (showHistory.value) {
458
  edges.push({ id: 'bfoPast', label: dict.bfoPastEdge, swatch: 'swatch-line-past' });
459
  }
460
  }
461
  } else {
462
- // ISO 15926 (4D)
463
  nodes.push({ id: 'iso4d', label: dict.iso4d, swatch: 'swatch-box-4d' });
464
  nodes.push({ id: 'isoState', label: dict.isoState, swatch: 'swatch-box-state' });
465
  nodes.push({ id: 'isoDefect', label: dict.isoDefect, swatch: 'swatch-diamond-defect' });
 
 
 
466
  edges.push({ id: 'isoPart', label: dict.isoPartRel, swatch: 'swatch-line-active' });
467
  edges.push({ id: 'isoSlice', label: dict.isoSliceRel, swatch: 'swatch-line-past' });
468
  edges.push({ id: 'isoDefectRel', label: dict.isoDefectRel, swatch: 'swatch-line-defect' });
469
 
470
- if (settings.perspective === 'SMS' && settings.storage === 'LPG') {
471
  edges.push({ id: 'isoTrans', label: dict.isoTransitionRel, swatch: 'swatch-line-transition' });
472
  }
473
  }
@@ -475,35 +408,51 @@ const App = {
475
  return { nodes, edges };
476
  });
477
 
478
- // Watch for graph or engine changes to trigger re-renders
479
- watch([activeGraph, () => settings.engine], () => {
480
  renderGraph();
481
  }, { deep: true });
482
 
483
  function renderGraph() {
484
  if (state.currentView === 'MENU') return;
485
  const graphToRender = activeGraph.value;
486
- if (settings.engine === 'vis') {
487
- const isComplex = settings.ontology === 'ISO15926' || settings.perspective === 'SMS';
488
  updateVisNetwork(graphToRender, isComplex);
489
  } else if (mermaidContainer.value) {
490
  updateMermaidNetwork(graphToRender, mermaidContainer.value);
491
  }
492
  }
493
 
494
- // Watch for view changes to initialise the canvas when entering the sandbox workspace
 
 
 
 
 
 
 
 
 
 
 
 
 
 
495
  watch(() => state.currentView, async (newView) => {
496
  if (newView !== 'SCENARIO') {
497
- stopAudio();
498
  }
499
  if (newView === 'SCENARIO') {
500
- userRole.value = 'UNI_ENGINEER';
501
- state.eventLog.length = 0;
502
  resetTimeline(0);
503
  state.currentTime = 0;
504
  state.ontology = 'NAIVE_NODE';
505
- state.perspective = 'SMRM';
506
  state.graphEngine = 'MERMAID';
 
 
507
  }
508
  if (newView !== 'MENU') {
509
  await nextTick();
@@ -514,38 +463,22 @@ const App = {
514
  }
515
  });
516
 
517
- // Lifecycle hooks
518
  onMounted(() => {
519
- // Inject initial seed data via pure factories and central dispatcher
520
- dispatch(createRegisterPartEvent('System_A', state.currentTime));
521
- dispatch(createRegisterPartEvent('SubAssembly_1', state.currentTime));
522
- dispatch(createAssembleEvent('System_A', 'SubAssembly_1', 'has_part', state.currentTime));
523
- dispatch(createRegisterPartEvent('Part_A', state.currentTime));
524
- dispatch(createAssembleEvent('System_A', 'Part_A', 'has_part', state.currentTime));
525
- dispatch(createRegisterPartEvent('Sensor_X', state.currentTime));
526
- dispatch(createAssembleEvent('SubAssembly_1', 'Sensor_X', 'has_part', state.currentTime));
527
-
528
  if (state.currentView !== 'MENU' && visContainer.value) {
529
  initVisNetwork(visContainer.value);
530
  renderGraph();
531
  }
532
  });
533
 
534
- // UI Event Handlers
535
- /**
536
- * Resets the application state, stops audio playback, and returns to the main menu.
537
- */
538
  function goHome() {
539
- stopAudio();
540
  state.currentView = 'MENU';
541
  state.eventLog.splice(0, state.eventLog.length);
542
  state.currentTime = 0;
543
  state.edgeSequence = 1;
544
  }
545
 
546
- /**
547
- * Starts audio playback.
548
- */
549
  function playAudio() {
550
  if (scenarioAudio.value) {
551
  scenarioAudio.value.play().catch(err => {
@@ -554,18 +487,12 @@ const App = {
554
  }
555
  }
556
 
557
- /**
558
- * Pauses audio playback.
559
- */
560
  function pauseAudio() {
561
  if (scenarioAudio.value) {
562
  scenarioAudio.value.pause();
563
  }
564
  }
565
 
566
- /**
567
- * Toggles audio playback between play and pause states.
568
- */
569
  function togglePlayAudio() {
570
  if (isPlaying.value) {
571
  pauseAudio();
@@ -574,10 +501,7 @@ const App = {
574
  }
575
  }
576
 
577
- /**
578
- * Stops audio playback, resets the timeline to zero, and clears the Event Sourcing log.
579
- */
580
- function stopAudio() {
581
  if (scenarioAudio.value) {
582
  scenarioAudio.value.pause();
583
  scenarioAudio.value.currentTime = 0;
@@ -586,61 +510,44 @@ const App = {
586
  audioCurrentTime.value = 0;
587
  currentSubtitleText.value = '';
588
 
589
- // Reset Event Sourcing state
590
- state.eventLog.length = 0;
591
- resetTimeline(0);
592
- state.ontology = 'NAIVE_NODE';
593
- state.perspective = 'SMRM';
594
- state.graphEngine = 'MERMAID';
595
- nextTick(() => {
596
- renderGraph();
597
- });
 
 
598
  }
599
 
600
- /**
601
- * Synchronises reactive audio time, subtitles, and timeline graph events with HTML5 audio playback.
602
- */
603
  function onTimeUpdate() {
604
  if (scenarioAudio.value) {
605
  audioCurrentTime.value = scenarioAudio.value.currentTime;
606
  }
607
-
608
  const currentTime = audioCurrentTime.value;
609
 
610
- // Subtitle synchronisation
611
  const activeSubtitle = vanishingCrackScenario.subtitles.find(
612
  sub => currentTime >= sub.start && currentTime <= sub.end
613
  );
614
  currentSubtitleText.value = activeSubtitle ? activeSubtitle.text : '';
615
 
616
- // Scenario timeline dispatcher
617
  dispatchScenarioTimeline(currentTime, state, renderGraph);
618
  }
619
 
620
- /**
621
- * Initialises audio duration when metadata is loaded.
622
- */
623
  function onLoadedMetadata() {
624
  if (scenarioAudio.value) {
625
  audioDuration.value = scenarioAudio.value.duration || 0;
626
  }
627
  }
628
 
629
- /**
630
- * Handles completion of audio playback.
631
- * Crucial: On audio end, do NOT rewind or reset the state. Let the graph remain on screen!
632
- */
633
  function onAudioEnded() {
634
  isPlaying.value = false;
635
  currentSubtitleText.value = '';
636
  }
637
 
638
- /**
639
- * Seeks to a specific position in the audio timeline based on range input,
640
- * resetting and re-evaluating the Event Sourcing state up to the new timestamp.
641
- *
642
- * @param {Event} event - The range slider input event.
643
- */
644
  function onSeek(event) {
645
  const targetTime = parseFloat(event.target.value);
646
  if (scenarioAudio.value) {
@@ -648,23 +555,16 @@ const App = {
648
  }
649
  audioCurrentTime.value = targetTime;
650
 
651
- // Reset Event Sourcing state for replay
652
- state.eventLog.length = 0;
653
  resetTimeline(0);
 
654
  state.ontology = 'NAIVE_NODE';
655
- state.perspective = 'SMRM';
656
  state.graphEngine = 'MERMAID';
657
 
658
- // Fast-forward timeline events and update subtitles to targetTime
659
  onTimeUpdate();
660
  }
661
 
662
- /**
663
- * Formats seconds into a human-readable mm:ss format.
664
- *
665
- * @param {number} seconds - Time duration in seconds.
666
- * @returns {string} Formatted time string.
667
- */
668
  function formatTime(seconds) {
669
  if (!seconds || isNaN(seconds) || seconds < 0) return '00:00';
670
  const mins = Math.floor(seconds / 60);
@@ -672,14 +572,14 @@ const App = {
672
  return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
673
  }
674
 
675
- /**
676
- * Universal Engineer Action 1: Register Asset
677
- */
678
  function handleBizRegister() {
679
  if (!bizRegId.value) return;
680
  const event = createRegisterPartEvent(
681
  bizRegId.value,
682
- state.currentTime
 
 
683
  );
684
  if (event) {
685
  dispatch(event);
@@ -687,36 +587,28 @@ const App = {
687
  }
688
  }
689
 
690
- /**
691
- * Universal Engineer Action 2: Install / Assemble
692
- */
693
  function handleBizInstall() {
694
  if (!bizInstSource.value || !bizInstTarget.value) return;
695
- const childId = bizInstSource.value;
696
- const parentId = bizInstTarget.value;
697
- if (childId === parentId) return;
698
 
699
  const event = createAssembleEvent(
700
- parentId,
701
- childId,
702
  'mounted_in',
703
- state.currentTime
 
704
  );
705
- if (event) {
706
- dispatch(event);
707
- }
708
  }
709
 
710
- /**
711
- * Universal Engineer Action 3: Report Defect
712
- */
713
  function handleBizReportDefect() {
714
  if (!bizDefTarget.value || !bizDefType.value) return;
715
  const event = createReportDefectEvent(
716
  bizDefTarget.value,
717
  bizDefType.value,
718
  bizDefVal.value || 'present',
719
- state.currentTime
 
720
  );
721
  if (event) {
722
  dispatch(event);
@@ -725,26 +617,30 @@ const App = {
725
  }
726
  }
727
 
728
- /**
729
- * Universal Engineer Action 4: Revoke Defect
730
- */
731
  function handleBizRevokeDefect() {
732
  if (!bizRevDefect.value) return;
733
  const item = activeDefectsList.value.find(d => d.id === bizRevDefect.value);
734
- if (item && item.logIndex !== undefined) {
735
- state.eventLog.splice(item.logIndex, 1);
736
- bizRevDefect.value = '';
 
 
 
 
 
 
 
 
 
737
  }
738
  }
739
 
740
- /**
741
- * Universal Engineer Action 5: Dismantle
742
- */
743
  function handleBizDismantle() {
744
  if (!bizDismPart.value) return;
745
  const event = createDismantleEvent(
746
  bizDismPart.value,
747
- state.currentTime
 
748
  );
749
  if (event) {
750
  dispatch(event);
@@ -752,73 +648,79 @@ const App = {
752
  }
753
  }
754
 
755
- /**
756
- * Universal Engineer Action 6: Move to Storage
757
- */
758
  function handleBizStore() {
759
  if (!bizStorePart.value || !bizStoreTarget.value) return;
760
  const event = createAssembleEvent(
761
  bizStoreTarget.value,
762
  bizStorePart.value,
763
  'stored_at',
764
- state.currentTime
 
765
  );
766
- if (event) {
767
- dispatch(event);
768
- }
769
  }
770
 
771
- // Resizer Logic (Vue way)
772
  function startResizeH(e) {
 
 
773
  const startX = e.clientX;
774
  const startWidth = layout.leftWidth;
775
  const containerWidth = document.querySelector('.main-container').getBoundingClientRect().width;
776
-
777
  const onMouseMove = (evt) => {
778
  const newWidth = startWidth + ((evt.clientX - startX) / containerWidth) * 100;
779
- if (newWidth > 20 && newWidth < 80) layout.leftWidth = newWidth;
780
  };
781
-
782
  const onMouseUp = () => {
 
783
  document.removeEventListener('mousemove', onMouseMove);
784
  document.removeEventListener('mouseup', onMouseUp);
785
  document.body.classList.remove('no-select');
786
  };
787
-
788
  document.addEventListener('mousemove', onMouseMove);
789
  document.addEventListener('mouseup', onMouseUp);
790
  document.body.classList.add('no-select');
791
  }
792
 
793
  function startResizeV(e, property, containerSelector) {
 
 
794
  const startY = e.clientY;
795
  const startHeight = layout[property];
796
  const containerHeight = document.querySelector(containerSelector).getBoundingClientRect().height;
797
-
798
  const onMouseMove = (evt) => {
799
  const newHeight = startHeight + ((evt.clientY - startY) / containerHeight) * 100;
800
- if (newHeight > 10 && newHeight < 80) layout[property] = newHeight;
801
  };
802
-
803
  const onMouseUp = () => {
 
804
  document.removeEventListener('mousemove', onMouseMove);
805
  document.removeEventListener('mouseup', onMouseUp);
806
  document.body.classList.remove('no-select');
807
  };
808
-
809
  document.addEventListener('mousemove', onMouseMove);
810
  document.addEventListener('mouseup', onMouseUp);
811
  document.body.classList.add('no-select');
812
  }
813
 
814
- /**
815
- * Serialises and exports the current Event Sourcing state to a JSON file.
816
- */
817
  function saveJson() {
818
  const exportData = {
819
- version: '1.0',
820
  savedAt: new Date().toISOString(),
821
- userRole: userRole.value,
 
 
 
 
 
 
822
  currentTime: state.currentTime,
823
  edgeSequence: state.edgeSequence,
824
  eventLog: JSON.parse(JSON.stringify(state.eventLog))
@@ -834,9 +736,6 @@ const App = {
834
  URL.revokeObjectURL(url);
835
  }
836
 
837
- /**
838
- * Opens the native file dialogue for loading a saved state JSON file.
839
- */
840
  function triggerLoadJson() {
841
  if (fileInput.value) {
842
  fileInput.value.value = '';
@@ -844,11 +743,6 @@ const App = {
844
  }
845
  }
846
 
847
- /**
848
- * Reads, deserialises, and restores the application domain state from a JSON file.
849
- *
850
- * @param {Event} e - HTML input change event.
851
- */
852
  function onFileLoaded(e) {
853
  const file = e.target.files?.[0];
854
  if (!file) return;
@@ -861,9 +755,6 @@ const App = {
861
  loadState({ eventLog: parsed });
862
  } else if (parsed && typeof parsed === 'object') {
863
  loadState(parsed);
864
- if (parsed.userRole && (parsed.userRole === 'DB_ADMIN' || parsed.userRole === 'UNI_ENGINEER')) {
865
- userRole.value = parsed.userRole;
866
- }
867
  }
868
  nextTick(() => {
869
  renderGraph();
@@ -877,44 +768,44 @@ const App = {
877
  }
878
 
879
  return {
880
- state, settings, t, layout, advanceTime, dispatch,
881
- userRole, fileInput, saveJson, triggerLoadJson, onFileLoaded,
882
  entityClasses: ENTITY_CLASSES,
883
  relationTypes: RELATION_TYPES,
884
- availableNodes, goHome, showHistory,
885
- activeNodes, activeEdges,
886
- activeVehicles, activeSlots, activeParts, activeWarehouses,
887
  bizRegType, bizRegId, handleBizRegister,
888
  bizInstSource, bizInstTarget, handleBizInstall, bizInstallSources, bizInstallTargets,
889
  bizDefTarget, bizDefType, bizDefVal, handleBizReportDefect,
890
  bizRevDefect, handleBizRevokeDefect, activeDefectsList,
891
  bizDismPart, handleBizDismantle, installedComponents,
892
  bizStorePart, bizStoreTarget, handleBizStore,
893
- bomTree, lpgRecords, rdfTtl, formatProperties, queryCode,
 
894
  visContainer, mermaidContainer,
895
  graphCaption, graphLegendItems,
896
  startResizeH, startResizeV,
897
  scenarioAudio, isPlaying, audioCurrentTime, audioDuration,
898
- showSubtitles, currentSubtitleText, executedTimelineActions,
899
  playAudio, pauseAudio, togglePlayAudio, stopAudio,
900
  onTimeUpdate, onLoadedMetadata, onAudioEnded, onSeek, formatTime
901
  };
902
  },
903
-
904
  template: `
905
  <!-- Main Menu (Landing View) -->
906
  <div v-if="state.currentView === 'MENU'" class="landing-view">
907
  <div class="menu-card">
908
  <div class="menu-badge">IEC 63339 Workbench</div>
909
  <h1 class="menu-title">Knowledge Architecture Workbench</h1>
910
- <p class="menu-subtitle">Top-Level Ontologies (SMIM), Storage Paradigms (SMS) &amp; Event Sourcing</p>
911
-
912
  <div class="menu-actions">
913
  <button class="menu-btn menu-btn-primary" @click="state.currentView = 'SANDBOX'">
914
  <span class="btn-main-text">Sandbox Mode</span>
915
  <span class="btn-sub-text">Interactive exploration of 3D/4D ontologies &amp; temporal graph models</span>
916
  </button>
917
-
918
  <button class="menu-btn menu-btn-scenario" @click="state.currentView = 'SCENARIO'">
919
  <span class="btn-main-text">Scenario: The Vanishing Crack</span>
920
  <span class="btn-sub-text">Guided demonstration of temporal defect tracing across lifecycle states</span>
@@ -923,331 +814,394 @@ const App = {
923
  </div>
924
  </div>
925
 
926
- <!-- Main Sandbox Workspace -->
927
  <div v-if="state.currentView !== 'MENU'" class="sandbox-workspace">
928
  <div class="control-deck">
929
- <!-- Tier 1: Architecture Settings -->
930
- <div class="deck-row deck-row-top">
931
- <div class="section-label">
932
- <span class="iec-badge">IEC 63339 / SMIM<span class="iec-tooltip" v-html="t.tooltips.smim"></span></span>
933
- <span>{{ t.ontoTitle }}</span>
934
- </div>
935
- <div class="btn-group">
936
- <button id="ctrl-onto-bfo" :class="{ active: settings.ontology === 'BFO' }" @click="settings.ontology = 'BFO'">BFO (3D)</button>
937
- <button id="ctrl-onto-iso" :class="{ active: settings.ontology === 'ISO15926' }" @click="settings.ontology = 'ISO15926'">ISO 15926 (4D)</button>
938
- <button id="ctrl-onto-naive-node" :class="{ active: settings.ontology === 'NAIVE_NODE' }" @click="settings.ontology = 'NAIVE_NODE'">{{ t.ontoNaiveNode }}</button>
939
- <button id="ctrl-onto-naive-edge" :class="{ active: settings.ontology === 'NAIVE_EDGE' }" @click="settings.ontology = 'NAIVE_EDGE'">{{ t.ontoNaiveEdge }}</button>
940
- </div>
941
-
942
- <div class="divider"></div>
943
-
944
- <div class="section-label">
945
- <span class="iec-badge">IEC 63339 / SMS<span class="iec-tooltip" v-html="t.tooltips.sms"></span></span>
946
- <span>{{ t.graphTitle }}</span>
947
- </div>
948
- <div class="btn-group">
949
- <button id="ctrl-storage-lpg" :class="{ active: settings.storage === 'LPG' }" @click="settings.storage = 'LPG'">LPG</button>
950
- <button id="ctrl-storage-rdf" :class="{ active: settings.storage === 'RDF' }" @click="settings.storage = 'RDF'">RDF (W3C)</button>
951
- </div>
952
-
953
- <div class="divider"></div>
954
-
955
- <div class="section-label">🎨 <span>{{ t.engineTitle }}</span></div>
956
- <div class="btn-group">
957
- <button id="ctrl-engine-vis" :class="{ active: settings.engine === 'vis' }" @click="settings.engine = 'vis'">🌐 Vis.js</button>
958
- <button id="ctrl-engine-mermaid" :class="{ active: settings.engine === 'mermaid' }" @click="settings.engine = 'mermaid'">📊 Mermaid</button>
959
- </div>
960
-
961
- <div class="divider"></div>
962
-
963
- <div class="section-label">👁️ <span>{{ t.viewTitle }}</span></div>
964
- <div class="btn-group">
965
- <button id="ctrl-view-smrm" :class="{ active: settings.perspective === 'SMRM' }" @click="settings.perspective = 'SMRM'">{{ t.viewSmrm }}</button>
966
- <button id="ctrl-view-logical" :class="{ active: settings.perspective === 'SMIM' }" @click="settings.perspective = 'SMIM'">{{ t.viewLogical }}</button>
967
- <button id="ctrl-view-physical" :class="{ active: settings.perspective === 'SMS' }" @click="settings.perspective = 'SMS'">{{ t.viewPhysical }}</button>
968
- </div>
969
-
970
- <div class="divider"></div>
971
-
972
- <!-- JSON State Operations -->
973
- <button id="ctrl-save-json" class="secondary" @click="saveJson" :title="t.saveBtn">{{ t.saveBtn }}</button>
974
- <button id="ctrl-load-json" class="secondary" @click="triggerLoadJson" :title="t.loadBtn">{{ t.loadBtn }}</button>
975
- <input ref="fileInput" type="file" accept=".json" style="display: none;" @change="onFileLoaded" />
976
-
977
- <div style="margin-left: auto;" class="btn-group flag-group">
978
- <button id="ctrl-lang-sv" :class="{ active: settings.language === 'sv' }" @click="settings.language = 'sv'" title="Svenska">🇸🇪</button>
979
- <button id="ctrl-lang-en" :class="{ active: settings.language === 'en' }" @click="settings.language = 'en'" title="British English">🇬🇧</button>
980
- <button id="ctrl-lang-ru" :class="{ active: settings.language === 'ru' }" @click="settings.language = 'ru'" title="Русский">🇷🇺</button>
981
- </div>
982
- </div>
983
-
984
- <!-- Universal Engineer Workspace -->
985
- <template v-if="userRole === 'UNI_ENGINEER'">
986
- <!-- Row 1: Time, Register, Install, Dismantle -->
987
- <div class="deck-row">
988
- <span id="biz-time-badge" class="badge">T = {{ state.currentTime }}</span>
989
- <button id="biz-step-btn" @click="advanceTime">{{ t.stepBtn }}</button>
990
-
991
- <div class="divider"></div>
992
-
993
- <!-- Action 1: Register Part -->
994
- <label style="font-size: 11px; font-weight: 700; color: #0284c7;">1. Register Part:</label>
995
- <input id="biz-reg-id" type="text" v-model="bizRegId" placeholder="Part ID" style="width: 110px;">
996
- <button id="biz-reg-btn" class="secondary" @click="handleBizRegister">Register</button>
997
-
998
- <div class="divider"></div>
999
-
1000
- <!-- Action 2: Install / Assemble -->
1001
- <label style="font-size: 11px; font-weight: 700; color: #0284c7;">2. Install:</label>
1002
- <select id="biz-inst-source" v-model="bizInstSource">
1003
- <option v-for="node in bizInstallSources" :key="'src_' + node" :value="node">{{ node }}</option>
1004
- </select>
1005
- <select id="biz-inst-target" v-model="bizInstTarget">
1006
- <option v-for="node in bizInstallTargets" :key="'tgt_' + node" :value="node">{{ node }}</option>
1007
- </select>
1008
- <button id="biz-inst-btn" class="secondary" @click="handleBizInstall">Install</button>
1009
 
1010
  <div class="divider"></div>
1011
 
1012
- <!-- Action 5: Dismantle -->
1013
- <label style="font-size: 11px; font-weight: 700; color: #0284c7;">5. Dismantle:</label>
1014
- <select id="biz-dism-part" v-model="bizDismPart">
1015
- <option v-for="comp in installedComponents" :key="'comp_' + comp" :value="comp">{{ comp }}</option>
1016
- </select>
1017
- <button id="biz-dism-btn" class="danger" @click="handleBizDismantle">Dismantle</button>
1018
- </div>
1019
-
1020
- <!-- Row 2: Defect, Revoke, Storage, History -->
1021
- <div class="deck-row" style="border-top: 1px solid #f1f5f9; background: #fafafa;">
1022
- <!-- Action 3: Report Defect -->
1023
- <label style="font-size: 11px; font-weight: 700; color: #0284c7;">3. Defect:</label>
1024
-
1025
- <select id="biz-def-target" v-model="bizDefTarget">
1026
- <option v-for="node in availableNodes" :key="'dn_' + node" :value="node">{{ node }}</option>
1027
- </select>
1028
- <input id="biz-def-type" type="text" v-model="bizDefType" placeholder="Type (e.g. crack)" style="width: 110px;">
1029
- <input id="biz-def-val" type="text" v-model="bizDefVal" placeholder="Value (e.g. 2mm)" style="width: 80px;">
1030
- <button id="biz-def-btn" class="secondary" @click="handleBizReportDefect">Report Defect</button>
1031
 
1032
  <div class="divider"></div>
1033
 
1034
- <!-- Action 4: Revoke Defect -->
1035
- <label style="font-size: 11px; font-weight: 700; color: #0284c7;">4. Revoke:</label>
1036
- <select id="biz-rev-defect" v-model="bizRevDefect">
1037
- <option v-for="d in activeDefectsList" :key="d.id" :value="d.id">{{ d.label }}</option>
1038
- </select>
1039
- <button id="biz-rev-btn" class="danger" @click="handleBizRevokeDefect">Revoke</button>
1040
 
1041
  <div class="divider"></div>
1042
 
1043
- <!-- Action 6: Move to Storage -->
1044
- <label style="font-size: 11px; font-weight: 700; color: #0284c7;">6. Storage:</label>
1045
- <select id="biz-store-part" v-model="bizStorePart">
1046
- <option v-for="part in activeParts" :key="'sp_' + part" :value="part">{{ part }}</option>
1047
- </select>
1048
- <select id="biz-store-target" v-model="bizStoreTarget">
1049
- <option v-for="wh in activeWarehouses" :key="'sw_' + wh" :value="wh">{{ wh }}</option>
1050
- </select>
1051
- <button id="biz-store-btn" class="secondary" @click="handleBizStore">Store</button>
1052
 
1053
- <div class="divider"></div>
1054
-
1055
- <!-- History Checkbox -->
1056
- <label style="font-size: 11px; display: flex; align-items: center; gap: 4px;">
1057
- <input type="checkbox" v-model="showHistory"> {{ t.history }}
1058
- </label>
1059
  </div>
1060
- </template>
1061
- </div>
1062
 
1063
- <div class="main-container">
1064
- <div id="canvas-container" :style="{ flex: \`0 0 \${layout.leftWidth}%\` }">
1065
- <div ref="visContainer" v-show="settings.engine === 'vis'" style="width: 100%; height: 100%; position: absolute;"></div>
1066
- <div ref="mermaidContainer" v-show="settings.engine === 'mermaid'" id="mermaid-container"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1067
 
1068
- <!-- Dynamic Graph Caption Overlay -->
1069
- <div class="graph-caption">
1070
- <span>📌</span>
1071
- <span>{{ graphCaption }}</span>
1072
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1073
 
1074
- <!-- Contextual Dynamic Legend Overlay -->
1075
- <div class="graph-legend">
1076
- <div class="legend-title">
1077
- <span>{{ t.legend.title }}</span>
1078
  </div>
1079
- <div v-if="graphLegendItems.nodes.length > 0" class="legend-section">
1080
- <div class="legend-section-title">{{ t.legend.nodes }}</div>
1081
- <div v-for="item in graphLegendItems.nodes" :key="item.id" class="legend-item">
1082
- <div :class="item.swatch"></div>
1083
- <span>{{ item.label }}</span>
1084
  </div>
1085
- </div>
1086
- <div v-if="graphLegendItems.edges.length > 0" class="legend-section">
1087
- <div class="legend-section-title">{{ t.legend.edges }}</div>
1088
- <div v-for="item in graphLegendItems.edges" :key="item.id" class="legend-item">
1089
- <div :class="item.swatch"></div>
1090
- <span>{{ item.label }}</span>
 
 
 
 
 
 
 
1091
  </div>
1092
- </div>
1093
- </div>
1094
- </div>
1095
-
1096
- <!-- Horizontal Resizer -->
1097
- <div class="resizer" id="panel-resizer" @mousedown="startResizeH"></div>
1098
-
1099
- <div class="sidebar" style="flex: 1;">
1100
- <!-- BOM View (URMSM) -->
1101
- <div class="card" :style="{ flex: \`0 0 \${layout.bomHeight}%\` }">
1102
- <h4>
1103
- <span>{{ t.titleBom }}</span>
1104
- <span class="iec-badge align-right">IEC 63339 / URMSM<span class="iec-tooltip" v-html="t.tooltips.urmsm"></span></span>
1105
- </h4>
1106
- <div id="tree-container" style="overflow-y: auto;">
1107
- <template v-if="bomTree">
1108
- <strong>{{ bomTree.id }}</strong>
1109
- <ul v-if="bomTree.children && bomTree.children.length > 0" class="tree">
1110
- <bom-node v-for="child in bomTree.children" :key="child.id" :node="child" />
1111
- </ul>
1112
- </template>
1113
  </div>
1114
  </div>
1115
 
1116
- <div class="resizer-h" @mousedown="(e) => startResizeV(e, 'bomHeight', '.sidebar')"></div>
1117
-
1118
- <!-- Storage View (SMS) -->
1119
- <div class="card" :style="{ flex: \`0 0 \${layout.storageHeight}%\` }">
1120
- <h4>
1121
- <span>{{ t.titleStorage }}</span>
1122
- <span style="color:#0284c7; font-weight: 600;">SMS / {{ settings.ontology }} / {{ settings.storage }}</span>
1123
- </h4>
1124
- <div id="storage-content" style="overflow-y: auto; display: flex; flex-direction: column;">
1125
- <table v-if="settings.storage === 'LPG' && lpgRecords.length > 0">
1126
- <thead>
1127
- <tr>
1128
- <th>{{ t.sourceCol }}</th>
1129
- <th>{{ t.targetCol }}</th>
1130
- <th>Type</th>
1131
- <th>Properties</th>
1132
- <th>{{ t.statusCol }}</th>
1133
- </tr>
1134
- </thead>
1135
- <tbody>
1136
- <tr v-for="row in lpgRecords" :key="row.id" :class="row.isActive ? 'active-row' : 'closed-row'">
1137
- <td>{{ row.from }}</td>
1138
- <td><strong>{{ row.to }}</strong></td>
1139
- <td><code>:{{ row.label }}</code></td>
1140
- <td>
1141
- <code v-if="formatProperties(row.properties)">{ {{ formatProperties(row.properties) }} }</code>
1142
- <span v-else>—</span>
1143
- </td>
1144
- <td>{{ row.isActive ? t.activeStatus : t.closedStatus }}</td>
1145
- </tr>
1146
- </tbody>
1147
- </table>
1148
- <div v-else-if="settings.storage === 'LPG'" style="color: #94a3b8; padding: 8px;">
1149
- No relationship records
1150
  </div>
1151
- <pre v-else class="code-block">{{ rdfTtl }}</pre>
1152
  </div>
1153
- </div>
1154
 
1155
- <div class="resizer-h" @mousedown="(e) => startResizeV(e, 'storageHeight', '.sidebar')"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1156
 
1157
- <!-- Query Generator -->
1158
- <div class="card" style="flex: 1;">
1159
- <h4>
1160
- <span>{{ t.queryTitle }}</span>
1161
- <span style="font-weight: normal; text-transform: none; color: #0d9488;">{{ t.queryDesc }}</span>
1162
- </h4>
1163
- <div id="query-content" style="display: flex; flex-direction: column; flex: 1;">
1164
- <pre class="code-block" :style="{ color: settings.ontology === 'BFO' ? '#0369a1' : '#3730a3' }">{{ queryCode }}</pre>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1165
  </div>
1166
  </div>
1167
  </div>
1168
- </div>
1169
 
1170
- <!-- Hidden Scenario Audio Element -->
1171
- <audio
1172
- ref="scenarioAudio"
1173
- src="./narratives/Ontologies_-_Disappearing_Crack.mp3"
1174
- preload="metadata"
1175
- @timeupdate="onTimeUpdate"
1176
- @loadedmetadata="onLoadedMetadata"
1177
- @play="isPlaying = true"
1178
- @pause="isPlaying = false"
1179
- @ended="onAudioEnded"
1180
- ></audio>
1181
-
1182
- <!-- Subtitles Overlay (Scenario Mode) -->
1183
- <div
1184
- v-if="state.currentView === 'SCENARIO' && showSubtitles"
1185
- class="scenario-subtitles"
1186
- >
1187
- {{ currentSubtitleText }}
1188
- </div>
1189
-
1190
- <!-- Floating Scenario Player Panel -->
1191
- <div
1192
- v-if="state.currentView === 'SCENARIO'"
1193
- class="scenario-player-panel"
1194
- >
1195
- <button
1196
- class="player-btn"
1197
- @click="togglePlayAudio"
1198
- :title="isPlaying ? 'Pause' : 'Play'"
1199
- :aria-label="isPlaying ? 'Pause' : 'Play'"
1200
  >
1201
- <span v-if="isPlaying">⏸</span>
1202
- <span v-else>▶</span>
1203
- </button>
1204
 
1205
- <button
1206
- class="player-btn"
1207
- @click="stopAudio"
1208
- title="Stop"
1209
- aria-label="Stop"
1210
  >
1211
- <span>⏹</span>
1212
- </button>
1213
-
1214
- <input
1215
- type="range"
1216
- class="player-progress"
1217
- min="0"
1218
- :max="audioDuration || 100"
1219
- step="0.1"
1220
- :value="audioCurrentTime"
1221
- @input="onSeek"
1222
- aria-label="Audio Timeline"
1223
- />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1224
 
1225
- <span class="player-time">{{ formatTime(audioCurrentTime) }} / {{ formatTime(audioDuration) }}</span>
 
 
 
 
 
 
 
 
 
 
 
1226
 
 
1227
  <button
1228
- class="player-btn player-btn-cc"
1229
- :class="{ active: showSubtitles }"
1230
- @click="showSubtitles = !showSubtitles"
1231
- title="Toggle Subtitles"
1232
- aria-label="Toggle Subtitles"
1233
  >
1234
- CC
 
 
 
1235
  </button>
1236
  </div>
1237
-
1238
- <!-- Floating Home Button -->
1239
- <button
1240
- v-if="state.currentView !== 'MENU'"
1241
- class="floating-home-btn"
1242
- @click="goHome"
1243
- title="Return to Main Menu (Resets Sandbox)"
1244
- aria-label="Return to Main Menu"
1245
- >
1246
- <svg viewBox="0 0 24 24" width="22" height="22" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round">
1247
- <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
1248
- <polyline points="9 22 9 12 15 12 15 22"></polyline>
1249
- </svg>
1250
- </button>
1251
  `
1252
  };
1253
 
 
1
  /**
2
  * APPLICATION ORCHESTRATOR (Vue 3)
3
  * --------------------------------
4
+ * Binds the single reactive state to the DOM, handles user inputs, and routes
5
+ * data between the Event Log, Domain Actions, Ontologies (SMIM), Storage (SMS),
6
+ * Viewpoints (URMSM), and Renderers.
7
  */
8
 
9
  import { createApp, computed, watch, onMounted, ref, reactive, nextTick } from 'vue';
10
+ import { state, advanceTime, loadState, setPerspective, nextSequence } from './state.js';
11
  import { i18n } from './i18n.js';
12
+ import {
13
+ createRegisterPartEvent,
14
+ createAssembleEvent,
15
+ createReportDefectEvent,
16
+ createRevokeDefectEvent,
17
+ createDismantleEvent
18
  } from './smrm/actions.js';
19
+ import { ENTITY_CLASSES, RELATION_TYPES, INTERNAL_PROPERTY_KEYS, inferEntityClass } from './smrm/definitions.js';
20
  import * as bfoAdapter from './smim/bfo.js';
21
  import * as isoAdapter from './smim/iso15926.js';
22
  import * as naiveAdapter from './smim/naive.js';
23
  import { initVisNetwork, updateVisNetwork } from './renderers/vis-adapter.js';
24
  import { updateMermaidNetwork } from './renderers/mermaid-adapter.js';
25
+ import { generateBomTrees } from './urmsm/bom-view.js';
26
  import { reduceGraphForSMRM } from './urmsm/smrm-view.js';
27
+ import {
28
+ generateLpgNodeStorage,
29
+ generateLpgStorage,
30
+ generateLpgQuery,
31
+ evaluateLiveQuery
32
+ } from './sms/lpg-generator.js';
33
  import { generateRdfStorage, generateRdfQuery } from './sms/rdf-generator.js';
34
  import { generatePhysicalGraph } from './sms/sms-graph-generator.js';
35
  import {
 
41
  /**
42
  * RECURSIVE BOM TREE NODE COMPONENT
43
  * ---------------------------------
44
+ * Renders hierarchical Bill of Materials nodes with entity class badges
45
+ * and surviving defect indicators at time T.
46
  */
47
  const BomNode = {
48
  name: 'BomNode',
 
54
  },
55
  template: `
56
  <li>
57
+ <span class="bom-node-row">
58
+ <strong>{{ node.id }}</strong>
59
+ <span class="bom-class-badge">{{ node.entityClass }}</span>
60
+ <span
61
+ v-for="def in (node.defects || [])"
62
+ :key="def"
63
+ class="bom-defect-badge"
64
+ >⚠️ {{ def }}</span>
65
+ </span>
66
  <ul v-if="node.children && node.children.length > 0" class="tree">
67
  <bom-node
68
  v-for="child in node.children"
 
79
  BomNode
80
  },
81
  setup() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  // DOM References for renderers & file operations
83
  const visContainer = ref(null);
84
  const mermaidContainer = ref(null);
85
  const fileInput = ref(null);
86
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  // Universal Engineer Business UI inputs
88
+ const bizRegType = ref('Part');
89
  const bizRegId = ref('');
90
  const bizInstSource = ref('');
91
  const bizInstTarget = ref('');
 
104
  const audioDuration = ref(0);
105
  const showSubtitles = ref(true);
106
  const currentSubtitleText = ref('');
 
107
 
108
  // Layout State (Flex percentages for resizers)
109
+ const layout = reactive({ leftWidth: 64, bomHeight: 24, storageHeight: 42 });
 
 
 
110
 
111
+ // Active dictionary based on state.language
112
+ const t = computed(() => i18n[state.language] || i18n.en);
 
 
 
 
 
 
 
 
 
 
 
 
113
 
114
+ // Reconstruct base registered entities up to state.currentTime
115
  const baseNodes = computed(() => {
116
  const nodeMap = new Map();
117
 
 
119
  if (event.valid_from !== undefined && event.valid_from > state.currentTime) return;
120
 
121
  if (event.type === 'REGISTER_PART' && event.partId) {
122
+ const cls = inferEntityClass(event.partId, event.entityClass);
123
  nodeMap.set(event.partId, {
124
  id: event.partId,
125
  label: event.partId,
126
+ type: cls,
127
+ entityClass: cls
128
  });
129
  } else if (event.type === 'ASSEMBLE') {
130
  if (event.parentId && !nodeMap.has(event.parentId)) {
131
+ const pCls = inferEntityClass(event.parentId);
132
  nodeMap.set(event.parentId, {
133
  id: event.parentId,
134
  label: event.parentId,
135
+ type: pCls,
136
+ entityClass: pCls
137
  });
138
  }
139
  if (event.childId && !nodeMap.has(event.childId)) {
140
+ const cCls = inferEntityClass(event.childId);
141
  nodeMap.set(event.childId, {
142
  id: event.childId,
143
  label: event.childId,
144
+ type: cCls,
145
+ entityClass: cCls
146
  });
147
  }
148
  }
 
151
  return Array.from(nodeMap.values());
152
  });
153
 
154
+ const availableNodes = computed(() => baseNodes.value.map(n => n.id));
 
 
155
 
156
+ const activeParts = computed(() =>
157
+ baseNodes.value.filter(n => n.entityClass !== 'Slot' && n.entityClass !== 'Warehouse').map(n => n.id)
158
+ );
159
 
160
+ const activeWarehouses = computed(() =>
161
+ baseNodes.value.filter(n => n.entityClass === 'Warehouse').map(n => n.id)
162
+ );
 
 
 
 
163
 
164
+ const bizInstallSources = computed(() =>
165
+ baseNodes.value.filter(n => n.entityClass === 'Part').map(n => n.id)
166
+ );
 
167
 
168
+ const bizInstallTargets = computed(() =>
169
+ baseNodes.value.filter(n => n.entityClass === 'Vehicle' || n.entityClass === 'Part').map(n => n.id)
170
+ );
171
+
172
+ /**
173
+ * Central Event Dispatcher (Pure Event Sourcing)
174
+ * Appends immutable business facts to the event log.
175
+ */
176
+ function dispatch(event) {
177
+ if (!event) return;
178
+ state.eventLog.push(event);
179
+ }
180
+
181
+ // Core Transformation Pipeline: Event Log -> SMIM Canonical Semantic Graph
182
+ const semanticGraph = computed(() => {
183
+ const opts = {
184
+ ontology: state.ontology,
185
+ showHistory: state.showHistory
186
+ };
187
 
188
+ if (state.ontology === 'BFO') {
189
+ return bfoAdapter.generateSemanticGraph(state.eventLog, baseNodes.value, state.currentTime, opts);
190
+ }
191
+ if (state.ontology === 'ISO15926') {
192
+ return isoAdapter.generateSemanticGraph(state.eventLog, baseNodes.value, state.currentTime, opts);
193
+ }
194
+ return naiveAdapter.generateSemanticGraph(state.eventLog, baseNodes.value, state.currentTime, opts);
195
  });
196
 
197
+ // Engineer SMRM reduced graph (used for BOM, SMRM view, and installed component discovery)
198
+ const smrmReducedGraph = computed(() => reduceGraphForSMRM(semanticGraph.value));
199
+
200
  const installedComponents = computed(() => {
201
  const comps = new Set();
202
+ smrmReducedGraph.value.edges.forEach(e => {
203
+ if (e.properties?.isCurrent === false) return;
204
+ if (e.label === 'has_part') {
205
+ comps.add(e.to);
206
+ } else if (e.label === 'mounted_in' || e.label === 'stored_at') {
207
+ comps.add(e.from);
208
  }
209
  });
210
  return Array.from(comps).filter(id => !id.startsWith('Slot_'));
211
  });
212
 
213
+ // Active Defects List (respecting REVOKE_DEFECT events)
214
  const activeDefectsList = computed(() => {
215
+ const activeMap = new Map();
216
+
217
+ state.eventLog.forEach((e, index) => {
218
+ if (e.valid_from !== undefined && e.valid_from > state.currentTime) return;
219
+
220
+ if (e.type === 'REPORT_DEFECT') {
221
+ if (e.valid_to !== null && e.valid_to !== undefined && e.valid_to <= state.currentTime) return;
222
+ const key = `${e.targetId}::${e.defectName}`;
223
+ activeMap.set(key, {
224
+ id: e.id || `def_${index}_${e.targetId}_${e.defectName}`,
225
+ logIndex: index,
226
  targetId: e.targetId,
227
  defectName: e.defectName,
228
  defectValue: e.defectValue,
229
  label: `${e.targetId} • ${e.defectName}=${e.defectValue}`
230
+ });
231
+ } else if (e.type === 'REVOKE_DEFECT') {
232
+ const key = `${e.targetId}::${e.defectName}`;
233
+ activeMap.delete(key);
234
+ }
235
+ });
236
+
237
+ return Array.from(activeMap.values());
238
  });
239
 
240
+ // Keep select dropdown defaults synchronised with available options
241
  watch(bizInstallSources, (sources) => {
242
+ if (!sources.includes(bizInstSource.value)) bizInstSource.value = sources[0] || '';
 
 
243
  }, { immediate: true });
244
 
245
  watch(bizInstallTargets, (targets) => {
246
+ if (!targets.includes(bizInstTarget.value)) bizInstTarget.value = targets[0] || '';
 
 
247
  }, { immediate: true });
248
 
249
  watch(activeParts, (parts) => {
250
+ if (!parts.includes(bizStorePart.value)) bizStorePart.value = parts[0] || '';
 
 
251
  }, { immediate: true });
252
 
253
  watch(activeWarehouses, (warehouses) => {
254
+ if (!warehouses.includes(bizStoreTarget.value)) bizStoreTarget.value = warehouses[0] || '';
 
 
255
  }, { immediate: true });
256
 
257
  watch(installedComponents, (comps) => {
258
+ if (!comps.includes(bizDismPart.value)) bizDismPart.value = comps[0] || '';
 
 
259
  }, { immediate: true });
260
 
261
  watch(activeDefectsList, (defects) => {
262
+ if (!defects.some(d => d.id === bizRevDefect.value)) bizRevDefect.value = defects[0]?.id || '';
 
 
263
  }, { immediate: true });
264
 
265
  watch(availableNodes, (nodes) => {
266
+ if (!nodes.includes(bizDefTarget.value)) bizDefTarget.value = nodes[0] || '';
 
 
267
  }, { immediate: true });
268
 
269
+ // Derived Views for Sidebar (URMSM & SMS)
270
+ const bomTrees = computed(() => generateBomTrees(semanticGraph.value));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
271
 
272
+ const lpgNodeRecords = computed(() => {
273
+ if (state.storage === 'LPG') {
274
+ return generateLpgNodeStorage(semanticGraph.value);
 
 
 
 
 
 
 
 
 
275
  }
276
+ return [];
277
  });
278
 
 
 
 
279
  const lpgRecords = computed(() => {
280
+ if (state.storage === 'LPG') {
281
  return generateLpgStorage(semanticGraph.value);
282
  }
283
  return [];
284
  });
285
 
286
  const rdfTtl = computed(() => {
287
+ if (state.storage === 'RDF') {
288
  return generateRdfStorage(semanticGraph.value);
289
  }
290
  return '';
 
292
 
293
  function formatProperties(props) {
294
  if (!props) return '';
295
+ const entries = Object.entries(props).filter(
296
+ ([key, val]) => !INTERNAL_PROPERTY_KEYS.includes(key) || key === 'validFrom' || key === 'validTo' || key === 'atTime'
297
+ ? (key !== 'isCurrent' && val !== undefined)
298
+ : false
299
+ );
300
  if (entries.length === 0) return '';
301
  return entries.map(([k, v]) => {
302
  const valStr = v === null ? 'null' : (typeof v === 'number' ? `T${v}` : `'${v}'`);
 
305
  }
306
 
307
  const queryCode = computed(() => {
308
+ if (state.storage === 'LPG') {
309
+ return generateLpgQuery(semanticGraph.value, state.currentTime, state.ontology);
 
 
310
  }
311
+ return generateRdfQuery(semanticGraph.value, state.currentTime, state.ontology);
312
  });
313
 
314
+ const liveQueryResult = computed(() =>
315
+ evaluateLiveQuery(semanticGraph.value, state.currentTime, state.ontology)
316
+ );
317
+
318
+ // Route active graph to canvas based on the 4 unified Perspectives
319
  const activeGraph = computed(() => {
320
+ if (state.perspective === 'SMS_LPG') {
321
+ return generatePhysicalGraph(semanticGraph.value, 'LPG');
322
+ }
323
+ if (state.perspective === 'SMS_RDF') {
324
+ return generatePhysicalGraph(semanticGraph.value, 'RDF');
325
  }
326
+ if (state.perspective === 'SMRM') {
327
+ return smrmReducedGraph.value;
328
  }
329
  return semanticGraph.value;
330
  });
331
 
332
+ // Reactive Graph Caption (truthful to the active perspective)
333
  const graphCaption = computed(() => {
334
  let ontoText = 'BFO (3D)';
335
+ if (state.ontology === 'ISO15926') ontoText = 'ISO 15926 (4D)';
336
+ if (state.ontology === 'NAIVE_NODE') ontoText = t.value.ontoNaiveNode;
337
+ if (state.ontology === 'NAIVE_EDGE') ontoText = t.value.ontoNaiveEdge;
338
+
339
  let viewText = t.value.viewLogicalCaption;
340
+ if (state.perspective === 'SMRM') viewText = t.value.viewSmrmCaption;
341
+ if (state.perspective === 'SMS_LPG') viewText = t.value.viewPhysicalLpgCaption;
342
+ if (state.perspective === 'SMS_RDF') viewText = t.value.viewPhysicalRdfCaption;
343
+
344
+ return `${ontoText} | ${viewText}`;
345
  });
346
 
347
+ // Contextual Dynamic Legend matching the rendered canvas
348
  const graphLegendItems = computed(() => {
349
  const nodes = [];
350
  const edges = [];
351
  const dict = t.value.legend;
352
 
353
+ // In Engineer (SMRM) Perspective or Naive ontologies, domain entities are shown
354
+ if (state.perspective === 'SMRM' || state.ontology === 'NAIVE_NODE' || state.ontology === 'NAIVE_EDGE') {
355
  nodes.push({ id: 'naiveVehicle', label: dict.naiveVehicle, swatch: 'swatch-box-vehicle' });
356
+ if (state.perspective !== 'SMRM') {
357
+ nodes.push({ id: 'naiveSlot', label: dict.naiveSlot, swatch: 'swatch-box-slot' });
358
+ }
359
  nodes.push({ id: 'naivePart', label: dict.naivePart, swatch: 'swatch-box-part' });
360
  nodes.push({ id: 'naiveDefect', label: dict.naiveDefect, swatch: 'swatch-diamond-defect' });
361
  nodes.push({ id: 'naiveWarehouse', label: dict.naiveWarehouse, swatch: 'swatch-box-warehouse' });
362
 
363
+ if (state.perspective === 'SMS_RDF') {
364
+ nodes.push({ id: 'rdfStmt', label: dict.rdfStmt, swatch: 'swatch-diamond' });
365
+ nodes.push({ id: 'rdfLit', label: dict.rdfLit, swatch: 'swatch-literal' });
366
+ edges.push({ id: 'rdfPred', label: dict.rdfPred, swatch: 'swatch-line-past' });
367
+ } else {
368
+ if (state.perspective !== 'SMRM') {
369
+ edges.push({ id: 'relHasSlot', label: dict.relHasSlot, swatch: 'swatch-line-slot' });
370
+ }
371
+ edges.push({ id: 'relMountedIn', label: dict.relMountedIn, swatch: 'swatch-line-mount' });
372
+ edges.push({ id: 'relStoredAt', label: dict.relStoredAt, swatch: 'swatch-line-store' });
373
+ edges.push({ id: 'relHasDefect', label: dict.relHasDefect, swatch: 'swatch-line-defect' });
374
+ }
375
+ } else if (state.ontology === 'BFO') {
376
  nodes.push({ id: 'bfoSys', label: dict.bfoSys, swatch: 'swatch-box-sys' });
377
  nodes.push({ id: 'bfoElem', label: dict.bfoElem, swatch: 'swatch-box-elem' });
378
  nodes.push({ id: 'bfoQuality', label: dict.bfoQuality, swatch: 'swatch-diamond' });
379
 
380
+ if (state.perspective === 'SMS_RDF') {
381
  nodes.push({ id: 'rdfStmt', label: dict.rdfStmt, swatch: 'swatch-diamond' });
382
  nodes.push({ id: 'rdfLit', label: dict.rdfLit, swatch: 'swatch-literal' });
383
  edges.push({ id: 'rdfPred', label: dict.rdfPred, swatch: 'swatch-line-past' });
384
  } else {
385
  edges.push({ id: 'bfoActive', label: dict.bfoActiveEdge, swatch: 'swatch-line-active' });
386
  edges.push({ id: 'bfoInheres', label: dict.bfoInheres, swatch: 'swatch-line-transition' });
387
+ if (state.showHistory) {
388
  edges.push({ id: 'bfoPast', label: dict.bfoPastEdge, swatch: 'swatch-line-past' });
389
  }
390
  }
391
  } else {
392
+ // ISO 15926 (4D) in SMIM / SMS_LPG / SMS_RDF
393
  nodes.push({ id: 'iso4d', label: dict.iso4d, swatch: 'swatch-box-4d' });
394
  nodes.push({ id: 'isoState', label: dict.isoState, swatch: 'swatch-box-state' });
395
  nodes.push({ id: 'isoDefect', label: dict.isoDefect, swatch: 'swatch-diamond-defect' });
396
+ if (state.perspective === 'SMS_RDF') {
397
+ nodes.push({ id: 'rdfLit', label: dict.rdfLit, swatch: 'swatch-literal' });
398
+ }
399
  edges.push({ id: 'isoPart', label: dict.isoPartRel, swatch: 'swatch-line-active' });
400
  edges.push({ id: 'isoSlice', label: dict.isoSliceRel, swatch: 'swatch-line-past' });
401
  edges.push({ id: 'isoDefectRel', label: dict.isoDefectRel, swatch: 'swatch-line-defect' });
402
 
403
+ if (state.perspective === 'SMS_LPG') {
404
  edges.push({ id: 'isoTrans', label: dict.isoTransitionRel, swatch: 'swatch-line-transition' });
405
  }
406
  }
 
408
  return { nodes, edges };
409
  });
410
 
411
+ // Re-render canvas when activeGraph or engine changes
412
+ watch([activeGraph, () => state.graphEngine], () => {
413
  renderGraph();
414
  }, { deep: true });
415
 
416
  function renderGraph() {
417
  if (state.currentView === 'MENU') return;
418
  const graphToRender = activeGraph.value;
419
+ if (state.graphEngine === 'VIS') {
420
+ const isComplex = state.ontology === 'ISO15926' || state.perspective.startsWith('SMS_');
421
  updateVisNetwork(graphToRender, isComplex);
422
  } else if (mermaidContainer.value) {
423
  updateMermaidNetwork(graphToRender, mermaidContainer.value);
424
  }
425
  }
426
 
427
+ function seedInitialSandboxData() {
428
+ state.eventLog.splice(0, state.eventLog.length);
429
+ state.currentTime = 0;
430
+ state.edgeSequence = 1;
431
+ dispatch(createRegisterPartEvent('System_A', 0, 'Vehicle', nextSequence()));
432
+ dispatch(createRegisterPartEvent('SubAssembly_1', 0, 'Part', nextSequence()));
433
+ dispatch(createAssembleEvent('System_A', 'SubAssembly_1', 'mounted_in', 0, nextSequence()));
434
+ dispatch(createRegisterPartEvent('Part_A', 0, 'Part', nextSequence()));
435
+ dispatch(createAssembleEvent('System_A', 'Part_A', 'mounted_in', 0, nextSequence()));
436
+ dispatch(createRegisterPartEvent('Sensor_X', 0, 'Part', nextSequence()));
437
+ dispatch(createAssembleEvent('SubAssembly_1', 'Sensor_X', 'mounted_in', 0, nextSequence()));
438
+ dispatch(createRegisterPartEvent('Warehouse_1', 0, 'Warehouse', nextSequence()));
439
+ }
440
+
441
+ // Watch for view transitions (MENU vs SANDBOX vs SCENARIO)
442
  watch(() => state.currentView, async (newView) => {
443
  if (newView !== 'SCENARIO') {
444
+ stopAudio(false);
445
  }
446
  if (newView === 'SCENARIO') {
447
+ state.userRole = 'UNI_ENGINEER';
448
+ state.eventLog.splice(0, state.eventLog.length);
449
  resetTimeline(0);
450
  state.currentTime = 0;
451
  state.ontology = 'NAIVE_NODE';
452
+ setPerspective('SMIM');
453
  state.graphEngine = 'MERMAID';
454
+ } else if (newView === 'SANDBOX' && state.eventLog.length === 0) {
455
+ seedInitialSandboxData();
456
  }
457
  if (newView !== 'MENU') {
458
  await nextTick();
 
463
  }
464
  });
465
 
 
466
  onMounted(() => {
467
+ seedInitialSandboxData();
 
 
 
 
 
 
 
 
468
  if (state.currentView !== 'MENU' && visContainer.value) {
469
  initVisNetwork(visContainer.value);
470
  renderGraph();
471
  }
472
  });
473
 
 
 
 
 
474
  function goHome() {
475
+ stopAudio(false);
476
  state.currentView = 'MENU';
477
  state.eventLog.splice(0, state.eventLog.length);
478
  state.currentTime = 0;
479
  state.edgeSequence = 1;
480
  }
481
 
 
 
 
482
  function playAudio() {
483
  if (scenarioAudio.value) {
484
  scenarioAudio.value.play().catch(err => {
 
487
  }
488
  }
489
 
 
 
 
490
  function pauseAudio() {
491
  if (scenarioAudio.value) {
492
  scenarioAudio.value.pause();
493
  }
494
  }
495
 
 
 
 
496
  function togglePlayAudio() {
497
  if (isPlaying.value) {
498
  pauseAudio();
 
501
  }
502
  }
503
 
504
+ function stopAudio(resetScenarioGraph = true) {
 
 
 
505
  if (scenarioAudio.value) {
506
  scenarioAudio.value.pause();
507
  scenarioAudio.value.currentTime = 0;
 
510
  audioCurrentTime.value = 0;
511
  currentSubtitleText.value = '';
512
 
513
+ if (resetScenarioGraph) {
514
+ state.eventLog.splice(0, state.eventLog.length);
515
+ resetTimeline(0);
516
+ state.currentTime = 0;
517
+ state.ontology = 'NAIVE_NODE';
518
+ setPerspective('SMIM');
519
+ state.graphEngine = 'MERMAID';
520
+ nextTick(() => {
521
+ renderGraph();
522
+ });
523
+ }
524
  }
525
 
 
 
 
526
  function onTimeUpdate() {
527
  if (scenarioAudio.value) {
528
  audioCurrentTime.value = scenarioAudio.value.currentTime;
529
  }
 
530
  const currentTime = audioCurrentTime.value;
531
 
 
532
  const activeSubtitle = vanishingCrackScenario.subtitles.find(
533
  sub => currentTime >= sub.start && currentTime <= sub.end
534
  );
535
  currentSubtitleText.value = activeSubtitle ? activeSubtitle.text : '';
536
 
 
537
  dispatchScenarioTimeline(currentTime, state, renderGraph);
538
  }
539
 
 
 
 
540
  function onLoadedMetadata() {
541
  if (scenarioAudio.value) {
542
  audioDuration.value = scenarioAudio.value.duration || 0;
543
  }
544
  }
545
 
 
 
 
 
546
  function onAudioEnded() {
547
  isPlaying.value = false;
548
  currentSubtitleText.value = '';
549
  }
550
 
 
 
 
 
 
 
551
  function onSeek(event) {
552
  const targetTime = parseFloat(event.target.value);
553
  if (scenarioAudio.value) {
 
555
  }
556
  audioCurrentTime.value = targetTime;
557
 
558
+ state.eventLog.splice(0, state.eventLog.length);
 
559
  resetTimeline(0);
560
+ state.currentTime = 0;
561
  state.ontology = 'NAIVE_NODE';
562
+ setPerspective('SMIM');
563
  state.graphEngine = 'MERMAID';
564
 
 
565
  onTimeUpdate();
566
  }
567
 
 
 
 
 
 
 
568
  function formatTime(seconds) {
569
  if (!seconds || isNaN(seconds) || seconds < 0) return '00:00';
570
  const mins = Math.floor(seconds / 60);
 
572
  return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
573
  }
574
 
575
+ // Engineer Domain Actions
 
 
576
  function handleBizRegister() {
577
  if (!bizRegId.value) return;
578
  const event = createRegisterPartEvent(
579
  bizRegId.value,
580
+ state.currentTime,
581
+ bizRegType.value,
582
+ nextSequence()
583
  );
584
  if (event) {
585
  dispatch(event);
 
587
  }
588
  }
589
 
 
 
 
590
  function handleBizInstall() {
591
  if (!bizInstSource.value || !bizInstTarget.value) return;
592
+ if (bizInstSource.value === bizInstTarget.value) return;
 
 
593
 
594
  const event = createAssembleEvent(
595
+ bizInstTarget.value,
596
+ bizInstSource.value,
597
  'mounted_in',
598
+ state.currentTime,
599
+ nextSequence()
600
  );
601
+ if (event) dispatch(event);
 
 
602
  }
603
 
 
 
 
604
  function handleBizReportDefect() {
605
  if (!bizDefTarget.value || !bizDefType.value) return;
606
  const event = createReportDefectEvent(
607
  bizDefTarget.value,
608
  bizDefType.value,
609
  bizDefVal.value || 'present',
610
+ state.currentTime,
611
+ nextSequence()
612
  );
613
  if (event) {
614
  dispatch(event);
 
617
  }
618
  }
619
 
 
 
 
620
  function handleBizRevokeDefect() {
621
  if (!bizRevDefect.value) return;
622
  const item = activeDefectsList.value.find(d => d.id === bizRevDefect.value);
623
+ if (item) {
624
+ const event = createRevokeDefectEvent(
625
+ item.targetId,
626
+ item.defectName,
627
+ state.currentTime,
628
+ item.id,
629
+ nextSequence()
630
+ );
631
+ if (event) {
632
+ dispatch(event);
633
+ bizRevDefect.value = '';
634
+ }
635
  }
636
  }
637
 
 
 
 
638
  function handleBizDismantle() {
639
  if (!bizDismPart.value) return;
640
  const event = createDismantleEvent(
641
  bizDismPart.value,
642
+ state.currentTime,
643
+ nextSequence()
644
  );
645
  if (event) {
646
  dispatch(event);
 
648
  }
649
  }
650
 
 
 
 
651
  function handleBizStore() {
652
  if (!bizStorePart.value || !bizStoreTarget.value) return;
653
  const event = createAssembleEvent(
654
  bizStoreTarget.value,
655
  bizStorePart.value,
656
  'stored_at',
657
+ state.currentTime,
658
+ nextSequence()
659
  );
660
+ if (event) dispatch(event);
 
 
661
  }
662
 
663
+ // Resizer Handlers with active .dragging visual feedback
664
  function startResizeH(e) {
665
+ const resizerEl = e.currentTarget;
666
+ resizerEl?.classList.add('dragging');
667
  const startX = e.clientX;
668
  const startWidth = layout.leftWidth;
669
  const containerWidth = document.querySelector('.main-container').getBoundingClientRect().width;
670
+
671
  const onMouseMove = (evt) => {
672
  const newWidth = startWidth + ((evt.clientX - startX) / containerWidth) * 100;
673
+ if (newWidth > 25 && newWidth < 78) layout.leftWidth = newWidth;
674
  };
675
+
676
  const onMouseUp = () => {
677
+ resizerEl?.classList.remove('dragging');
678
  document.removeEventListener('mousemove', onMouseMove);
679
  document.removeEventListener('mouseup', onMouseUp);
680
  document.body.classList.remove('no-select');
681
  };
682
+
683
  document.addEventListener('mousemove', onMouseMove);
684
  document.addEventListener('mouseup', onMouseUp);
685
  document.body.classList.add('no-select');
686
  }
687
 
688
  function startResizeV(e, property, containerSelector) {
689
+ const resizerEl = e.currentTarget;
690
+ resizerEl?.classList.add('dragging');
691
  const startY = e.clientY;
692
  const startHeight = layout[property];
693
  const containerHeight = document.querySelector(containerSelector).getBoundingClientRect().height;
694
+
695
  const onMouseMove = (evt) => {
696
  const newHeight = startHeight + ((evt.clientY - startY) / containerHeight) * 100;
697
+ if (newHeight > 12 && newHeight < 75) layout[property] = newHeight;
698
  };
699
+
700
  const onMouseUp = () => {
701
+ resizerEl?.classList.remove('dragging');
702
  document.removeEventListener('mousemove', onMouseMove);
703
  document.removeEventListener('mouseup', onMouseUp);
704
  document.body.classList.remove('no-select');
705
  };
706
+
707
  document.addEventListener('mousemove', onMouseMove);
708
  document.addEventListener('mouseup', onMouseUp);
709
  document.body.classList.add('no-select');
710
  }
711
 
712
+ // Full JSON State Export & Import
 
 
713
  function saveJson() {
714
  const exportData = {
715
+ version: '2.0',
716
  savedAt: new Date().toISOString(),
717
+ userRole: state.userRole,
718
+ ontology: state.ontology,
719
+ perspective: state.perspective,
720
+ storage: state.storage,
721
+ graphEngine: state.graphEngine,
722
+ language: state.language,
723
+ showHistory: state.showHistory,
724
  currentTime: state.currentTime,
725
  edgeSequence: state.edgeSequence,
726
  eventLog: JSON.parse(JSON.stringify(state.eventLog))
 
736
  URL.revokeObjectURL(url);
737
  }
738
 
 
 
 
739
  function triggerLoadJson() {
740
  if (fileInput.value) {
741
  fileInput.value.value = '';
 
743
  }
744
  }
745
 
 
 
 
 
 
746
  function onFileLoaded(e) {
747
  const file = e.target.files?.[0];
748
  if (!file) return;
 
755
  loadState({ eventLog: parsed });
756
  } else if (parsed && typeof parsed === 'object') {
757
  loadState(parsed);
 
 
 
758
  }
759
  nextTick(() => {
760
  renderGraph();
 
768
  }
769
 
770
  return {
771
+ state, t, layout, advanceTime, setPerspective, dispatch,
772
+ fileInput, saveJson, triggerLoadJson, onFileLoaded,
773
  entityClasses: ENTITY_CLASSES,
774
  relationTypes: RELATION_TYPES,
775
+ availableNodes, goHome,
776
+ activeParts, activeWarehouses,
 
777
  bizRegType, bizRegId, handleBizRegister,
778
  bizInstSource, bizInstTarget, handleBizInstall, bizInstallSources, bizInstallTargets,
779
  bizDefTarget, bizDefType, bizDefVal, handleBizReportDefect,
780
  bizRevDefect, handleBizRevokeDefect, activeDefectsList,
781
  bizDismPart, handleBizDismantle, installedComponents,
782
  bizStorePart, bizStoreTarget, handleBizStore,
783
+ bomTrees, lpgNodeRecords, lpgRecords, rdfTtl, formatProperties,
784
+ queryCode, liveQueryResult,
785
  visContainer, mermaidContainer,
786
  graphCaption, graphLegendItems,
787
  startResizeH, startResizeV,
788
  scenarioAudio, isPlaying, audioCurrentTime, audioDuration,
789
+ showSubtitles, currentSubtitleText,
790
  playAudio, pauseAudio, togglePlayAudio, stopAudio,
791
  onTimeUpdate, onLoadedMetadata, onAudioEnded, onSeek, formatTime
792
  };
793
  },
794
+
795
  template: `
796
  <!-- Main Menu (Landing View) -->
797
  <div v-if="state.currentView === 'MENU'" class="landing-view">
798
  <div class="menu-card">
799
  <div class="menu-badge">IEC 63339 Workbench</div>
800
  <h1 class="menu-title">Knowledge Architecture Workbench</h1>
801
+ <p class="menu-subtitle">Top-Level Ontologies (SMIM), Physical Storage (SMS) &amp; Event Sourcing</p>
802
+
803
  <div class="menu-actions">
804
  <button class="menu-btn menu-btn-primary" @click="state.currentView = 'SANDBOX'">
805
  <span class="btn-main-text">Sandbox Mode</span>
806
  <span class="btn-sub-text">Interactive exploration of 3D/4D ontologies &amp; temporal graph models</span>
807
  </button>
808
+
809
  <button class="menu-btn menu-btn-scenario" @click="state.currentView = 'SCENARIO'">
810
  <span class="btn-main-text">Scenario: The Vanishing Crack</span>
811
  <span class="btn-sub-text">Guided demonstration of temporal defect tracing across lifecycle states</span>
 
814
  </div>
815
  </div>
816
 
817
+ <!-- Main Sandbox & Scenario Workspace -->
818
  <div v-if="state.currentView !== 'MENU'" class="sandbox-workspace">
819
  <div class="control-deck">
820
+ <!-- Tier 1: Unified Architecture & Perspective Controls -->
821
+ <div class="deck-row deck-row-top">
822
+ <div class="section-label">
823
+ <span class="iec-badge">IEC 63339 / SMIM<span class="iec-tooltip" v-html="t.tooltips.smim"></span></span>
824
+ <span>{{ t.ontoTitle }}</span>
825
+ </div>
826
+ <div class="btn-group">
827
+ <button id="ctrl-onto-bfo" :class="{ active: state.ontology === 'BFO' }" @click="state.ontology = 'BFO'">BFO (3D)</button>
828
+ <button id="ctrl-onto-iso" :class="{ active: state.ontology === 'ISO15926' }" @click="state.ontology = 'ISO15926'">ISO 15926 (4D)</button>
829
+ <button id="ctrl-onto-naive-node" :class="{ active: state.ontology === 'NAIVE_NODE' }" @click="state.ontology = 'NAIVE_NODE'">{{ t.ontoNaiveNode }}</button>
830
+ <button id="ctrl-onto-naive-edge" :class="{ active: state.ontology === 'NAIVE_EDGE' }" @click="state.ontology = 'NAIVE_EDGE'">{{ t.ontoNaiveEdge }}</button>
831
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
832
 
833
  <div class="divider"></div>
834
 
835
+ <!-- Unified 4-Perspective Switcher (SMRM | SMIM | SMS_LPG | SMS_RDF) -->
836
+ <div class="section-label">
837
+ <span class="iec-badge">IEC 63339<span class="iec-tooltip" v-html="t.tooltips.sms"></span></span>
838
+ <span>👁️ {{ t.viewTitle }}</span>
839
+ </div>
840
+ <div class="btn-group">
841
+ <button id="ctrl-view-smrm" :class="{ active: state.perspective === 'SMRM' }" @click="setPerspective('SMRM')">{{ t.viewSmrm }}</button>
842
+ <button id="ctrl-view-logical" :class="{ active: state.perspective === 'SMIM' }" @click="setPerspective('SMIM')">{{ t.viewLogical }}</button>
843
+ <button id="ctrl-view-physical-lpg" :class="{ active: state.perspective === 'SMS_LPG' }" @click="setPerspective('SMS_LPG')">{{ t.viewPhysicalLpg }}</button>
844
+ <button id="ctrl-view-physical-rdf" :class="{ active: state.perspective === 'SMS_RDF' }" @click="setPerspective('SMS_RDF')">{{ t.viewPhysicalRdf }}</button>
845
+ </div>
 
 
 
 
 
 
 
 
846
 
847
  <div class="divider"></div>
848
 
849
+ <div class="section-label">🎨 <span>{{ t.engineTitle }}</span></div>
850
+ <div class="btn-group">
851
+ <button id="ctrl-engine-vis" :class="{ active: state.graphEngine === 'VIS' }" @click="state.graphEngine = 'VIS'">🌐 Vis.js</button>
852
+ <button id="ctrl-engine-mermaid" :class="{ active: state.graphEngine === 'MERMAID' }" @click="state.graphEngine = 'MERMAID'">📊 Mermaid</button>
853
+ </div>
 
854
 
855
  <div class="divider"></div>
856
 
857
+ <!-- JSON State Operations -->
858
+ <button id="ctrl-save-json" class="secondary" @click="saveJson" :title="t.saveBtn">{{ t.saveBtn }}</button>
859
+ <button id="ctrl-load-json" class="secondary" @click="triggerLoadJson" :title="t.loadBtn">{{ t.loadBtn }}</button>
860
+ <input ref="fileInput" type="file" accept=".json" style="display: none;" @change="onFileLoaded" />
 
 
 
 
 
861
 
862
+ <div style="margin-left: auto;" class="btn-group flag-group">
863
+ <button id="ctrl-lang-sv" :class="{ active: state.language === 'sv' }" @click="state.language = 'sv'" title="Svenska">🇸🇪</button>
864
+ <button id="ctrl-lang-en" :class="{ active: state.language === 'en' }" @click="state.language = 'en'" title="British English">🇬🇧</button>
865
+ <button id="ctrl-lang-ru" :class="{ active: state.language === 'ru' }" @click="state.language = 'ru'" title="Русский">🇷🇺</button>
866
+ </div>
 
867
  </div>
 
 
868
 
869
+ <!-- Tier 2: Engineer Lifecycle Actions -->
870
+ <template v-if="state.userRole === 'UNI_ENGINEER'">
871
+ <!-- Row 1: Time, Register, Install, Dismantle -->
872
+ <div class="deck-row">
873
+ <span id="biz-time-badge" class="badge">T = {{ state.currentTime }}</span>
874
+ <button id="biz-step-btn" @click="advanceTime">{{ t.stepBtn }}</button>
875
+
876
+ <div class="divider"></div>
877
+
878
+ <!-- Action 1: Register Entity -->
879
+ <label class="action-label">1. Register:</label>
880
+ <select id="biz-reg-type" v-model="bizRegType" title="Entity Class">
881
+ <option value="Vehicle">Vehicle</option>
882
+ <option value="Part">Part</option>
883
+ <option value="Warehouse">Warehouse</option>
884
+ </select>
885
+ <input id="biz-reg-id" type="text" v-model="bizRegId" placeholder="ID (e.g. Part_B)" style="width: 110px;" @keyup.enter="handleBizRegister">
886
+ <button id="biz-reg-btn" class="secondary" @click="handleBizRegister">Register</button>
887
+
888
+ <div class="divider"></div>
889
+
890
+ <!-- Action 2: Install / Assemble -->
891
+ <label class="action-label">2. Install:</label>
892
+ <select id="biz-inst-source" v-model="bizInstSource" title="Component to install">
893
+ <option v-for="node in bizInstallSources" :key="'src_' + node" :value="node">{{ node }}</option>
894
+ </select>
895
+ <span style="font-size: 11px; color: #64748b;">→</span>
896
+ <select id="biz-inst-target" v-model="bizInstTarget" title="Target Parent / System">
897
+ <option v-for="node in bizInstallTargets" :key="'tgt_' + node" :value="node">{{ node }}</option>
898
+ </select>
899
+ <button id="biz-inst-btn" class="secondary" @click="handleBizInstall">Install</button>
900
+
901
+ <div class="divider"></div>
902
+
903
+ <!-- Action 5: Dismantle -->
904
+ <label class="action-label">5. Dismantle:</label>
905
+ <select id="biz-dism-part" v-model="bizDismPart">
906
+ <option v-for="comp in installedComponents" :key="'comp_' + comp" :value="comp">{{ comp }}</option>
907
+ </select>
908
+ <button id="biz-dism-btn" class="danger" @click="handleBizDismantle">Dismantle</button>
909
+ </div>
910
 
911
+ <!-- Row 2: Defect, Revoke, Storage, History -->
912
+ <div class="deck-row" style="border-top: 1px solid #f1f5f9; background: #fafafa;">
913
+ <!-- Action 3: Report Defect -->
914
+ <label class="action-label">3. Defect:</label>
915
+ <select id="biz-def-target" v-model="bizDefTarget">
916
+ <option v-for="node in availableNodes" :key="'dn_' + node" :value="node">{{ node }}</option>
917
+ </select>
918
+ <input id="biz-def-type" type="text" v-model="bizDefType" placeholder="Type (e.g. crack)" style="width: 110px;">
919
+ <input id="biz-def-val" type="text" v-model="bizDefVal" placeholder="Value (e.g. 2mm)" style="width: 85px;">
920
+ <button id="biz-def-btn" class="secondary" @click="handleBizReportDefect">Report Defect</button>
921
+
922
+ <div class="divider"></div>
923
+
924
+ <!-- Action 4: Revoke Defect -->
925
+ <label class="action-label">4. Revoke:</label>
926
+ <select id="biz-rev-defect" v-model="bizRevDefect">
927
+ <option v-for="d in activeDefectsList" :key="d.id" :value="d.id">{{ d.label }}</option>
928
+ </select>
929
+ <button id="biz-rev-btn" class="danger" @click="handleBizRevokeDefect">Revoke</button>
930
+
931
+ <div class="divider"></div>
932
+
933
+ <!-- Action 6: Move to Storage -->
934
+ <label class="action-label">6. Storage:</label>
935
+ <select id="biz-store-part" v-model="bizStorePart">
936
+ <option v-for="part in activeParts" :key="'sp_' + part" :value="part">{{ part }}</option>
937
+ </select>
938
+ <span style="font-size: 11px; color: #64748b;">→</span>
939
+ <select id="biz-store-target" v-model="bizStoreTarget">
940
+ <option v-for="wh in activeWarehouses" :key="'sw_' + wh" :value="wh">{{ wh }}</option>
941
+ </select>
942
+ <button id="biz-store-btn" class="secondary" @click="handleBizStore">Store</button>
943
+
944
+ <div class="divider"></div>
945
+
946
+ <!-- History Checkbox -->
947
+ <label style="font-size: 11px; display: flex; align-items: center; gap: 4px; cursor: pointer;">
948
+ <input type="checkbox" v-model="state.showHistory"> {{ t.history }}
949
+ </label>
950
+ </div>
951
+ </template>
952
+ </div>
953
+
954
+ <div class="main-container">
955
+ <div id="canvas-container" :style="{ flex: \`0 0 \${layout.leftWidth}%\` }">
956
+ <div ref="visContainer" id="vis-container" v-show="state.graphEngine === 'VIS'"></div>
957
+ <div ref="mermaidContainer" id="mermaid-container" v-show="state.graphEngine === 'MERMAID'"></div>
958
 
959
+ <!-- Dynamic Graph Caption Overlay -->
960
+ <div class="graph-caption">
961
+ <span>📌</span>
962
+ <span>{{ graphCaption }}</span>
963
  </div>
964
+
965
+ <!-- Contextual Dynamic Legend Overlay -->
966
+ <div class="graph-legend">
967
+ <div class="legend-title">
968
+ <span>{{ t.legend.title }}</span>
969
  </div>
970
+ <div v-if="graphLegendItems.nodes.length > 0" class="legend-section">
971
+ <div class="legend-section-title">{{ t.legend.nodes }}</div>
972
+ <div v-for="item in graphLegendItems.nodes" :key="item.id" class="legend-item">
973
+ <div :class="item.swatch"></div>
974
+ <span>{{ item.label }}</span>
975
+ </div>
976
+ </div>
977
+ <div v-if="graphLegendItems.edges.length > 0" class="legend-section">
978
+ <div class="legend-section-title">{{ t.legend.edges }}</div>
979
+ <div v-for="item in graphLegendItems.edges" :key="item.id" class="legend-item">
980
+ <div :class="item.swatch"></div>
981
+ <span>{{ item.label }}</span>
982
+ </div>
983
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
984
  </div>
985
  </div>
986
 
987
+ <!-- Horizontal Resizer -->
988
+ <div class="resizer" id="panel-resizer" @mousedown="startResizeH"></div>
989
+
990
+ <div class="sidebar" style="flex: 1;">
991
+ <!-- 1. BOM View (URMSM) -->
992
+ <div class="card" :style="{ flex: \`0 0 \${layout.bomHeight}%\` }">
993
+ <h4>
994
+ <span>{{ t.titleBom }}</span>
995
+ <span class="iec-badge align-right">IEC 63339 / URMSM<span class="iec-tooltip" v-html="t.tooltips.urmsm"></span></span>
996
+ </h4>
997
+ <div id="tree-container" class="card-scroll-body">
998
+ <template v-if="bomTrees.length > 0">
999
+ <ul class="tree root-tree">
1000
+ <bom-node v-for="root in bomTrees" :key="root.id" :node="root" />
1001
+ </ul>
1002
+ </template>
1003
+ <div v-else class="empty-placeholder">
1004
+ {{ t.emptyBom }}{{ state.currentTime }}
1005
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1006
  </div>
 
1007
  </div>
 
1008
 
1009
+ <div class="resizer-h" @mousedown="(e) => startResizeV(e, 'bomHeight', '.sidebar')"></div>
1010
+
1011
+ <!-- 2. Storage View: UNDER THE BONNET (SMS) -->
1012
+ <div class="card" :style="{ flex: \`0 0 \${layout.storageHeight}%\` }">
1013
+ <h4>
1014
+ <span>{{ t.titleStorage }} <small style="color:#0284c7; font-weight: 700;">({{ state.ontology }})</small></span>
1015
+ <div class="btn-group mini-toggle">
1016
+ <button :class="{ active: state.storage === 'LPG' }" @click="state.storage = 'LPG'">LPG</button>
1017
+ <button :class="{ active: state.storage === 'RDF' }" @click="state.storage = 'RDF'">RDF (W3C)</button>
1018
+ </div>
1019
+ </h4>
1020
+ <div id="storage-content" class="card-scroll-body">
1021
+ <template v-if="state.storage === 'LPG'">
1022
+ <!-- Node Store Table -->
1023
+ <div class="store-sub-header">{{ t.nodeStoreTitle }} ({{ lpgNodeRecords.length }})</div>
1024
+ <table v-if="lpgNodeRecords.length > 0" class="storage-table">
1025
+ <thead>
1026
+ <tr>
1027
+ <th>Node ID</th>
1028
+ <th>Label / Class</th>
1029
+ <th>Properties</th>
1030
+ </tr>
1031
+ </thead>
1032
+ <tbody>
1033
+ <tr v-for="n in lpgNodeRecords" :key="'nd_' + n.id" :class="n.isActive ? 'active-row' : 'closed-row'">
1034
+ <td><strong>{{ n.id }}</strong></td>
1035
+ <td><code>:{{ n.type }}</code></td>
1036
+ <td>
1037
+ <code v-if="formatProperties(n.properties)">{ {{ formatProperties(n.properties) }} }</code>
1038
+ <span v-else>—</span>
1039
+ </td>
1040
+ </tr>
1041
+ </tbody>
1042
+ </table>
1043
+ <div v-else class="empty-placeholder">0 nodes</div>
1044
+
1045
+ <!-- Relationship Store Table -->
1046
+ <div class="store-sub-header" style="margin-top: 8px;">{{ t.edgeStoreTitle }} ({{ lpgRecords.length }})</div>
1047
+ <table v-if="lpgRecords.length > 0" class="storage-table">
1048
+ <thead>
1049
+ <tr>
1050
+ <th>{{ t.sourceCol }}</th>
1051
+ <th>{{ t.targetCol }}</th>
1052
+ <th>Type</th>
1053
+ <th>Properties</th>
1054
+ <th>{{ t.statusCol }}</th>
1055
+ </tr>
1056
+ </thead>
1057
+ <tbody>
1058
+ <tr v-for="row in lpgRecords" :key="row.id" :class="row.isActive ? 'active-row' : 'closed-row'">
1059
+ <td>{{ row.from }}</td>
1060
+ <td><strong>{{ row.to }}</strong></td>
1061
+ <td><code>:{{ row.label }}</code></td>
1062
+ <td>
1063
+ <code v-if="formatProperties(row.properties)">{ {{ formatProperties(row.properties) }} }</code>
1064
+ <span v-else>—</span>
1065
+ </td>
1066
+ <td>{{ row.isActive ? t.activeStatus : t.closedStatus }}</td>
1067
+ </tr>
1068
+ </tbody>
1069
+ </table>
1070
+ <div v-else class="empty-placeholder">0 relationships</div>
1071
+ </template>
1072
+ <pre v-else class="code-block">{{ rdfTtl }}</pre>
1073
+ </div>
1074
+ </div>
1075
 
1076
+ <div class="resizer-h" @mousedown="(e) => startResizeV(e, 'storageHeight', '.sidebar')"></div>
1077
+
1078
+ <!-- 3. Database Query & Live Execution Result -->
1079
+ <div class="card" style="flex: 1;">
1080
+ <h4>
1081
+ <span>{{ t.queryTitle }} ({{ state.storage === 'LPG' ? 'Cypher' : 'SPARQL' }})</span>
1082
+ <span style="font-weight: 600; text-transform: none; color: #0d9488;">{{ t.queryDesc }}{{ state.currentTime }})</span>
1083
+ </h4>
1084
+ <div id="query-content" class="card-scroll-body">
1085
+ <pre class="code-block query-code-block" :style="{ color: state.ontology === 'BFO' ? '#0369a1' : '#3730a3' }">{{ queryCode }}</pre>
1086
+
1087
+ <!-- Live Query Execution Result Box -->
1088
+ <div class="query-result-box">
1089
+ <div class="query-result-header">
1090
+ <span>{{ t.queryResultTitle }}</span>
1091
+ <span
1092
+ class="defect-counter-badge"
1093
+ :class="liveQueryResult.defectiveCount > 0 ? 'defect-alert-found' : 'defect-alert-zero'"
1094
+ >
1095
+ {{ t.queryDefectsFound }} {{ liveQueryResult.defectiveCount }}
1096
+ </span>
1097
+ </div>
1098
+ <table v-if="liveQueryResult.rows.length > 0" class="storage-table">
1099
+ <thead>
1100
+ <tr>
1101
+ <th>part</th>
1102
+ <th>location</th>
1103
+ <th>defect</th>
1104
+ </tr>
1105
+ </thead>
1106
+ <tbody>
1107
+ <tr v-for="(r, idx) in liveQueryResult.rows" :key="idx">
1108
+ <td><strong>{{ r.part }}</strong></td>
1109
+ <td>{{ r.location }}</td>
1110
+ <td>
1111
+ <span :class="r.hasDefect ? 'bom-defect-badge' : 'null-badge'">{{ r.defect }}</span>
1112
+ </td>
1113
+ </tr>
1114
+ </tbody>
1115
+ </table>
1116
+ <div v-else class="empty-placeholder">0 rows returned at T = {{ state.currentTime }}</div>
1117
+ </div>
1118
+ </div>
1119
  </div>
1120
  </div>
1121
  </div>
 
1122
 
1123
+ <!-- Hidden Scenario Audio Element -->
1124
+ <audio
1125
+ ref="scenarioAudio"
1126
+ src="./narratives/Ontologies_-_Disappearing_Crack.mp3"
1127
+ preload="metadata"
1128
+ @timeupdate="onTimeUpdate"
1129
+ @loadedmetadata="onLoadedMetadata"
1130
+ @play="isPlaying = true"
1131
+ @pause="isPlaying = false"
1132
+ @ended="onAudioEnded"
1133
+ ></audio>
1134
+
1135
+ <!-- Subtitles Overlay (Scenario Mode) -->
1136
+ <div
1137
+ v-if="state.currentView === 'SCENARIO' && showSubtitles && currentSubtitleText"
1138
+ class="scenario-subtitles"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1139
  >
1140
+ {{ currentSubtitleText }}
1141
+ </div>
 
1142
 
1143
+ <!-- Floating Scenario Player Panel -->
1144
+ <div
1145
+ v-if="state.currentView === 'SCENARIO'"
1146
+ class="scenario-player-panel"
 
1147
  >
1148
+ <button
1149
+ class="player-btn"
1150
+ @click="togglePlayAudio"
1151
+ :title="isPlaying ? 'Pause' : 'Play'"
1152
+ :aria-label="isPlaying ? 'Pause' : 'Play'"
1153
+ >
1154
+ <span v-if="isPlaying">⏸</span>
1155
+ <span v-else>▶</span>
1156
+ </button>
1157
+
1158
+ <button
1159
+ class="player-btn"
1160
+ @click="stopAudio(true)"
1161
+ title="Stop"
1162
+ aria-label="Stop"
1163
+ >
1164
+ <span>⏹</span>
1165
+ </button>
1166
+
1167
+ <input
1168
+ type="range"
1169
+ class="player-progress"
1170
+ min="0"
1171
+ :max="audioDuration || 100"
1172
+ step="0.1"
1173
+ :value="audioCurrentTime"
1174
+ @input="onSeek"
1175
+ aria-label="Audio Timeline"
1176
+ />
1177
 
1178
+ <span class="player-time">{{ formatTime(audioCurrentTime) }} / {{ formatTime(audioDuration) }}</span>
1179
+
1180
+ <button
1181
+ class="player-btn player-btn-cc"
1182
+ :class="{ active: showSubtitles }"
1183
+ @click="showSubtitles = !showSubtitles"
1184
+ title="Toggle Subtitles"
1185
+ aria-label="Toggle Subtitles"
1186
+ >
1187
+ CC
1188
+ </button>
1189
+ </div>
1190
 
1191
+ <!-- Floating Home Button -->
1192
  <button
1193
+ v-if="state.currentView !== 'MENU'"
1194
+ class="floating-home-btn"
1195
+ @click="goHome"
1196
+ title="Return to Main Menu"
1197
+ aria-label="Return to Main Menu"
1198
  >
1199
+ <svg viewBox="0 0 24 24" width="22" height="22" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round">
1200
+ <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
1201
+ <polyline points="9 22 9 12 15 12 15 22"></polyline>
1202
+ </svg>
1203
  </button>
1204
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1205
  `
1206
  };
1207
 
js/i18n.js CHANGED
@@ -1,78 +1,67 @@
1
  /**
2
  * INTERNATIONALISATION (i18n) DICTIONARIES
3
  * ----------------------------------------
4
- * This module exports the dictionary containing all UI strings, labels,
5
- * and tooltips for Swedish ('sv'), British English ('en'), and Russian ('ru').
6
  */
7
  export const i18n = {
8
  sv: {
9
  ontoTitle: "Ontologi:",
10
- graphTitle: "Datamodell:",
11
  engineTitle: "Grafmotor:",
12
  viewTitle: "Perspektiv:",
13
  viewSmrm: "Ingenjör (SMRM)",
14
  viewLogical: "Logisk (SMIM)",
15
- viewPhysical: "Fysisk (SMS)",
 
16
  viewSmrmCaption: "Ingenjörsperspektiv (SMRM)",
17
  viewLogicalCaption: "Logiskt perspektiv (SMIM)",
18
- viewPhysicalCaption: "Fysiskt perspektiv (SMS)",
 
19
  stepBtn: "Lås steg (T+1)",
20
- parent: "Överordnad:",
21
- partPlaceholder: "Komponentnamn",
22
- addBtn: "+ Lägg till",
23
- removeBtn: "Uteslut relation",
24
  history: "Historik",
25
  titleBom: "Struktur (BOM)",
 
26
  titleStorage: "Under huven",
 
 
27
  activeStatus: "Aktiv",
28
  closedStatus: "Avslutad",
29
  sourceCol: "Källa",
30
  targetCol: "Mål",
31
  statusCol: "Status",
32
- smrmStatus: "Domänontologi (Låst: System/IDO)",
33
  saveBtn: "💾 Spara JSON",
34
  loadBtn: "📂 Ladda JSON",
35
  queryTitle: "Databasfråga",
36
- queryDesc: "Visa aktuell struktur (T = T_nu)",
37
- ontoNaive: "Naiv Graf",
 
38
  ontoNaiveNode: "Naiv Graf (Node)",
39
  ontoNaiveEdge: "Naiv Graf (Edge)",
40
- nodeClass: "Klass:",
41
- nodeIdPlaceholder: "Nod-ID",
42
- addNodeBtn: "+ Nod",
43
- relTypeLabel: "Relation:",
44
- addRelBtn: "+ Relation",
45
- propTarget: "Mål:",
46
- propKeyPlaceholder: "Nyckel (t.ex. crack)",
47
- propValPlaceholder: "Värde (t.ex. 2mm)",
48
- addPropBtn: "+ Egenskap",
49
- targetNode: "Nod",
50
- targetEdge: "Relation",
51
  tooltips: {
52
- urmsm: "<b>URMSM (Dimension & Aspect)</b><br/>Definierar intressentens perspektiv (ISO 42010 Viewpoint). Användaren ser bara BOM-strukturen vid tidpunkten T, oavsett underliggande IT-lager.",
53
- smrm: "<b>SMRM (Concept Repository)</b><br/>Domänontologi och gemensam begreppsbank (System, Component, Physical/Functional Object). Här låst till standardiserad produktnedbrytning.",
54
- smim: "<b>SMIM (Information Elements & Rules)</b><br/>Top-Level Ontology (TLO). Formella regler för hur begreppen existerar i tiden: 3D-objekt med tidsintervall (BFO) eller 4D-rumtidsindivider (ISO 15926).",
55
- sms: "<b>SMS (Data in Tool)</b><br/>Den faktiska IT-implementationen och datalagringen (Property Graph / Neo4j kontra W3C RDF Triples)."
56
  },
57
  legend: {
58
  title: "Teckenförklaring",
59
  nodes: "Noder",
60
  edges: "Relationer",
61
- bfoSys: "Continuant / System (:System)",
62
- bfoElem: "Continuant / Komponent (:Element)",
63
- bfoActiveEdge: "Aktiv relation [giltig till nu]",
64
  bfoPastEdge: "Historisk relation (avslutad)",
65
- bfoQuality: "Kvalitet / Egenskap (:Quality)",
66
  bfoInheres: "inheres_in (egenskap i bärare)",
67
- iso4d: "4D-individ (Spatio-Temporal Extent)",
68
- isoState: "Temporal del / Tillstånd (State)",
69
- isoPartRel: "Tidlös komposition (hasPart)",
70
  isoSliceRel: "temporalPartOf (del av 4D-individ)",
71
  isoTransitionRel: "TRANSITION_TO (tillståndskedja i LPG)",
72
- isoDefect: "Defekt (Tillståndsklassificering)",
73
  isoDefectRel: "has_defect (tillståndsklassificering)",
74
  rdfStmt: "Reifiering (_:statement)",
75
- rdfLit: "Literal (tidsstämpel xsd:int)",
76
  rdfPred: "RDF-predikat",
77
  naiveVehicle: "Fordon (:Vehicle)",
78
  naiveSlot: "Plats (:Slot)",
@@ -80,79 +69,68 @@ export const i18n = {
80
  naiveDefect: "Defekt (:Defect)",
81
  naiveWarehouse: "Lager (:Warehouse)",
82
  relHasSlot: "has_slot (fordonsslot)",
83
- relMountedIn: "mounted_in (monterad i)",
84
  relStoredAt: "stored_at (lagrad på)",
85
  relHasDefect: "has_defect (identifierad defekt)"
86
  }
87
  },
88
  en: {
89
  ontoTitle: "Ontology:",
90
- graphTitle: "Data Model:",
91
  engineTitle: "Renderer:",
92
  viewTitle: "Perspective:",
93
  viewSmrm: "Engineer (SMRM)",
94
  viewLogical: "Logical (SMIM)",
95
- viewPhysical: "Physical (SMS)",
 
96
  viewSmrmCaption: "Engineer Perspective (SMRM)",
97
  viewLogicalCaption: "Logical Perspective (SMIM)",
98
- viewPhysicalCaption: "Physical Perspective (SMS)",
 
99
  stepBtn: "Commit step (T+1)",
100
- parent: "Parent:",
101
- partPlaceholder: "Component name",
102
- addBtn: "+ Add",
103
- removeBtn: "Sever relation",
104
  history: "History",
105
  titleBom: "BOM Tree Structure",
 
106
  titleStorage: "Under the Bonnet",
 
 
107
  activeStatus: "Active",
108
  closedStatus: "Closed",
109
  sourceCol: "Source",
110
  targetCol: "Target",
111
  statusCol: "Status",
112
- smrmStatus: "Domain Ontology (Fixed: System/IDO)",
113
  saveBtn: "💾 Save JSON",
114
  loadBtn: "📂 Load JSON",
115
  queryTitle: "Database Query",
116
- queryDesc: "Show current structure (T = T_now)",
117
- ontoNaive: "Naive Graph",
 
118
  ontoNaiveNode: "Naive Graph (Node)",
119
  ontoNaiveEdge: "Naive Graph (Edge)",
120
- nodeClass: "Class:",
121
- nodeIdPlaceholder: "Node ID",
122
- addNodeBtn: "+ Node",
123
- relTypeLabel: "Relation:",
124
- addRelBtn: "+ Relation",
125
- propTarget: "Target:",
126
- propKeyPlaceholder: "Key (e.g. crack)",
127
- propValPlaceholder: "Value (e.g. 2mm)",
128
- addPropBtn: "+ Property",
129
- targetNode: "Node",
130
- targetEdge: "Relation",
131
  tooltips: {
132
- urmsm: "<b>URMSM (Dimension & Aspect)</b><br/>Captures the stakeholder concern and viewpoint (ISO 42010). The engineer always sees a unified BOM snapshot at time T, decoupled from graph mechanics.",
133
- smrm: "<b>SMRM (Concept Repository)</b><br/>Domain Ontology: the shared engineering lexicon (System, System Element, Physical/Functional Object). Fixed here to physical decomposition.",
134
  smim: "<b>SMIM (Information Elements & Rules)</b><br/>Top-Level Ontology (TLO). Axiomatic rules governing time and identity: 3D Continuants (BFO) vs 4D Spatio-Temporal Extents (ISO 15926).",
135
- sms: "<b>SMS (Data in Tool)</b><br/>Low-level physical IT implementation: Labeled Property Graphs (Neo4j style) vs W3C RDF Triples and serialised statements."
136
  },
137
  legend: {
138
  title: "Graph Legend",
139
  nodes: "Node Types",
140
  edges: "Relationships",
141
- bfoSys: "Continuant / System (:System)",
142
- bfoElem: "Continuant / Component (:Element)",
143
- bfoActiveEdge: "Active relation [valid to now]",
144
  bfoPastEdge: "Historical relation (closed)",
145
  bfoQuality: "Dependent Continuant (:Quality)",
146
  bfoInheres: "inheres_in (Quality inherence)",
147
- iso4d: "4D Individual (Spatio-Temporal Extent)",
148
- isoState: "Temporal Part / State",
149
- isoPartRel: "Tenseless Composition (hasPart)",
150
  isoSliceRel: "temporalPartOf (Extent link)",
151
  isoTransitionRel: "TRANSITION_TO (State Chain in LPG)",
152
- isoDefect: "Defect (State Classification)",
153
  isoDefectRel: "has_defect (State Classification)",
154
  rdfStmt: "Reification Node (_:statement)",
155
- rdfLit: "Literal (xsd:int timestamp)",
156
  rdfPred: "RDF Predicate",
157
  naiveVehicle: "Vehicle (:Vehicle)",
158
  naiveSlot: "Slot (:Slot)",
@@ -160,79 +138,68 @@ export const i18n = {
160
  naiveDefect: "Defect (:Defect)",
161
  naiveWarehouse: "Warehouse (:Warehouse)",
162
  relHasSlot: "has_slot (Vehicle Slot)",
163
- relMountedIn: "mounted_in (Installed in Slot)",
164
  relStoredAt: "stored_at (Stored in Warehouse)",
165
  relHasDefect: "has_defect (Identified Defect)"
166
  }
167
  },
168
  ru: {
169
  ontoTitle: "Онтология:",
170
- graphTitle: "Модель данных:",
171
  engineTitle: "Движок:",
172
  viewTitle: "Ракурс:",
173
  viewSmrm: "Инженер (SMRM)",
174
  viewLogical: "Логический (SMIM)",
175
- viewPhysical: "Физический (SMS)",
 
176
  viewSmrmCaption: "Инженерный ракурс (SMRM)",
177
  viewLogicalCaption: "Логический ракурс (SMIM)",
178
- viewPhysicalCaption: "Физический ракурс (SMS)",
 
179
  stepBtn: "Зафиксировать шаг (T+1)",
180
- parent: "Родитель:",
181
- partPlaceholder: "Имя компонента",
182
- addBtn: "+ Добавить",
183
- removeBtn: "Исключить связь",
184
  history: "История",
185
  titleBom: "Состав изделия (BOM)",
 
186
  titleStorage: "Под капотом",
 
 
187
  activeStatus: "Активна",
188
  closedStatus: "Закрыта",
189
  sourceCol: "Источник",
190
  targetCol: "Цель",
191
  statusCol: "Статус",
192
- smrmStatus: "Domain Ontology (Зафиксирована: System/IDO)",
193
  saveBtn: "💾 Сохранить JSON",
194
  loadBtn: "📂 Загрузить JSON",
195
  queryTitle: "Запрос к БД",
196
- queryDesc: "Показать текущую структуру (T = T_now)",
197
- ontoNaive: "Наивный граф",
 
198
  ontoNaiveNode: "Наивный граф (Узел)",
199
  ontoNaiveEdge: "Наивный граф (Ребро)",
200
- nodeClass: "Класс:",
201
- nodeIdPlaceholder: "ID узла",
202
- addNodeBtn: "+ Узел",
203
- relTypeLabel: "Связь:",
204
- addRelBtn: "+ Связь",
205
- propTarget: "Цель:",
206
- propKeyPlaceholder: "Ключ (напр. crack)",
207
- propValPlaceholder: "Значение (напр. 2mm)",
208
- addPropBtn: "+ Свойство",
209
- targetNode: "Узел",
210
- targetEdge: "Связь",
211
  tooltips: {
212
- urmsm: "<b>URMSM (Dimension & Aspect)</b><br/>Уровень точки зрения пользователя (ISO 42010). Инженер всегда видит чистый срез BOM на момент времени T, абстрагированный от структуры базы данных.",
213
- smrm: "<b>SMRM (Concept Repository)</b><br/>Онтология предметной области (Domain Ontology). Общий словарь понятий проекта (System, Component, Physical Object). Зафиксирована на базовой декомпозиции.",
214
- smim: "<b>SMIM (Information Elements & Rules)</b><br/>Верхнеуровневая онтология (TLO). Математические аксиомы бытия и времени: 3D-тела с интервалами связей (BFO) или 4D-объемы пространства-времени (ISO 15926).",
215
- sms: "<b>SMS (Data in Tool)</b><br/>Физическое IT-хранилище в конкретной СУБД: узлы и свойства на ребрах (LPG) против атомарных триплетов URI (W3C RDF)."
216
  },
217
  legend: {
218
  title: "Легенда графа",
219
  nodes: "Типы узлов",
220
  edges: "Типы связей",
221
- bfoSys: "Континуант / Система (:System)",
222
- bfoElem: "Континуант / Компонент (:Element)",
223
  bfoActiveEdge: "Активная связь [действует сейчас]",
224
  bfoPastEdge: "Архивная связь (закрыта)",
225
  bfoQuality: "Зависимый континуант (:Quality)",
226
  bfoInheres: "inheres_in (присущ носителю)",
227
- iso4d: "4D-индивид (Пространственно-временной объем)",
228
- isoState: "Темпоральный срез / Состояние",
229
- isoPartRel: "Вневременной состав (hasPart)",
230
  isoSliceRel: "temporalPartOf (связь с 4D-целым)",
231
  isoTransitionRel: "TRANSITION_TO (цепочка состояний в LPG)",
232
- isoDefect: "Дефект (Классификация среза)",
233
  isoDefectRel: "has_defect (классификация среза)",
234
  rdfStmt: "Узел реификации (_:statement)",
235
- rdfLit: "Литерал (метка времени xsd:int)",
236
  rdfPred: "RDF-предикат",
237
  naiveVehicle: "Транспорт (:Vehicle)",
238
  naiveSlot: "Слот (:Slot)",
@@ -240,7 +207,7 @@ export const i18n = {
240
  naiveDefect: "Дефект (:Defect)",
241
  naiveWarehouse: "Склад (:Warehouse)",
242
  relHasSlot: "has_slot (слот транспорта)",
243
- relMountedIn: "mounted_in (установлен в)",
244
  relStoredAt: "stored_at (хранится на складе)",
245
  relHasDefect: "has_defect (выявлен дефект)"
246
  }
 
1
  /**
2
  * INTERNATIONALISATION (i18n) DICTIONARIES
3
  * ----------------------------------------
4
+ * Exports UI strings, labels, and tooltips for Swedish ('sv'),
5
+ * British English ('en'), and Russian ('ru').
6
  */
7
  export const i18n = {
8
  sv: {
9
  ontoTitle: "Ontologi:",
 
10
  engineTitle: "Grafmotor:",
11
  viewTitle: "Perspektiv:",
12
  viewSmrm: "Ingenjör (SMRM)",
13
  viewLogical: "Logisk (SMIM)",
14
+ viewPhysicalLpg: "Fysisk LPG (SMS)",
15
+ viewPhysicalRdf: "Fysisk RDF (SMS)",
16
  viewSmrmCaption: "Ingenjörsperspektiv (SMRM)",
17
  viewLogicalCaption: "Logiskt perspektiv (SMIM)",
18
+ viewPhysicalLpgCaption: "Fysiskt LPG-perspektiv (SMS)",
19
+ viewPhysicalRdfCaption: "Fysiskt RDF-perspektiv (SMS)",
20
  stepBtn: "Lås steg (T+1)",
 
 
 
 
21
  history: "Historik",
22
  titleBom: "Struktur (BOM)",
23
+ emptyBom: "Inga aktiva enheter vid T = ",
24
  titleStorage: "Under huven",
25
+ nodeStoreTitle: "Noder (Node Store)",
26
+ edgeStoreTitle: "Relationer (Relationship Store)",
27
  activeStatus: "Aktiv",
28
  closedStatus: "Avslutad",
29
  sourceCol: "Källa",
30
  targetCol: "Mål",
31
  statusCol: "Status",
 
32
  saveBtn: "💾 Spara JSON",
33
  loadBtn: "📂 Ladda JSON",
34
  queryTitle: "Databasfråga",
35
+ queryDesc: "Aktuellt tillstånd & defekter (T = ",
36
+ queryResultTitle: "Exekveringsresultat (Live):",
37
+ queryDefectsFound: "Defekta komponenter på nuvarande plats:",
38
  ontoNaiveNode: "Naiv Graf (Node)",
39
  ontoNaiveEdge: "Naiv Graf (Edge)",
 
 
 
 
 
 
 
 
 
 
 
40
  tooltips: {
41
+ urmsm: "<b>URMSM (Dimension & Aspect)</b><br/>Definierar intressentens perspektiv (ISO 42010 Viewpoint). Ingenjören ser produktnedbrytningen (BOM) och aktiva defekter vid tidpunkten T.",
42
+ smrm: "<b>SMRM (Concept Repository)</b><br/>Domänontologi och gemensam begreppsbank (Vehicle, Slot, Part, Warehouse, Defect).",
43
+ smim: "<b>SMIM (Information Elements & Rules)</b><br/>Top-Level Ontology (TLO). Formella regler för tid och identitet: 3D-kontinuanter (BFO) eller 4D-rumtidsindivider (ISO 15926).",
44
+ sms: "<b>SMS (Data in Tool)</b><br/>Den fysiska datalagringen: Labeled Property Graph (Neo4j / Cypher) kontra W3C RDF Triples (Turtle / SPARQL)."
45
  },
46
  legend: {
47
  title: "Teckenförklaring",
48
  nodes: "Noder",
49
  edges: "Relationer",
50
+ bfoSys: "Continuant / System (:MaterialEntity)",
51
+ bfoElem: "Continuant / Komponent (:MaterialEntity)",
52
+ bfoActiveEdge: "Aktiv relation [giltig nu]",
53
  bfoPastEdge: "Historisk relation (avslutad)",
54
+ bfoQuality: "Kvalitet / Defekt (:Quality)",
55
  bfoInheres: "inheres_in (egenskap i bärare)",
56
+ iso4d: "4D-individ (:ActualIndividual)",
57
+ isoState: "Temporal del / Tillstånd (:State)",
58
+ isoPartRel: "Komposition / Montering",
59
  isoSliceRel: "temporalPartOf (del av 4D-individ)",
60
  isoTransitionRel: "TRANSITION_TO (tillståndskedja i LPG)",
61
+ isoDefect: "Defekt (:Defect)",
62
  isoDefectRel: "has_defect (tillståndsklassificering)",
63
  rdfStmt: "Reifiering (_:statement)",
64
+ rdfLit: "Literal (xsd:integer / xsd:string)",
65
  rdfPred: "RDF-predikat",
66
  naiveVehicle: "Fordon (:Vehicle)",
67
  naiveSlot: "Plats (:Slot)",
 
69
  naiveDefect: "Defekt (:Defect)",
70
  naiveWarehouse: "Lager (:Warehouse)",
71
  relHasSlot: "has_slot (fordonsslot)",
72
+ relMountedIn: "mounted_in / has_part",
73
  relStoredAt: "stored_at (lagrad på)",
74
  relHasDefect: "has_defect (identifierad defekt)"
75
  }
76
  },
77
  en: {
78
  ontoTitle: "Ontology:",
 
79
  engineTitle: "Renderer:",
80
  viewTitle: "Perspective:",
81
  viewSmrm: "Engineer (SMRM)",
82
  viewLogical: "Logical (SMIM)",
83
+ viewPhysicalLpg: "Physical LPG (SMS)",
84
+ viewPhysicalRdf: "Physical RDF (SMS)",
85
  viewSmrmCaption: "Engineer Perspective (SMRM)",
86
  viewLogicalCaption: "Logical Perspective (SMIM)",
87
+ viewPhysicalLpgCaption: "Physical LPG Perspective (SMS)",
88
+ viewPhysicalRdfCaption: "Physical RDF Perspective (SMS)",
89
  stepBtn: "Commit step (T+1)",
 
 
 
 
90
  history: "History",
91
  titleBom: "BOM Tree Structure",
92
+ emptyBom: "No active assets at T = ",
93
  titleStorage: "Under the Bonnet",
94
+ nodeStoreTitle: "Nodes (Node Store)",
95
+ edgeStoreTitle: "Relationships (Relationship Store)",
96
  activeStatus: "Active",
97
  closedStatus: "Closed",
98
  sourceCol: "Source",
99
  targetCol: "Target",
100
  statusCol: "Status",
 
101
  saveBtn: "💾 Save JSON",
102
  loadBtn: "📂 Load JSON",
103
  queryTitle: "Database Query",
104
+ queryDesc: "Current structure & defects (T = ",
105
+ queryResultTitle: "Live Query Output:",
106
+ queryDefectsFound: "Defective parts at current location:",
107
  ontoNaiveNode: "Naive Graph (Node)",
108
  ontoNaiveEdge: "Naive Graph (Edge)",
 
 
 
 
 
 
 
 
 
 
 
109
  tooltips: {
110
+ urmsm: "<b>URMSM (Dimension & Aspect)</b><br/>Captures the stakeholder viewpoint (ISO 42010). The engineer sees a clean BOM breakdown and active defects at time T, decoupled from graph mechanics.",
111
+ smrm: "<b>SMRM (Concept Repository)</b><br/>Domain Ontology: shared engineering lexicon (Vehicle, Slot, Part, Warehouse, Defect).",
112
  smim: "<b>SMIM (Information Elements & Rules)</b><br/>Top-Level Ontology (TLO). Axiomatic rules governing time and identity: 3D Continuants (BFO) vs 4D Spatio-Temporal Extents (ISO 15926).",
113
+ sms: "<b>SMS (Data in Tool)</b><br/>Low-level physical IT storage: Labeled Property Graphs (Neo4j / Cypher) vs W3C RDF Triples (Turtle / SPARQL)."
114
  },
115
  legend: {
116
  title: "Graph Legend",
117
  nodes: "Node Types",
118
  edges: "Relationships",
119
+ bfoSys: "Continuant / System (:MaterialEntity)",
120
+ bfoElem: "Continuant / Component (:MaterialEntity)",
121
+ bfoActiveEdge: "Active relation [valid now]",
122
  bfoPastEdge: "Historical relation (closed)",
123
  bfoQuality: "Dependent Continuant (:Quality)",
124
  bfoInheres: "inheres_in (Quality inherence)",
125
+ iso4d: "4D Individual (:ActualIndividual)",
126
+ isoState: "Temporal Part / State (:State)",
127
+ isoPartRel: "Composition / Assembly",
128
  isoSliceRel: "temporalPartOf (Extent link)",
129
  isoTransitionRel: "TRANSITION_TO (State Chain in LPG)",
130
+ isoDefect: "Defect (:Defect)",
131
  isoDefectRel: "has_defect (State Classification)",
132
  rdfStmt: "Reification Node (_:statement)",
133
+ rdfLit: "Literal (xsd:integer / xsd:string)",
134
  rdfPred: "RDF Predicate",
135
  naiveVehicle: "Vehicle (:Vehicle)",
136
  naiveSlot: "Slot (:Slot)",
 
138
  naiveDefect: "Defect (:Defect)",
139
  naiveWarehouse: "Warehouse (:Warehouse)",
140
  relHasSlot: "has_slot (Vehicle Slot)",
141
+ relMountedIn: "mounted_in / has_part",
142
  relStoredAt: "stored_at (Stored in Warehouse)",
143
  relHasDefect: "has_defect (Identified Defect)"
144
  }
145
  },
146
  ru: {
147
  ontoTitle: "Онтология:",
 
148
  engineTitle: "Движок:",
149
  viewTitle: "Ракурс:",
150
  viewSmrm: "Инженер (SMRM)",
151
  viewLogical: "Логический (SMIM)",
152
+ viewPhysicalLpg: "Физический LPG (SMS)",
153
+ viewPhysicalRdf: "Физический RDF (SMS)",
154
  viewSmrmCaption: "Инженерный ракурс (SMRM)",
155
  viewLogicalCaption: "Логический ракурс (SMIM)",
156
+ viewPhysicalLpgCaption: "Физический ракурс LPG (SMS)",
157
+ viewPhysicalRdfCaption: "Физический ракурс RDF (SMS)",
158
  stepBtn: "Зафиксировать шаг (T+1)",
 
 
 
 
159
  history: "История",
160
  titleBom: "Состав изделия (BOM)",
161
+ emptyBom: "Нет активных объектов при T = ",
162
  titleStorage: "Под капотом",
163
+ nodeStoreTitle: "Узлы (Node Store)",
164
+ edgeStoreTitle: "Связи (Relationship Store)",
165
  activeStatus: "Активна",
166
  closedStatus: "Закрыта",
167
  sourceCol: "Источник",
168
  targetCol: "Цель",
169
  statusCol: "Статус",
 
170
  saveBtn: "💾 Сохранить JSON",
171
  loadBtn: "📂 Загрузить JSON",
172
  queryTitle: "Запрос к БД",
173
+ queryDesc: "Текущий состав и дефекты (T = ",
174
+ queryResultTitle: "Результат выполнения запроса (Live):",
175
+ queryDefectsFound: "Дефектных деталей на текущей позиции:",
176
  ontoNaiveNode: "Наивный граф (Узел)",
177
  ontoNaiveEdge: "Наивный граф (Ребро)",
 
 
 
 
 
 
 
 
 
 
 
178
  tooltips: {
179
+ urmsm: "<b>URMSM (Dimension & Aspect)</b><br/>Точка зрения заинтересованного лица (ISO 42010). Инженер видит чистый срез BOM и активные дефекты на момент T без деталей реализации БД.",
180
+ smrm: "<b>SMRM (Concept Repository)</b><br/>Онтология предметной области: общий инженерный словарь (Vehicle, Slot, Part, Warehouse, Defect).",
181
+ smim: "<b>SMIM (Information Elements & Rules)</b><br/>Верхнеуровневая онтология (TLO). Математические аксиомы времени и идентичности: 3D-континуанты (BFO) или 4D-экстенты (ISO 15926).",
182
+ sms: "<b>SMS (Data in Tool)</b><br/>Физическое хранение данных в СУБД: граф со свойствами на ребрах (LPG / Cypher) против атомарных триплетов W3C RDF (Turtle / SPARQL)."
183
  },
184
  legend: {
185
  title: "Легенда графа",
186
  nodes: "Типы узлов",
187
  edges: "Типы связей",
188
+ bfoSys: "Континуант / Система (:MaterialEntity)",
189
+ bfoElem: "Континуант / Компонент (:MaterialEntity)",
190
  bfoActiveEdge: "Активная связь [действует сейчас]",
191
  bfoPastEdge: "Архивная связь (закрыта)",
192
  bfoQuality: "Зависимый континуант (:Quality)",
193
  bfoInheres: "inheres_in (присущ носителю)",
194
+ iso4d: "4D-индивид (:ActualIndividual)",
195
+ isoState: "Темпоральный срез / Состояние (:State)",
196
+ isoPartRel: "Вневременной состав / Монтаж",
197
  isoSliceRel: "temporalPartOf (связь с 4D-целым)",
198
  isoTransitionRel: "TRANSITION_TO (цепочка состояний в LPG)",
199
+ isoDefect: "Дефект (:Defect)",
200
  isoDefectRel: "has_defect (классификация среза)",
201
  rdfStmt: "Узел реификации (_:statement)",
202
+ rdfLit: "Литерал (xsd:integer / xsd:string)",
203
  rdfPred: "RDF-предикат",
204
  naiveVehicle: "Транспорт (:Vehicle)",
205
  naiveSlot: "Слот (:Slot)",
 
207
  naiveDefect: "Дефект (:Defect)",
208
  naiveWarehouse: "Склад (:Warehouse)",
209
  relHasSlot: "has_slot (слот транспорта)",
210
+ relMountedIn: "mounted_in / has_part",
211
  relStoredAt: "stored_at (хранится на складе)",
212
  relHasDefect: "has_defect (выявлен дефект)"
213
  }
js/renderers/mermaid-adapter.js CHANGED
@@ -2,79 +2,64 @@
2
  * MERMAID RENDERER ADAPTER
3
  * ------------------------
4
  * Translates the abstract Semantic Graph into Mermaid flowchart syntax
5
- * and renders it to the DOM. Like the Vis.js adapter, it relies solely
6
- * on the abstract node/edge types, decoupled from the domain logic.
7
  */
8
 
9
- // Keep track of rendering state to prevent race conditions from rapid UI clicks
 
10
  let isRendering = false;
11
  let pendingDefinition = null;
12
  let renderCounter = 0;
13
 
14
  /**
15
  * Updates the Mermaid graph in the specified container.
16
- *
17
  * @param {Object} semanticGraph - { nodes: [], edges: [] }
18
  * @param {HTMLElement} container - The DOM element to host the SVG.
19
  */
20
  export async function updateMermaidNetwork(semanticGraph, container) {
 
 
 
 
 
 
 
21
  const chartDefinition = buildMermaidDefinition(semanticGraph);
22
 
23
- // If a render is already in progress, queue this definition and exit
24
  if (isRendering) {
25
  pendingDefinition = chartDefinition;
26
  return;
27
  }
28
 
29
- isRendering = true;
30
-
31
- try {
32
- const renderId = 'mermaid_svg_' + (++renderCounter);
33
- // We assume 'mermaid' is globally available from the CDN in index.html
34
- const { svg } = await mermaid.render(renderId, chartDefinition);
35
- container.innerHTML = svg;
36
- } catch (err) {
37
- console.error("[Mermaid Adapter] Render Error:", err);
38
- } finally {
39
- isRendering = false;
40
- // If a new graph was requested while we were rendering, process it now
41
- if (pendingDefinition) {
42
- const nextDefinition = pendingDefinition;
43
- pendingDefinition = null;
44
- // We pass a dummy object here because the definition string is already built,
45
- // but to keep the signature clean for this recursive pattern, it's better
46
- // to extract the render call. For simplicity, we just rebuild it:
47
- executePendingRender(nextDefinition, container);
48
- }
49
- }
50
  }
51
 
52
- async function executePendingRender(definition, container) {
53
  isRendering = true;
54
  try {
55
- const { svg } = await mermaid.render('mermaid_svg_' + (++renderCounter), definition);
 
56
  container.innerHTML = svg;
57
- } catch (e) {
58
- console.error(e);
59
  } finally {
60
  isRendering = false;
61
  if (pendingDefinition) {
62
  const next = pendingDefinition;
63
  pendingDefinition = null;
64
- executePendingRender(next, container);
65
  }
66
  }
67
  }
68
 
69
  /**
70
  * Builds the string definition for the Mermaid flowchart.
71
- * Pure renderer for canonical 3D (BFO) and 4D (ISO 15926) graphs.
72
- *
73
  * @param {Object} semanticGraph - Standardised Semantic Graph.
74
  * @returns {string} Mermaid chart definition string.
75
  */
76
  function buildMermaidDefinition(semanticGraph) {
77
- // 1. Initialise the graph direction and styling classes
78
  let def = 'flowchart TD\n';
79
  def += ' classDef default fill:#f8fafc,stroke:#cbd5e1,color:#334155,stroke-width:1px\n';
80
  def += ' classDef bfoSys fill:#0284c7,stroke:#0369a1,color:#ffffff,stroke-width:2px\n';
@@ -84,26 +69,28 @@ function buildMermaidDefinition(semanticGraph) {
84
  def += ' classDef rdfLit fill:#f1f5f9,stroke:#cbd5e1,color:#334155,stroke-dasharray:3 3\n';
85
  def += ' classDef iso4d fill:#4f46e5,stroke:#3730a3,color:#ffffff,stroke-width:2px\n';
86
  def += ' classDef isoState fill:#0d9488,stroke:#0f766e,color:#ffffff,stroke-width:1px\n';
 
87
  def += ' classDef naiveVehicle fill:#0284c7,stroke:#0369a1,color:#ffffff,stroke-width:2px\n';
88
  def += ' classDef naiveSlot fill:#64748b,stroke:#475569,color:#ffffff,stroke-width:1px\n';
89
  def += ' classDef naivePart fill:#10b981,stroke:#059669,color:#ffffff,stroke-width:2px\n';
90
  def += ' classDef naiveDefect fill:#ef4444,stroke:#dc2626,color:#ffffff,stroke-width:2px\n';
91
  def += ' classDef naiveWarehouse fill:#f97316,stroke:#ea580c,color:#ffffff,stroke-width:2px\n\n';
92
 
93
- // 2. Add Nodes directly from the Semantic Graph (pure renderer)
94
  (semanticGraph.nodes || []).forEach(node => {
95
  const safeId = sanitizeId(node.id);
96
  const styleClass = getNodeClass(node);
97
- let labelText = node.label || '';
 
98
  if (node.properties) {
99
  const customProps = Object.entries(node.properties).filter(
100
- ([k]) => !['isCurrent', 'originalId', 'entityType', 'validFrom', 'validTo', 'ownerId'].includes(k)
101
  );
102
  if (customProps.length > 0) {
103
  const propStr = customProps.map(([k, v]) => `${k}: ${v}`).join(', ');
104
  labelText = `${labelText} {${propStr}}`;
105
  }
106
  }
 
107
  const safeLabel = labelText
108
  .replace(/\n/g, '<br/>')
109
  .replace(/"/g, '&quot;');
@@ -113,10 +100,10 @@ function buildMermaidDefinition(semanticGraph) {
113
 
114
  const entityType = node.type || node.properties?.entityClass;
115
  if (entityType === 'TemporalPartMembership' || entityType === 'RelationalQuality' || entityType === 'Defect' || /^def/i.test(node.id)) {
116
- shapeOpen = '{{"'; // Rhombus / diamond shape
117
  shapeClose = '"}}';
118
  } else if (entityType === 'Literal' || entityType === 'Quality' || entityType === 'DependentContinuant' || /^qual/i.test(node.id)) {
119
- shapeOpen = '(["'; // Oval / capsule shape for Quality and Literals
120
  shapeClose = '"])';
121
  }
122
 
@@ -125,7 +112,6 @@ function buildMermaidDefinition(semanticGraph) {
125
 
126
  def += '\n';
127
 
128
- // 4. Add Edges and collect explicit linkStyle declarations
129
  const linkStyles = [];
130
  (semanticGraph.edges || []).forEach((edge, idx) => {
131
  const safeFrom = sanitizeId(edge.from);
@@ -134,16 +120,15 @@ function buildMermaidDefinition(semanticGraph) {
134
 
135
  let linkArrow = '-->';
136
  if (edge.type === 'transition') {
137
- linkArrow = '==>'; // Thick arrow for LPG state transitions
138
  } else if (!isCurrent || edge.type === 'classification' || edge.type === 'reification' || edge.type === 'predicate') {
139
- linkArrow = '-.->'; // Dashed line for inactive, classification, or reification links
140
  }
141
 
142
- // Format edge label and append custom properties (e.g. defect observations) if present
143
  let rawLabel = edge.label || '';
144
  if (edge.properties) {
145
  const customEdgeProps = Object.entries(edge.properties).filter(
146
- ([k]) => !['isCurrent', 'validFrom', 'validTo', 'originalId', 'ownerId', 'entityType', 'childId', 'parentId'].includes(k)
147
  );
148
  if (customEdgeProps.length > 0) {
149
  const propStr = customEdgeProps.map(([k, v]) => `${k}: ${v}`).join(', ');
@@ -151,12 +136,9 @@ function buildMermaidDefinition(semanticGraph) {
151
  }
152
  }
153
 
154
- // Escape quotes in the label
155
  const safeEdgeLabel = rawLabel.replace(/"/g, "'");
156
-
157
  def += ` ${safeFrom} ${linkArrow}|"${safeEdgeLabel}"| ${safeTo}\n`;
158
 
159
- // Calculate edge colour and line styling to match the legend
160
  const style = getMermaidEdgeStyle(edge);
161
  let styleDecl = `stroke:${style.stroke},stroke-width:${style.width}px`;
162
  if (style.dash) {
@@ -172,13 +154,6 @@ function buildMermaidDefinition(semanticGraph) {
172
  return def;
173
  }
174
 
175
- /**
176
- * Determines the stroke colour, width, and dash style for a Mermaid edge.
177
- * Perfectly synchronises canvas edge rendering with the UI legend colours.
178
- *
179
- * @param {Object} edge - Canonical semantic edge.
180
- * @returns {{ stroke: string, width: number, dash?: string }} Styling descriptor.
181
- */
182
  function getMermaidEdgeStyle(edge) {
183
  const isCurrent = edge.properties?.isCurrent !== false;
184
 
@@ -190,33 +165,23 @@ function getMermaidEdgeStyle(edge) {
190
  const label = (edge.label || '').toLowerCase();
191
 
192
  if (rel === 'has_defect' || label === 'has_defect' || label === 'defect') {
193
- return { stroke: '#ef4444', width: 2 }; // Red
194
  }
195
-
196
- if (rel === 'inherence' || label === 'inheres_in' || label === 'bearer_of') {
197
- return { stroke: '#f59e0b', width: 2 }; // Amber
198
  }
199
-
200
  if (rel === 'has_slot' || label === 'has_slot') {
201
- return { stroke: '#0284c7', width: 2 }; // Blue
202
  }
203
-
204
- if (rel === 'mounted_in' || label === 'mounted_in') {
205
- return { stroke: '#10b981', width: 2 }; // Green
206
  }
207
-
208
  if (rel === 'stored_at' || label === 'stored_at') {
209
- return { stroke: '#f97316', width: 2 }; // Orange
210
- }
211
-
212
- if (rel === 'composition' || rel === 'has_part' || label === 'has_part' || label === 'haspart' || label === 'partof') {
213
- return { stroke: '#10b981', width: 2 }; // Emerald green
214
  }
215
-
216
  if (rel === 'transition') {
217
- return { stroke: '#f59e0b', width: 2 }; // Amber
218
  }
219
-
220
  if (rel === 'classification' || rel === 'reification' || rel === 'predicate' || label === 'temporalpartof') {
221
  return { stroke: '#94a3b8', width: 1, dash: '3 3' };
222
  }
@@ -224,9 +189,6 @@ function getMermaidEdgeStyle(edge) {
224
  return { stroke: '#94a3b8', width: 1 };
225
  }
226
 
227
- /**
228
- * Maps ontological node types to Mermaid CSS classes.
229
- */
230
  function getNodeClass(node) {
231
  const entityType = node.type || node.properties?.entityClass || node.properties?.entityType;
232
 
@@ -242,7 +204,9 @@ function getNodeClass(node) {
242
  case 'Warehouse':
243
  return 'naiveWarehouse';
244
  case 'MaterialEntity':
245
- return node.properties?.entityType === 'system' ? 'bfoSys' : 'bfoElem';
 
 
246
  case 'Quality':
247
  case 'DependentContinuant':
248
  return 'bfoQuality';
@@ -254,7 +218,7 @@ function getNodeClass(node) {
254
  case 'ActualIndividual':
255
  return 'iso4d';
256
  case 'State':
257
- return 'isoState';
258
  default:
259
  if (/^def/i.test(node.id)) return 'naiveDefect';
260
  if (/^qual/i.test(node.id)) return 'bfoQuality';
@@ -262,9 +226,6 @@ function getNodeClass(node) {
262
  }
263
  }
264
 
265
- /**
266
- * Removes unsupported characters from IDs for Mermaid compatibility.
267
- */
268
  function sanitizeId(id) {
269
- return id.replace(/[^a-zA-Z0-9_]/g, '_');
270
  }
 
2
  * MERMAID RENDERER ADAPTER
3
  * ------------------------
4
  * Translates the abstract Semantic Graph into Mermaid flowchart syntax
5
+ * and renders it to the DOM without leaking internal metadata properties.
 
6
  */
7
 
8
+ import { INTERNAL_PROPERTY_KEYS } from '../smrm/definitions.js';
9
+
10
  let isRendering = false;
11
  let pendingDefinition = null;
12
  let renderCounter = 0;
13
 
14
  /**
15
  * Updates the Mermaid graph in the specified container.
16
+ *
17
  * @param {Object} semanticGraph - { nodes: [], edges: [] }
18
  * @param {HTMLElement} container - The DOM element to host the SVG.
19
  */
20
  export async function updateMermaidNetwork(semanticGraph, container) {
21
+ if (!container) return;
22
+
23
+ if (!semanticGraph || !Array.isArray(semanticGraph.nodes) || semanticGraph.nodes.length === 0) {
24
+ container.innerHTML = `<div style="margin: auto; color: #94a3b8; font-size: 13px; font-weight: 500;">Empty Graph Canvas (T = 0)</div>`;
25
+ return;
26
+ }
27
+
28
  const chartDefinition = buildMermaidDefinition(semanticGraph);
29
 
 
30
  if (isRendering) {
31
  pendingDefinition = chartDefinition;
32
  return;
33
  }
34
 
35
+ await executeRender(chartDefinition, container);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  }
37
 
38
+ async function executeRender(definition, container) {
39
  isRendering = true;
40
  try {
41
+ const renderId = 'mermaid_svg_' + (++renderCounter);
42
+ const { svg } = await mermaid.render(renderId, definition);
43
  container.innerHTML = svg;
44
+ } catch (err) {
45
+ console.error('[Mermaid Adapter] Render Error:', err);
46
  } finally {
47
  isRendering = false;
48
  if (pendingDefinition) {
49
  const next = pendingDefinition;
50
  pendingDefinition = null;
51
+ await executeRender(next, container);
52
  }
53
  }
54
  }
55
 
56
  /**
57
  * Builds the string definition for the Mermaid flowchart.
58
+ *
 
59
  * @param {Object} semanticGraph - Standardised Semantic Graph.
60
  * @returns {string} Mermaid chart definition string.
61
  */
62
  function buildMermaidDefinition(semanticGraph) {
 
63
  let def = 'flowchart TD\n';
64
  def += ' classDef default fill:#f8fafc,stroke:#cbd5e1,color:#334155,stroke-width:1px\n';
65
  def += ' classDef bfoSys fill:#0284c7,stroke:#0369a1,color:#ffffff,stroke-width:2px\n';
 
69
  def += ' classDef rdfLit fill:#f1f5f9,stroke:#cbd5e1,color:#334155,stroke-dasharray:3 3\n';
70
  def += ' classDef iso4d fill:#4f46e5,stroke:#3730a3,color:#ffffff,stroke-width:2px\n';
71
  def += ' classDef isoState fill:#0d9488,stroke:#0f766e,color:#ffffff,stroke-width:1px\n';
72
+ def += ' classDef isoStatePast fill:#94a3b8,stroke:#64748b,color:#ffffff,stroke-width:1px,stroke-dasharray:3 3\n';
73
  def += ' classDef naiveVehicle fill:#0284c7,stroke:#0369a1,color:#ffffff,stroke-width:2px\n';
74
  def += ' classDef naiveSlot fill:#64748b,stroke:#475569,color:#ffffff,stroke-width:1px\n';
75
  def += ' classDef naivePart fill:#10b981,stroke:#059669,color:#ffffff,stroke-width:2px\n';
76
  def += ' classDef naiveDefect fill:#ef4444,stroke:#dc2626,color:#ffffff,stroke-width:2px\n';
77
  def += ' classDef naiveWarehouse fill:#f97316,stroke:#ea580c,color:#ffffff,stroke-width:2px\n\n';
78
 
 
79
  (semanticGraph.nodes || []).forEach(node => {
80
  const safeId = sanitizeId(node.id);
81
  const styleClass = getNodeClass(node);
82
+ let labelText = node.label || node.id;
83
+
84
  if (node.properties) {
85
  const customProps = Object.entries(node.properties).filter(
86
+ ([k, v]) => !INTERNAL_PROPERTY_KEYS.includes(k) && v !== undefined && v !== null
87
  );
88
  if (customProps.length > 0) {
89
  const propStr = customProps.map(([k, v]) => `${k}: ${v}`).join(', ');
90
  labelText = `${labelText} {${propStr}}`;
91
  }
92
  }
93
+
94
  const safeLabel = labelText
95
  .replace(/\n/g, '<br/>')
96
  .replace(/"/g, '&quot;');
 
100
 
101
  const entityType = node.type || node.properties?.entityClass;
102
  if (entityType === 'TemporalPartMembership' || entityType === 'RelationalQuality' || entityType === 'Defect' || /^def/i.test(node.id)) {
103
+ shapeOpen = '{{"';
104
  shapeClose = '"}}';
105
  } else if (entityType === 'Literal' || entityType === 'Quality' || entityType === 'DependentContinuant' || /^qual/i.test(node.id)) {
106
+ shapeOpen = '(["';
107
  shapeClose = '"])';
108
  }
109
 
 
112
 
113
  def += '\n';
114
 
 
115
  const linkStyles = [];
116
  (semanticGraph.edges || []).forEach((edge, idx) => {
117
  const safeFrom = sanitizeId(edge.from);
 
120
 
121
  let linkArrow = '-->';
122
  if (edge.type === 'transition') {
123
+ linkArrow = '==>';
124
  } else if (!isCurrent || edge.type === 'classification' || edge.type === 'reification' || edge.type === 'predicate') {
125
+ linkArrow = '-.->';
126
  }
127
 
 
128
  let rawLabel = edge.label || '';
129
  if (edge.properties) {
130
  const customEdgeProps = Object.entries(edge.properties).filter(
131
+ ([k, v]) => !INTERNAL_PROPERTY_KEYS.includes(k) && v !== undefined && v !== null
132
  );
133
  if (customEdgeProps.length > 0) {
134
  const propStr = customEdgeProps.map(([k, v]) => `${k}: ${v}`).join(', ');
 
136
  }
137
  }
138
 
 
139
  const safeEdgeLabel = rawLabel.replace(/"/g, "'");
 
140
  def += ` ${safeFrom} ${linkArrow}|"${safeEdgeLabel}"| ${safeTo}\n`;
141
 
 
142
  const style = getMermaidEdgeStyle(edge);
143
  let styleDecl = `stroke:${style.stroke},stroke-width:${style.width}px`;
144
  if (style.dash) {
 
154
  return def;
155
  }
156
 
 
 
 
 
 
 
 
157
  function getMermaidEdgeStyle(edge) {
158
  const isCurrent = edge.properties?.isCurrent !== false;
159
 
 
165
  const label = (edge.label || '').toLowerCase();
166
 
167
  if (rel === 'has_defect' || label === 'has_defect' || label === 'defect') {
168
+ return { stroke: '#ef4444', width: 2 };
169
  }
170
+ if (rel === 'inherence' || label === 'inheres_in') {
171
+ return { stroke: '#f59e0b', width: 2 };
 
172
  }
 
173
  if (rel === 'has_slot' || label === 'has_slot') {
174
+ return { stroke: '#0284c7', width: 2 };
175
  }
176
+ if (rel === 'mounted_in' || label === 'mounted_in' || rel === 'composition' || rel === 'has_part' || label === 'has_part') {
177
+ return { stroke: '#10b981', width: 2 };
 
178
  }
 
179
  if (rel === 'stored_at' || label === 'stored_at') {
180
+ return { stroke: '#f97316', width: 2 };
 
 
 
 
181
  }
 
182
  if (rel === 'transition') {
183
+ return { stroke: '#f59e0b', width: 2 };
184
  }
 
185
  if (rel === 'classification' || rel === 'reification' || rel === 'predicate' || label === 'temporalpartof') {
186
  return { stroke: '#94a3b8', width: 1, dash: '3 3' };
187
  }
 
189
  return { stroke: '#94a3b8', width: 1 };
190
  }
191
 
 
 
 
192
  function getNodeClass(node) {
193
  const entityType = node.type || node.properties?.entityClass || node.properties?.entityType;
194
 
 
204
  case 'Warehouse':
205
  return 'naiveWarehouse';
206
  case 'MaterialEntity':
207
+ return (node.properties?.entityType === 'system' || node.properties?.entityClass === 'Vehicle')
208
+ ? 'bfoSys'
209
+ : 'bfoElem';
210
  case 'Quality':
211
  case 'DependentContinuant':
212
  return 'bfoQuality';
 
218
  case 'ActualIndividual':
219
  return 'iso4d';
220
  case 'State':
221
+ return node.properties?.isCurrent === false ? 'isoStatePast' : 'isoState';
222
  default:
223
  if (/^def/i.test(node.id)) return 'naiveDefect';
224
  if (/^qual/i.test(node.id)) return 'bfoQuality';
 
226
  }
227
  }
228
 
 
 
 
229
  function sanitizeId(id) {
230
+ return String(id).replace(/[^a-zA-Z0-9_]/g, '_');
231
  }
js/renderers/vis-adapter.js CHANGED
@@ -1,58 +1,54 @@
1
  /**
2
  * VIS.JS RENDERER ADAPTER
3
  * -----------------------
4
- * This module is responsible for bridging the abstract Semantic Graph
5
- * with the visual representations required by the Vis.js Network library.
6
- * It strictly handles styling (colours, shapes, physics) based on node/edge types.
7
  */
8
 
9
- // We assume 'vis' is available globally via the CDN imported in index.html.
 
10
  let network = null;
11
  const visNodes = new vis.DataSet();
12
  const visEdges = new vis.DataSet();
13
 
14
  /**
15
  * Initialises the Vis.js network instance in the specified DOM container.
16
- *
17
  * @param {HTMLElement} container - The DOM element to host the canvas.
18
  */
19
  export function initVisNetwork(container) {
 
20
  if (network) {
21
  network.destroy();
22
  network = null;
23
  }
 
 
24
  const data = { nodes: visNodes, edges: visEdges };
25
  network = new vis.Network(container, data, getBaseOptions(false));
26
  }
27
 
28
  /**
29
- * Updates the network with a new Semantic Graph.
30
- *
31
  * @param {Object} semanticGraph - { nodes: [], edges: [] }
32
- * @param {boolean} isComplex - Flag to adjust physics for denser graphs (e.g. ISO 15926 4D).
33
  */
34
  export function updateVisNetwork(semanticGraph, isComplex = false) {
35
- if (!network) {
36
- console.error("[Vis Adapter] Network not initialised. Call initVisNetwork first.");
37
- return;
38
- }
39
 
40
- // Update physics options based on graph complexity
41
  network.setOptions(getBaseOptions(isComplex));
42
 
43
- // Clear existing data
44
- visNodes.clear();
45
- visEdges.clear();
46
-
47
- // 1. Map and style Nodes directly from the Semantic Graph (pure renderer)
48
  const styledNodes = (semanticGraph.nodes || []).map(node => {
 
49
  const style = getNodeStyle(node);
50
-
51
- // Display custom properties directly on the node label if present
52
  let displayLabel = node.label || node.id;
53
  if (node.properties) {
54
  const customProps = Object.entries(node.properties).filter(
55
- ([k]) => !['isCurrent', 'originalId', 'entityType', 'validFrom', 'validTo', 'ownerId'].includes(k)
56
  );
57
  if (customProps.length > 0) {
58
  const propStr = customProps.map(([k, v]) => `${k}: ${v}`).join(', ');
@@ -78,34 +74,39 @@ export function updateVisNetwork(semanticGraph, isComplex = false) {
78
 
79
  return styledNode;
80
  });
81
- visNodes.add(styledNodes);
 
 
 
 
 
82
 
83
  // 2. Map and style Edges
84
- // We need to handle overlapping edges (multiple events between same parent/child)
85
  const pairMap = {};
86
- semanticGraph.edges.forEach(edge => {
87
  const pairKey = `${edge.from}->${edge.to}`;
88
  if (!pairMap[pairKey]) pairMap[pairKey] = [];
89
  pairMap[pairKey].push(edge);
90
  });
91
 
 
92
  const styledEdges = [];
 
93
  Object.values(pairMap).forEach(edgesGroup => {
94
  const count = edgesGroup.length;
95
-
96
  edgesGroup.forEach((edge, idx) => {
97
- // Create a smooth curve if multiple edges exist between the same nodes
98
- const smoothConfig = count === 1
99
  ? { type: 'continuous', roundness: 0.2 }
100
  : { type: 'curvedCW', roundness: Math.min(0.14 + (idx * 0.14), 0.85) };
101
 
102
  const style = getEdgeStyle(edge);
103
 
104
- // Display custom properties directly on the edge label if present
105
- let displayEdgeLabel = edge.label;
106
  if (edge.properties) {
107
  const customEdgeProps = Object.entries(edge.properties).filter(
108
- ([k]) => !['isCurrent', 'validFrom', 'validTo'].includes(k)
109
  );
110
  if (customEdgeProps.length > 0) {
111
  const propStr = customEdgeProps.map(([k, v]) => `${k}: ${v}`).join(', ');
@@ -127,110 +128,59 @@ export function updateVisNetwork(semanticGraph, isComplex = false) {
127
  });
128
  });
129
 
130
- visEdges.add(styledEdges);
 
 
 
131
  }
132
 
133
- /**
134
- * Determines the visual style of a node based on its ontological type.
135
- * Only handles canonical 3D (BFO) and 4D (ISO 15926) domain entities.
136
- *
137
- * @param {Object} node - Canonical semantic node.
138
- * @returns {Object} Vis.js node styling object.
139
- */
140
  function getNodeStyle(node) {
141
  const entityType = node.type || node.properties?.entityClass || node.properties?.entityType;
142
 
143
  switch (entityType) {
144
- // Specific Manufacturing Domain Classes (Naive Mode)
145
  case 'Vehicle':
146
- return {
147
- shape: 'box',
148
- color: '#0284c7', // Blue
149
- font: { color: '#ffffff', bold: true }
150
- };
151
-
152
  case 'Slot':
153
- return {
154
- shape: 'box',
155
- color: '#64748b', // Grey
156
- font: { color: '#ffffff' }
157
- };
158
-
159
  case 'Part':
160
- return {
161
- shape: 'box',
162
- color: '#10b981', // Green
163
- font: { color: '#ffffff', bold: true }
164
- };
165
-
166
  case 'Defect':
167
- return {
168
- shape: 'diamond',
169
- size: 25,
170
- color: '#ef4444', // Red
171
- font: { color: '#ffffff', bold: true }
172
- };
173
-
174
  case 'Warehouse':
175
- return {
176
- shape: 'box',
177
- color: '#f97316', // Orange
178
- font: { color: '#ffffff', bold: true }
179
- };
180
-
181
- // BFO 3D Continuants
182
  case 'MaterialEntity': {
183
- const isSystem = node.properties?.entityType === 'system';
184
  return {
185
  shape: 'box',
186
- color: isSystem ? '#0284c7' : '#64748b', // Blue for System, Slate for Element
187
  font: { color: '#ffffff', bold: isSystem }
188
  };
189
  }
190
-
191
  case 'Quality':
192
  case 'DependentContinuant':
193
- return {
194
- shape: 'diamond',
195
- size: 25,
196
- color: '#f59e0b', // Amber / orange for Dependent Continuant
197
- font: { color: '#000000', bold: true }
198
- };
199
-
200
- // W3C RDF Reification and Literals (SMS physical layer)
201
  case 'RelationalQuality':
202
  case 'TemporalPartMembership': {
203
  const isStmtActive = node.properties?.isCurrent !== false;
204
  return {
205
  shape: 'diamond',
206
- size: 15,
207
- color: isStmtActive ? '#f59e0b' : '#e2e8f0', // Amber if active, light grey if past
208
  font: { size: 10, color: isStmtActive ? '#000000' : '#64748b', bold: true }
209
  };
210
  }
211
-
212
  case 'Literal':
213
- return {
214
- shape: 'ellipse', // Oval shape
215
- color: '#f8fafc',
216
- font: { size: 8, color: '#334155' }
217
- };
218
-
219
- // ISO 15926 4D Perdurants
220
  case 'ActualIndividual':
 
 
 
221
  return {
222
  shape: 'box',
223
- color: '#4f46e5', // Indigo
224
- font: { color: '#ffffff', bold: true }
225
- };
226
-
227
- case 'State':
228
- return {
229
- shape: 'box',
230
- color: '#0d9488', // Teal
231
  font: { color: '#ffffff' }
232
  };
233
-
234
  default: {
235
  if (/^def/i.test(node.id)) {
236
  return { shape: 'diamond', color: '#ef4444', font: { color: '#ffffff', bold: true } };
@@ -243,94 +193,49 @@ function getNodeStyle(node) {
243
  }
244
  }
245
 
246
- /**
247
- * Determines the visual style of an edge based on its ontological type and temporal validity.
248
- *
249
- * @param {Object} edge - Canonical semantic edge with properties.
250
- * @returns {Object} Vis.js edge styling object.
251
- */
252
  function getEdgeStyle(edge) {
253
  const isCurrent = edge.properties?.isCurrent !== false;
 
254
 
255
- switch (edge.type) {
256
- case 'has_slot':
257
- return {
258
- color: isCurrent ? { color: '#0284c7' } : { color: '#cbd5e1' }, // Blue
259
- dashes: !isCurrent,
260
- width: isCurrent ? 2 : 1
261
- };
262
 
 
 
 
263
  case 'mounted_in':
264
- return {
265
- color: isCurrent ? { color: '#10b981' } : { color: '#cbd5e1' }, // Green
266
- dashes: !isCurrent,
267
- width: isCurrent ? 2 : 1
268
- };
269
-
270
  case 'stored_at':
271
- return {
272
- color: isCurrent ? { color: '#f97316' } : { color: '#cbd5e1' }, // Orange
273
- dashes: !isCurrent,
274
- width: isCurrent ? 2 : 1
275
- };
276
-
277
  case 'has_defect':
278
- return {
279
- color: isCurrent ? { color: '#ef4444' } : { color: '#cbd5e1' }, // Red
280
- dashes: !isCurrent,
281
- width: 2
282
- };
283
-
284
  case 'inherence':
285
- return {
286
- color: isCurrent ? { color: '#f59e0b' } : { color: '#cbd5e1' }, // Amber for inheres_in
287
- dashes: !isCurrent,
288
- width: 2
289
- };
290
-
291
- case 'composition':
292
- case 'has_part':
293
- return {
294
- color: isCurrent ? { color: '#10b981' } : { color: '#cbd5e1' }, // Emerald active, Slate past
295
- dashes: !isCurrent,
296
- width: isCurrent ? 2 : 1
297
- };
298
-
299
  case 'transition':
300
- return {
301
- color: { color: '#f59e0b' }, // Amber for LPG state transitions
302
- dashes: false,
303
- width: 2
304
- };
305
-
306
  case 'classification':
307
  case 'reification':
308
  case 'predicate':
309
- return {
310
- color: { color: '#94a3b8' }, // Subtle slate for structural 4D or RDF links
311
- dashes: !isCurrent,
312
- width: 1
313
- };
314
-
315
  default:
316
- return { color: { color: '#94a3b8' }, dashes: !isCurrent, width: 1 };
317
  }
318
  }
319
 
320
- /**
321
- * Generates the physics configuration for the Vis.js canvas.
322
- */
323
  function getBaseOptions(isComplex) {
324
  return {
325
  physics: {
326
- stabilization: { iterations: 120 },
327
- barnesHut: {
328
- gravitationalConstant: isComplex ? -24000 : -10000,
329
- centralGravity: 0.25,
330
- springLength: isComplex ? 220 : 150,
331
- springConstant: 0.04,
332
- damping: 0.09,
333
- avoidOverlap: 1
334
  }
335
  },
336
  edges: { arrows: 'to' }
 
1
  /**
2
  * VIS.JS RENDERER ADAPTER
3
  * -----------------------
4
+ * Bridges the abstract Semantic Graph with Vis.js Network.
5
+ * Uses incremental DataSet diffing so the graph layout stays stable across time steps.
 
6
  */
7
 
8
+ import { INTERNAL_PROPERTY_KEYS } from '../smrm/definitions.js';
9
+
10
  let network = null;
11
  const visNodes = new vis.DataSet();
12
  const visEdges = new vis.DataSet();
13
 
14
  /**
15
  * Initialises the Vis.js network instance in the specified DOM container.
16
+ *
17
  * @param {HTMLElement} container - The DOM element to host the canvas.
18
  */
19
  export function initVisNetwork(container) {
20
+ if (!container) return;
21
  if (network) {
22
  network.destroy();
23
  network = null;
24
  }
25
+ visNodes.clear();
26
+ visEdges.clear();
27
  const data = { nodes: visNodes, edges: visEdges };
28
  network = new vis.Network(container, data, getBaseOptions(false));
29
  }
30
 
31
  /**
32
+ * Updates the network with a new Semantic Graph using non-destructive DataSet diffing.
33
+ *
34
  * @param {Object} semanticGraph - { nodes: [], edges: [] }
35
+ * @param {boolean} isComplex - Flag to adjust physics for denser graphs.
36
  */
37
  export function updateVisNetwork(semanticGraph, isComplex = false) {
38
+ if (!network || !semanticGraph) return;
 
 
 
39
 
 
40
  network.setOptions(getBaseOptions(isComplex));
41
 
42
+ // 1. Map and style Nodes
43
+ const incomingNodeIds = new Set();
 
 
 
44
  const styledNodes = (semanticGraph.nodes || []).map(node => {
45
+ incomingNodeIds.add(node.id);
46
  const style = getNodeStyle(node);
47
+
 
48
  let displayLabel = node.label || node.id;
49
  if (node.properties) {
50
  const customProps = Object.entries(node.properties).filter(
51
+ ([k, v]) => !INTERNAL_PROPERTY_KEYS.includes(k) && v !== undefined && v !== null
52
  );
53
  if (customProps.length > 0) {
54
  const propStr = customProps.map(([k, v]) => `${k}: ${v}`).join(', ');
 
74
 
75
  return styledNode;
76
  });
77
+
78
+ // Remove obsolete nodes and update/add current nodes
79
+ const existingNodeIds = visNodes.getIds();
80
+ const nodesToRemove = existingNodeIds.filter(id => !incomingNodeIds.has(id));
81
+ if (nodesToRemove.length > 0) visNodes.remove(nodesToRemove);
82
+ if (styledNodes.length > 0) visNodes.update(styledNodes);
83
 
84
  // 2. Map and style Edges
 
85
  const pairMap = {};
86
+ (semanticGraph.edges || []).forEach(edge => {
87
  const pairKey = `${edge.from}->${edge.to}`;
88
  if (!pairMap[pairKey]) pairMap[pairKey] = [];
89
  pairMap[pairKey].push(edge);
90
  });
91
 
92
+ const incomingEdgeIds = new Set();
93
  const styledEdges = [];
94
+
95
  Object.values(pairMap).forEach(edgesGroup => {
96
  const count = edgesGroup.length;
97
+
98
  edgesGroup.forEach((edge, idx) => {
99
+ incomingEdgeIds.add(edge.id);
100
+ const smoothConfig = count === 1
101
  ? { type: 'continuous', roundness: 0.2 }
102
  : { type: 'curvedCW', roundness: Math.min(0.14 + (idx * 0.14), 0.85) };
103
 
104
  const style = getEdgeStyle(edge);
105
 
106
+ let displayEdgeLabel = edge.label || '';
 
107
  if (edge.properties) {
108
  const customEdgeProps = Object.entries(edge.properties).filter(
109
+ ([k, v]) => !INTERNAL_PROPERTY_KEYS.includes(k) && v !== undefined && v !== null
110
  );
111
  if (customEdgeProps.length > 0) {
112
  const propStr = customEdgeProps.map(([k, v]) => `${k}: ${v}`).join(', ');
 
128
  });
129
  });
130
 
131
+ const existingEdgeIds = visEdges.getIds();
132
+ const edgesToRemove = existingEdgeIds.filter(id => !incomingEdgeIds.has(id));
133
+ if (edgesToRemove.length > 0) visEdges.remove(edgesToRemove);
134
+ if (styledEdges.length > 0) visEdges.update(styledEdges);
135
  }
136
 
 
 
 
 
 
 
 
137
  function getNodeStyle(node) {
138
  const entityType = node.type || node.properties?.entityClass || node.properties?.entityType;
139
 
140
  switch (entityType) {
 
141
  case 'Vehicle':
142
+ return { shape: 'box', color: '#0284c7', font: { color: '#ffffff', bold: true } };
 
 
 
 
 
143
  case 'Slot':
144
+ return { shape: 'box', color: '#64748b', font: { color: '#ffffff' } };
 
 
 
 
 
145
  case 'Part':
146
+ return { shape: 'box', color: '#10b981', font: { color: '#ffffff', bold: true } };
 
 
 
 
 
147
  case 'Defect':
148
+ return { shape: 'diamond', size: 25, color: '#ef4444', font: { color: '#ffffff', bold: true } };
 
 
 
 
 
 
149
  case 'Warehouse':
150
+ return { shape: 'box', color: '#f97316', font: { color: '#ffffff', bold: true } };
 
 
 
 
 
 
151
  case 'MaterialEntity': {
152
+ const isSystem = node.properties?.entityType === 'system' || node.properties?.entityClass === 'Vehicle';
153
  return {
154
  shape: 'box',
155
+ color: isSystem ? '#0284c7' : '#64748b',
156
  font: { color: '#ffffff', bold: isSystem }
157
  };
158
  }
 
159
  case 'Quality':
160
  case 'DependentContinuant':
161
+ return { shape: 'diamond', size: 25, color: '#f59e0b', font: { color: '#000000', bold: true } };
 
 
 
 
 
 
 
162
  case 'RelationalQuality':
163
  case 'TemporalPartMembership': {
164
  const isStmtActive = node.properties?.isCurrent !== false;
165
  return {
166
  shape: 'diamond',
167
+ size: 16,
168
+ color: isStmtActive ? '#f59e0b' : '#e2e8f0',
169
  font: { size: 10, color: isStmtActive ? '#000000' : '#64748b', bold: true }
170
  };
171
  }
 
172
  case 'Literal':
173
+ return { shape: 'ellipse', color: '#f8fafc', font: { size: 9, color: '#334155' } };
 
 
 
 
 
 
174
  case 'ActualIndividual':
175
+ return { shape: 'box', color: '#4f46e5', font: { color: '#ffffff', bold: true } };
176
+ case 'State': {
177
+ const isStateActive = node.properties?.isCurrent !== false;
178
  return {
179
  shape: 'box',
180
+ color: isStateActive ? '#0d9488' : '#94a3b8',
 
 
 
 
 
 
 
181
  font: { color: '#ffffff' }
182
  };
183
+ }
184
  default: {
185
  if (/^def/i.test(node.id)) {
186
  return { shape: 'diamond', color: '#ef4444', font: { color: '#ffffff', bold: true } };
 
193
  }
194
  }
195
 
 
 
 
 
 
 
196
  function getEdgeStyle(edge) {
197
  const isCurrent = edge.properties?.isCurrent !== false;
198
+ const rel = edge.type || edge.label;
199
 
200
+ if (!isCurrent) {
201
+ return { color: { color: '#cbd5e1' }, dashes: true, width: 1 };
202
+ }
 
 
 
 
203
 
204
+ switch (rel) {
205
+ case 'has_slot':
206
+ return { color: { color: '#0284c7' }, dashes: false, width: 2 };
207
  case 'mounted_in':
208
+ case 'composition':
209
+ case 'has_part':
210
+ return { color: { color: '#10b981' }, dashes: false, width: 2 };
 
 
 
211
  case 'stored_at':
212
+ return { color: { color: '#f97316' }, dashes: false, width: 2 };
 
 
 
 
 
213
  case 'has_defect':
214
+ return { color: { color: '#ef4444' }, dashes: false, width: 2 };
 
 
 
 
 
215
  case 'inherence':
216
+ return { color: { color: '#f59e0b' }, dashes: false, width: 2 };
 
 
 
 
 
 
 
 
 
 
 
 
 
217
  case 'transition':
218
+ return { color: { color: '#f59e0b' }, dashes: false, width: 2 };
 
 
 
 
 
219
  case 'classification':
220
  case 'reification':
221
  case 'predicate':
222
+ return { color: { color: '#94a3b8' }, dashes: true, width: 1 };
 
 
 
 
 
223
  default:
224
+ return { color: { color: '#94a3b8' }, dashes: false, width: 1 };
225
  }
226
  }
227
 
 
 
 
228
  function getBaseOptions(isComplex) {
229
  return {
230
  physics: {
231
+ stabilization: { iterations: 80, fit: true },
232
+ barnesHut: {
233
+ gravitationalConstant: isComplex ? -22000 : -10000,
234
+ centralGravity: 0.25,
235
+ springLength: isComplex ? 200 : 140,
236
+ springConstant: 0.04,
237
+ damping: 0.12,
238
+ avoidOverlap: 0.9
239
  }
240
  },
241
  edges: { arrows: 'to' }
js/scenarios/scenario-vanishing-crack.js CHANGED
@@ -1,10 +1,8 @@
1
  /**
2
  * SCENARIO: THE VANISHING CRACK
3
  * -----------------------------
4
- * Contains narrative subtitle data and the chronological action timeline
5
  * synchronised with audio playback.
6
- *
7
- * Timestamps are represented in seconds (float).
8
  */
9
 
10
  import {
@@ -13,11 +11,11 @@ import {
13
  createReportDefectEvent,
14
  createDismantleEvent
15
  } from '../smrm/actions.js';
 
16
 
17
  /**
18
  * Highlights specified UI elements by IDs with a temporary glowing effect.
19
- * Simulates a Phantom User interacting with the workbench controls.
20
- *
21
  * @param {string[]} elementIds - Array of DOM element IDs to highlight.
22
  * @param {number} [duration=1200] - Duration of the highlight effect in milliseconds.
23
  */
@@ -78,162 +76,144 @@ const subtitles = [
78
  export const vanishingCrackScenario = {
79
  subtitles,
80
  timeline: [
81
- // 1. t = 1.0: Initialize Naive Node mode, SMRM perspective, and Mermaid engine
82
  {
83
  id: 'step_1_0_init_naive',
84
  time: 1.0,
85
  executed: false,
86
- action(state, renderGraph) {
87
  state.ontology = 'NAIVE_NODE';
88
- state.perspective = 'SMRM';
89
  state.graphEngine = 'MERMAID';
90
  }
91
  },
92
 
93
- // 2. t = 15.5: highlight UI. Action: createRegisterPartEvent('System_A', time)
94
  {
95
  id: 'step_15_5_reg_vehicle',
96
  time: 15.5,
97
  executed: false,
98
- action(state, renderGraph) {
99
  highlightUI(['biz-reg-id', 'biz-reg-btn']);
100
  state.currentTime = 15.5;
101
- const event = createRegisterPartEvent('System_A', state.currentTime);
102
- if (event) {
103
- state.eventLog.push(event);
104
- }
105
  }
106
  },
107
 
108
- // 3. t = 18.0: Audio mentions slot: Simply register Part_M01 and assemble System_A and Part_M01
109
- // The smart adapters implicitly generate the contextual slot!
110
  {
111
  id: 'step_18_0_reg_part_install',
112
  time: 18.0,
113
  executed: false,
114
- action(state, renderGraph) {
115
  highlightUI(['biz-reg-id', 'biz-inst-btn']);
116
  state.currentTime = 18.0;
117
- const regEvent = createRegisterPartEvent('Part_M01', state.currentTime);
118
- if (regEvent) {
119
- state.eventLog.push(regEvent);
120
- }
121
  const asmEvent = createAssembleEvent('System_A', 'Part_M01', 'mounted_in', state.currentTime);
122
- if (asmEvent) {
123
- state.eventLog.push(asmEvent);
124
- }
125
  }
126
  },
127
 
128
- // 4. t = 43.0: highlight UI. Action: createReportDefectEvent('Part_M01', 'crack', '2mm', time) -> Adapter will attach to NODE
129
  {
130
  id: 'step_43_0_attach_crack_node',
131
  time: 43.0,
132
  executed: false,
133
- action(state, renderGraph) {
134
  highlightUI(['biz-def-target', 'biz-def-btn']);
135
  state.currentTime = 43.0;
136
  const event = createReportDefectEvent('Part_M01', 'crack', '2mm', state.currentTime);
137
- if (event) {
138
- state.eventLog.push(event);
139
- }
140
  }
141
  },
142
 
143
- // 5. t = 56.5: Revoke the defect
144
  {
145
  id: 'step_56_5_revoke_crack_node',
146
  time: 56.5,
147
  executed: false,
148
- action(state, renderGraph) {
149
  highlightUI(['biz-rev-defect', 'biz-rev-btn']);
150
  state.currentTime = 56.5;
151
  const idx = state.eventLog.findIndex(e => e.type === 'REPORT_DEFECT' && e.targetId === 'Part_M01');
152
- if (idx !== -1) {
153
- state.eventLog.splice(idx, 1);
154
- }
155
  }
156
  },
157
 
158
- // 6. t = 58.5: Switch state.ontology = 'NAIVE_EDGE' and highlight top header button
159
  {
160
  id: 'step_58_5_switch_strategy_edge',
161
  time: 58.5,
162
  executed: false,
163
- action(state, renderGraph) {
164
  highlightUI(['ctrl-onto-naive-edge']);
165
  state.ontology = 'NAIVE_EDGE';
166
  }
167
  },
168
 
169
- // 7. t = 61.0: highlight UI. Action: createReportDefectEvent('Part_M01', 'crack', '2mm', time) -> Adapter attaches to EDGE
170
  {
171
  id: 'step_61_0_attach_crack_edge',
172
  time: 61.0,
173
  executed: false,
174
- action(state, renderGraph) {
175
  highlightUI(['biz-def-target', 'biz-def-btn']);
176
  state.currentTime = 61.0;
177
  const event = createReportDefectEvent('Part_M01', 'crack', '2mm', state.currentTime);
178
- if (event) {
179
- state.eventLog.push(event);
180
- }
181
  }
182
  },
183
 
184
- // 8. t = 78.0: highlight UI. Action: createDismantleEvent('Part_M01', time)
185
  {
186
  id: 'step_78_0_dismantle_part',
187
  time: 78.0,
188
  executed: false,
189
- action(state, renderGraph) {
190
  highlightUI(['biz-dism-part', 'biz-dism-btn']);
191
  state.currentTime = 78.0;
192
  const event = createDismantleEvent('Part_M01', state.currentTime);
193
- if (event) {
194
- state.eventLog.push(event);
195
- }
196
  }
197
  },
198
 
199
- // 9. t = 82.0: highlight UI. Action: createRegisterPartEvent('Warehouse_1', time)
200
  {
201
  id: 'step_82_0_reg_warehouse',
202
  time: 82.0,
203
  executed: false,
204
- action(state, renderGraph) {
205
  highlightUI(['biz-reg-id', 'biz-reg-btn']);
206
  state.currentTime = 82.0;
207
- const event = createRegisterPartEvent('Warehouse_1', state.currentTime);
208
- if (event) {
209
- state.eventLog.push(event);
210
- }
211
  }
212
  },
213
 
214
- // 10. t = 84.0: highlight UI. Action: createAssembleEvent('Warehouse_1', 'Part_M01', 'stored_at', time)
215
  {
216
  id: 'step_84_0_store_part',
217
  time: 84.0,
218
  executed: false,
219
- action(state, renderGraph) {
220
  highlightUI(['biz-store-part', 'biz-store-target', 'biz-store-btn']);
221
  state.currentTime = 84.0;
222
  const event = createAssembleEvent('Warehouse_1', 'Part_M01', 'stored_at', state.currentTime);
223
- if (event) {
224
- state.eventLog.push(event);
225
- }
226
  }
227
  },
228
 
229
- // 11. t = 122.0: highlight UI (ontology switch). Action: state.ontology = 'ISO15926'
230
  {
231
  id: 'step_122_0_switch_iso',
232
  time: 122.0,
233
  executed: false,
234
- action(state, renderGraph) {
235
- highlightUI(['ctrl-onto-iso']);
236
  state.ontology = 'ISO15926';
 
237
  }
238
  }
239
  ]
@@ -241,18 +221,12 @@ export const vanishingCrackScenario = {
241
 
242
  /**
243
  * Dispatches pending timeline actions based on current playback time.
244
- * Resolves float chunking using `>= step.time && !step.executed`.
245
- *
246
- * @param {number} currentTime - Current audio playback time in seconds.
247
- * @param {Object} state - The reactive domain state.
248
- * @param {Function} [renderGraph] - Function to trigger canvas redraw.
249
  */
250
  export function onTimeUpdate(currentTime, state, renderGraph) {
251
  if (!Array.isArray(vanishingCrackScenario.timeline)) return;
252
 
253
  vanishingCrackScenario.timeline.forEach(step => {
254
  if (currentTime >= step.time && !step.executed) {
255
- console.log('Executing step:', step.time);
256
  step.executed = true;
257
  const fn = step.action || step.execute;
258
  if (typeof fn === 'function') {
@@ -267,8 +241,6 @@ export function onTimeUpdate(currentTime, state, renderGraph) {
267
 
268
  /**
269
  * Resets execution flags on timeline steps when seeking, stopping, or rewinding.
270
- *
271
- * @param {number} [targetTime=0] - Audio target time. Steps with step.time >= targetTime are reset to unexecuted.
272
  */
273
  export function resetTimeline(targetTime = 0) {
274
  if (!Array.isArray(vanishingCrackScenario.timeline)) return;
@@ -277,4 +249,4 @@ export function resetTimeline(targetTime = 0) {
277
  step.executed = false;
278
  }
279
  });
280
- }
 
1
  /**
2
  * SCENARIO: THE VANISHING CRACK
3
  * -----------------------------
4
+ * Narrative subtitle data and chronological action timeline
5
  * synchronised with audio playback.
 
 
6
  */
7
 
8
  import {
 
11
  createReportDefectEvent,
12
  createDismantleEvent
13
  } from '../smrm/actions.js';
14
+ import { setPerspective } from '../state.js';
15
 
16
  /**
17
  * Highlights specified UI elements by IDs with a temporary glowing effect.
18
+ *
 
19
  * @param {string[]} elementIds - Array of DOM element IDs to highlight.
20
  * @param {number} [duration=1200] - Duration of the highlight effect in milliseconds.
21
  */
 
76
  export const vanishingCrackScenario = {
77
  subtitles,
78
  timeline: [
79
+ // 1. t = 1.0: Initialize Naive Node mode, Logical (SMIM) perspective so slot & mounted_in edge are visible
80
  {
81
  id: 'step_1_0_init_naive',
82
  time: 1.0,
83
  executed: false,
84
+ action(state) {
85
  state.ontology = 'NAIVE_NODE';
86
+ setPerspective('SMIM');
87
  state.graphEngine = 'MERMAID';
88
  }
89
  },
90
 
91
+ // 2. t = 15.5: Register Vehicle System_A
92
  {
93
  id: 'step_15_5_reg_vehicle',
94
  time: 15.5,
95
  executed: false,
96
+ action(state) {
97
  highlightUI(['biz-reg-id', 'biz-reg-btn']);
98
  state.currentTime = 15.5;
99
+ const event = createRegisterPartEvent('System_A', state.currentTime, 'Vehicle');
100
+ if (event) state.eventLog.push(event);
 
 
101
  }
102
  },
103
 
104
+ // 3. t = 18.0: Register Part_M01 and install in System_A (creates Slot_System_A_Part_M01)
 
105
  {
106
  id: 'step_18_0_reg_part_install',
107
  time: 18.0,
108
  executed: false,
109
+ action(state) {
110
  highlightUI(['biz-reg-id', 'biz-inst-btn']);
111
  state.currentTime = 18.0;
112
+ const regEvent = createRegisterPartEvent('Part_M01', state.currentTime, 'Part');
113
+ if (regEvent) state.eventLog.push(regEvent);
 
 
114
  const asmEvent = createAssembleEvent('System_A', 'Part_M01', 'mounted_in', state.currentTime);
115
+ if (asmEvent) state.eventLog.push(asmEvent);
 
 
116
  }
117
  },
118
 
119
+ // 4. t = 43.0: Report Defect crack=2mm on Part_M01 (attached to NODE)
120
  {
121
  id: 'step_43_0_attach_crack_node',
122
  time: 43.0,
123
  executed: false,
124
+ action(state) {
125
  highlightUI(['biz-def-target', 'biz-def-btn']);
126
  state.currentTime = 43.0;
127
  const event = createReportDefectEvent('Part_M01', 'crack', '2mm', state.currentTime);
128
+ if (event) state.eventLog.push(event);
 
 
129
  }
130
  },
131
 
132
+ // 5. t = 56.5: Rewind thought-experiment defect before switching strategy to NAIVE_EDGE
133
  {
134
  id: 'step_56_5_revoke_crack_node',
135
  time: 56.5,
136
  executed: false,
137
+ action(state) {
138
  highlightUI(['biz-rev-defect', 'biz-rev-btn']);
139
  state.currentTime = 56.5;
140
  const idx = state.eventLog.findIndex(e => e.type === 'REPORT_DEFECT' && e.targetId === 'Part_M01');
141
+ if (idx !== -1) state.eventLog.splice(idx, 1);
 
 
142
  }
143
  },
144
 
145
+ // 6. t = 58.5: Switch to NAIVE_EDGE
146
  {
147
  id: 'step_58_5_switch_strategy_edge',
148
  time: 58.5,
149
  executed: false,
150
+ action(state) {
151
  highlightUI(['ctrl-onto-naive-edge']);
152
  state.ontology = 'NAIVE_EDGE';
153
  }
154
  },
155
 
156
+ // 7. t = 61.0: Report Defect crack=2mm on Part_M01 (attached to mounted_in EDGE)
157
  {
158
  id: 'step_61_0_attach_crack_edge',
159
  time: 61.0,
160
  executed: false,
161
+ action(state) {
162
  highlightUI(['biz-def-target', 'biz-def-btn']);
163
  state.currentTime = 61.0;
164
  const event = createReportDefectEvent('Part_M01', 'crack', '2mm', state.currentTime);
165
+ if (event) state.eventLog.push(event);
 
 
166
  }
167
  },
168
 
169
+ // 8. t = 78.0: Dismantle Part_M01
170
  {
171
  id: 'step_78_0_dismantle_part',
172
  time: 78.0,
173
  executed: false,
174
+ action(state) {
175
  highlightUI(['biz-dism-part', 'biz-dism-btn']);
176
  state.currentTime = 78.0;
177
  const event = createDismantleEvent('Part_M01', state.currentTime);
178
+ if (event) state.eventLog.push(event);
 
 
179
  }
180
  },
181
 
182
+ // 9. t = 82.0: Register Warehouse_1
183
  {
184
  id: 'step_82_0_reg_warehouse',
185
  time: 82.0,
186
  executed: false,
187
+ action(state) {
188
  highlightUI(['biz-reg-id', 'biz-reg-btn']);
189
  state.currentTime = 82.0;
190
+ const event = createRegisterPartEvent('Warehouse_1', state.currentTime, 'Warehouse');
191
+ if (event) state.eventLog.push(event);
 
 
192
  }
193
  },
194
 
195
+ // 10. t = 84.0: Move Part_M01 to Warehouse_1
196
  {
197
  id: 'step_84_0_store_part',
198
  time: 84.0,
199
  executed: false,
200
+ action(state) {
201
  highlightUI(['biz-store-part', 'biz-store-target', 'biz-store-btn']);
202
  state.currentTime = 84.0;
203
  const event = createAssembleEvent('Warehouse_1', 'Part_M01', 'stored_at', state.currentTime);
204
+ if (event) state.eventLog.push(event);
 
 
205
  }
206
  },
207
 
208
+ // 11. t = 122.0: Switch to ISO 15926 (4D) in Logical (SMIM) Perspective
209
  {
210
  id: 'step_122_0_switch_iso',
211
  time: 122.0,
212
  executed: false,
213
+ action(state) {
214
+ highlightUI(['ctrl-onto-iso', 'ctrl-view-logical']);
215
  state.ontology = 'ISO15926';
216
+ setPerspective('SMIM');
217
  }
218
  }
219
  ]
 
221
 
222
  /**
223
  * Dispatches pending timeline actions based on current playback time.
 
 
 
 
 
224
  */
225
  export function onTimeUpdate(currentTime, state, renderGraph) {
226
  if (!Array.isArray(vanishingCrackScenario.timeline)) return;
227
 
228
  vanishingCrackScenario.timeline.forEach(step => {
229
  if (currentTime >= step.time && !step.executed) {
 
230
  step.executed = true;
231
  const fn = step.action || step.execute;
232
  if (typeof fn === 'function') {
 
241
 
242
  /**
243
  * Resets execution flags on timeline steps when seeking, stopping, or rewinding.
 
 
244
  */
245
  export function resetTimeline(targetTime = 0) {
246
  if (!Array.isArray(vanishingCrackScenario.timeline)) return;
 
249
  step.executed = false;
250
  }
251
  });
252
+ }
js/smim/bfo.js CHANGED
@@ -2,44 +2,50 @@
2
  * BFO ONTOLOGY ADAPTER (3D Continuant)
3
  * ------------------------------------
4
  * Maps Domain Business Events into a canonical 3D BFO Semantic Graph.
5
- *
6
  * Ontological Axioms:
7
- * - Entities (Vehicle, Part, Warehouse) are Independent Continuants ('MaterialEntity')
8
- * that endure wholly through time without temporal parts or states.
9
- * - Defect observations are Specifically Dependent Continuants ('Quality')
10
- * that exist only by inhering in an independent continuant via the 'inheres_in' relation.
11
- * - Relationships between continuants (mounted_in, stored_at, etc.) hold directly
12
- * between the continuants at a given time, carrying temporal validity intervals.
13
- * - Both the Quality node and the 'inheres_in' edge record validFrom: timestamp.
14
- * - NEVER generates 'State' or 'Temporal Part' nodes.
15
- *
16
- * @param {Array} eventLog - The projection of temporal business events.
17
- * @param {Array} [baseNodes=[]] - The projection of base continuant entities.
18
- * @param {number|null} [currentTime=null] - Global time to evaluate current validity.
19
- * @returns {{ nodes: Array, edges: Array }} The canonical Semantic Graph.
20
  */
21
 
22
- export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime = null) {
 
 
 
 
 
 
 
 
 
 
 
23
  const nodeMap = new Map();
24
  const edges = [];
 
25
 
26
  function ensureContinuantNode(partId, overrideClass = null) {
27
  if (!nodeMap.has(partId)) {
 
28
  nodeMap.set(partId, {
29
  id: partId,
30
  label: partId,
31
  type: 'MaterialEntity',
32
  properties: {
33
  originalId: partId,
34
- entityClass: overrideClass || inferContinuantClass(partId)
 
35
  }
36
  });
37
  }
 
38
  }
39
 
40
- // 1. Initialise base continuant entities (if provided)
41
  (baseNodes || []).forEach(bn => {
42
- ensureContinuantNode(bn.id);
43
  });
44
 
45
  // 2. Filter and sort business events by timestamp
@@ -48,12 +54,11 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
48
  .slice()
49
  .sort((a, b) => (a.valid_from || 0) - (b.valid_from || 0));
50
 
51
-
52
  // 3. Process business events in temporal order
53
  eventsToProcess.forEach(event => {
54
  switch (event.type) {
55
  case 'REGISTER_PART': {
56
- ensureContinuantNode(event.partId);
57
  break;
58
  }
59
 
@@ -84,12 +89,11 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
84
  break;
85
  }
86
 
87
- // Implicit contextual Slot generation: Slot_${parentId}_${childId}
88
  const slotId = `Slot_${parentId}_${childId}`;
89
  ensureContinuantNode(slotId, 'Slot');
90
 
91
- // Link 1: Parent -> Slot (has_slot)
92
- const slotEdgeId = `edge_slot_${parentId}_${slotId}_${timestamp}`;
93
  edges.push({
94
  id: slotEdgeId,
95
  from: parentId,
@@ -97,7 +101,7 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
97
  label: 'has_slot',
98
  type: 'has_slot',
99
  childId: slotId,
100
- parentId: parentId,
101
  properties: {
102
  validFrom: timestamp,
103
  validTo: null,
@@ -105,7 +109,6 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
105
  }
106
  });
107
 
108
- // Link 2: Child -> Slot (mounted_in)
109
  const mountEdgeId = event.id ? `edge_${event.id}` : `edge_${childId}_${slotId}_${timestamp}`;
110
  edges.push({
111
  id: mountEdgeId,
@@ -113,7 +116,7 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
113
  to: slotId,
114
  label: 'mounted_in',
115
  type: 'composition',
116
- childId: childId,
117
  parentId: slotId,
118
  properties: {
119
  validFrom: timestamp,
@@ -128,7 +131,6 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
128
  const { childId } = event;
129
  const dismantleTime = event.valid_from;
130
 
131
- // Locate active assembly relationship for the child component
132
  const activeEdge = edges.find(
133
  e => (e.childId === childId || e.from === childId) &&
134
  (e.label === 'mounted_in' || e.label === 'stored_at') &&
@@ -136,11 +138,17 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
136
  );
137
  if (activeEdge) {
138
  activeEdge.properties.validTo = dismantleTime;
139
- activeEdge.properties.isCurrent = (currentTime !== null && currentTime !== undefined)
140
- ? dismantleTime > currentTime
141
- : false;
142
- } else {
143
- console.warn(`[SMIM BFO] No active assembly relationship found to dismantle child part: ${childId}`);
 
 
 
 
 
 
144
  }
145
  break;
146
  }
@@ -154,7 +162,6 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
154
  const qualityId = `qual_${targetId}_${defectName}`;
155
  const qualityLabel = `${capitalize(defectName)}: ${defectValue}`;
156
 
157
- // Specifically Dependent Continuant (Quality) node recording validFrom
158
  nodeMap.set(qualityId, {
159
  id: qualityId,
160
  label: qualityLabel,
@@ -165,11 +172,12 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
165
  value: defectValue,
166
  bearerId: targetId,
167
  entityClass: 'Quality',
168
- validFrom: timestamp
 
 
169
  }
170
  });
171
 
172
- // Canonical BFO relation: Quality inheres_in Independent Continuant (with validFrom)
173
  const edgeId = `inh_${targetId}_${defectName}_${timestamp}`;
174
  edges.push({
175
  id: edgeId,
@@ -186,6 +194,24 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
186
  break;
187
  }
188
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
  default:
190
  break;
191
  }
@@ -197,31 +223,30 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
197
  (currentTime !== null && currentTime !== undefined ? edge.properties.validTo > currentTime : false);
198
  });
199
 
200
- return {
201
- nodes: Array.from(nodeMap.values()),
202
- edges
203
- };
204
- }
 
205
 
206
- /**
207
- * Fallback helper to infer continuant entity class.
208
- *
209
- * @param {string} id - Continuant identifier.
210
- * @returns {string} Inferred entity class name.
211
- */
212
- function inferContinuantClass(id) {
213
- if (/^veh/i.test(id)) return 'Vehicle';
214
- if (/^slot/i.test(id)) return 'Slot';
215
- if (/^ware|^wh/i.test(id)) return 'Warehouse';
216
- return 'Part';
 
 
 
217
  }
218
 
219
- /**
220
- * Capitalises the first letter of a string.
221
- * @param {string} str
222
- * @returns {string}
223
- */
224
  function capitalize(str) {
225
  if (!str) return '';
226
  return str.charAt(0).toUpperCase() + str.slice(1);
227
- }
 
2
  * BFO ONTOLOGY ADAPTER (3D Continuant)
3
  * ------------------------------------
4
  * Maps Domain Business Events into a canonical 3D BFO Semantic Graph.
5
+ *
6
  * Ontological Axioms:
7
+ * - Physical entities (Vehicle, Part, Warehouse, Slot) are Independent Continuants ('MaterialEntity').
8
+ * - Defect observations are Specifically Dependent Continuants ('Quality') inhering in their
9
+ * bearer via 'inheres_in'. Because the Quality inheres in the Part itself (not the mounting slot),
10
+ * moving the Part to a Warehouse preserves its Quality!
 
 
 
 
 
 
 
 
 
11
  */
12
 
13
+ import { inferEntityClass } from '../smrm/definitions.js';
14
+
15
+ /**
16
+ * Generates the 3D BFO Semantic Graph from the event log up to currentTime.
17
+ *
18
+ * @param {Array} eventLog - Temporal business events.
19
+ * @param {Array} [baseNodes=[]] - Initial base entities.
20
+ * @param {number|null} [currentTime=null] - Global evaluation time T.
21
+ * @param {Object} [options={}] - { showHistory: boolean }.
22
+ * @returns {{ nodes: Array, edges: Array }}
23
+ */
24
+ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime = null, options = {}) {
25
  const nodeMap = new Map();
26
  const edges = [];
27
+ const showHistory = options.showHistory !== undefined ? Boolean(options.showHistory) : true;
28
 
29
  function ensureContinuantNode(partId, overrideClass = null) {
30
  if (!nodeMap.has(partId)) {
31
+ const entityClass = inferEntityClass(partId, overrideClass);
32
  nodeMap.set(partId, {
33
  id: partId,
34
  label: partId,
35
  type: 'MaterialEntity',
36
  properties: {
37
  originalId: partId,
38
+ entityClass,
39
+ entityType: entityClass === 'Vehicle' ? 'system' : 'element'
40
  }
41
  });
42
  }
43
+ return nodeMap.get(partId);
44
  }
45
 
46
+ // 1. Initialise base continuant entities
47
  (baseNodes || []).forEach(bn => {
48
+ ensureContinuantNode(bn.id, bn.entityClass || bn.type);
49
  });
50
 
51
  // 2. Filter and sort business events by timestamp
 
54
  .slice()
55
  .sort((a, b) => (a.valid_from || 0) - (b.valid_from || 0));
56
 
 
57
  // 3. Process business events in temporal order
58
  eventsToProcess.forEach(event => {
59
  switch (event.type) {
60
  case 'REGISTER_PART': {
61
+ ensureContinuantNode(event.partId, event.entityClass);
62
  break;
63
  }
64
 
 
89
  break;
90
  }
91
 
92
+ // Contextual Slot generation: Slot_${parentId}_${childId}
93
  const slotId = `Slot_${parentId}_${childId}`;
94
  ensureContinuantNode(slotId, 'Slot');
95
 
96
+ const slotEdgeId = `edge_slot_${event.id || `${parentId}_${slotId}_${timestamp}`}`;
 
97
  edges.push({
98
  id: slotEdgeId,
99
  from: parentId,
 
101
  label: 'has_slot',
102
  type: 'has_slot',
103
  childId: slotId,
104
+ parentId,
105
  properties: {
106
  validFrom: timestamp,
107
  validTo: null,
 
109
  }
110
  });
111
 
 
112
  const mountEdgeId = event.id ? `edge_${event.id}` : `edge_${childId}_${slotId}_${timestamp}`;
113
  edges.push({
114
  id: mountEdgeId,
 
116
  to: slotId,
117
  label: 'mounted_in',
118
  type: 'composition',
119
+ childId,
120
  parentId: slotId,
121
  properties: {
122
  validFrom: timestamp,
 
131
  const { childId } = event;
132
  const dismantleTime = event.valid_from;
133
 
 
134
  const activeEdge = edges.find(
135
  e => (e.childId === childId || e.from === childId) &&
136
  (e.label === 'mounted_in' || e.label === 'stored_at') &&
 
138
  );
139
  if (activeEdge) {
140
  activeEdge.properties.validTo = dismantleTime;
141
+
142
+ if (activeEdge.to && activeEdge.to.startsWith('Slot_')) {
143
+ const slotEdge = edges.find(
144
+ e => e.to === activeEdge.to &&
145
+ e.label === 'has_slot' &&
146
+ e.properties.validTo === null
147
+ );
148
+ if (slotEdge) {
149
+ slotEdge.properties.validTo = dismantleTime;
150
+ }
151
+ }
152
  }
153
  break;
154
  }
 
162
  const qualityId = `qual_${targetId}_${defectName}`;
163
  const qualityLabel = `${capitalize(defectName)}: ${defectValue}`;
164
 
 
165
  nodeMap.set(qualityId, {
166
  id: qualityId,
167
  label: qualityLabel,
 
172
  value: defectValue,
173
  bearerId: targetId,
174
  entityClass: 'Quality',
175
+ validFrom: timestamp,
176
+ validTo: null,
177
+ isCurrent: true
178
  }
179
  });
180
 
 
181
  const edgeId = `inh_${targetId}_${defectName}_${timestamp}`;
182
  edges.push({
183
  id: edgeId,
 
194
  break;
195
  }
196
 
197
+ case 'REVOKE_DEFECT': {
198
+ const { targetId, defectName } = event;
199
+ const revokeTime = event.valid_from;
200
+ const qualityId = `qual_${targetId}_${defectName}`;
201
+
202
+ const activeInherence = edges.find(
203
+ e => e.from === qualityId && e.to === targetId && e.label === 'inheres_in' && e.properties.validTo === null
204
+ );
205
+ if (activeInherence) {
206
+ activeInherence.properties.validTo = revokeTime;
207
+ }
208
+ const qNode = nodeMap.get(qualityId);
209
+ if (qNode && qNode.properties) {
210
+ qNode.properties.validTo = revokeTime;
211
+ }
212
+ break;
213
+ }
214
+
215
  default:
216
  break;
217
  }
 
223
  (currentTime !== null && currentTime !== undefined ? edge.properties.validTo > currentTime : false);
224
  });
225
 
226
+ nodeMap.forEach(node => {
227
+ if (node.type === 'Quality' && node.properties) {
228
+ node.properties.isCurrent = node.properties.validTo === null ||
229
+ (currentTime !== null && currentTime !== undefined ? node.properties.validTo > currentTime : false);
230
+ }
231
+ });
232
 
233
+ const filteredEdges = showHistory
234
+ ? edges
235
+ : edges.filter(e => e.properties.isCurrent !== false);
236
+
237
+ const filteredNodes = Array.from(nodeMap.values()).filter(n => {
238
+ if (showHistory) return true;
239
+ if (n.type === 'Quality') return n.properties?.isCurrent !== false;
240
+ return true;
241
+ });
242
+
243
+ return {
244
+ nodes: filteredNodes,
245
+ edges: filteredEdges
246
+ };
247
  }
248
 
 
 
 
 
 
249
  function capitalize(str) {
250
  if (!str) return '';
251
  return str.charAt(0).toUpperCase() + str.slice(1);
252
+ }
js/smim/iso15926.js CHANGED
@@ -2,26 +2,31 @@
2
  * ISO 15926 ONTOLOGY ADAPTER (4D Perdurant)
3
  * -----------------------------------------
4
  * Maps Domain Business Events into a canonical 4D ISO 15926 Semantic Graph.
5
- *
6
  * Ontological Axioms:
7
- * - Physical entities (Vehicle, Part, Warehouse) are 4D individuals ('ActualIndividual')
8
  * with spatio-temporal extents across their entire lifetime.
9
- * - Relationships (mounted_in, stored_at, etc.) do not connect 4D extents directly;
10
- * they connect temporal slices ('State') of those entities during specific validity periods.
11
- * - Each 'State' is linked to its whole 4D extent via the 'temporalPartOf' relation.
12
- * - A 'Defect' is an independent condition/classification entity that classifies a specific
13
- * temporal part ('State') of an entity during a specific time slice, NOT the root 4D individual.
14
- *
15
- * @param {Array} eventLog - The projection of temporal business events.
16
- * @param {Array} [baseNodes=[]] - The projection of base entities.
17
- * @param {number|null} [currentTime=null] - Global time to evaluate current validity.
18
- * @returns {{ nodes: Array, edges: Array }} The canonical Semantic Graph.
19
  */
20
 
21
- export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime = null) {
 
 
 
 
 
 
 
 
 
 
 
22
  const nodeMap = new Map();
23
  const edges = [];
24
  const stateNodesByOwner = new Map();
 
25
 
26
  function registerStateNode(ownerId, stateNode) {
27
  if (!stateNodesByOwner.has(ownerId)) {
@@ -33,22 +38,23 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
33
  function ensureExtentNode(partId, overrideClass = null) {
34
  const extentId = `4d_${partId}`;
35
  if (!nodeMap.has(extentId)) {
 
36
  nodeMap.set(extentId, {
37
  id: extentId,
38
  label: `${partId}_4D`,
39
  type: 'ActualIndividual',
40
  properties: {
41
  originalId: partId,
42
- entityClass: overrideClass || inferEntityType(partId)
43
  }
44
  });
45
  }
46
  return extentId;
47
  }
48
 
49
- // 1. Initialise base 4D extent nodes (if provided)
50
  (baseNodes || []).forEach(bn => {
51
- ensureExtentNode(bn.id);
52
  });
53
 
54
  // 2. Filter and sort business events by timestamp
@@ -57,12 +63,11 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
57
  .slice()
58
  .sort((a, b) => (a.valid_from || 0) - (b.valid_from || 0));
59
 
60
-
61
  // 3. Process business events in temporal order
62
  eventsToProcess.forEach(event => {
63
  switch (event.type) {
64
  case 'REGISTER_PART': {
65
- ensureExtentNode(event.partId);
66
  break;
67
  }
68
 
@@ -123,12 +128,14 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
123
  type: 'composition',
124
  childId,
125
  parentId,
 
 
126
  properties: { validFrom: timestamp, validTo: null, isCurrent: true }
127
  });
128
  break;
129
  }
130
 
131
- // Implicit contextual Slot generation: Slot_${parentId}_${childId}
132
  const slotId = `Slot_${parentId}_${childId}`;
133
  const slotExtentId = ensureExtentNode(slotId, 'Slot');
134
 
@@ -140,12 +147,7 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
140
  id: parentStateId,
141
  label: `${parentId}_State`,
142
  type: 'State',
143
- properties: {
144
- ownerId: parentId,
145
- validFrom: timestamp,
146
- validTo: null,
147
- isCurrent: true
148
- }
149
  };
150
  nodeMap.set(parentStateId, parentState);
151
  registerStateNode(parentId, parentState);
@@ -154,12 +156,7 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
154
  id: slotStateId,
155
  label: `${slotId}_State`,
156
  type: 'State',
157
- properties: {
158
- ownerId: slotId,
159
- validFrom: timestamp,
160
- validTo: null,
161
- isCurrent: true
162
- }
163
  };
164
  nodeMap.set(slotStateId, slotState);
165
  registerStateNode(slotId, slotState);
@@ -168,17 +165,11 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
168
  id: childStateId,
169
  label: `${childId}_State`,
170
  type: 'State',
171
- properties: {
172
- ownerId: childId,
173
- validFrom: timestamp,
174
- validTo: null,
175
- isCurrent: true
176
- }
177
  };
178
  nodeMap.set(childStateId, childState);
179
  registerStateNode(childId, childState);
180
 
181
- // Link temporal states to their 4D extents
182
  edges.push({
183
  id: `tpo_${parentStateId}`,
184
  from: parentStateId,
@@ -215,14 +206,10 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
215
  label: 'has_slot',
216
  type: 'has_slot',
217
  childId: slotId,
218
- parentId: parentId,
219
  childStateId: slotStateId,
220
- parentStateId: parentStateId,
221
- properties: {
222
- validFrom: timestamp,
223
- validTo: null,
224
- isCurrent: true
225
- }
226
  });
227
 
228
  // Link 2: Child State -> Slot State (mounted_in)
@@ -237,11 +224,7 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
237
  parentId: slotId,
238
  childStateId,
239
  parentStateId: slotStateId,
240
- properties: {
241
- validFrom: timestamp,
242
- validTo: null,
243
- isCurrent: true
244
- }
245
  });
246
  break;
247
  }
@@ -250,7 +233,6 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
250
  const { childId } = event;
251
  const dismantleTime = event.valid_from;
252
 
253
- // Find active assembly or storage edge for this child
254
  const activeAssembly = edges.find(
255
  e => (e.childId === childId || e.from.startsWith(`st_${childId}_`)) &&
256
  (e.label === 'mounted_in' || e.label === 'stored_at') &&
@@ -258,25 +240,43 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
258
  );
259
  if (activeAssembly) {
260
  activeAssembly.properties.validTo = dismantleTime;
261
- activeAssembly.properties.isCurrent = (currentTime !== null && currentTime !== undefined)
262
- ? dismantleTime > currentTime
263
- : false;
264
 
265
- // Close the child state slice
266
  const childState = nodeMap.get(activeAssembly.childStateId);
267
  if (childState) {
268
  childState.properties.validTo = dismantleTime;
269
- childState.properties.isCurrent = activeAssembly.properties.isCurrent;
270
  }
271
-
272
- // Close the child's temporalPartOf link
273
  const tpoChild = edges.find(e => e.id === `tpo_${activeAssembly.childStateId}`);
274
  if (tpoChild) {
275
  tpoChild.properties.validTo = dismantleTime;
276
- tpoChild.properties.isCurrent = activeAssembly.properties.isCurrent;
277
  }
278
- } else {
279
- console.warn(`[SMIM ISO15926] No active assembly found to dismantle child part: ${childId}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
280
  }
281
  break;
282
  }
@@ -287,9 +287,8 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
287
 
288
  const targetExtentId = ensureExtentNode(targetId);
289
 
290
- // Find current active State of the target part
291
  const existingStates = stateNodesByOwner.get(targetId) || [];
292
- let targetState = existingStates.find(st => st.properties?.isCurrent && st.properties?.validTo === null) ||
293
  existingStates[existingStates.length - 1];
294
 
295
  if (!targetState) {
@@ -331,7 +330,9 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
331
  value: defectValue,
332
  targetId,
333
  entityClass: 'Defect',
334
- validFrom: timestamp
 
 
335
  }
336
  });
337
 
@@ -345,49 +346,69 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
345
  properties: {
346
  validFrom: timestamp,
347
  validTo: null,
348
- isCurrent: targetState.properties.isCurrent
349
  }
350
  });
351
  break;
352
  }
353
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
354
  default:
355
  break;
356
  }
357
  });
358
 
359
- // 4. Synchronise final temporal currentness flags
 
 
 
 
 
 
 
 
 
 
360
  edges.forEach(edge => {
361
- edge.properties.isCurrent = edge.properties.validTo === null ||
362
- (currentTime !== null && currentTime !== undefined ? edge.properties.validTo > currentTime : false);
363
  });
364
 
365
- return {
366
- nodes: Array.from(nodeMap.values()),
367
- edges
368
- };
369
- }
 
 
 
 
 
 
 
370
 
371
- /**
372
- * Fallback helper to infer entity type from common identifier naming conventions.
373
- *
374
- * @param {string} id - Node identifier.
375
- * @returns {string} Inferred entity class name.
376
- */
377
- function inferEntityType(id) {
378
- if (/^veh/i.test(id)) return 'Vehicle';
379
- if (/^slot/i.test(id)) return 'Slot';
380
- if (/^def/i.test(id)) return 'Defect';
381
- if (/^ware|^wh/i.test(id)) return 'Warehouse';
382
- return 'Part';
383
  }
384
 
385
- /**
386
- * Capitalises the first letter of a string.
387
- * @param {string} str
388
- * @returns {string}
389
- */
390
  function capitalize(str) {
391
  if (!str) return '';
392
  return str.charAt(0).toUpperCase() + str.slice(1);
393
- }
 
2
  * ISO 15926 ONTOLOGY ADAPTER (4D Perdurant)
3
  * -----------------------------------------
4
  * Maps Domain Business Events into a canonical 4D ISO 15926 Semantic Graph.
5
+ *
6
  * Ontological Axioms:
7
+ * - Physical entities (Vehicle, Part, Warehouse, Slot) are 4D individuals ('ActualIndividual')
8
  * with spatio-temporal extents across their entire lifetime.
9
+ * - Relationships connect temporal slices ('State') of those entities during specific validity periods.
10
+ * - Each 'State' is linked to its 4D extent via 'temporalPartOf'.
11
+ * - A 'Defect' classifies the temporal 'State' of the entity.
 
 
 
 
 
 
 
12
  */
13
 
14
+ import { inferEntityClass } from '../smrm/definitions.js';
15
+
16
+ /**
17
+ * Generates the 4D ISO 15926 Semantic Graph from the event log up to currentTime.
18
+ *
19
+ * @param {Array} eventLog - Temporal business events.
20
+ * @param {Array} [baseNodes=[]] - Initial base entities.
21
+ * @param {number|null} [currentTime=null] - Global evaluation time T.
22
+ * @param {Object} [options={}] - { showHistory: boolean }.
23
+ * @returns {{ nodes: Array, edges: Array }}
24
+ */
25
+ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime = null, options = {}) {
26
  const nodeMap = new Map();
27
  const edges = [];
28
  const stateNodesByOwner = new Map();
29
+ const showHistory = options.showHistory !== undefined ? Boolean(options.showHistory) : true;
30
 
31
  function registerStateNode(ownerId, stateNode) {
32
  if (!stateNodesByOwner.has(ownerId)) {
 
38
  function ensureExtentNode(partId, overrideClass = null) {
39
  const extentId = `4d_${partId}`;
40
  if (!nodeMap.has(extentId)) {
41
+ const entityClass = inferEntityClass(partId, overrideClass);
42
  nodeMap.set(extentId, {
43
  id: extentId,
44
  label: `${partId}_4D`,
45
  type: 'ActualIndividual',
46
  properties: {
47
  originalId: partId,
48
+ entityClass
49
  }
50
  });
51
  }
52
  return extentId;
53
  }
54
 
55
+ // 1. Initialise base 4D extent nodes
56
  (baseNodes || []).forEach(bn => {
57
+ ensureExtentNode(bn.id, bn.entityClass || bn.type);
58
  });
59
 
60
  // 2. Filter and sort business events by timestamp
 
63
  .slice()
64
  .sort((a, b) => (a.valid_from || 0) - (b.valid_from || 0));
65
 
 
66
  // 3. Process business events in temporal order
67
  eventsToProcess.forEach(event => {
68
  switch (event.type) {
69
  case 'REGISTER_PART': {
70
+ ensureExtentNode(event.partId, event.entityClass);
71
  break;
72
  }
73
 
 
128
  type: 'composition',
129
  childId,
130
  parentId,
131
+ childStateId,
132
+ parentStateId,
133
  properties: { validFrom: timestamp, validTo: null, isCurrent: true }
134
  });
135
  break;
136
  }
137
 
138
+ // Contextual Slot generation: Slot_${parentId}_${childId}
139
  const slotId = `Slot_${parentId}_${childId}`;
140
  const slotExtentId = ensureExtentNode(slotId, 'Slot');
141
 
 
147
  id: parentStateId,
148
  label: `${parentId}_State`,
149
  type: 'State',
150
+ properties: { ownerId: parentId, validFrom: timestamp, validTo: null, isCurrent: true }
 
 
 
 
 
151
  };
152
  nodeMap.set(parentStateId, parentState);
153
  registerStateNode(parentId, parentState);
 
156
  id: slotStateId,
157
  label: `${slotId}_State`,
158
  type: 'State',
159
+ properties: { ownerId: slotId, validFrom: timestamp, validTo: null, isCurrent: true }
 
 
 
 
 
160
  };
161
  nodeMap.set(slotStateId, slotState);
162
  registerStateNode(slotId, slotState);
 
165
  id: childStateId,
166
  label: `${childId}_State`,
167
  type: 'State',
168
+ properties: { ownerId: childId, validFrom: timestamp, validTo: null, isCurrent: true }
 
 
 
 
 
169
  };
170
  nodeMap.set(childStateId, childState);
171
  registerStateNode(childId, childState);
172
 
 
173
  edges.push({
174
  id: `tpo_${parentStateId}`,
175
  from: parentStateId,
 
206
  label: 'has_slot',
207
  type: 'has_slot',
208
  childId: slotId,
209
+ parentId,
210
  childStateId: slotStateId,
211
+ parentStateId,
212
+ properties: { validFrom: timestamp, validTo: null, isCurrent: true }
 
 
 
 
213
  });
214
 
215
  // Link 2: Child State -> Slot State (mounted_in)
 
224
  parentId: slotId,
225
  childStateId,
226
  parentStateId: slotStateId,
227
+ properties: { validFrom: timestamp, validTo: null, isCurrent: true }
 
 
 
 
228
  });
229
  break;
230
  }
 
233
  const { childId } = event;
234
  const dismantleTime = event.valid_from;
235
 
 
236
  const activeAssembly = edges.find(
237
  e => (e.childId === childId || e.from.startsWith(`st_${childId}_`)) &&
238
  (e.label === 'mounted_in' || e.label === 'stored_at') &&
 
240
  );
241
  if (activeAssembly) {
242
  activeAssembly.properties.validTo = dismantleTime;
 
 
 
243
 
244
+ // Close child state slice and its temporalPartOf link
245
  const childState = nodeMap.get(activeAssembly.childStateId);
246
  if (childState) {
247
  childState.properties.validTo = dismantleTime;
 
248
  }
 
 
249
  const tpoChild = edges.find(e => e.id === `tpo_${activeAssembly.childStateId}`);
250
  if (tpoChild) {
251
  tpoChild.properties.validTo = dismantleTime;
 
252
  }
253
+
254
+ // Close contextual slot state, its has_slot edge, and the parent state slice
255
+ const slotStateId = activeAssembly.parentStateId;
256
+ if (slotStateId && slotStateId.startsWith('st_Slot_')) {
257
+ const slotState = nodeMap.get(slotStateId);
258
+ if (slotState) {
259
+ slotState.properties.validTo = dismantleTime;
260
+ }
261
+ const tpoSlot = edges.find(e => e.id === `tpo_${slotStateId}`);
262
+ if (tpoSlot) {
263
+ tpoSlot.properties.validTo = dismantleTime;
264
+ }
265
+ const hasSlotEdge = edges.find(
266
+ e => e.to === slotStateId && e.label === 'has_slot' && e.properties.validTo === null
267
+ );
268
+ if (hasSlotEdge) {
269
+ hasSlotEdge.properties.validTo = dismantleTime;
270
+ const parentState = nodeMap.get(hasSlotEdge.parentStateId);
271
+ if (parentState) {
272
+ parentState.properties.validTo = dismantleTime;
273
+ }
274
+ const tpoParent = edges.find(e => e.id === `tpo_${hasSlotEdge.parentStateId}`);
275
+ if (tpoParent) {
276
+ tpoParent.properties.validTo = dismantleTime;
277
+ }
278
+ }
279
+ }
280
  }
281
  break;
282
  }
 
287
 
288
  const targetExtentId = ensureExtentNode(targetId);
289
 
 
290
  const existingStates = stateNodesByOwner.get(targetId) || [];
291
+ let targetState = existingStates.find(st => st.properties?.validTo === null) ||
292
  existingStates[existingStates.length - 1];
293
 
294
  if (!targetState) {
 
330
  value: defectValue,
331
  targetId,
332
  entityClass: 'Defect',
333
+ validFrom: timestamp,
334
+ validTo: null,
335
+ isCurrent: true
336
  }
337
  });
338
 
 
346
  properties: {
347
  validFrom: timestamp,
348
  validTo: null,
349
+ isCurrent: true
350
  }
351
  });
352
  break;
353
  }
354
 
355
+ case 'REVOKE_DEFECT': {
356
+ const { targetId, defectName } = event;
357
+ const revokeTime = event.valid_from;
358
+ const defectNodeId = `def_${targetId}_${defectName}`;
359
+
360
+ edges.forEach(e => {
361
+ if (e.to === defectNodeId && e.label === 'has_defect' && e.properties.validTo === null) {
362
+ e.properties.validTo = revokeTime;
363
+ }
364
+ });
365
+ const dNode = nodeMap.get(defectNodeId);
366
+ if (dNode && dNode.properties) {
367
+ dNode.properties.validTo = revokeTime;
368
+ }
369
+ break;
370
+ }
371
+
372
  default:
373
  break;
374
  }
375
  });
376
 
377
+ // 4. Synchronise final temporal currentness flags across States, Defects, and Edges
378
+ const isTimeActive = (validTo) =>
379
+ validTo === null || validTo === undefined ||
380
+ (currentTime !== null && currentTime !== undefined ? validTo > currentTime : false);
381
+
382
+ nodeMap.forEach(node => {
383
+ if ((node.type === 'State' || node.type === 'Defect') && node.properties) {
384
+ node.properties.isCurrent = isTimeActive(node.properties.validTo);
385
+ }
386
+ });
387
+
388
  edges.forEach(edge => {
389
+ edge.properties.isCurrent = isTimeActive(edge.properties.validTo);
 
390
  });
391
 
392
+ if (!showHistory) {
393
+ const activeEdges = edges.filter(e => e.properties.isCurrent !== false);
394
+ const activeNodeIds = new Set();
395
+ activeEdges.forEach(e => {
396
+ activeNodeIds.add(e.from);
397
+ activeNodeIds.add(e.to);
398
+ });
399
+ const activeNodes = Array.from(nodeMap.values()).filter(
400
+ n => n.type === 'ActualIndividual' || n.properties?.isCurrent !== false || activeNodeIds.has(n.id)
401
+ );
402
+ return { nodes: activeNodes, edges: activeEdges };
403
+ }
404
 
405
+ return {
406
+ nodes: Array.from(nodeMap.values()),
407
+ edges
408
+ };
 
 
 
 
 
 
 
 
409
  }
410
 
 
 
 
 
 
411
  function capitalize(str) {
412
  if (!str) return '';
413
  return str.charAt(0).toUpperCase() + str.slice(1);
414
+ }
js/smim/naive.js CHANGED
@@ -1,44 +1,51 @@
1
  /**
2
  * NAIVE GRAPH ADAPTER (Direct Property Graph)
3
  * --------------------------------------------
4
- * Maps Domain Business Events into a direct, unconstrained Property Graph without
5
- * enforcing 3D Continuant (BFO) or 4D Perdurant (ISO 15926) semantic rules.
6
- *
7
- * In this mode:
8
- * - Entities (Vehicle, Part, Warehouse, etc.) are direct nodes.
9
- * - Assembly events create direct relationships (e.g. 'mounted_in', 'stored_at')
10
- * directed from child component to parent entity.
11
- * - Dismantling events locate the active assembly relationship and close its temporal validity.
12
- * - Defect reports are attached directly to Node properties (if naiveDefectTarget === 'NODE')
13
- * or to the active assembly Edge properties (if naiveDefectTarget === 'EDGE').
14
- *
15
- * Decoupled from database storage (SMS) and UI concerns, strictly adhering
16
- * to the SemanticGraph data contract ({ nodes: [], edges: [] }).
17
- *
18
- * @param {Array} eventLog - The projection of temporal business events.
19
- * @param {Array} [baseNodes=[]] - The projection of base entities.
20
- * @param {number|null} [currentTime=null] - Global time to evaluate current validity.
21
- * @param {Object} [options={}] - Optional configuration overrides.
22
- * @returns {{ nodes: Array, edges: Array }} The canonical Semantic Graph.
23
  */
24
 
25
- import { state } from '../state.js';
26
 
 
 
 
 
 
 
 
 
 
27
  export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime = null, options = {}) {
28
  const nodeMap = new Map();
29
  const edges = [];
30
 
31
- const ontologyMode = options.ontology || state?.ontology || 'NAIVE_NODE';
32
- const defectTargetMode = ontologyMode === 'NAIVE_EDGE' ? 'EDGE' : (options.naiveDefectTarget || state?.naiveDefectTarget || 'NODE');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
- // 1. Initialise base nodes from projection (if provided)
35
  (baseNodes || []).forEach(bn => {
36
- nodeMap.set(bn.id, {
37
- id: bn.id,
38
- label: bn.label || bn.id,
39
- type: bn.entityClass || bn.type || inferEntityType(bn.id),
40
- properties: { ...(bn.properties || {}) }
41
- });
42
  });
43
 
44
  // 2. Filter and sort business events by timestamp
@@ -47,48 +54,24 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
47
  .slice()
48
  .sort((a, b) => (a.valid_from || 0) - (b.valid_from || 0));
49
 
50
-
51
- // 3. Process business events in temporal order
52
  eventsToProcess.forEach(event => {
53
  switch (event.type) {
54
  case 'REGISTER_PART': {
55
- const partId = event.partId;
56
- if (!nodeMap.has(partId)) {
57
- nodeMap.set(partId, {
58
- id: partId,
59
- label: partId,
60
- type: inferEntityType(partId),
61
- properties: {}
62
- });
63
- }
64
  break;
65
  }
66
 
67
  case 'ASSEMBLE': {
68
  const { parentId, childId } = event;
69
  const relType = event.relationType || 'mounted_in';
 
70
 
71
- // Ensure both parent and child nodes exist in the node map
72
- if (!nodeMap.has(parentId)) {
73
- nodeMap.set(parentId, {
74
- id: parentId,
75
- label: parentId,
76
- type: inferEntityType(parentId),
77
- properties: {}
78
- });
79
- }
80
- if (!nodeMap.has(childId)) {
81
- nodeMap.set(childId, {
82
- id: childId,
83
- label: childId,
84
- type: inferEntityType(childId),
85
- properties: {}
86
- });
87
- }
88
 
89
- // Warehouse storage links child directly to warehouse
90
  if (relType === 'stored_at') {
91
- const edgeId = event.id ? `edge_${event.id}` : `edge_${childId}_${parentId}_${event.valid_from}`;
92
  edges.push({
93
  id: edgeId,
94
  from: childId,
@@ -98,7 +81,7 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
98
  childId,
99
  parentId,
100
  properties: {
101
- validFrom: event.valid_from,
102
  validTo: null,
103
  isCurrent: true
104
  }
@@ -106,19 +89,11 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
106
  break;
107
  }
108
 
109
- // Implicit contextual Slot generation: Slot_${parentId}_${childId}
110
  const slotId = `Slot_${parentId}_${childId}`;
111
- if (!nodeMap.has(slotId)) {
112
- nodeMap.set(slotId, {
113
- id: slotId,
114
- label: slotId,
115
- type: 'Slot',
116
- properties: { entityClass: 'Slot' }
117
- });
118
- }
119
 
120
- // Link 1: Parent -> Slot (has_slot)
121
- const slotEdgeId = `edge_slot_${parentId}_${slotId}_${event.valid_from}`;
122
  edges.push({
123
  id: slotEdgeId,
124
  from: parentId,
@@ -126,16 +101,15 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
126
  label: 'has_slot',
127
  type: 'has_slot',
128
  childId: slotId,
129
- parentId: parentId,
130
  properties: {
131
- validFrom: event.valid_from,
132
  validTo: null,
133
  isCurrent: true
134
  }
135
  });
136
 
137
- // Link 2: Child -> Slot (mounted_in)
138
- const mountEdgeId = event.id ? `edge_${event.id}` : `edge_${childId}_${slotId}_${event.valid_from}`;
139
  edges.push({
140
  id: mountEdgeId,
141
  from: childId,
@@ -145,7 +119,7 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
145
  childId,
146
  parentId: slotId,
147
  properties: {
148
- validFrom: event.valid_from,
149
  validTo: null,
150
  isCurrent: true
151
  }
@@ -157,7 +131,7 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
157
  const { childId } = event;
158
  const dismantleTime = event.valid_from;
159
 
160
- // Locate active assembly/storage edge for the child component
161
  const activeEdge = edges.find(
162
  e => (e.childId === childId || e.from === childId) &&
163
  (e.label === 'mounted_in' || e.label === 'stored_at') &&
@@ -165,11 +139,18 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
165
  );
166
  if (activeEdge) {
167
  activeEdge.properties.validTo = dismantleTime;
168
- activeEdge.properties.isCurrent = (currentTime !== null && currentTime !== undefined)
169
- ? dismantleTime > currentTime
170
- : false;
171
- } else {
172
- console.warn(`[SMIM Naive] No active assembly edge found to dismantle child part: ${childId}`);
 
 
 
 
 
 
 
173
  }
174
  break;
175
  }
@@ -178,61 +159,56 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
178
  const { targetId, defectName, defectValue } = event;
179
 
180
  if (defectTargetMode === 'NODE') {
181
- let targetNode = nodeMap.get(targetId);
182
- if (!targetNode) {
183
- targetNode = {
184
- id: targetId,
185
- label: targetId,
186
- type: inferEntityType(targetId),
187
- properties: {}
188
- };
189
- nodeMap.set(targetId, targetNode);
190
- }
191
  targetNode.properties[defectName] = defectValue;
192
- } else if (defectTargetMode === 'EDGE') {
193
- // Find the active assembly edge where targetId is the mounted component
194
  const activeEdge = edges.find(
195
  e => (e.childId === targetId || e.from === targetId) &&
196
- (e.label === 'mounted_in' || e.type === 'mounted_in') &&
197
  (e.properties.validTo === null || e.properties.validTo >= event.valid_from)
198
  );
199
  if (activeEdge) {
200
  activeEdge.properties[defectName] = defectValue;
201
- } else {
202
- console.warn(`[SMIM Naive] No active edge found for defect report on part: ${targetId}`);
203
  }
204
  }
205
  break;
206
  }
207
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
208
  default:
209
  break;
210
  }
211
  });
212
 
213
- // 4. Synchronise final temporal currentness flags
214
  edges.forEach(edge => {
215
- edge.properties.isCurrent = edge.properties.validTo === null ||
216
  (currentTime !== null && currentTime !== undefined ? edge.properties.validTo > currentTime : false);
217
  });
218
 
 
 
 
 
219
  return {
220
  nodes: Array.from(nodeMap.values()),
221
- edges
222
  };
223
- }
224
-
225
- /**
226
- * Fallback helper to infer entity type from common identifier naming conventions.
227
- *
228
- * @param {string} id - Node identifier.
229
- * @returns {string} Inferred entity class name.
230
- */
231
- function inferEntityType(id) {
232
- if (/^veh/i.test(id)) return 'Vehicle';
233
- if (/^slot/i.test(id)) return 'Slot';
234
- if (/^def/i.test(id)) return 'Defect';
235
- if (/^ware|^wh/i.test(id)) return 'Warehouse';
236
- return 'Part';
237
- }
238
-
 
1
  /**
2
  * NAIVE GRAPH ADAPTER (Direct Property Graph)
3
  * --------------------------------------------
4
+ * Pure function mapping Domain Business Events into an unconstrained Property Graph
5
+ * without 3D Continuant (BFO) or 4D Perdurant (ISO 15926) formalisms.
6
+ *
7
+ * - In 'NAIVE_NODE' mode: Defect reports mutate node properties directly (overwriting history).
8
+ * - In 'NAIVE_EDGE' mode: Defect reports attach to the active assembly edge (vanishing upon dismantle).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  */
10
 
11
+ import { inferEntityClass } from '../smrm/definitions.js';
12
 
13
+ /**
14
+ * Generates the Naive Semantic Graph from the event log up to currentTime.
15
+ *
16
+ * @param {Array} eventLog - Temporal business events.
17
+ * @param {Array} [baseNodes=[]] - Initial base entities.
18
+ * @param {number|null} [currentTime=null] - Global evaluation time T.
19
+ * @param {Object} [options={}] - { ontology: 'NAIVE_NODE' | 'NAIVE_EDGE', showHistory: boolean }.
20
+ * @returns {{ nodes: Array, edges: Array }}
21
+ */
22
  export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime = null, options = {}) {
23
  const nodeMap = new Map();
24
  const edges = [];
25
 
26
+ const ontologyMode = options.ontology || 'NAIVE_NODE';
27
+ const defectTargetMode = ontologyMode === 'NAIVE_EDGE' ? 'EDGE' : 'NODE';
28
+ const showHistory = options.showHistory !== undefined ? Boolean(options.showHistory) : true;
29
+
30
+ function ensureNode(id, explicitClass = null, extraProps = {}) {
31
+ if (!nodeMap.has(id)) {
32
+ const resolvedClass = inferEntityClass(id, explicitClass);
33
+ nodeMap.set(id, {
34
+ id,
35
+ label: id,
36
+ type: resolvedClass,
37
+ properties: {
38
+ entityClass: resolvedClass,
39
+ ...extraProps
40
+ }
41
+ });
42
+ }
43
+ return nodeMap.get(id);
44
+ }
45
 
46
+ // 1. Initialise base nodes
47
  (baseNodes || []).forEach(bn => {
48
+ ensureNode(bn.id, bn.entityClass || bn.type, bn.properties || {});
 
 
 
 
 
49
  });
50
 
51
  // 2. Filter and sort business events by timestamp
 
54
  .slice()
55
  .sort((a, b) => (a.valid_from || 0) - (b.valid_from || 0));
56
 
57
+ // 3. Process business events in chronological order
 
58
  eventsToProcess.forEach(event => {
59
  switch (event.type) {
60
  case 'REGISTER_PART': {
61
+ ensureNode(event.partId, event.entityClass);
 
 
 
 
 
 
 
 
62
  break;
63
  }
64
 
65
  case 'ASSEMBLE': {
66
  const { parentId, childId } = event;
67
  const relType = event.relationType || 'mounted_in';
68
+ const timestamp = event.valid_from;
69
 
70
+ ensureNode(parentId);
71
+ ensureNode(childId);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
 
 
73
  if (relType === 'stored_at') {
74
+ const edgeId = event.id ? `edge_${event.id}` : `edge_${childId}_${parentId}_${timestamp}`;
75
  edges.push({
76
  id: edgeId,
77
  from: childId,
 
81
  childId,
82
  parentId,
83
  properties: {
84
+ validFrom: timestamp,
85
  validTo: null,
86
  isCurrent: true
87
  }
 
89
  break;
90
  }
91
 
92
+ // Contextual Slot generation: Slot_${parentId}_${childId}
93
  const slotId = `Slot_${parentId}_${childId}`;
94
+ ensureNode(slotId, 'Slot');
 
 
 
 
 
 
 
95
 
96
+ const slotEdgeId = `edge_slot_${event.id || `${parentId}_${slotId}_${timestamp}`}`;
 
97
  edges.push({
98
  id: slotEdgeId,
99
  from: parentId,
 
101
  label: 'has_slot',
102
  type: 'has_slot',
103
  childId: slotId,
104
+ parentId,
105
  properties: {
106
+ validFrom: timestamp,
107
  validTo: null,
108
  isCurrent: true
109
  }
110
  });
111
 
112
+ const mountEdgeId = event.id ? `edge_${event.id}` : `edge_${childId}_${slotId}_${timestamp}`;
 
113
  edges.push({
114
  id: mountEdgeId,
115
  from: childId,
 
119
  childId,
120
  parentId: slotId,
121
  properties: {
122
+ validFrom: timestamp,
123
  validTo: null,
124
  isCurrent: true
125
  }
 
131
  const { childId } = event;
132
  const dismantleTime = event.valid_from;
133
 
134
+ // Close active mounted_in or stored_at edge for this child
135
  const activeEdge = edges.find(
136
  e => (e.childId === childId || e.from === childId) &&
137
  (e.label === 'mounted_in' || e.label === 'stored_at') &&
 
139
  );
140
  if (activeEdge) {
141
  activeEdge.properties.validTo = dismantleTime;
142
+
143
+ // Also close the contextual has_slot edge if mounted in a Slot
144
+ if (activeEdge.to && activeEdge.to.startsWith('Slot_')) {
145
+ const slotEdge = edges.find(
146
+ e => e.to === activeEdge.to &&
147
+ e.label === 'has_slot' &&
148
+ e.properties.validTo === null
149
+ );
150
+ if (slotEdge) {
151
+ slotEdge.properties.validTo = dismantleTime;
152
+ }
153
+ }
154
  }
155
  break;
156
  }
 
159
  const { targetId, defectName, defectValue } = event;
160
 
161
  if (defectTargetMode === 'NODE') {
162
+ const targetNode = ensureNode(targetId);
 
 
 
 
 
 
 
 
 
163
  targetNode.properties[defectName] = defectValue;
164
+ } else {
165
+ // EDGE mode: attach defect to currently active assembly/storage edge
166
  const activeEdge = edges.find(
167
  e => (e.childId === targetId || e.from === targetId) &&
168
+ (e.label === 'mounted_in' || e.label === 'stored_at') &&
169
  (e.properties.validTo === null || e.properties.validTo >= event.valid_from)
170
  );
171
  if (activeEdge) {
172
  activeEdge.properties[defectName] = defectValue;
 
 
173
  }
174
  }
175
  break;
176
  }
177
 
178
+ case 'REVOKE_DEFECT': {
179
+ const { targetId, defectName } = event;
180
+ if (defectTargetMode === 'NODE') {
181
+ const targetNode = nodeMap.get(targetId);
182
+ if (targetNode && targetNode.properties) {
183
+ delete targetNode.properties[defectName];
184
+ }
185
+ } else {
186
+ edges.forEach(e => {
187
+ if ((e.childId === targetId || e.from === targetId) && e.properties && defectName in e.properties) {
188
+ delete e.properties[defectName];
189
+ }
190
+ });
191
+ }
192
+ break;
193
+ }
194
+
195
  default:
196
  break;
197
  }
198
  });
199
 
200
+ // 4. Synchronise temporal currentness flags
201
  edges.forEach(edge => {
202
+ edge.properties.isCurrent = edge.properties.validTo === null ||
203
  (currentTime !== null && currentTime !== undefined ? edge.properties.validTo > currentTime : false);
204
  });
205
 
206
+ const filteredEdges = showHistory
207
+ ? edges
208
+ : edges.filter(e => e.properties.isCurrent !== false);
209
+
210
  return {
211
  nodes: Array.from(nodeMap.values()),
212
+ edges: filteredEdges
213
  };
214
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
js/smrm/actions.js CHANGED
@@ -3,57 +3,62 @@
3
  * ------------------------------------
4
  * This module contains pure factory functions responsible for creating
5
  * immutable business Domain Events for the Event Sourcing architecture.
6
- *
7
- * In accordance with Domain-Driven Event Sourcing:
8
- * - Events represent pure business facts, NOT low-level graph mutations.
9
- * - No graph concepts (e.g. 'NODE', 'EDGE') exist at this layer.
10
- *
11
  * Strict Architectural Rule:
12
  * Functions here MUST NOT import the singleton state or directly mutate data.
13
  * All functions are pure factories returning event descriptors to be dispatched.
14
  */
15
 
16
- import { RELATION_TYPES } from './definitions.js';
 
 
 
 
 
 
17
 
18
  /**
19
- * Creates a business event representing the registration/creation of a domain part.
20
- * Pure factory function - does not mutate any external state.
21
- *
22
- * @param {string} partId - The unique business identifier of the part or component.
23
- * @param {number} [timestamp=0] - The discrete system timestamp of the registration.
 
24
  * @returns {Object|null} The created registration business event, or null if validation fails.
25
  */
26
- export function createRegisterPartEvent(partId, timestamp = 0) {
27
  if (!partId) return null;
28
  const trimmedId = String(partId).trim();
29
  if (!trimmedId) return null;
30
 
 
 
31
  return {
32
- id: `evt_reg_${trimmedId}`,
33
  type: 'REGISTER_PART',
34
  partId: trimmedId,
 
35
  valid_from: timestamp,
36
  valid_to: null
37
  };
38
  }
39
 
40
  /**
41
- * Creates a business event representing the assembly or containment of a child part into a parent entity.
42
- * Pure factory function - does not mutate any external state.
43
- *
44
- * @param {string} parentId - The unique business identifier of the parent entity/container.
45
- * @param {string} childId - The unique business identifier of the assembled child component.
46
- * @param {string} [relationType='mounted_in'] - The relationship category (e.g. 'mounted_in', 'stored_at').
47
- * @param {number} [timestamp=0] - The discrete system timestamp of the assembly.
48
  * @returns {Object|null} The created assembly business event, or null if validation fails.
49
  */
50
- export function createAssembleEvent(parentId, childId, relationType = 'mounted_in', timestamp = 0) {
51
  if (!parentId || !childId) return null;
52
 
53
  const trimmedParent = String(parentId).trim();
54
  const trimmedChild = String(childId).trim();
55
 
56
- // Rule 1: An entity cannot be assembled into itself
57
  if (!trimmedParent || !trimmedChild || trimmedParent === trimmedChild) {
58
  return null;
59
  }
@@ -63,7 +68,7 @@ export function createAssembleEvent(parentId, childId, relationType = 'mounted_i
63
  : (relationType ? String(relationType).trim() : 'mounted_in');
64
 
65
  return {
66
- id: `evt_asm_${trimmedChild}_${timestamp}`,
67
  type: 'ASSEMBLE',
68
  parentId: trimmedParent,
69
  childId: trimmedChild,
@@ -75,15 +80,15 @@ export function createAssembleEvent(parentId, childId, relationType = 'mounted_i
75
 
76
  /**
77
  * Creates a business event reporting a physical defect or quality observation on an entity.
78
- * Pure factory function - targetId is ALWAYS the Part/Vehicle ID, never a relationship/edge ID.
79
- *
80
- * @param {string} targetId - The unique identifier of the part or vehicle bearing the defect.
81
- * @param {string} defectName - The characteristic or defect category (e.g. 'crack', 'dent', 'wear').
82
- * @param {string|number} defectValue - The recorded measurement or severity value (e.g. '2mm', 'high').
83
- * @param {number} [timestamp=0] - The discrete system timestamp of the defect observation.
84
  * @returns {Object|null} The created defect report business event, or null if validation fails.
85
  */
86
- export function createReportDefectEvent(targetId, defectName, defectValue, timestamp = 0) {
87
  if (!targetId || !defectName) return null;
88
 
89
  const trimmedTarget = String(targetId).trim();
@@ -94,35 +99,62 @@ export function createReportDefectEvent(targetId, defectName, defectValue, times
94
  }
95
 
96
  return {
97
- id: `evt_def_${trimmedTarget}_${trimmedDefect}_${timestamp}`,
98
  type: 'REPORT_DEFECT',
99
  targetId: trimmedTarget,
100
  defectName: trimmedDefect,
101
- defectValue: defectValue,
102
  valid_from: timestamp,
103
  valid_to: null
104
  };
105
  }
106
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
  /**
108
  * Creates a business event representing the dismantling or removal of a component.
109
- * Pure factory function - does not mutate any external state.
110
- *
111
- * @param {string} childId - The unique business identifier of the dismantled component.
112
- * @param {number} [timestamp=0] - The discrete system timestamp when dismantling occurred.
113
  * @returns {Object|null} The created dismantling business event, or null if validation fails.
114
  */
115
- export function createDismantleEvent(childId, timestamp = 0) {
116
  if (!childId) return null;
117
 
118
  const trimmedChild = String(childId).trim();
119
  if (!trimmedChild) return null;
120
 
121
  return {
122
- id: `evt_dsm_${trimmedChild}_${timestamp}`,
123
  type: 'DISMANTLE',
124
  childId: trimmedChild,
125
  valid_from: timestamp
126
  };
127
- }
128
-
 
3
  * ------------------------------------
4
  * This module contains pure factory functions responsible for creating
5
  * immutable business Domain Events for the Event Sourcing architecture.
6
+ *
 
 
 
 
7
  * Strict Architectural Rule:
8
  * Functions here MUST NOT import the singleton state or directly mutate data.
9
  * All functions are pure factories returning event descriptors to be dispatched.
10
  */
11
 
12
+ import { RELATION_TYPES, inferEntityClass } from './definitions.js';
13
+
14
+ let localCounter = 1;
15
+
16
+ function nextIdSuffix(seq) {
17
+ return seq !== null && seq !== undefined ? seq : localCounter++;
18
+ }
19
 
20
  /**
21
+ * Creates a business event representing the registration/creation of a domain entity.
22
+ *
23
+ * @param {string} partId - Unique business identifier of the entity.
24
+ * @param {number} [timestamp=0] - Discrete system timestamp of registration.
25
+ * @param {string|null} [entityClass=null] - Optional explicit domain class ('Vehicle' | 'Part' | 'Warehouse').
26
+ * @param {number|null} [seq=null] - Optional monotonic sequence number.
27
  * @returns {Object|null} The created registration business event, or null if validation fails.
28
  */
29
+ export function createRegisterPartEvent(partId, timestamp = 0, entityClass = null, seq = null) {
30
  if (!partId) return null;
31
  const trimmedId = String(partId).trim();
32
  if (!trimmedId) return null;
33
 
34
+ const resolvedClass = inferEntityClass(trimmedId, entityClass);
35
+
36
  return {
37
+ id: `evt_reg_${trimmedId}_${timestamp}_${nextIdSuffix(seq)}`,
38
  type: 'REGISTER_PART',
39
  partId: trimmedId,
40
+ entityClass: resolvedClass,
41
  valid_from: timestamp,
42
  valid_to: null
43
  };
44
  }
45
 
46
  /**
47
+ * Creates a business event representing the assembly or storage of a child part into a parent entity.
48
+ *
49
+ * @param {string} parentId - Unique business identifier of the parent entity/container.
50
+ * @param {string} childId - Unique business identifier of the assembled child component.
51
+ * @param {string} [relationType='mounted_in'] - Relationship category ('mounted_in' | 'stored_at' | 'has_part').
52
+ * @param {number} [timestamp=0] - Discrete system timestamp of the assembly.
53
+ * @param {number|null} [seq=null] - Optional monotonic sequence number.
54
  * @returns {Object|null} The created assembly business event, or null if validation fails.
55
  */
56
+ export function createAssembleEvent(parentId, childId, relationType = 'mounted_in', timestamp = 0, seq = null) {
57
  if (!parentId || !childId) return null;
58
 
59
  const trimmedParent = String(parentId).trim();
60
  const trimmedChild = String(childId).trim();
61
 
 
62
  if (!trimmedParent || !trimmedChild || trimmedParent === trimmedChild) {
63
  return null;
64
  }
 
68
  : (relationType ? String(relationType).trim() : 'mounted_in');
69
 
70
  return {
71
+ id: `evt_asm_${trimmedChild}_${timestamp}_${nextIdSuffix(seq)}`,
72
  type: 'ASSEMBLE',
73
  parentId: trimmedParent,
74
  childId: trimmedChild,
 
80
 
81
  /**
82
  * Creates a business event reporting a physical defect or quality observation on an entity.
83
+ *
84
+ * @param {string} targetId - Unique identifier of the part or vehicle bearing the defect.
85
+ * @param {string} defectName - Defect category (e.g. 'crack', 'dent', 'wear').
86
+ * @param {string|number} defectValue - Recorded measurement or severity value (e.g. '2mm').
87
+ * @param {number} [timestamp=0] - Discrete system timestamp of the observation.
88
+ * @param {number|null} [seq=null] - Optional monotonic sequence number.
89
  * @returns {Object|null} The created defect report business event, or null if validation fails.
90
  */
91
+ export function createReportDefectEvent(targetId, defectName, defectValue, timestamp = 0, seq = null) {
92
  if (!targetId || !defectName) return null;
93
 
94
  const trimmedTarget = String(targetId).trim();
 
99
  }
100
 
101
  return {
102
+ id: `evt_def_${trimmedTarget}_${trimmedDefect}_${timestamp}_${nextIdSuffix(seq)}`,
103
  type: 'REPORT_DEFECT',
104
  targetId: trimmedTarget,
105
  defectName: trimmedDefect,
106
+ defectValue: defectValue || 'present',
107
  valid_from: timestamp,
108
  valid_to: null
109
  };
110
  }
111
 
112
+ /**
113
+ * Creates a business event representing the revocation/repair of a previously reported defect.
114
+ * Preserves Event Sourcing immutability instead of splicing the event log.
115
+ *
116
+ * @param {string} targetId - Identifier of the entity bearing the defect.
117
+ * @param {string} defectName - Defect category being revoked/repaired.
118
+ * @param {number} [timestamp=0] - Discrete system timestamp when revocation occurred.
119
+ * @param {string|null} [targetEventId=null] - Optional specific REPORT_DEFECT event ID to close.
120
+ * @param {number|null} [seq=null] - Optional monotonic sequence number.
121
+ * @returns {Object|null} The created defect revocation business event.
122
+ */
123
+ export function createRevokeDefectEvent(targetId, defectName, timestamp = 0, targetEventId = null, seq = null) {
124
+ if (!targetId || !defectName) return null;
125
+
126
+ const trimmedTarget = String(targetId).trim();
127
+ const trimmedDefect = String(defectName).trim();
128
+ if (!trimmedTarget || !trimmedDefect) return null;
129
+
130
+ return {
131
+ id: `evt_rev_${trimmedTarget}_${trimmedDefect}_${timestamp}_${nextIdSuffix(seq)}`,
132
+ type: 'REVOKE_DEFECT',
133
+ targetId: trimmedTarget,
134
+ defectName: trimmedDefect,
135
+ targetEventId: targetEventId || null,
136
+ valid_from: timestamp
137
+ };
138
+ }
139
+
140
  /**
141
  * Creates a business event representing the dismantling or removal of a component.
142
+ *
143
+ * @param {string} childId - Unique business identifier of the dismantled component.
144
+ * @param {number} [timestamp=0] - Discrete system timestamp when dismantling occurred.
145
+ * @param {number|null} [seq=null] - Optional monotonic sequence number.
146
  * @returns {Object|null} The created dismantling business event, or null if validation fails.
147
  */
148
+ export function createDismantleEvent(childId, timestamp = 0, seq = null) {
149
  if (!childId) return null;
150
 
151
  const trimmedChild = String(childId).trim();
152
  if (!trimmedChild) return null;
153
 
154
  return {
155
+ id: `evt_dsm_${trimmedChild}_${timestamp}_${nextIdSuffix(seq)}`,
156
  type: 'DISMANTLE',
157
  childId: trimmedChild,
158
  valid_from: timestamp
159
  };
160
+ }
 
js/smrm/definitions.js CHANGED
@@ -1,8 +1,8 @@
1
  /**
2
  * SMRM DEFINITIONS & DOMAIN VOCABULARY
3
  * ------------------------------------
4
- * Standard domain vocabulary and reference model definitions for the
5
- * IEC 63339 Knowledge Architecture Workbench.
6
  */
7
 
8
  /**
@@ -25,6 +25,53 @@ export const RELATION_TYPES = Object.freeze([
25
  'has_slot',
26
  'mounted_in',
27
  'stored_at',
28
- 'has_defect'
 
29
  ]);
30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  /**
2
  * SMRM DEFINITIONS & DOMAIN VOCABULARY
3
  * ------------------------------------
4
+ * Standard domain vocabulary, reference model definitions, and shared
5
+ * classification helpers for the IEC 63339 Knowledge Architecture Workbench.
6
  */
7
 
8
  /**
 
25
  'has_slot',
26
  'mounted_in',
27
  'stored_at',
28
+ 'has_defect',
29
+ 'has_part'
30
  ]);
31
 
32
+ /**
33
+ * Internal metadata property keys that should be hidden when rendering
34
+ * domain/custom property payloads on graph nodes, edges, and tables.
35
+ * @type {ReadonlyArray<string>}
36
+ */
37
+ export const INTERNAL_PROPERTY_KEYS = Object.freeze([
38
+ 'isCurrent',
39
+ 'originalId',
40
+ 'entityType',
41
+ 'entityClass',
42
+ 'validFrom',
43
+ 'validTo',
44
+ 'ownerId',
45
+ 'childId',
46
+ 'parentId',
47
+ 'childStateId',
48
+ 'parentStateId',
49
+ 'qualityType',
50
+ 'bearerId',
51
+ 'defectType',
52
+ 'targetId',
53
+ 'value',
54
+ 'atTime',
55
+ 'originalEdgeId'
56
+ ]);
57
+
58
+ /**
59
+ * Infers the manufacturing domain entity class from an explicit override
60
+ * or identifier naming conventions.
61
+ *
62
+ * @param {string} id - Node or part identifier.
63
+ * @param {string|null} [explicitClass=null] - Optional explicit class override.
64
+ * @returns {string} Resolved entity class ('Vehicle' | 'Slot' | 'Part' | 'Defect' | 'Warehouse').
65
+ */
66
+ export function inferEntityClass(id, explicitClass = null) {
67
+ if (explicitClass && ENTITY_CLASSES.includes(explicitClass)) {
68
+ return explicitClass;
69
+ }
70
+ if (!id) return 'Part';
71
+ const cleanId = String(id).replace(/^(4d_|st_)/i, '');
72
+ if (/^(veh|sys|car)/i.test(cleanId)) return 'Vehicle';
73
+ if (/^slot/i.test(cleanId)) return 'Slot';
74
+ if (/^(ware|wh|stor)/i.test(cleanId)) return 'Warehouse';
75
+ if (/^(def|qual)/i.test(cleanId)) return 'Defect';
76
+ return 'Part';
77
+ }
js/sms/lpg-generator.js CHANGED
@@ -1,23 +1,79 @@
1
  /**
2
- * SMS LAYER: LPG GENERATOR
3
- * ------------------------
4
  * Simulates how the Semantic Graph is persisted in a Labeled Property Graph
5
- * database (such as Neo4j).
6
- * Pure storage generator decoupled from ontologies and presentation markup.
7
  */
8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  /**
10
- * Generates an array of LPG edge storage records from a Semantic Graph.
 
11
  *
12
- * @param {Object} semanticGraph - Standardised Semantic Graph with nodes and edges.
13
- * @returns {Array<Object>} Clean array of relationship records for LPG storage table.
14
  */
15
  export function generateLpgStorage(semanticGraph) {
16
  if (!semanticGraph || !Array.isArray(semanticGraph.edges)) {
17
  return [];
18
  }
19
 
20
- return semanticGraph.edges.map(edge => {
 
 
21
  const props = edge.properties ? { ...edge.properties } : {};
22
  const isActive = props.isCurrent !== undefined
23
  ? Boolean(props.isCurrent)
@@ -35,43 +91,136 @@ export function generateLpgStorage(semanticGraph) {
35
  }
36
 
37
  /**
38
- * Generates a Cypher query for the specified Semantic Graph at the given point in time.
39
  *
40
- * @param {Object} semanticGraph - Standardised Semantic Graph with nodes and edges.
41
- * @param {number} currentTime - Current discrete time step.
 
42
  * @returns {string} Cypher query string.
43
  */
44
- export function generateLpgQuery(semanticGraph, currentTime) {
45
- if (!semanticGraph) return '';
 
 
46
 
47
- const nodes = semanticGraph.nodes || [];
48
-
49
- const is4D = nodes.some(n => n.type === 'State');
50
  if (is4D) {
51
- return `// Cypher (Neo4j) - 4D Perdurant Full Graph Topology
52
- MATCH (n)-[r]->(m)
53
- RETURN n, r, m`;
 
 
 
54
  }
55
 
56
- // Check for BFO Quality (Dependent Continuant)
57
- const hasQuality = nodes.some(n => n.type === 'Quality');
58
- if (hasQuality) {
59
- return `// Cypher (Neo4j) - 3D BFO Full Graph Topology & Inherence
60
- MATCH (n)-[r]->(m)
61
- RETURN n, r, m`;
 
62
  }
63
 
64
- // Check if we are in domain / naive mode
65
- const domainClasses = ['Vehicle', 'Slot', 'Part', 'Defect', 'Warehouse'];
66
- const hasDomainNodes = nodes.some(n => domainClasses.includes(n.type));
67
-
68
- if (hasDomainNodes) {
69
- return `// Cypher (Neo4j) - Domain Graph Full Topology & Properties
70
- MATCH (n)-[r]->(m)
71
- RETURN n, r, m`;
72
  }
73
 
74
- return `// Cypher (Neo4j) - Full Structural Topology & Edge Properties
75
- MATCH (n)-[r]->(m)
76
- RETURN n, r, m`;
 
 
77
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  /**
2
+ * SMS LAYER: LPG GENERATOR & LIVE QUERY ENGINE
3
+ * --------------------------------------------
4
  * Simulates how the Semantic Graph is persisted in a Labeled Property Graph
5
+ * database (Neo4j) and executes live temporal queries at time T.
 
6
  */
7
 
8
+ import { INTERNAL_PROPERTY_KEYS } from '../smrm/definitions.js';
9
+ import { generatePhysicalGraph } from './sms-graph-generator.js';
10
+ import { reduceGraphForSMRM } from '../urmsm/smrm-view.js';
11
+
12
+ /**
13
+ * Generates an array of LPG node records (Node Store) from a Semantic Graph.
14
+ * Crucial for showing node-level properties (such as crack: '2mm' in NAIVE_NODE mode).
15
+ *
16
+ * @param {Object} semanticGraph - Standardised Semantic Graph.
17
+ * @returns {Array<{ id: string, label: string, type: string, properties: Object, isActive: boolean }>}
18
+ */
19
+ export function generateLpgNodeStorage(semanticGraph) {
20
+ if (!semanticGraph || !Array.isArray(semanticGraph.nodes)) {
21
+ return [];
22
+ }
23
+
24
+ const physGraph = generatePhysicalGraph(semanticGraph, 'LPG');
25
+
26
+ return physGraph.nodes.map(node => {
27
+ const rawProps = node.properties ? { ...node.properties } : {};
28
+ const displayProps = {};
29
+
30
+ Object.entries(rawProps).forEach(([k, v]) => {
31
+ if (!INTERNAL_PROPERTY_KEYS.includes(k) && v !== undefined && v !== null) {
32
+ displayProps[k] = v;
33
+ }
34
+ });
35
+
36
+ if (node.type === 'Quality' || node.type === 'Defect') {
37
+ const dKey = rawProps.qualityType || rawProps.defectType || 'defect';
38
+ if (rawProps.value !== undefined) displayProps[dKey] = rawProps.value;
39
+ }
40
+
41
+ if (rawProps.validFrom !== undefined && rawProps.validFrom !== null) {
42
+ displayProps.validFrom = rawProps.validFrom;
43
+ }
44
+ if (rawProps.validTo !== undefined && rawProps.validTo !== null) {
45
+ displayProps.validTo = rawProps.validTo;
46
+ }
47
+
48
+ const isActive = rawProps.isCurrent !== undefined
49
+ ? Boolean(rawProps.isCurrent)
50
+ : (rawProps.validTo === null || rawProps.validTo === undefined);
51
+
52
+ return {
53
+ id: node.id,
54
+ label: node.label || node.id,
55
+ type: node.type || rawProps.entityClass || 'Node',
56
+ properties: displayProps,
57
+ isActive
58
+ };
59
+ });
60
+ }
61
+
62
  /**
63
+ * Generates an array of LPG edge storage records (Relationship Store) from a Semantic Graph.
64
+ * Uses the physical LPG graph so that 4D TRANSITION_TO chains are included.
65
  *
66
+ * @param {Object} semanticGraph - Standardised Semantic Graph.
67
+ * @returns {Array<Object>} Relationship records for LPG storage table.
68
  */
69
  export function generateLpgStorage(semanticGraph) {
70
  if (!semanticGraph || !Array.isArray(semanticGraph.edges)) {
71
  return [];
72
  }
73
 
74
+ const physGraph = generatePhysicalGraph(semanticGraph, 'LPG');
75
+
76
+ return physGraph.edges.map(edge => {
77
  const props = edge.properties ? { ...edge.properties } : {};
78
  const isActive = props.isCurrent !== undefined
79
  ? Boolean(props.isCurrent)
 
91
  }
92
 
93
  /**
94
+ * Generates an ontology-aware temporal Cypher query for time T.
95
  *
96
+ * @param {Object} semanticGraph - Standardised Semantic Graph.
97
+ * @param {number} currentTime - Current discrete timestamp T.
98
+ * @param {string} [ontology='NAIVE_NODE'] - Active ontology identifier.
99
  * @returns {string} Cypher query string.
100
  */
101
+ export function generateLpgQuery(semanticGraph, currentTime = 0, ontology = 'NAIVE_NODE') {
102
+ const t = currentTime ?? 0;
103
+ const is4D = ontology === 'ISO15926' || semanticGraph?.nodes?.some(n => n.type === 'State');
104
+ const isBFO = ontology === 'BFO' || semanticGraph?.nodes?.some(n => n.type === 'MaterialEntity');
105
 
 
 
 
106
  if (is4D) {
107
+ return `// Cypher (Neo4j) — ISO 15926 (4D) State & Defect Audit at T = ${t}
108
+ MATCH (item:ActualIndividual)<-[:temporalPartOf]-(st:State)-[rel:mounted_in|stored_at]->(locState:State)-[:temporalPartOf]->(loc:ActualIndividual)
109
+ WHERE st.validFrom <= ${t} AND (st.validTo IS NULL OR st.validTo > ${t})
110
+ OPTIONAL MATCH (item)<-[:temporalPartOf]-(anyState:State)-[hd:has_defect]->(def:Defect)
111
+ WHERE hd.validFrom <= ${t} AND (hd.validTo IS NULL OR hd.validTo > ${t})
112
+ RETURN item.originalId AS part, loc.originalId AS location, def.label AS defect`;
113
  }
114
 
115
+ if (isBFO) {
116
+ return `// Cypher (Neo4j) — BFO (3D) Continuant & Quality Inherence at T = ${t}
117
+ MATCH (part:MaterialEntity)-[rel:mounted_in|stored_at]->(target:MaterialEntity)
118
+ WHERE rel.validFrom <= ${t} AND (rel.validTo IS NULL OR rel.validTo > ${t})
119
+ OPTIONAL MATCH (q:Quality)-[inh:inheres_in]->(part)
120
+ WHERE inh.validFrom <= ${t} AND (inh.validTo IS NULL OR inh.validTo > ${t})
121
+ RETURN part.id AS part, target.id AS location, q.label AS defect`;
122
  }
123
 
124
+ if (ontology === 'NAIVE_EDGE') {
125
+ return `// Cypher (Neo4j) — Naive Graph (Edge) Active Structure at T = ${t}
126
+ // WARNING: Reads defect property from active relationship r!
127
+ MATCH (part:Part)-[r:mounted_in|stored_at]->(target)
128
+ WHERE r.validFrom <= ${t} AND (r.validTo IS NULL OR r.validTo > ${t})
129
+ RETURN part.id AS part, target.id AS location, r.crack AS defect`;
 
 
130
  }
131
 
132
+ return `// Cypher (Neo4j) — Naive Graph (Node) Active Structure at T = ${t}
133
+ // WARNING: Node properties have no temporal history (overwritten in-place)!
134
+ MATCH (part:Part)-[r:mounted_in|stored_at]->(target)
135
+ WHERE r.validFrom <= ${t} AND (r.validTo IS NULL OR r.validTo > ${t})
136
+ RETURN part.id AS part, target.id AS location, part.crack AS defect`;
137
  }
138
+
139
+ /**
140
+ * Executes the active temporal query against the current Semantic Graph in memory.
141
+ * Demonstrates why NAIVE_EDGE loses the defect upon moving to the warehouse,
142
+ * while BFO and ISO 15926 preserve it.
143
+ *
144
+ * @param {Object} semanticGraph - Current SMIM Semantic Graph.
145
+ * @param {number} currentTime - Current system time T.
146
+ * @param {string} [ontology='NAIVE_NODE'] - Active ontology mode.
147
+ * @returns {{ rows: Array<{ part: string, location: string, defect: string, hasDefect: boolean }>, defectiveCount: number }}
148
+ */
149
+ export function evaluateLiveQuery(semanticGraph, currentTime = 0, ontology = 'NAIVE_NODE') {
150
+ if (!semanticGraph || !Array.isArray(semanticGraph.nodes) || semanticGraph.nodes.length === 0) {
151
+ return { rows: [], defectiveCount: 0 };
152
+ }
153
+
154
+ const smrm = reduceGraphForSMRM(semanticGraph);
155
+ const activeEdges = smrm.edges.filter(e => e.properties?.isCurrent !== false);
156
+ const rows = [];
157
+
158
+ // Map active defects in SMRM projection
159
+ const defectMap = new Map();
160
+ const addDefect = (id, text) => {
161
+ if (!defectMap.has(id)) defectMap.set(id, []);
162
+ defectMap.get(id).push(text);
163
+ };
164
+
165
+ smrm.nodes.forEach(n => {
166
+ if (n.type === 'Defect') return;
167
+ if (n.properties) {
168
+ Object.entries(n.properties).forEach(([k, v]) => {
169
+ if (!INTERNAL_PROPERTY_KEYS.includes(k) && v !== undefined && v !== null) {
170
+ addDefect(n.id, `${k}: ${v}`);
171
+ }
172
+ });
173
+ }
174
+ });
175
+
176
+ activeEdges.forEach(e => {
177
+ if (e.label === 'has_defect' || e.type === 'has_defect') {
178
+ const defNode = smrm.nodes.find(n => n.id === e.to);
179
+ if (defNode) {
180
+ const dType = defNode.properties?.defectType || defNode.properties?.qualityType || 'defect';
181
+ const dVal = defNode.properties?.value || 'present';
182
+ addDefect(e.from, `${dType}: ${dVal}`);
183
+ }
184
+ return;
185
+ }
186
+
187
+ const childId = e.childId || (e.label === 'stored_at' || e.label === 'mounted_in' ? e.from : e.to);
188
+ if (e.properties && childId) {
189
+ Object.entries(e.properties).forEach(([k, v]) => {
190
+ if (!INTERNAL_PROPERTY_KEYS.includes(k) && v !== undefined && v !== null) {
191
+ addDefect(childId, `${k}: ${v}`);
192
+ }
193
+ });
194
+ }
195
+ });
196
+
197
+ // Collect active placements (has_part / stored_at)
198
+ activeEdges.forEach(e => {
199
+ if (e.label === 'has_defect' || e.type === 'has_defect') return;
200
+
201
+ let partId = null;
202
+ let locationId = null;
203
+
204
+ if (e.label === 'has_part') {
205
+ locationId = e.from;
206
+ partId = e.to;
207
+ } else if (e.label === 'stored_at' || e.label === 'mounted_in') {
208
+ partId = e.from;
209
+ locationId = e.to;
210
+ }
211
+
212
+ if (partId && locationId) {
213
+ const defs = defectMap.get(partId) || [];
214
+ const hasDefect = defs.length > 0;
215
+ rows.push({
216
+ part: partId,
217
+ location: locationId,
218
+ defect: hasDefect ? defs.join(', ') : 'NULL (0 defects)',
219
+ hasDefect
220
+ });
221
+ }
222
+ });
223
+
224
+ const defectiveCount = rows.filter(r => r.hasDefect).length;
225
+ return { rows, defectiveCount };
226
+ }
js/sms/rdf-generator.js CHANGED
@@ -2,229 +2,281 @@
2
  * SMS LAYER: RDF GENERATOR
3
  * ------------------------
4
  * Simulates how the Semantic Graph is persisted in a W3C RDF Triplestore.
5
- * Generates pure Turtle (.ttl) serialisation and SPARQL queries.
6
- * Pure storage generator decoupled from ontologies and presentation markup.
7
  */
8
 
 
 
9
  /**
10
  * Generates a W3C RDF Turtle (.ttl) serialisation string from a Semantic Graph.
11
- * If the graph contains 3D continuant entities, performs autonomous W3C Reification
12
- * for temporal binary relations. If the graph contains 4D perdurants, serialises
13
- * standard binary triples for temporal parts and states.
14
  *
15
  * @param {Object} semanticGraph - Standardised Semantic Graph with nodes and edges.
16
  * @returns {string} Pure Turtle (.ttl) serialisation text.
17
  */
18
  export function generateRdfStorage(semanticGraph) {
19
- if (!semanticGraph) {
20
- return '';
21
  }
22
 
23
- const is4D = semanticGraph.nodes?.some(n => n.type === 'State');
 
24
  let ttl = '';
25
 
 
26
  if (is4D) {
27
  ttl += `@prefix iso: <http://example.org/iso15926/> .\n`;
28
  ttl += `@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .\n\n`;
29
  ttl += `# 4D Perdurant Graph persisted as Pure Binary RDF Triples\n`;
30
 
31
- const individuals = (semanticGraph.nodes || []).filter(n => n.type === 'ActualIndividual');
32
  individuals.forEach(ind => {
33
- ttl += `:${ind.id} a iso:ActualIndividual .\n`;
 
34
  });
35
 
36
- const defectNodes = (semanticGraph.nodes || []).filter(n => n.type === 'Defect');
37
  defectNodes.forEach(d => {
38
- ttl += `:${d.id} a iso:DefectClass .\n`;
 
 
39
  });
40
 
41
- const states = (semanticGraph.nodes || []).filter(n => n.type === 'State');
42
  states.forEach(st => {
43
- const tpoEdges = (semanticGraph.edges || []).filter(
44
- e => e.from === st.id && (e.label === 'temporalPartOf' || e.type === 'classification') && !e.to.startsWith('def')
45
- );
46
- const partOfEdges = (semanticGraph.edges || []).filter(
47
- e => e.from === st.id && (e.label === 'mounted_in' || e.type === 'mounted_in' || e.label === 'partOf')
48
- );
49
- const hasPartEdges = (semanticGraph.edges || []).filter(
50
- e => e.from === st.id && (e.label === 'hasPart' || (e.type === 'composition' && e.label !== 'mounted_in' && e.type !== 'mounted_in'))
51
- );
52
- const hasSlotEdges = (semanticGraph.edges || []).filter(
53
- e => e.from === st.id && (e.label === 'has_slot' || e.type === 'has_slot')
54
- );
55
- const storedAtEdges = (semanticGraph.edges || []).filter(
56
- e => e.from === st.id && (e.label === 'stored_at' || e.type === 'stored_at')
57
- );
58
- const hasDefectEdges = (semanticGraph.edges || []).filter(
59
- e => e.from === st.id && (e.label === 'has_defect' || e.to.startsWith('def'))
60
- );
61
-
62
  const vFrom = st.properties?.validFrom;
63
  const vTo = st.properties?.validTo;
64
 
65
  ttl += `\n:${st.id} a iso:State`;
66
 
67
- tpoEdges.forEach(e => {
68
- ttl += ` ;\n iso:temporalPartOf :${e.to}`;
 
 
 
 
 
 
 
 
 
 
69
  });
70
 
71
- partOfEdges.forEach(e => {
72
- ttl += ` ;\n iso:partOf :${e.to}`;
73
- });
 
 
 
74
 
75
- hasPartEdges.forEach(e => {
76
- ttl += ` ;\n iso:hasPart :${e.to}`;
77
- });
78
 
79
- hasSlotEdges.forEach(e => {
80
- ttl += ` ;\n iso:hasSlot :${e.to}`;
81
- });
82
 
83
- storedAtEdges.forEach(e => {
84
- ttl += ` ;\n iso:storedAt :${e.to}`;
85
- });
 
 
86
 
87
- hasDefectEdges.forEach(e => {
88
- ttl += ` ;\n iso:hasDefect :${e.to}`;
89
- });
 
90
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  if (vFrom !== undefined && vFrom !== null) {
92
- ttl += ` ;\n iso:validFrom "${vFrom}"^^xsd:integer`;
93
  }
94
  if (vTo !== undefined && vTo !== null) {
95
- ttl += ` ;\n iso:validTo "${vTo}"^^xsd:integer`;
96
  }
 
 
 
 
 
 
 
 
 
 
97
 
 
 
 
 
 
 
 
 
 
 
 
98
  ttl += ` .\n`;
99
  });
100
- } else {
101
- const domainClasses = ['Vehicle', 'Slot', 'Part', 'Defect', 'Warehouse'];
102
- const hasDomainNodes = (semanticGraph.nodes || []).some(n => domainClasses.includes(n.type));
103
 
104
- ttl += `@prefix : <http://example.org/> .\n`;
105
- ttl += `@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .\n\n`;
106
 
107
- if (hasDomainNodes) {
108
- ttl += `# Naive Domain Graph persisted as Direct Triples & Properties\n`;
109
-
110
- // 1. Domain entities and inline properties
111
- (semanticGraph.nodes || []).forEach(n => {
112
- ttl += `:${n.id} a :${n.type || 'Part'}`;
113
- if (n.properties) {
114
- const customProps = Object.entries(n.properties).filter(
115
- ([k]) => !['isCurrent', 'originalId', 'entityType', 'validFrom', 'validTo', 'ownerId'].includes(k)
116
- );
117
- customProps.forEach(([k, v]) => {
118
- ttl += ` ;\n :${k} "${v}"`;
119
- });
120
  }
121
- ttl += ` .\n`;
122
  });
 
 
 
123
 
124
- // 2. Direct relationships
125
- (semanticGraph.edges || []).forEach(edge => {
126
- const edgeRel = edge.label || 'has_part';
127
- ttl += `:${edge.from} :${edgeRel} :${edge.to} .\n`;
128
- });
129
- } else {
130
- ttl += `# 3D Continuant Graph persisted via W3C Reification & Quality Inherence\n`;
131
 
132
- // 1. Base continuant entities
133
- (semanticGraph.nodes || []).filter(n => n.type === 'MaterialEntity').forEach(n => {
134
- ttl += `:${n.id} a :MaterialEntity .\n`;
135
- });
136
 
137
- // 1b. Dependent Continuants (Quality)
138
- (semanticGraph.nodes || []).filter(n => n.type === 'Quality').forEach(q => {
139
- ttl += `:${q.id} a :Quality`;
140
- const inh = (semanticGraph.edges || []).find(e => e.from === q.id && e.label === 'inheres_in');
141
- if (inh) {
142
- ttl += ` ;\n :inheres_in :${inh.to}`;
143
- }
144
- ttl += ` .\n`;
145
- });
146
 
147
- // 2. Synthesise reified statements autonomously for temporal relations
148
- (semanticGraph.edges || []).filter(e => e.type !== 'inherence').forEach(edge => {
149
- const vFrom = edge.properties?.validFrom;
150
- const vTo = edge.properties?.validTo;
151
- const rel = edge.label || edge.relationType || edge.type;
152
-
153
- // Respect semantic directionality of predicates:
154
- // - mounted_in: Part (from) is mounted in Slot/Vehicle (to) => parent is edge.to, child is edge.from
155
- // - has_slot / has_part / composition: parent is edge.from, child is edge.to
156
- const isMountedIn = rel === 'mounted_in';
157
- const parentId = isMountedIn ? edge.to : edge.from;
158
- const childId = isMountedIn ? edge.from : edge.to;
159
-
160
- ttl += `\n_:${edge.id} a :RelationalQuality ;\n`;
161
- ttl += ` :hasParent :${parentId} ;\n`;
162
- ttl += ` :hasChild :${childId}`;
163
-
164
- if (vFrom !== undefined && vFrom !== null) {
165
- ttl += ` ;\n :validFrom "${vFrom}"^^xsd:integer`;
166
- }
167
- if (vTo !== undefined && vTo !== null) {
168
- ttl += ` ;\n :validTo "${vTo}"^^xsd:integer`;
169
  }
170
-
171
- ttl += ` .\n`;
172
  });
173
  }
174
- }
 
175
 
176
  return ttl;
177
  }
178
 
179
  /**
180
- * Generates a SPARQL query for the specified Semantic Graph at the given point in time.
181
  *
182
- * @param {Object} semanticGraph - Standardised Semantic Graph with nodes and edges.
183
- * @param {number} currentTime - Current discrete time step.
 
184
  * @returns {string} SPARQL query string.
185
  */
186
- export function generateRdfQuery(semanticGraph, currentTime) {
187
- const is4D = semanticGraph?.nodes?.some(n => n.type === 'State');
 
 
188
 
189
  if (is4D) {
190
- return `# SPARQL (W3C RDF) - 4D Perdurant Full Graph Topology
191
  PREFIX iso: <http://example.org/iso15926/>
192
- PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
193
- SELECT ?subject ?predicate ?object
194
  WHERE {
195
- ?subject ?predicate ?object .
 
 
 
 
 
 
 
 
 
 
 
 
196
  }`;
197
  }
198
 
199
- const hasQuality = semanticGraph?.nodes?.some(n => n.type === 'Quality');
200
- if (hasQuality) {
201
- return `# SPARQL (W3C RDF) - 3D BFO Full Graph Topology & Inherence
202
- PREFIX : <http://example.org/>
203
- PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
204
- SELECT ?subject ?predicate ?object
205
  WHERE {
206
- ?subject ?predicate ?object .
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
  }`;
208
  }
209
 
210
- const domainClasses = ['Vehicle', 'Slot', 'Part', 'Defect', 'Warehouse'];
211
- const hasDomainNodes = (semanticGraph?.nodes || []).some(n => domainClasses.includes(n.type));
212
-
213
- if (hasDomainNodes) {
214
- return `# SPARQL (W3C RDF) - Domain Graph Full Topology & Properties
215
- PREFIX : <http://example.org/>
216
- PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
217
- SELECT ?subject ?predicate ?object
218
  WHERE {
219
- ?subject ?predicate ?object .
 
 
 
 
 
 
 
 
220
  }`;
221
  }
222
 
223
- return `# SPARQL (W3C RDF) - Full Graph Topology
224
- PREFIX : <http://example.org/>
225
- PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
226
- SELECT ?subject ?predicate ?object
 
227
  WHERE {
228
- ?subject ?predicate ?object .
 
 
 
 
 
 
229
  }`;
230
- }
 
2
  * SMS LAYER: RDF GENERATOR
3
  * ------------------------
4
  * Simulates how the Semantic Graph is persisted in a W3C RDF Triplestore.
5
+ * Generates pure Turtle (.ttl) serialisation and ontology-specific temporal SPARQL queries.
 
6
  */
7
 
8
+ import { INTERNAL_PROPERTY_KEYS } from '../smrm/definitions.js';
9
+
10
  /**
11
  * Generates a W3C RDF Turtle (.ttl) serialisation string from a Semantic Graph.
 
 
 
12
  *
13
  * @param {Object} semanticGraph - Standardised Semantic Graph with nodes and edges.
14
  * @returns {string} Pure Turtle (.ttl) serialisation text.
15
  */
16
  export function generateRdfStorage(semanticGraph) {
17
+ if (!semanticGraph || !Array.isArray(semanticGraph.nodes) || semanticGraph.nodes.length === 0) {
18
+ return '# Empty RDF Graph (0 triples)';
19
  }
20
 
21
+ const is4D = semanticGraph.nodes.some(n => n.type === 'State' || n.type === 'ActualIndividual');
22
+ const isBFO = semanticGraph.nodes.some(n => n.type === 'MaterialEntity' || n.type === 'Quality');
23
  let ttl = '';
24
 
25
+ // 1. 4D ISO 15926 Turtle Serialisation
26
  if (is4D) {
27
  ttl += `@prefix iso: <http://example.org/iso15926/> .\n`;
28
  ttl += `@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .\n\n`;
29
  ttl += `# 4D Perdurant Graph persisted as Pure Binary RDF Triples\n`;
30
 
31
+ const individuals = semanticGraph.nodes.filter(n => n.type === 'ActualIndividual');
32
  individuals.forEach(ind => {
33
+ const cls = ind.properties?.entityClass || 'Part';
34
+ ttl += `:${ind.id} a iso:ActualIndividual , iso:${cls} .\n`;
35
  });
36
 
37
+ const defectNodes = semanticGraph.nodes.filter(n => n.type === 'Defect');
38
  defectNodes.forEach(d => {
39
+ const dType = d.properties?.defectType || 'defect';
40
+ const dVal = d.properties?.value || 'present';
41
+ ttl += `:${d.id} a iso:DefectClass ;\n iso:defectType "${dType}" ;\n iso:severity "${dVal}" .\n`;
42
  });
43
 
44
+ const states = semanticGraph.nodes.filter(n => n.type === 'State');
45
  states.forEach(st => {
46
+ const outgoing = (semanticGraph.edges || []).filter(e => e.from === st.id);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  const vFrom = st.properties?.validFrom;
48
  const vTo = st.properties?.validTo;
49
 
50
  ttl += `\n:${st.id} a iso:State`;
51
 
52
+ outgoing.forEach(e => {
53
+ if (e.label === 'temporalPartOf') {
54
+ ttl += ` ;\n iso:temporalPartOf :${e.to}`;
55
+ } else if (e.label === 'mounted_in') {
56
+ ttl += ` ;\n iso:mountedIn :${e.to}`;
57
+ } else if (e.label === 'has_slot') {
58
+ ttl += ` ;\n iso:hasSlot :${e.to}`;
59
+ } else if (e.label === 'stored_at') {
60
+ ttl += ` ;\n iso:storedAt :${e.to}`;
61
+ } else if (e.label === 'has_defect') {
62
+ ttl += ` ;\n iso:hasDefect :${e.to}`;
63
+ }
64
  });
65
 
66
+ if (vFrom !== undefined && vFrom !== null) {
67
+ ttl += ` ;\n iso:validFrom "${vFrom}"^^xsd:integer`;
68
+ }
69
+ if (vTo !== undefined && vTo !== null) {
70
+ ttl += ` ;\n iso:validTo "${vTo}"^^xsd:integer`;
71
+ }
72
 
73
+ ttl += ` .\n`;
74
+ });
 
75
 
76
+ return ttl;
77
+ }
 
78
 
79
+ // 2. 3D BFO Turtle Serialisation (with W3C Reification)
80
+ if (isBFO) {
81
+ ttl += `@prefix : <http://example.org/bfo/> .\n`;
82
+ ttl += `@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .\n\n`;
83
+ ttl += `# 3D Continuant Graph persisted via W3C Reification & Quality Inherence\n`;
84
 
85
+ semanticGraph.nodes.filter(n => n.type === 'MaterialEntity').forEach(n => {
86
+ const cls = n.properties?.entityClass || 'Part';
87
+ ttl += `:${n.id} a :MaterialEntity , :${cls} .\n`;
88
+ });
89
 
90
+ semanticGraph.nodes.filter(n => n.type === 'Quality').forEach(q => {
91
+ const inh = (semanticGraph.edges || []).find(e => e.from === q.id && e.label === 'inheres_in');
92
+ const vFrom = q.properties?.validFrom;
93
+ const vTo = q.properties?.validTo;
94
+ ttl += `\n:${q.id} a :Quality`;
95
+ if (q.properties?.qualityType) {
96
+ ttl += ` ;\n :qualityType "${q.properties.qualityType}"`;
97
+ }
98
+ if (q.properties?.value) {
99
+ ttl += ` ;\n :value "${q.properties.value}"`;
100
+ }
101
+ if (inh) {
102
+ ttl += ` ;\n :inheres_in :${inh.to}`;
103
+ }
104
  if (vFrom !== undefined && vFrom !== null) {
105
+ ttl += ` ;\n :validFrom "${vFrom}"^^xsd:integer`;
106
  }
107
  if (vTo !== undefined && vTo !== null) {
108
+ ttl += ` ;\n :validTo "${vTo}"^^xsd:integer`;
109
  }
110
+ ttl += ` .\n`;
111
+ });
112
+
113
+ (semanticGraph.edges || []).filter(e => e.type !== 'inherence').forEach(edge => {
114
+ const vFrom = edge.properties?.validFrom;
115
+ const vTo = edge.properties?.validTo;
116
+ const rel = edge.label || edge.type;
117
+ const isChildToParent = rel === 'mounted_in' || rel === 'stored_at';
118
+ const parentId = isChildToParent ? edge.to : edge.from;
119
+ const childId = isChildToParent ? edge.from : edge.to;
120
 
121
+ ttl += `\n_:${edge.id} a :RelationalQuality ;\n`;
122
+ ttl += ` :relationType :${rel} ;\n`;
123
+ ttl += ` :hasParent :${parentId} ;\n`;
124
+ ttl += ` :hasChild :${childId}`;
125
+
126
+ if (vFrom !== undefined && vFrom !== null) {
127
+ ttl += ` ;\n :validFrom "${vFrom}"^^xsd:integer`;
128
+ }
129
+ if (vTo !== undefined && vTo !== null) {
130
+ ttl += ` ;\n :validTo "${vTo}"^^xsd:integer`;
131
+ }
132
  ttl += ` .\n`;
133
  });
 
 
 
134
 
135
+ return ttl;
136
+ }
137
 
138
+ // 3. Naive Graph (Node / Edge) Turtle Serialisation with Reified Edge Properties
139
+ ttl += `@prefix : <http://example.org/naive/> .\n`;
140
+ ttl += `@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .\n`;
141
+ ttl += `@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .\n\n`;
142
+ ttl += `# Naive Domain Graph persisted as RDF Triples & Reified Statements\n`;
143
+
144
+ semanticGraph.nodes.forEach(n => {
145
+ ttl += `:${n.id} a :${n.type || 'Part'}`;
146
+ if (n.properties) {
147
+ Object.entries(n.properties).forEach(([k, v]) => {
148
+ if (!INTERNAL_PROPERTY_KEYS.includes(k) && v !== undefined && v !== null) {
149
+ ttl += ` ;\n :${k} "${v}"`;
 
150
  }
 
151
  });
152
+ }
153
+ ttl += ` .\n`;
154
+ });
155
 
156
+ (semanticGraph.edges || []).forEach(edge => {
157
+ const edgeRel = edge.label || 'has_part';
158
+ const vFrom = edge.properties?.validFrom;
159
+ const vTo = edge.properties?.validTo;
 
 
 
160
 
161
+ // Emit direct triple if currently active
162
+ if (edge.properties?.isCurrent !== false) {
163
+ ttl += `\n:${edge.from} :${edgeRel} :${edge.to} .\n`;
164
+ }
165
 
166
+ // Emit W3C Statement reification to hold validFrom/validTo and edge-attached defects
167
+ ttl += `_:${edge.id} a rdf:Statement ;\n`;
168
+ ttl += ` rdf:subject :${edge.from} ;\n`;
169
+ ttl += ` rdf:predicate :${edgeRel} ;\n`;
170
+ ttl += ` rdf:object :${edge.to}`;
 
 
 
 
171
 
172
+ if (vFrom !== undefined && vFrom !== null) {
173
+ ttl += ` ;\n :validFrom "${vFrom}"^^xsd:integer`;
174
+ }
175
+ if (vTo !== undefined && vTo !== null) {
176
+ ttl += ` ;\n :validTo "${vTo}"^^xsd:integer`;
177
+ }
178
+ if (edge.properties) {
179
+ Object.entries(edge.properties).forEach(([k, v]) => {
180
+ if (!INTERNAL_PROPERTY_KEYS.includes(k) && v !== undefined && v !== null) {
181
+ ttl += ` ;\n :${k} "${v}"`;
 
 
 
 
 
 
 
 
 
 
 
 
182
  }
 
 
183
  });
184
  }
185
+ ttl += ` .\n`;
186
+ });
187
 
188
  return ttl;
189
  }
190
 
191
  /**
192
+ * Generates an ontology-aware temporal SPARQL query for time T.
193
  *
194
+ * @param {Object} semanticGraph - Standardised Semantic Graph.
195
+ * @param {number} currentTime - Current discrete timestamp T.
196
+ * @param {string} [ontology='NAIVE_NODE'] - Active ontology identifier.
197
  * @returns {string} SPARQL query string.
198
  */
199
+ export function generateRdfQuery(semanticGraph, currentTime = 0, ontology = 'NAIVE_NODE') {
200
+ const t = currentTime ?? 0;
201
+ const is4D = ontology === 'ISO15926' || semanticGraph?.nodes?.some(n => n.type === 'State');
202
+ const isBFO = ontology === 'BFO' || semanticGraph?.nodes?.some(n => n.type === 'MaterialEntity');
203
 
204
  if (is4D) {
205
+ return `# SPARQL (W3C RDF) — ISO 15926 (4D) State & Defect Audit at T = ${t}
206
  PREFIX iso: <http://example.org/iso15926/>
207
+ SELECT ?part ?location ?defectType ?severity
 
208
  WHERE {
209
+ ?st iso:temporalPartOf ?part ;
210
+ iso:mountedIn|iso:storedAt ?locState ;
211
+ iso:validFrom ?vf .
212
+ ?locState iso:temporalPartOf ?location .
213
+ OPTIONAL { ?st iso:validTo ?vt }
214
+ FILTER (?vf <= ${t} && (!BOUND(?vt) || ?vt > ${t}))
215
+
216
+ OPTIONAL {
217
+ ?defState iso:temporalPartOf ?part ;
218
+ iso:hasDefect ?def .
219
+ ?def iso:defectType ?defectType ;
220
+ iso:severity ?severity .
221
+ }
222
  }`;
223
  }
224
 
225
+ if (isBFO) {
226
+ return `# SPARQL (W3C RDF) — BFO (3D) Reified Placement & Quality at T = ${t}
227
+ PREFIX : <http://example.org/bfo/>
228
+ SELECT ?part ?location ?qualityType ?value
 
 
229
  WHERE {
230
+ ?stmt a :RelationalQuality ;
231
+ :hasChild ?part ;
232
+ :hasParent ?location ;
233
+ :validFrom ?vf .
234
+ OPTIONAL { ?stmt :validTo ?vt }
235
+ FILTER (?vf <= ${t} && (!BOUND(?vt) || ?vt > ${t}))
236
+
237
+ OPTIONAL {
238
+ ?qual a :Quality ;
239
+ :inheres_in ?part ;
240
+ :qualityType ?qualityType ;
241
+ :value ?value ;
242
+ :validFrom ?qvf .
243
+ OPTIONAL { ?qual :validTo ?qvt }
244
+ FILTER (?qvf <= ${t} && (!BOUND(?qvt) || ?qvt > ${t}))
245
+ }
246
  }`;
247
  }
248
 
249
+ if (ontology === 'NAIVE_EDGE') {
250
+ return `# SPARQL (W3C RDF) — Naive Graph (Edge) Reified Statement Query at T = ${t}
251
+ # WARNING: Reads :crack from active placement statement ?stmt!
252
+ PREFIX : <http://example.org/naive/>
253
+ PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
254
+ SELECT ?part ?location ?crack
 
 
255
  WHERE {
256
+ ?stmt a rdf:Statement ;
257
+ rdf:subject ?part ;
258
+ rdf:predicate ?pred ;
259
+ rdf:object ?location ;
260
+ :validFrom ?vf .
261
+ OPTIONAL { ?stmt :validTo ?vt }
262
+ OPTIONAL { ?stmt :crack ?crack }
263
+ FILTER (?pred IN (:mounted_in, :stored_at))
264
+ FILTER (?vf <= ${t} && (!BOUND(?vt) || ?vt > ${t}))
265
  }`;
266
  }
267
 
268
+ return `# SPARQL (W3C RDF) — Naive Graph (Node) Query at T = ${t}
269
+ # WARNING: :crack is attached directly to ?part without temporal bounds!
270
+ PREFIX : <http://example.org/naive/>
271
+ PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
272
+ SELECT ?part ?location ?crack
273
  WHERE {
274
+ ?stmt a rdf:Statement ;
275
+ rdf:subject ?part ;
276
+ rdf:object ?location ;
277
+ :validFrom ?vf .
278
+ OPTIONAL { ?stmt :validTo ?vt }
279
+ OPTIONAL { ?part :crack ?crack }
280
+ FILTER (?vf <= ${t} && (!BOUND(?vt) || ?vt > ${t}))
281
  }`;
282
+ }
js/sms/sms-graph-generator.js CHANGED
@@ -2,15 +2,18 @@
2
  * SMS LAYER: PHYSICAL GRAPH GENERATOR
3
  * -----------------------------------
4
  * Transforms a canonical Semantic Graph (SMIM) into physical storage (SMS) per IEC 63339.
5
- * RDF: Reifies 3D composition relations carrying temporal properties into W3C statement nodes.
6
- * LPG: Links sequential 4D states of the same entity via TRANSITION_TO edges.
 
7
  */
8
 
 
 
9
  /**
10
  * Generates a physical graph projection from a canonical semantic graph.
11
  *
12
  * @param {Object} semanticGraph - Standardised Semantic Graph ({ nodes: Array, edges: Array }).
13
- * @param {string} storageMode - Physical storage paradigm ('LPG' or 'RDF').
14
  * @returns {{ nodes: Array, edges: Array }} Transformed physical graph.
15
  */
16
  export function generatePhysicalGraph(semanticGraph, storageMode) {
@@ -18,7 +21,6 @@ export function generatePhysicalGraph(semanticGraph, storageMode) {
18
  return { nodes: [], edges: [] };
19
  }
20
 
21
- // Deep clone nodes and edges to maintain immutability
22
  const physicalNodes = semanticGraph.nodes.map(node => ({
23
  ...node,
24
  properties: node.properties ? { ...node.properties } : {}
@@ -29,97 +31,233 @@ export function generatePhysicalGraph(semanticGraph, storageMode) {
29
  properties: edge.properties ? { ...edge.properties } : {}
30
  }));
31
 
32
- const is4D = physicalNodes.some(n => n.type === 'State');
33
 
34
- // Case 1: W3C RDF Mode
 
 
35
  if (storageMode === 'RDF') {
36
- if (!is4D) {
37
- const finalEdges = [];
 
 
 
38
 
39
- physicalEdges.forEach(edge => {
40
- const hasTemporalProps = edge.properties && (
41
- edge.properties.validFrom !== undefined ||
42
- edge.properties.validTo !== undefined
43
- );
44
 
45
- if (edge.type === 'composition' && hasTemporalProps) {
46
- const stmtId = `stmt_${edge.id}`;
47
- const isCurrent = edge.properties.isCurrent !== false;
 
48
 
49
- physicalNodes.push({
50
- id: stmtId,
51
- label: `_:${edge.id}`,
52
- type: 'RelationalQuality',
53
- properties: { isCurrent, originalEdgeId: edge.id }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  finalEdges.push({
57
- id: `rel_p_${edge.id}`,
58
  from: stmtId,
59
- to: edge.from,
60
- label: ':hasParent',
61
  type: 'reification',
62
  properties: { isCurrent }
63
  });
 
64
 
 
 
 
 
 
 
 
 
65
  finalEdges.push({
66
- id: `rel_c_${edge.id}`,
67
  from: stmtId,
68
- to: edge.to,
69
- label: ':hasChild',
70
  type: 'reification',
71
- properties: { isCurrent }
72
  });
 
73
 
74
- if (edge.properties.validFrom !== undefined && edge.properties.validFrom !== null) {
75
- const litFromId = `lit_vf_${edge.id}`;
 
 
76
  physicalNodes.push({
77
- id: litFromId,
78
- label: `"${edge.properties.validFrom}"^^xsd:integer`,
79
  type: 'Literal',
80
- properties: { value: edge.properties.validFrom }
81
  });
82
-
83
  finalEdges.push({
84
- id: `rel_vf_${edge.id}`,
85
  from: stmtId,
86
- to: litFromId,
87
- label: ':validFrom',
88
  type: 'reification',
89
  properties: { isCurrent }
90
  });
91
  }
 
 
 
 
 
92
 
93
- if (edge.properties.validTo !== undefined && edge.properties.validTo !== null) {
94
- const litToId = `lit_vt_${edge.id}`;
95
- physicalNodes.push({
96
- id: litToId,
97
- label: `"${edge.properties.validTo}"^^xsd:integer`,
98
- type: 'Literal',
99
- properties: { value: edge.properties.validTo }
100
- });
101
-
102
- finalEdges.push({
103
- id: `rel_vt_${edge.id}`,
104
- from: stmtId,
105
- to: litToId,
106
- label: ':validTo',
107
- type: 'reification',
108
- properties: { isCurrent: false }
109
- });
110
- }
111
- } else {
112
- finalEdges.push(edge);
113
- }
114
- });
115
-
116
- return { nodes: physicalNodes, edges: finalEdges };
117
- }
118
-
119
- return { nodes: physicalNodes, edges: physicalEdges };
120
  }
121
 
122
- // Case 2: Labeled Property Graph (LPG) Mode
 
 
123
  if (storageMode === 'LPG' && is4D) {
124
  const stateNodes = physicalNodes.filter(n => n.type === 'State');
125
  const entityStates = new Map();
@@ -158,6 +296,7 @@ export function generatePhysicalGraph(semanticGraph, storageMode) {
158
  type: 'transition',
159
  properties: {
160
  atTime: nextState.properties?.validFrom,
 
161
  isCurrent: nextState.properties?.isCurrent ?? true
162
  }
163
  });
@@ -169,4 +308,4 @@ export function generatePhysicalGraph(semanticGraph, storageMode) {
169
  }
170
 
171
  return { nodes: physicalNodes, edges: physicalEdges };
172
- }
 
2
  * SMS LAYER: PHYSICAL GRAPH GENERATOR
3
  * -----------------------------------
4
  * Transforms a canonical Semantic Graph (SMIM) into physical storage (SMS) per IEC 63339.
5
+ * - RDF Mode: Reifies binary relations carrying temporal/custom properties into W3C
6
+ * statement nodes and extracts scalar properties into explicit RDF Literal nodes.
7
+ * - LPG Mode: Links sequential 4D states of the same entity via TRANSITION_TO edges.
8
  */
9
 
10
+ import { INTERNAL_PROPERTY_KEYS } from '../smrm/definitions.js';
11
+
12
  /**
13
  * Generates a physical graph projection from a canonical semantic graph.
14
  *
15
  * @param {Object} semanticGraph - Standardised Semantic Graph ({ nodes: Array, edges: Array }).
16
+ * @param {'LPG'|'RDF'} storageMode - Physical storage paradigm ('LPG' or 'RDF').
17
  * @returns {{ nodes: Array, edges: Array }} Transformed physical graph.
18
  */
19
  export function generatePhysicalGraph(semanticGraph, storageMode) {
 
21
  return { nodes: [], edges: [] };
22
  }
23
 
 
24
  const physicalNodes = semanticGraph.nodes.map(node => ({
25
  ...node,
26
  properties: node.properties ? { ...node.properties } : {}
 
31
  properties: edge.properties ? { ...edge.properties } : {}
32
  }));
33
 
34
+ const is4D = physicalNodes.some(n => n.type === 'State' || n.type === 'ActualIndividual');
35
 
36
+ // =========================================================================
37
+ // CASE 1: W3C RDF Mode (Triples, Statement Reification & Explicit Literals)
38
+ // =========================================================================
39
  if (storageMode === 'RDF') {
40
+ if (is4D) {
41
+ // In 4D RDF, States and Defects cannot hold internal property maps;
42
+ // their timestamps and values are persisted as explicit RDF Literal nodes.
43
+ const rdf4dNodes = [];
44
+ const rdf4dEdges = [...physicalEdges];
45
 
46
+ physicalNodes.forEach(node => {
47
+ rdf4dNodes.push(node);
 
 
 
48
 
49
+ if (node.type === 'State') {
50
+ const vFrom = node.properties?.validFrom;
51
+ const vTo = node.properties?.validTo;
52
+ const isCurrent = node.properties?.isCurrent !== false;
53
 
54
+ if (vFrom !== undefined && vFrom !== null) {
55
+ const litFromId = `lit_vf_${node.id}`;
56
+ rdf4dNodes.push({
57
+ id: litFromId,
58
+ label: `"${vFrom}"^^xsd:integer`,
59
+ type: 'Literal',
60
+ properties: { value: vFrom }
61
+ });
62
+ rdf4dEdges.push({
63
+ id: `edge_vf_${node.id}`,
64
+ from: node.id,
65
+ to: litFromId,
66
+ label: 'iso:validFrom',
67
+ type: 'predicate',
68
+ properties: { isCurrent }
69
+ });
70
+ }
71
+
72
+ if (vTo !== undefined && vTo !== null) {
73
+ const litToId = `lit_vt_${node.id}`;
74
+ rdf4dNodes.push({
75
+ id: litToId,
76
+ label: `"${vTo}"^^xsd:integer`,
77
+ type: 'Literal',
78
+ properties: { value: vTo }
79
+ });
80
+ rdf4dEdges.push({
81
+ id: `edge_vt_${node.id}`,
82
+ from: node.id,
83
+ to: litToId,
84
+ label: 'iso:validTo',
85
+ type: 'predicate',
86
+ properties: { isCurrent: false }
87
+ });
88
+ }
89
+ } else if (node.type === 'Defect' && node.properties?.value) {
90
+ const litValId = `lit_val_${node.id}`;
91
+ rdf4dNodes.push({
92
+ id: litValId,
93
+ label: `"${node.properties.value}"^^xsd:string`,
94
+ type: 'Literal',
95
+ properties: { value: node.properties.value }
96
  });
97
+ rdf4dEdges.push({
98
+ id: `edge_val_${node.id}`,
99
+ from: node.id,
100
+ to: litValId,
101
+ label: 'iso:severity',
102
+ type: 'predicate',
103
+ properties: { isCurrent: node.properties?.isCurrent !== false }
104
+ });
105
+ }
106
+ });
107
+
108
+ return { nodes: rdf4dNodes, edges: rdf4dEdges };
109
+ }
110
+
111
+ // 3D BFO and Naive Graph RDF Reification
112
+ const finalEdges = [];
113
+
114
+ // Extract custom properties on nodes (e.g. Naive Node crack="2mm") into RDF Literals
115
+ physicalNodes.slice().forEach(node => {
116
+ if (node.properties) {
117
+ Object.entries(node.properties).forEach(([k, v]) => {
118
+ if (!INTERNAL_PROPERTY_KEYS.includes(k) && v !== undefined && v !== null) {
119
+ const litId = `lit_node_${node.id}_${k}`;
120
+ physicalNodes.push({
121
+ id: litId,
122
+ label: `"${v}"^^xsd:string`,
123
+ type: 'Literal',
124
+ properties: { value: v }
125
+ });
126
+ finalEdges.push({
127
+ id: `rel_lit_${node.id}_${k}`,
128
+ from: node.id,
129
+ to: litId,
130
+ label: `:${k}`,
131
+ type: 'predicate',
132
+ properties: { isCurrent: true }
133
+ });
134
+ }
135
+ });
136
+ }
137
+ });
138
+
139
+ physicalEdges.forEach(edge => {
140
+ const hasTemporalProps = edge.properties && (
141
+ edge.properties.validFrom !== undefined ||
142
+ edge.properties.validTo !== undefined
143
+ );
144
+ const isReifiableRelation = [
145
+ 'composition',
146
+ 'mounted_in',
147
+ 'has_slot',
148
+ 'stored_at',
149
+ 'has_part'
150
+ ].includes(edge.type) || [
151
+ 'mounted_in',
152
+ 'has_slot',
153
+ 'stored_at',
154
+ 'has_part'
155
+ ].includes(edge.label);
156
+
157
+ if (isReifiableRelation && hasTemporalProps) {
158
+ const stmtId = `stmt_${edge.id}`;
159
+ const isCurrent = edge.properties.isCurrent !== false;
160
+ const rel = edge.label || edge.type;
161
 
162
+ // Correct semantic directionality:
163
+ // For mounted_in / stored_at, edge.from is the Child (Part) and edge.to is the Parent (Slot/Warehouse).
164
+ // For has_slot / has_part, edge.from is the Parent and edge.to is the Child.
165
+ const isChildToParent = rel === 'mounted_in' || rel === 'stored_at';
166
+ const parentNodeId = isChildToParent ? edge.to : edge.from;
167
+ const childNodeId = isChildToParent ? edge.from : edge.to;
168
+
169
+ physicalNodes.push({
170
+ id: stmtId,
171
+ label: `_:${edge.id}`,
172
+ type: 'RelationalQuality',
173
+ properties: { isCurrent, originalEdgeId: edge.id }
174
+ });
175
+
176
+ finalEdges.push({
177
+ id: `rel_p_${edge.id}`,
178
+ from: stmtId,
179
+ to: parentNodeId,
180
+ label: ':hasParent',
181
+ type: 'reification',
182
+ properties: { isCurrent }
183
+ });
184
+
185
+ finalEdges.push({
186
+ id: `rel_c_${edge.id}`,
187
+ from: stmtId,
188
+ to: childNodeId,
189
+ label: ':hasChild',
190
+ type: 'reification',
191
+ properties: { isCurrent }
192
+ });
193
+
194
+ if (edge.properties.validFrom !== undefined && edge.properties.validFrom !== null) {
195
+ const litFromId = `lit_vf_${edge.id}`;
196
+ physicalNodes.push({
197
+ id: litFromId,
198
+ label: `"${edge.properties.validFrom}"^^xsd:integer`,
199
+ type: 'Literal',
200
+ properties: { value: edge.properties.validFrom }
201
+ });
202
  finalEdges.push({
203
+ id: `rel_vf_${edge.id}`,
204
  from: stmtId,
205
+ to: litFromId,
206
+ label: ':validFrom',
207
  type: 'reification',
208
  properties: { isCurrent }
209
  });
210
+ }
211
 
212
+ if (edge.properties.validTo !== undefined && edge.properties.validTo !== null) {
213
+ const litToId = `lit_vt_${edge.id}`;
214
+ physicalNodes.push({
215
+ id: litToId,
216
+ label: `"${edge.properties.validTo}"^^xsd:integer`,
217
+ type: 'Literal',
218
+ properties: { value: edge.properties.validTo }
219
+ });
220
  finalEdges.push({
221
+ id: `rel_vt_${edge.id}`,
222
  from: stmtId,
223
+ to: litToId,
224
+ label: ':validTo',
225
  type: 'reification',
226
+ properties: { isCurrent: false }
227
  });
228
+ }
229
 
230
+ // Also reify custom edge properties (e.g. crack="2mm" in NAIVE_EDGE mode)
231
+ Object.entries(edge.properties).forEach(([k, v]) => {
232
+ if (!INTERNAL_PROPERTY_KEYS.includes(k) && v !== undefined && v !== null) {
233
+ const litPropId = `lit_prop_${edge.id}_${k}`;
234
  physicalNodes.push({
235
+ id: litPropId,
236
+ label: `"${v}"^^xsd:string`,
237
  type: 'Literal',
238
+ properties: { value: v }
239
  });
 
240
  finalEdges.push({
241
+ id: `rel_prop_${edge.id}_${k}`,
242
  from: stmtId,
243
+ to: litPropId,
244
+ label: `:${k}`,
245
  type: 'reification',
246
  properties: { isCurrent }
247
  });
248
  }
249
+ });
250
+ } else {
251
+ finalEdges.push(edge);
252
+ }
253
+ });
254
 
255
+ return { nodes: physicalNodes, edges: finalEdges };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
256
  }
257
 
258
+ // =========================================================================
259
+ // CASE 2: Labeled Property Graph (LPG) Mode
260
+ // =========================================================================
261
  if (storageMode === 'LPG' && is4D) {
262
  const stateNodes = physicalNodes.filter(n => n.type === 'State');
263
  const entityStates = new Map();
 
296
  type: 'transition',
297
  properties: {
298
  atTime: nextState.properties?.validFrom,
299
+ validFrom: nextState.properties?.validFrom,
300
  isCurrent: nextState.properties?.isCurrent ?? true
301
  }
302
  });
 
308
  }
309
 
310
  return { nodes: physicalNodes, edges: physicalEdges };
311
+ }
js/state.js CHANGED
@@ -1,18 +1,16 @@
1
  import { reactive } from 'vue';
2
- export { ENTITY_CLASSES, RELATION_TYPES } from './smrm/definitions.js';
3
 
4
  /**
5
- * SINGLE SOURCE OF TRUTH (Event Sourcing)
6
- * ---------------------------------------
7
- * This module holds the pure domain reactive state of the application.
8
- * According to architecture guidelines, UI settings and visual graph
9
- * configurations are never stored here.
10
- *
11
- * We only store the absolute domain truth:
12
- * - Current Time dimension
13
- * - Event Log (domain business events over time)
14
- * - Edge/Event sequence counter
15
- * - Domain settings (e.g. Naive defect target variant)
16
  */
17
  export const state = reactive({
18
  // 1. Navigation View ('MENU' | 'SANDBOX' | 'SCENARIO')
@@ -27,65 +25,104 @@ export const state = reactive({
27
  // 4. Current Domain Ontology ('NAIVE_NODE' | 'NAIVE_EDGE' | 'BFO' | 'ISO15926')
28
  ontology: 'NAIVE_NODE',
29
 
30
- // 5. The Event Log (The core of our Domain-Driven Event Sourcing)
31
- // Format: Pure business events (e.g. REGISTER_PART, ASSEMBLE, REPORT_DEFECT, DISMANTLE)
32
- eventLog: [],
33
 
34
- // 6. Counter to ensure unique identifiers for events
35
- edgeSequence: 1,
 
36
 
37
- // 7. Perspective View ('SMRM' | 'SMIM' | 'SMS')
38
- perspective: 'SMRM',
 
 
 
39
 
40
- // 8. Visual Graph Engine ('VIS' | 'MERMAID')
41
- graphEngine: 'VIS'
 
 
 
 
 
 
42
  });
43
 
44
  /**
45
  * Advances the global system time by one unit (T = T + 1).
46
- * Because 'state' is reactive, calling this will automatically trigger UI updates.
47
  */
48
  export function advanceTime() {
49
  state.currentTime++;
50
  }
51
 
52
  /**
53
- * Completely replaces the current domain state.
54
- * Useful for loading a saved JSON file or injecting a predefined scenario.
55
- *
56
- * @param {Object} newState - The data object containing the state to inject.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  */
58
  export function loadState(newState) {
 
 
59
  if (newState.currentView !== undefined) {
60
  state.currentView = newState.currentView;
61
  }
62
-
63
  if (newState.userRole !== undefined) {
64
  state.userRole = newState.userRole;
65
  }
66
-
67
  if (newState.ontology !== undefined) {
68
- state.ontology = newState.ontology;
 
 
 
 
 
 
 
69
  }
70
-
71
  if (newState.currentTime !== undefined) {
72
  state.currentTime = newState.currentTime;
73
  }
74
-
75
- if (newState.eventLog) {
76
  state.eventLog.splice(0, state.eventLog.length, ...newState.eventLog);
77
  }
78
-
79
  if (newState.edgeSequence !== undefined) {
80
  state.edgeSequence = newState.edgeSequence;
81
  }
82
-
83
- if (newState.perspective !== undefined) {
84
- state.perspective = newState.perspective;
85
- }
86
-
87
  if (newState.graphEngine !== undefined) {
88
- state.graphEngine = newState.graphEngine;
89
  }
90
- }
91
-
 
 
 
 
 
 
1
  import { reactive } from 'vue';
2
+ export { ENTITY_CLASSES, RELATION_TYPES, inferEntityClass } from './smrm/definitions.js';
3
 
4
  /**
5
+ * SINGLE SOURCE OF TRUTH (Event Sourcing & Workbench State)
6
+ * ---------------------------------------------------------
7
+ * Central reactive store eliminating duplicate state/settings watchers.
8
+ *
9
+ * Perspective values (4 unified viewpoints):
10
+ * - 'SMRM' : Engineer Perspective (domain hierarchy, slots/states collapsed)
11
+ * - 'SMIM' : Logical Perspective (canonical 3D BFO / 4D ISO 15926 / Naive topology)
12
+ * - 'SMS_LPG' : Physical LPG Perspective (Property Graph storage & state transitions)
13
+ * - 'SMS_RDF' : Physical RDF Perspective (W3C RDF triples, reification & literals)
 
 
14
  */
15
  export const state = reactive({
16
  // 1. Navigation View ('MENU' | 'SANDBOX' | 'SCENARIO')
 
25
  // 4. Current Domain Ontology ('NAIVE_NODE' | 'NAIVE_EDGE' | 'BFO' | 'ISO15926')
26
  ontology: 'NAIVE_NODE',
27
 
28
+ // 5. Unified Perspective ('SMRM' | 'SMIM' | 'SMS_LPG' | 'SMS_RDF')
29
+ perspective: 'SMRM',
 
30
 
31
+ // 6. Active Physical Storage Paradigm ('LPG' | 'RDF')
32
+ // Automatically switched when selecting 'SMS_LPG' or 'SMS_RDF'
33
+ storage: 'LPG',
34
 
35
+ // 7. Visual Graph Engine ('VIS' | 'MERMAID')
36
+ graphEngine: 'VIS',
37
+
38
+ // 8. UI Locale ('sv' | 'en' | 'ru')
39
+ language: 'en',
40
 
41
+ // 9. Toggle visibility of historical (closed) relationships
42
+ showHistory: true,
43
+
44
+ // 10. The Event Log (Pure business events)
45
+ eventLog: [],
46
+
47
+ // 11. Monotonic counter for unique event IDs
48
+ edgeSequence: 1
49
  });
50
 
51
  /**
52
  * Advances the global system time by one unit (T = T + 1).
 
53
  */
54
  export function advanceTime() {
55
  state.currentTime++;
56
  }
57
 
58
  /**
59
+ * Returns the next monotonic sequence integer and increments the counter.
60
+ * @returns {number}
61
+ */
62
+ export function nextSequence() {
63
+ return state.edgeSequence++;
64
+ }
65
+
66
+ /**
67
+ * Sets the active perspective and synchronises the physical storage mode ('LPG' | 'RDF')
68
+ * so that UNDER THE BONNET and DATABASE QUERY always match the selected Physical view.
69
+ *
70
+ * @param {'SMRM'|'SMIM'|'SMS_LPG'|'SMS_RDF'|'SMS'} newPerspective
71
+ */
72
+ export function setPerspective(newPerspective) {
73
+ if (newPerspective === 'SMS_LPG') {
74
+ state.perspective = 'SMS_LPG';
75
+ state.storage = 'LPG';
76
+ } else if (newPerspective === 'SMS_RDF') {
77
+ state.perspective = 'SMS_RDF';
78
+ state.storage = 'RDF';
79
+ } else if (newPerspective === 'SMS') {
80
+ state.perspective = state.storage === 'RDF' ? 'SMS_RDF' : 'SMS_LPG';
81
+ } else if (newPerspective === 'SMIM' || newPerspective === 'SMRM') {
82
+ state.perspective = newPerspective;
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Completely replaces or updates the current state from a saved JSON object or scenario.
88
+ *
89
+ * @param {Object} newState - State payload to inject.
90
  */
91
  export function loadState(newState) {
92
+ if (!newState || typeof newState !== 'object') return;
93
+
94
  if (newState.currentView !== undefined) {
95
  state.currentView = newState.currentView;
96
  }
 
97
  if (newState.userRole !== undefined) {
98
  state.userRole = newState.userRole;
99
  }
 
100
  if (newState.ontology !== undefined) {
101
+ const onto = newState.ontology;
102
+ state.ontology = onto === 'ISO_15926_4D' ? 'ISO15926' : (onto === 'NAIVE' ? 'NAIVE_NODE' : onto);
103
+ }
104
+ if (newState.storage === 'LPG' || newState.storage === 'RDF') {
105
+ state.storage = newState.storage;
106
+ }
107
+ if (newState.perspective !== undefined) {
108
+ setPerspective(newState.perspective);
109
  }
 
110
  if (newState.currentTime !== undefined) {
111
  state.currentTime = newState.currentTime;
112
  }
113
+ if (Array.isArray(newState.eventLog)) {
 
114
  state.eventLog.splice(0, state.eventLog.length, ...newState.eventLog);
115
  }
 
116
  if (newState.edgeSequence !== undefined) {
117
  state.edgeSequence = newState.edgeSequence;
118
  }
 
 
 
 
 
119
  if (newState.graphEngine !== undefined) {
120
+ state.graphEngine = String(newState.graphEngine).toUpperCase() === 'MERMAID' ? 'MERMAID' : 'VIS';
121
  }
122
+ if (newState.language !== undefined) {
123
+ state.language = newState.language;
124
+ }
125
+ if (newState.showHistory !== undefined) {
126
+ state.showHistory = Boolean(newState.showHistory);
127
+ }
128
+ }
js/urmsm/bom-view.js CHANGED
@@ -1,88 +1,137 @@
1
  /**
2
  * URMSM LAYER: BOM VIEW (Stakeholder Viewpoint)
3
  * ---------------------------------------------
4
- * This module generates the user-facing Bill of Materials (BOM) tree.
5
- * According to ISO 42010, this is a specific viewpoint for an engineer
6
- * who only cares about the physical breakdown at time T, abstracting away
7
- * the underlying database mechanics (LPG/RDF) and temporal ontologies.
8
- *
9
- * Returns pure hierarchical data structures without any DOM or HTML coupling.
 
10
  */
11
 
12
- /**
13
- * Recursively builds a pure hierarchical tree structure for the BOM.
14
- *
15
- * @param {string} rootId - The ID of the top-level system.
16
- * @param {Array<Object>} activeEdges - Array of currently active edges (filtered for Time = T).
17
- * @returns {Object} Hierarchical node structure: { id: string, children: Array<Object> }.
18
- */
19
- export function generateBomTree(rootId, activeEdges) {
20
- return buildTreeRecursive(rootId, activeEdges || [], new Set());
21
- }
22
 
23
  /**
24
- * Helper function to recursively populate tree nodes while guarding against circular references.
25
- * Strictly traverses the ontological hierarchy: Parent -> Slot -> Child.
26
- *
27
- * - When at a Parent (non-Slot): traverses outgoing has_slot or has_part edges to find contextual Slots or sub-components.
28
- * - When at a Slot: traverses incoming mounted_in edges to locate the mounted Child component.
29
- * - Under no circumstances does a Slot traverse backwards to its Parent, or a Child backwards to its Slot.
30
- *
31
- * @param {string} nodeId - Current node identifier.
32
- * @param {Array<Object>} activeEdges - Array of active relationship edges.
33
- * @param {Set<string>} visited - Set of previously visited node identifiers to avoid cycles.
34
- * @returns {Object} Tree node object.
35
  */
36
- function buildTreeRecursive(nodeId, activeEdges, visited = new Set()) {
37
- if (visited.has(nodeId)) {
38
- return { id: nodeId, children: [] };
39
  }
40
 
41
- const nextVisited = new Set(visited);
42
- nextVisited.add(nodeId);
43
-
44
- const childIds = new Set();
45
- const edges = activeEdges || [];
46
 
47
- // Determine if the current node is a contextual Slot
48
- const isSlotNode = nodeId.startsWith('Slot_') ||
49
- nodeId.startsWith('st_Slot_') ||
50
- edges.some(e => e.to === nodeId && (e.label === 'has_slot' || e.type === 'has_slot'));
 
 
51
 
52
- if (isSlotNode) {
53
- // A Slot ONLY contains components mounted into it (Child -[mounted_in]-> Slot)
54
- edges.forEach(e => {
55
- const rel = e.label || e.relationType || e.type;
56
- if (e.to === nodeId && (rel === 'mounted_in' || e.label === 'mounted_in')) {
57
- if (!nextVisited.has(e.from)) {
58
- childIds.add(e.from);
59
  }
60
- }
61
- });
62
- } else {
63
- // A non-Slot component/system traverses forward:
64
- // 1. Outgoing slots: Parent -[has_slot]-> Slot
65
- // 2. Direct sub-components: Parent -[has_part]-> Child
66
- edges.forEach(e => {
67
- const rel = e.label || e.relationType || e.type;
68
- if (e.from === nodeId) {
69
- if (rel === 'has_slot' || e.label === 'has_slot') {
70
- if (!nextVisited.has(e.to)) {
71
- childIds.add(e.to);
72
- }
73
- } else if (rel === 'has_part' || e.label === 'has_part') {
74
- if (!nextVisited.has(e.to)) {
75
- childIds.add(e.to);
76
- }
77
  }
 
 
 
 
 
 
 
 
 
 
 
 
78
  }
79
- });
80
- }
81
 
82
- const children = Array.from(childIds).map(childId => buildTreeRecursive(childId, edges, nextVisited));
 
 
83
 
84
- return {
85
- id: nodeId,
86
- children
87
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  }
 
 
 
 
 
 
 
 
 
 
 
 
1
  /**
2
  * URMSM LAYER: BOM VIEW (Stakeholder Viewpoint)
3
  * ---------------------------------------------
4
+ * Generates the user-facing Bill of Materials (BOM) hierarchy for the active
5
+ * ontology at time T:
6
+ * - Dynamically discovers all active root containers (Vehicles, Warehouses).
7
+ * - Never renders a phantom 'System_A' when the graph is empty.
8
+ * - Collapses technical Slot nodes via SMRM reduction.
9
+ * - Attaches active defect indicators (e.g. 'crack: 2mm') to components if and only if
10
+ * the defect survives in the active ontology at time T.
11
  */
12
 
13
+ import { INTERNAL_PROPERTY_KEYS } from '../smrm/definitions.js';
14
+ import { reduceGraphForSMRM } from './smrm-view.js';
 
 
 
 
 
 
 
 
15
 
16
  /**
17
+ * Generates an array of root BOM trees from the current canonical Semantic Graph.
18
+ *
19
+ * @param {Object} semanticGraph - Canonical SMIM Semantic Graph ({ nodes, edges }).
20
+ * @returns {Array<{ id: string, entityClass: string, defects: Array<string>, children: Array }>}
 
 
 
 
 
 
 
21
  */
22
+ export function generateBomTrees(semanticGraph) {
23
+ if (!semanticGraph || !Array.isArray(semanticGraph.nodes) || semanticGraph.nodes.length === 0) {
24
+ return [];
25
  }
26
 
27
+ // Reduce to Engineer SMRM graph so Slots, 4D Extents, and Qualities are normalised
28
+ const smrmGraph = reduceGraphForSMRM(semanticGraph);
29
+ const activeEdges = smrmGraph.edges.filter(e => e.properties?.isCurrent !== false);
 
 
30
 
31
+ // Map active defects per component ID
32
+ const defectsByNode = new Map();
33
+ const addDefect = (nodeId, label) => {
34
+ if (!defectsByNode.has(nodeId)) defectsByNode.set(nodeId, new Set());
35
+ defectsByNode.get(nodeId).add(label);
36
+ };
37
 
38
+ // 1a. Check custom properties on nodes (Naive Node mode)
39
+ smrmGraph.nodes.forEach(node => {
40
+ if (node.type === 'Defect') return;
41
+ if (node.properties) {
42
+ Object.entries(node.properties).forEach(([k, v]) => {
43
+ if (!INTERNAL_PROPERTY_KEYS.includes(k) && v !== undefined && v !== null) {
44
+ addDefect(node.id, `${k}: ${v}`);
45
  }
46
+ });
47
+ }
48
+ });
49
+
50
+ // 1b. Check custom properties on active structural edges (Naive Edge mode)
51
+ activeEdges.forEach(edge => {
52
+ const childId = edge.childId || (edge.label === 'stored_at' || edge.label === 'mounted_in' ? edge.from : edge.to);
53
+ if (edge.properties && childId) {
54
+ Object.entries(edge.properties).forEach(([k, v]) => {
55
+ if (!INTERNAL_PROPERTY_KEYS.includes(k) && v !== undefined && v !== null) {
56
+ addDefect(childId, `${k}: ${v}`);
 
 
 
 
 
 
57
  }
58
+ });
59
+ }
60
+ });
61
+
62
+ // 1c. Check explicit Defect nodes linked via active 'has_defect' edges (BFO & ISO 15926)
63
+ activeEdges.forEach(edge => {
64
+ if (edge.label === 'has_defect' || edge.type === 'has_defect') {
65
+ const defectNode = smrmGraph.nodes.find(n => n.id === edge.to);
66
+ if (defectNode) {
67
+ const dType = defectNode.properties?.defectType || defectNode.properties?.qualityType || 'defect';
68
+ const dVal = defectNode.properties?.value || 'present';
69
+ addDefect(edge.from, `${dType}: ${dVal}`);
70
  }
71
+ }
72
+ });
73
 
74
+ // Build parent -> children adjacency for active structural relations
75
+ const childrenMap = new Map();
76
+ const hasParentSet = new Set();
77
 
78
+ activeEdges.forEach(edge => {
79
+ if (edge.label === 'has_defect' || edge.type === 'has_defect') return;
80
+
81
+ let parentId = null;
82
+ let childId = null;
83
+
84
+ if (edge.label === 'has_part' || edge.label === 'has_slot') {
85
+ parentId = edge.from;
86
+ childId = edge.to;
87
+ } else if (edge.label === 'mounted_in' || edge.label === 'stored_at') {
88
+ parentId = edge.to;
89
+ childId = edge.from;
90
+ }
91
+
92
+ if (parentId && childId && parentId !== childId) {
93
+ if (!childrenMap.has(parentId)) childrenMap.set(parentId, new Set());
94
+ childrenMap.get(parentId).add(childId);
95
+ hasParentSet.add(childId);
96
+ }
97
+ });
98
+
99
+ // Identify root nodes (Vehicles, Warehouses, or top-level assemblies without a parent)
100
+ const candidateRoots = smrmGraph.nodes.filter(n => {
101
+ if (n.type === 'Defect' || n.type === 'Slot') return false;
102
+ if (hasParentSet.has(n.id)) return false;
103
+ return n.type === 'Vehicle' || n.type === 'Warehouse' || childrenMap.has(n.id);
104
+ });
105
+
106
+ function buildNode(nodeId, visited = new Set()) {
107
+ if (visited.has(nodeId)) {
108
+ return { id: nodeId, defects: [], children: [] };
109
+ }
110
+ const nextVisited = new Set(visited);
111
+ nextVisited.add(nodeId);
112
+
113
+ const nodeObj = smrmGraph.nodes.find(n => n.id === nodeId);
114
+ const childIds = Array.from(childrenMap.get(nodeId) || []);
115
+ const defects = Array.from(defectsByNode.get(nodeId) || []);
116
+
117
+ return {
118
+ id: nodeId,
119
+ entityClass: nodeObj?.type || 'Part',
120
+ defects,
121
+ children: childIds.map(cid => buildNode(cid, nextVisited))
122
+ };
123
+ }
124
+
125
+ return candidateRoots.map(root => buildNode(root.id));
126
  }
127
+
128
+ /**
129
+ * Backwards-compatible single-root helper.
130
+ */
131
+ export function generateBomTree(rootId, activeEdges) {
132
+ const trees = generateBomTrees({
133
+ nodes: [{ id: rootId, type: 'Vehicle', properties: {} }],
134
+ edges: activeEdges || []
135
+ });
136
+ return trees[0] || null;
137
+ }
js/urmsm/smrm-view.js CHANGED
@@ -1,33 +1,187 @@
1
  /**
2
  * URMSM LAYER: SMRM GRAPH REDUCTION VIEWPOINT (ISO 42010)
3
  * -------------------------------------------------------
4
- * Transforms a canonical SMIM Semantic Graph into the engineer's SMRM perspective.
5
- * Visually collapses intermediate contextual Slot nodes and renders direct
6
- * transitive relationships between parent and child components.
7
- *
8
- * Crucially preserves and transfers properties (such as defects like { crack: '2mm' })
9
- * from the original mounting relationship onto the direct transitive relationship.
10
- *
11
- * Adheres strictly to the Rule of Isolation:
12
- * Pure graph-to-graph reduction middleware without DOM or Vue coupling.
13
- *
14
- * @param {Object} semanticGraph - Canonical Semantic Graph ({ nodes: Array, edges: Array }).
15
- * @returns {Object} Reduced Semantic Graph ({ nodes: Array, edges: Array }) for SMRM rendering.
16
  */
17
 
 
 
 
 
 
 
 
 
18
  export function reduceGraphForSMRM(semanticGraph) {
19
  if (!semanticGraph || !Array.isArray(semanticGraph.nodes) || !Array.isArray(semanticGraph.edges)) {
20
  return { nodes: [], edges: [] };
21
  }
22
 
23
- // 1. Identify all Slot nodes in the semantic graph
24
- const slotNodeIds = new Set();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  semanticGraph.nodes.forEach(node => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  const isSlot = node.type === 'Slot' ||
27
  node.properties?.entityClass === 'Slot' ||
28
- node.id.startsWith('Slot_') ||
29
- node.id.startsWith('4d_Slot_') ||
30
- node.id.startsWith('st_Slot_');
31
  if (isSlot) {
32
  slotNodeIds.add(node.id);
33
  }
@@ -35,24 +189,19 @@ export function reduceGraphForSMRM(semanticGraph) {
35
 
36
  if (slotNodeIds.size === 0) {
37
  return {
38
- nodes: semanticGraph.nodes.map(n => ({ ...n, properties: { ...(n.properties || {}) } })),
39
- edges: semanticGraph.edges.map(e => ({ ...e, properties: { ...(e.properties || {}) } }))
40
  };
41
  }
42
 
43
- const nodesToRemove = new Set(slotNodeIds);
44
  const edgesToRemove = new Set();
45
  const directTransitiveEdges = [];
46
 
47
- // 2. Locate and collapse [Parent] -[has_slot]-> [Slot] <-[mounted_in]- [Child]
48
  slotNodeIds.forEach(slotId => {
49
- // Find has_slot edges from Parent to Slot
50
- const hasSlotEdges = semanticGraph.edges.filter(
51
  e => e.to === slotId && (e.label === 'has_slot' || e.type === 'has_slot')
52
  );
53
-
54
- // Find mounted_in edges from Child to Slot
55
- const mountedInEdges = semanticGraph.edges.filter(
56
  e => e.to === slotId && (e.label === 'mounted_in' || e.type === 'mounted_in' || e.type === 'composition')
57
  );
58
 
@@ -63,8 +212,6 @@ export function reduceGraphForSMRM(semanticGraph) {
63
  mountedInEdges.forEach(mountedInEdge => {
64
  edgesToRemove.add(mountedInEdge.id);
65
  const childNodeId = mountedInEdge.from;
66
-
67
- // Formulate direct transitive edge: Parent -> Child
68
  const directEdgeId = `smrm_${parentNodeId}_${childNodeId}_${mountedInEdge.properties?.validFrom ?? 0}`;
69
 
70
  directTransitiveEdges.push({
@@ -81,20 +228,13 @@ export function reduceGraphForSMRM(semanticGraph) {
81
  });
82
  });
83
  });
84
-
85
- // Also remove temporalPartOf edges linking slot state slices to 4D extents
86
- semanticGraph.edges.forEach(e => {
87
- if ((e.from === slotId || e.to === slotId) && e.label === 'temporalPartOf') {
88
- edgesToRemove.add(e.id);
89
- }
90
- });
91
  });
92
 
93
- const remainingNodes = semanticGraph.nodes
94
- .filter(n => !nodesToRemove.has(n.id))
95
  .map(n => ({ ...n, properties: { ...(n.properties || {}) } }));
96
 
97
- const remainingEdges = semanticGraph.edges
98
  .filter(e => !edgesToRemove.has(e.id))
99
  .map(e => ({ ...e, properties: { ...(e.properties || {}) } }))
100
  .concat(directTransitiveEdges);
@@ -103,4 +243,4 @@ export function reduceGraphForSMRM(semanticGraph) {
103
  nodes: remainingNodes,
104
  edges: remainingEdges
105
  };
106
- }
 
1
  /**
2
  * URMSM LAYER: SMRM GRAPH REDUCTION VIEWPOINT (ISO 42010)
3
  * -------------------------------------------------------
4
+ * Transforms any canonical SMIM Semantic Graph (Naive, 3D BFO, or 4D ISO 15926)
5
+ * into the Engineer's SMRM Perspective:
6
+ * - Collapses intermediate contextual Slot nodes into direct 'has_part' relationships.
7
+ * - Abstracts away 4D Spatio-Temporal Extents ('ActualIndividual') and 'State' slices
8
+ * into clean domain entities ('Vehicle', 'Part', 'Warehouse', 'Defect'), showing
9
+ * the engineer purely what domain knowledge survives at time T.
10
+ * - Maps 3D BFO 'MaterialEntity' and 'Quality' nodes into engineering domain classes.
 
 
 
 
 
11
  */
12
 
13
+ import { inferEntityClass } from '../smrm/definitions.js';
14
+
15
+ /**
16
+ * Reduces a canonical SMIM graph into the Engineer (SMRM) viewpoint.
17
+ *
18
+ * @param {Object} semanticGraph - Canonical Semantic Graph ({ nodes: Array, edges: Array }).
19
+ * @returns {{ nodes: Array, edges: Array }}
20
+ */
21
  export function reduceGraphForSMRM(semanticGraph) {
22
  if (!semanticGraph || !Array.isArray(semanticGraph.nodes) || !Array.isArray(semanticGraph.edges)) {
23
  return { nodes: [], edges: [] };
24
  }
25
 
26
+ const is4D = semanticGraph.nodes.some(n => n.type === 'ActualIndividual' || n.type === 'State');
27
+ const isBFO = semanticGraph.nodes.some(n => n.type === 'MaterialEntity' || n.type === 'Quality');
28
+
29
+ // Case A: 4D ISO 15926 Graph -> Project onto domain entities
30
+ if (is4D) {
31
+ return reduce4DToSMRM(semanticGraph);
32
+ }
33
+
34
+ // Case B: 3D BFO Graph -> Normalize MaterialEntity/Quality & collapse Slots
35
+ if (isBFO) {
36
+ const normalizedNodes = semanticGraph.nodes.map(n => {
37
+ if (n.type === 'MaterialEntity') {
38
+ const cls = inferEntityClass(n.id, n.properties?.entityClass);
39
+ return {
40
+ ...n,
41
+ type: cls,
42
+ properties: { ...(n.properties || {}), entityClass: cls }
43
+ };
44
+ }
45
+ if (n.type === 'Quality') {
46
+ return {
47
+ ...n,
48
+ type: 'Defect',
49
+ properties: { ...(n.properties || {}), entityClass: 'Defect' }
50
+ };
51
+ }
52
+ return { ...n, properties: { ...(n.properties || {}) } };
53
+ });
54
+
55
+ const normalizedEdges = semanticGraph.edges.map(e => {
56
+ if (e.label === 'inheres_in' || e.type === 'inherence') {
57
+ // Flip Quality -> Part inheres_in into Part -> Defect has_defect for Engineer View
58
+ return {
59
+ ...e,
60
+ from: e.to,
61
+ to: e.from,
62
+ label: 'has_defect',
63
+ type: 'has_defect',
64
+ properties: { ...(e.properties || {}) }
65
+ };
66
+ }
67
+ return { ...e, properties: { ...(e.properties || {}) } };
68
+ });
69
+
70
+ return collapseSlots({ nodes: normalizedNodes, edges: normalizedEdges });
71
+ }
72
+
73
+ // Case C: Naive Graph -> Collapse Slots directly while transferring edge properties
74
+ return collapseSlots(semanticGraph);
75
+ }
76
+
77
+ /**
78
+ * Projects a 4D ISO 15926 Semantic Graph into a clean domain graph for the Engineer View,
79
+ * collapsing 4D Extents, States, and Slots while preserving surviving defects.
80
+ */
81
+ function reduce4DToSMRM(semanticGraph) {
82
+ const domainNodes = new Map();
83
+ const stateToOwner = new Map();
84
+
85
  semanticGraph.nodes.forEach(node => {
86
+ if (node.type === 'ActualIndividual') {
87
+ const origId = node.properties?.originalId || node.id.replace(/^4d_/, '');
88
+ const cls = inferEntityClass(origId, node.properties?.entityClass);
89
+ if (cls !== 'Slot') {
90
+ domainNodes.set(origId, {
91
+ id: origId,
92
+ label: origId,
93
+ type: cls,
94
+ properties: { entityClass: cls }
95
+ });
96
+ }
97
+ } else if (node.type === 'State') {
98
+ const ownerId = node.properties?.ownerId || node.id.replace(/^st_/, '').split('_evt_')[0];
99
+ stateToOwner.set(node.id, ownerId);
100
+ } else if (node.type === 'Defect') {
101
+ domainNodes.set(node.id, {
102
+ ...node,
103
+ type: 'Defect',
104
+ properties: { ...(node.properties || {}), entityClass: 'Defect' }
105
+ });
106
+ }
107
+ });
108
+
109
+ const projectedEdges = [];
110
+
111
+ semanticGraph.edges.forEach(edge => {
112
+ if (edge.label === 'temporalPartOf') return;
113
+
114
+ const fromOwner = stateToOwner.get(edge.from) || edge.from;
115
+ const toOwner = stateToOwner.get(edge.to) || edge.to;
116
+
117
+ if (edge.label === 'has_defect') {
118
+ projectedEdges.push({
119
+ id: `smrm_def_${fromOwner}_${edge.to}`,
120
+ from: fromOwner,
121
+ to: edge.to,
122
+ label: 'has_defect',
123
+ type: 'has_defect',
124
+ properties: {
125
+ ...(edge.properties || {}),
126
+ isCurrent: edge.properties?.validTo === null
127
+ }
128
+ });
129
+ return;
130
+ }
131
+
132
+ if (edge.label === 'stored_at') {
133
+ projectedEdges.push({
134
+ id: `smrm_store_${fromOwner}_${toOwner}_${edge.properties?.validFrom ?? 0}`,
135
+ from: fromOwner,
136
+ to: toOwner,
137
+ label: 'stored_at',
138
+ type: 'stored_at',
139
+ childId: fromOwner,
140
+ parentId: toOwner,
141
+ properties: { ...(edge.properties || {}) }
142
+ });
143
+ return;
144
+ }
145
+
146
+ if (edge.label === 'has_slot' || edge.label === 'mounted_in') {
147
+ // Ensure intermediate slot exists temporarily so collapseSlots can merge the pair cleanly
148
+ if (toOwner.startsWith('Slot_') && !domainNodes.has(toOwner)) {
149
+ domainNodes.set(toOwner, {
150
+ id: toOwner,
151
+ label: toOwner,
152
+ type: 'Slot',
153
+ properties: { entityClass: 'Slot' }
154
+ });
155
+ }
156
+ projectedEdges.push({
157
+ id: edge.id,
158
+ from: fromOwner,
159
+ to: toOwner,
160
+ label: edge.label,
161
+ type: edge.label,
162
+ childId: edge.childId,
163
+ parentId: edge.parentId,
164
+ properties: { ...(edge.properties || {}) }
165
+ });
166
+ }
167
+ });
168
+
169
+ return collapseSlots({
170
+ nodes: Array.from(domainNodes.values()),
171
+ edges: projectedEdges
172
+ });
173
+ }
174
+
175
+ /**
176
+ * Collapses [Parent] -[has_slot]-> [Slot] <-[mounted_in]- [Child] into
177
+ * a direct [Parent] -[has_part]-> [Child] edge, preserving mounting properties.
178
+ */
179
+ function collapseSlots(graph) {
180
+ const slotNodeIds = new Set();
181
+ graph.nodes.forEach(node => {
182
  const isSlot = node.type === 'Slot' ||
183
  node.properties?.entityClass === 'Slot' ||
184
+ node.id.startsWith('Slot_');
 
 
185
  if (isSlot) {
186
  slotNodeIds.add(node.id);
187
  }
 
189
 
190
  if (slotNodeIds.size === 0) {
191
  return {
192
+ nodes: graph.nodes.map(n => ({ ...n, properties: { ...(n.properties || {}) } })),
193
+ edges: graph.edges.map(e => ({ ...e, properties: { ...(e.properties || {}) } }))
194
  };
195
  }
196
 
 
197
  const edgesToRemove = new Set();
198
  const directTransitiveEdges = [];
199
 
 
200
  slotNodeIds.forEach(slotId => {
201
+ const hasSlotEdges = graph.edges.filter(
 
202
  e => e.to === slotId && (e.label === 'has_slot' || e.type === 'has_slot')
203
  );
204
+ const mountedInEdges = graph.edges.filter(
 
 
205
  e => e.to === slotId && (e.label === 'mounted_in' || e.type === 'mounted_in' || e.type === 'composition')
206
  );
207
 
 
212
  mountedInEdges.forEach(mountedInEdge => {
213
  edgesToRemove.add(mountedInEdge.id);
214
  const childNodeId = mountedInEdge.from;
 
 
215
  const directEdgeId = `smrm_${parentNodeId}_${childNodeId}_${mountedInEdge.properties?.validFrom ?? 0}`;
216
 
217
  directTransitiveEdges.push({
 
228
  });
229
  });
230
  });
 
 
 
 
 
 
 
231
  });
232
 
233
+ const remainingNodes = graph.nodes
234
+ .filter(n => !slotNodeIds.has(n.id))
235
  .map(n => ({ ...n, properties: { ...(n.properties || {}) } }));
236
 
237
+ const remainingEdges = graph.edges
238
  .filter(e => !edgesToRemove.has(e.id))
239
  .map(e => ({ ...e, properties: { ...(e.properties || {}) } }))
240
  .concat(directTransitiveEdges);
 
243
  nodes: remainingNodes,
244
  edges: remainingEdges
245
  };
246
+ }
styles/main.css CHANGED
@@ -5,65 +5,399 @@
5
  font-style: normal;
6
  }
7
 
8
- * { box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
9
- body { margin: 0; padding: 12px; background: #f8fafc; color: #0f172a; display: flex; flex-direction: column; height: 100vh; }
10
-
11
- .control-deck { background: #ffffff; border: 1px solid #cbd5e1; border-radius: 8px; margin-bottom: 10px; box-shadow: 0 1px 3px rgba(0,0,0,0.05); }
12
- .deck-row { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; padding: 8px 14px; }
13
- .deck-row-top { background: #f1f5f9; border-bottom: 1px solid #e2e8f0; border-top-left-radius: 7px; border-top-right-radius: 7px; }
14
-
15
- .badge { background: #0284c7; color: #ffffff; font-weight: bold; padding: 4px 10px; border-radius: 6px; font-size: 13px; }
16
- .section-label { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; color: #475569; display: flex; align-items: center; gap: 4px; }
17
-
18
- .iec-badge { display: inline-flex; align-items: center; gap: 3px; background: #e0f2fe; color: #0369a1; border: 1px solid #7dd3fc; padding: 2px 7px; border-radius: 4px; font-size: 10px; font-weight: 700; letter-spacing: 0.04em; cursor: help; position: relative; }
19
- .iec-badge:hover .iec-tooltip { visibility: visible; opacity: 1; }
20
- .iec-tooltip { visibility: hidden; opacity: 0; position: absolute; top: 130%; left: 0; z-index: 100; background: #0f172a; color: #f8fafc; padding: 8px 12px; border-radius: 6px; font-size: 11px; font-weight: 400; line-height: 1.35; min-width: 250px; max-width: 320px; box-shadow: 0 4px 14px rgba(0,0,0,0.2); transition: opacity 0.15s ease-in-out; pointer-events: none; }
21
- .iec-tooltip::after { content: ""; position: absolute; bottom: 100%; left: 14px; border-width: 5px; border-style: solid; border-color: transparent transparent #0f172a transparent; }
22
-
23
- /* Правка тултипа для элементов у правого края */
24
- .iec-badge.align-right .iec-tooltip { left: auto; right: -5px; }
25
- .iec-badge.align-right .iec-tooltip::after { left: auto; right: 20px; }
26
-
27
- button { background: #0f172a; color: #ffffff; border: none; padding: 5px 12px; border-radius: 6px; cursor: pointer; font-size: 12px; font-weight: 500; }
28
- button:hover { background: #334155; }
29
- button.secondary { background: #e2e8f0; color: #0f172a; }
30
- button.secondary:hover { background: #cbd5e1; }
31
- button.danger { background: #ef4444; }
32
- button.danger:hover { background: #dc2626; }
33
-
34
- .btn-group { display: flex; border: 1px solid #cbd5e1; border-radius: 6px; overflow: hidden; background: #ffffff; }
35
- .btn-group button { border-radius: 0; background: #ffffff; color: #475569; padding: 4px 10px; border-right: 1px solid #cbd5e1; font-size: 12px; font-weight: 600; line-height: 1.2; }
36
- .btn-group button:last-child { border-right: none; }
37
- .btn-group button.active { background: #0284c7; color: #ffffff; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  .btn-group.flag-group button {
39
  font-family: 'Twemoji', system-ui, sans-serif;
40
- font-size: 15px;
41
- padding: 3px 8px;
42
  }
43
 
44
- input, select { padding: 5px 8px; border: 1px solid #cbd5e1; border-radius: 6px; font-size: 12px; }
45
- .divider { height: 18px; width: 1px; background: #cbd5e1; margin: 0 4px; }
 
 
 
46
 
47
- .main-container { display: flex; flex: 1; gap: 10px; min-height: 0; }
48
- #canvas-container { flex: 2; background: #ffffff; border: 1px solid #e2e8f0; border-radius: 8px; position: relative; overflow: hidden; min-width: 20%; }
49
- #vis-container { width: 100%; height: 100%; position: absolute; top: 0; left: 0; }
50
- #mermaid-container { width: 100%; height: 100%; position: absolute; top: 0; left: 0; overflow: auto; padding: 24px 24px 140px 24px; display: flex; justify-content: center; align-items: flex-start; background: #ffffff; }
51
- #mermaid-container svg { width: auto; height: auto; max-width: none; }
 
 
52
 
53
- /* Ресайзеры (горизонтальный и вертикальный) */
54
- .resizer { width: 8px; background-color: #f1f5f9; border: 1px solid #e2e8f0; border-radius: 4px; cursor: col-resize; transition: background-color 0.2s, border-color 0.2s; flex-shrink: 0; }
55
- .resizer:hover, .resizer.dragging { background-color: #cbd5e1; border-color: #94a3b8; }
56
- .resizer-h { height: 8px; background-color: #f1f5f9; border: 1px solid #e2e8f0; border-radius: 4px; cursor: row-resize; transition: background-color 0.2s, border-color 0.2s; flex-shrink: 0; }
57
- .resizer-h:hover, .resizer-h.dragging { background-color: #cbd5e1; border-color: #94a3b8; }
58
- body.no-select { user-select: none; }
 
59
 
60
- .graph-caption { position: absolute; top: 12px; left: 12px; background: rgba(255, 255, 255, 0.94); backdrop-filter: blur(4px); border: 1px solid #cbd5e1; border-radius: 6px; padding: 6px 12px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); font-size: 11px; font-weight: 700; color: #1e293b; z-index: 20; display: flex; align-items: center; gap: 6px; letter-spacing: 0.02em; pointer-events: none; }
61
- .graph-legend { position: absolute; bottom: 12px; left: 12px; background: rgba(255, 255, 255, 0.96); backdrop-filter: blur(4px); border: 1px solid #cbd5e1; border-radius: 8px; padding: 10px 14px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); font-size: 11px; color: #334155; z-index: 20; max-width: 330px; }
62
- .legend-title { font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; color: #475569; margin-bottom: 6px; display: flex; justify-content: space-between; border-bottom: 1px solid #e2e8f0; padding-bottom: 4px; }
63
- .legend-section { margin-top: 5px; }
64
- .legend-section-title { font-weight: 600; color: #64748b; font-size: 10px; margin-bottom: 3px; text-transform: uppercase; }
65
- .legend-item { display: flex; align-items: center; margin-bottom: 3px; line-height: 1.25; }
 
66
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  .swatch-box-sys { width: 13px; height: 11px; background: #0284c7; border: 1px solid #0369a1; border-radius: 2px; margin-right: 8px; flex-shrink: 0; }
68
  .swatch-box-elem { width: 13px; height: 11px; background: #64748b; border: 1px solid #475569; border-radius: 2px; margin-right: 8px; flex-shrink: 0; }
69
  .swatch-box-4d { width: 13px; height: 11px; background: #4f46e5; border: 1px solid #3730a3; border-radius: 2px; margin-right: 8px; flex-shrink: 0; }
@@ -83,38 +417,215 @@ body.no-select { user-select: none; }
83
  .swatch-line-store { width: 16px; height: 2px; background: #f97316; margin-right: 8px; flex-shrink: 0; }
84
  .swatch-line-defect { width: 16px; height: 2px; background: #ef4444; margin-right: 8px; flex-shrink: 0; }
85
 
86
-
87
- .sidebar { flex: 1; display: flex; flex-direction: column; gap: 10px; min-width: 350px; }
88
- .card { background: #ffffff; border: 1px solid #e2e8f0; border-radius: 8px; padding: 12px; overflow-y: auto; display: flex; flex-direction: column; }
89
- .card h4 { margin: 0 0 8px 0; font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; color: #64748b; display: flex; justify-content: space-between; align-items: center; }
90
- ul.tree, ul.tree ul { list-style-type: none; padding-left: 16px; margin: 4px 0; }
91
- ul.tree li { position: relative; padding: 2px 0; font-size: 13px; }
92
- ul.tree li::before { content: "├─ "; color: #94a3b8; }
93
- ul.tree li:last-child::before { content: "└─ "; color: #94a3b8; }
94
- table { width: 100%; border-collapse: collapse; font-size: 11px; }
95
- th, td { text-align: left; padding: 5px 6px; border-bottom: 1px solid #f1f5f9; }
96
- th { color: #64748b; font-weight: 600; }
97
- .active-row { background: #f0fdf4; }
98
- .closed-row { color: #94a3b8; text-decoration: line-through; }
99
- pre.code-block { margin: 0; font-family: monospace; font-size: 11px; color: #334155; line-height: 1.4; background: #f8fafc; padding: 8px; border-radius: 4px; border: 1px solid #e2e8f0; overflow-y: auto; flex: 1; }
100
-
101
- /* Fix for Vue #app wrapper to fill the viewport */
102
- #app {
103
  display: flex;
104
  flex-direction: column;
105
- flex: 1;
106
- height: 100%;
107
  min-height: 0;
108
  }
109
 
110
- /* Sandbox Workspace Wrapper */
111
- .sandbox-workspace {
 
 
 
112
  display: flex;
113
  flex-direction: column;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  flex: 1;
 
 
115
  min-height: 0;
116
  }
117
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
  /* Main Menu / Landing View */
119
  .landing-view {
120
  display: flex;
@@ -186,6 +697,7 @@ pre.code-block { margin: 0; font-family: monospace; font-size: 11px; color: #334
186
  cursor: pointer;
187
  transition: all 0.2s ease-in-out;
188
  width: 100%;
 
189
  }
190
 
191
  .menu-btn .btn-main-text {
@@ -378,31 +890,6 @@ pre.code-block { margin: 0; font-family: monospace; font-size: 11px; color: #334
378
  cursor: pointer;
379
  }
380
 
381
- /* Role Switcher Styling */
382
- .btn-group.role-group button {
383
- font-weight: 600;
384
- display: inline-flex;
385
- align-items: center;
386
- gap: 5px;
387
- }
388
-
389
- .btn-group.role-group button.active {
390
- background: #4f46e5;
391
- color: #ffffff;
392
- }
393
-
394
- /* Universal Engineer Workspace Placeholder */
395
- .uni-engineer-placeholder {
396
- min-height: 52px;
397
- background: #f8fafc;
398
- border-top: 1px solid #e2e8f0;
399
- border-bottom-left-radius: 7px;
400
- border-bottom-right-radius: 7px;
401
- display: flex;
402
- align-items: center;
403
- padding: 12px 16px;
404
- }
405
-
406
  .player-time {
407
  font-size: 12px;
408
  font-variant-numeric: tabular-nums;
@@ -417,4 +904,4 @@ pre.code-block { margin: 0; font-family: monospace; font-size: 11px; color: #334
417
  box-shadow: 0 0 10px 3px rgba(255, 215, 0, 0.8) !important;
418
  background-color: rgba(255, 255, 200, 0.3) !important;
419
  transition: all 0.3s ease;
420
- }
 
5
  font-style: normal;
6
  }
7
 
8
+ * {
9
+ box-sizing: border-box;
10
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
11
+ }
12
+
13
+ body {
14
+ margin: 0;
15
+ padding: 10px 12px;
16
+ background: #f8fafc;
17
+ color: #0f172a;
18
+ display: flex;
19
+ flex-direction: column;
20
+ height: 100vh;
21
+ overflow: hidden;
22
+ }
23
+
24
+ #app {
25
+ display: flex;
26
+ flex-direction: column;
27
+ flex: 1;
28
+ height: 100%;
29
+ min-height: 0;
30
+ }
31
+
32
+ .sandbox-workspace {
33
+ display: flex;
34
+ flex-direction: column;
35
+ flex: 1;
36
+ min-height: 0;
37
+ }
38
+
39
+ /* Top Control Deck */
40
+ .control-deck {
41
+ background: #ffffff;
42
+ border: 1px solid #cbd5e1;
43
+ border-radius: 8px;
44
+ margin-bottom: 8px;
45
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
46
+ flex-shrink: 0;
47
+ }
48
+
49
+ .deck-row {
50
+ display: flex;
51
+ gap: 8px;
52
+ align-items: center;
53
+ flex-wrap: wrap;
54
+ padding: 6px 12px;
55
+ }
56
+
57
+ .deck-row-top {
58
+ background: #f1f5f9;
59
+ border-bottom: 1px solid #e2e8f0;
60
+ border-top-left-radius: 7px;
61
+ border-top-right-radius: 7px;
62
+ }
63
+
64
+ .action-label {
65
+ font-size: 11px;
66
+ font-weight: 700;
67
+ color: #0284c7;
68
+ white-space: nowrap;
69
+ }
70
+
71
+ .badge {
72
+ background: #0284c7;
73
+ color: #ffffff;
74
+ font-weight: bold;
75
+ padding: 4px 9px;
76
+ border-radius: 6px;
77
+ font-size: 12px;
78
+ white-space: nowrap;
79
+ }
80
+
81
+ .section-label {
82
+ font-size: 11px;
83
+ font-weight: 700;
84
+ text-transform: uppercase;
85
+ letter-spacing: 0.04em;
86
+ color: #475569;
87
+ display: flex;
88
+ align-items: center;
89
+ gap: 4px;
90
+ white-space: nowrap;
91
+ }
92
+
93
+ /* IEC 63339 Badges & Tooltips */
94
+ .iec-badge {
95
+ display: inline-flex;
96
+ align-items: center;
97
+ gap: 3px;
98
+ background: #e0f2fe;
99
+ color: #0369a1;
100
+ border: 1px solid #7dd3fc;
101
+ padding: 2px 6px;
102
+ border-radius: 4px;
103
+ font-size: 10px;
104
+ font-weight: 700;
105
+ letter-spacing: 0.03em;
106
+ cursor: help;
107
+ position: relative;
108
+ }
109
+
110
+ .iec-badge:hover .iec-tooltip {
111
+ visibility: visible;
112
+ opacity: 1;
113
+ }
114
+
115
+ .iec-tooltip {
116
+ visibility: hidden;
117
+ opacity: 0;
118
+ position: absolute;
119
+ top: 130%;
120
+ left: 0;
121
+ z-index: 200;
122
+ background: #0f172a;
123
+ color: #f8fafc;
124
+ padding: 8px 12px;
125
+ border-radius: 6px;
126
+ font-size: 11px;
127
+ font-weight: 400;
128
+ text-transform: none;
129
+ line-height: 1.35;
130
+ min-width: 250px;
131
+ max-width: 320px;
132
+ box-shadow: 0 4px 14px rgba(0, 0, 0, 0.25);
133
+ transition: opacity 0.15s ease-in-out;
134
+ pointer-events: none;
135
+ }
136
+
137
+ .iec-tooltip::after {
138
+ content: "";
139
+ position: absolute;
140
+ bottom: 100%;
141
+ left: 14px;
142
+ border-width: 5px;
143
+ border-style: solid;
144
+ border-color: transparent transparent #0f172a transparent;
145
+ }
146
+
147
+ .iec-badge.align-right .iec-tooltip {
148
+ left: auto;
149
+ right: -5px;
150
+ }
151
+
152
+ .iec-badge.align-right .iec-tooltip::after {
153
+ left: auto;
154
+ right: 20px;
155
+ }
156
+
157
+ /* Buttons & Inputs */
158
+ button {
159
+ background: #0f172a;
160
+ color: #ffffff;
161
+ border: none;
162
+ padding: 5px 10px;
163
+ border-radius: 6px;
164
+ cursor: pointer;
165
+ font-size: 12px;
166
+ font-weight: 500;
167
+ white-space: nowrap;
168
+ }
169
+
170
+ button:hover {
171
+ background: #334155;
172
+ }
173
+
174
+ button.secondary {
175
+ background: #e2e8f0;
176
+ color: #0f172a;
177
+ }
178
+
179
+ button.secondary:hover {
180
+ background: #cbd5e1;
181
+ }
182
+
183
+ button.danger {
184
+ background: #ef4444;
185
+ }
186
+
187
+ button.danger:hover {
188
+ background: #dc2626;
189
+ }
190
+
191
+ .btn-group {
192
+ display: flex;
193
+ border: 1px solid #cbd5e1;
194
+ border-radius: 6px;
195
+ overflow: hidden;
196
+ background: #ffffff;
197
+ flex-shrink: 0;
198
+ }
199
+
200
+ .btn-group button {
201
+ border-radius: 0;
202
+ background: #ffffff;
203
+ color: #475569;
204
+ padding: 4px 9px;
205
+ border-right: 1px solid #cbd5e1;
206
+ font-size: 11.5px;
207
+ font-weight: 600;
208
+ line-height: 1.2;
209
+ }
210
+
211
+ .btn-group button:last-child {
212
+ border-right: none;
213
+ }
214
+
215
+ .btn-group button.active {
216
+ background: #0284c7;
217
+ color: #ffffff;
218
+ }
219
+
220
  .btn-group.flag-group button {
221
  font-family: 'Twemoji', system-ui, sans-serif;
222
+ font-size: 14px;
223
+ padding: 3px 7px;
224
  }
225
 
226
+ .btn-group.mini-toggle button {
227
+ font-size: 10px;
228
+ padding: 2px 7px;
229
+ text-transform: uppercase;
230
+ }
231
 
232
+ input,
233
+ select {
234
+ padding: 4px 7px;
235
+ border: 1px solid #cbd5e1;
236
+ border-radius: 6px;
237
+ font-size: 12px;
238
+ }
239
 
240
+ .divider {
241
+ height: 18px;
242
+ width: 1px;
243
+ background: #cbd5e1;
244
+ margin: 0 2px;
245
+ flex-shrink: 0;
246
+ }
247
 
248
+ /* Main Split Workspace & Graph Canvas */
249
+ .main-container {
250
+ display: flex;
251
+ flex: 1;
252
+ gap: 8px;
253
+ min-height: 0;
254
+ }
255
 
256
+ #canvas-container {
257
+ flex: 2;
258
+ background: #ffffff;
259
+ border: 1px solid #e2e8f0;
260
+ border-radius: 8px;
261
+ position: relative;
262
+ overflow: hidden;
263
+ min-width: 25%;
264
+ }
265
+
266
+ #vis-container {
267
+ width: 100%;
268
+ height: 100%;
269
+ position: absolute;
270
+ top: 0;
271
+ left: 0;
272
+ }
273
+
274
+ #mermaid-container {
275
+ width: 100%;
276
+ height: 100%;
277
+ position: absolute;
278
+ top: 0;
279
+ left: 0;
280
+ overflow: auto;
281
+ padding: 24px 24px 140px 24px;
282
+ display: flex;
283
+ justify-content: center;
284
+ align-items: flex-start;
285
+ background: #ffffff;
286
+ }
287
+
288
+ #mermaid-container svg {
289
+ width: auto;
290
+ height: auto;
291
+ max-width: none;
292
+ }
293
+
294
+ /* Resizers */
295
+ .resizer {
296
+ width: 7px;
297
+ background-color: #f1f5f9;
298
+ border: 1px solid #e2e8f0;
299
+ border-radius: 4px;
300
+ cursor: col-resize;
301
+ transition: background-color 0.2s, border-color 0.2s;
302
+ flex-shrink: 0;
303
+ }
304
+
305
+ .resizer:hover,
306
+ .resizer.dragging {
307
+ background-color: #94a3b8;
308
+ border-color: #64748b;
309
+ }
310
+
311
+ .resizer-h {
312
+ height: 7px;
313
+ background-color: #f1f5f9;
314
+ border: 1px solid #e2e8f0;
315
+ border-radius: 4px;
316
+ cursor: row-resize;
317
+ transition: background-color 0.2s, border-color 0.2s;
318
+ flex-shrink: 0;
319
+ }
320
+
321
+ .resizer-h:hover,
322
+ .resizer-h.dragging {
323
+ background-color: #94a3b8;
324
+ border-color: #64748b;
325
+ }
326
+
327
+ body.no-select {
328
+ user-select: none;
329
+ }
330
+
331
+ /* Canvas Overlays: Caption & Legend */
332
+ .graph-caption {
333
+ position: absolute;
334
+ top: 12px;
335
+ left: 12px;
336
+ background: rgba(255, 255, 255, 0.94);
337
+ backdrop-filter: blur(4px);
338
+ border: 1px solid #cbd5e1;
339
+ border-radius: 6px;
340
+ padding: 6px 12px;
341
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
342
+ font-size: 11px;
343
+ font-weight: 700;
344
+ color: #1e293b;
345
+ z-index: 20;
346
+ display: flex;
347
+ align-items: center;
348
+ gap: 6px;
349
+ letter-spacing: 0.02em;
350
+ pointer-events: none;
351
+ }
352
+
353
+ .graph-legend {
354
+ position: absolute;
355
+ bottom: 12px;
356
+ left: 12px;
357
+ background: rgba(255, 255, 255, 0.96);
358
+ backdrop-filter: blur(4px);
359
+ border: 1px solid #cbd5e1;
360
+ border-radius: 8px;
361
+ padding: 10px 14px;
362
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
363
+ font-size: 11px;
364
+ color: #334155;
365
+ z-index: 20;
366
+ max-width: 330px;
367
+ }
368
+
369
+ .legend-title {
370
+ font-weight: 700;
371
+ text-transform: uppercase;
372
+ letter-spacing: 0.05em;
373
+ color: #475569;
374
+ margin-bottom: 6px;
375
+ display: flex;
376
+ justify-content: space-between;
377
+ border-bottom: 1px solid #e2e8f0;
378
+ padding-bottom: 4px;
379
+ }
380
+
381
+ .legend-section {
382
+ margin-top: 5px;
383
+ }
384
+
385
+ .legend-section-title {
386
+ font-weight: 600;
387
+ color: #64748b;
388
+ font-size: 10px;
389
+ margin-bottom: 3px;
390
+ text-transform: uppercase;
391
+ }
392
+
393
+ .legend-item {
394
+ display: flex;
395
+ align-items: center;
396
+ margin-bottom: 3px;
397
+ line-height: 1.25;
398
+ }
399
+
400
+ /* Legend Swatches */
401
  .swatch-box-sys { width: 13px; height: 11px; background: #0284c7; border: 1px solid #0369a1; border-radius: 2px; margin-right: 8px; flex-shrink: 0; }
402
  .swatch-box-elem { width: 13px; height: 11px; background: #64748b; border: 1px solid #475569; border-radius: 2px; margin-right: 8px; flex-shrink: 0; }
403
  .swatch-box-4d { width: 13px; height: 11px; background: #4f46e5; border: 1px solid #3730a3; border-radius: 2px; margin-right: 8px; flex-shrink: 0; }
 
417
  .swatch-line-store { width: 16px; height: 2px; background: #f97316; margin-right: 8px; flex-shrink: 0; }
418
  .swatch-line-defect { width: 16px; height: 2px; background: #ef4444; margin-right: 8px; flex-shrink: 0; }
419
 
420
+ /* Right Sidebar Cards */
421
+ .sidebar {
422
+ flex: 1;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
423
  display: flex;
424
  flex-direction: column;
425
+ gap: 6px;
426
+ min-width: 360px;
427
  min-height: 0;
428
  }
429
 
430
+ .card {
431
+ background: #ffffff;
432
+ border: 1px solid #e2e8f0;
433
+ border-radius: 8px;
434
+ padding: 10px 12px;
435
  display: flex;
436
  flex-direction: column;
437
+ min-height: 0;
438
+ position: relative;
439
+ }
440
+
441
+ .card h4 {
442
+ margin: 0 0 6px 0;
443
+ font-size: 11px;
444
+ text-transform: uppercase;
445
+ letter-spacing: 0.05em;
446
+ color: #64748b;
447
+ display: flex;
448
+ justify-content: space-between;
449
+ align-items: center;
450
+ flex-shrink: 0;
451
+ }
452
+
453
+ .card-scroll-body {
454
+ overflow-y: auto;
455
  flex: 1;
456
+ display: flex;
457
+ flex-direction: column;
458
  min-height: 0;
459
  }
460
 
461
+ /* BOM Tree & Badges */
462
+ ul.tree,
463
+ ul.tree ul {
464
+ list-style-type: none;
465
+ padding-left: 16px;
466
+ margin: 2px 0;
467
+ }
468
+
469
+ ul.tree.root-tree {
470
+ padding-left: 4px;
471
+ }
472
+
473
+ ul.tree li {
474
+ position: relative;
475
+ padding: 2px 0;
476
+ font-size: 12.5px;
477
+ }
478
+
479
+ ul.tree ul li::before {
480
+ content: "├─ ";
481
+ color: #94a3b8;
482
+ }
483
+
484
+ ul.tree ul li:last-child::before {
485
+ content: "└─ ";
486
+ color: #94a3b8;
487
+ }
488
+
489
+ .bom-node-row {
490
+ display: inline-flex;
491
+ align-items: center;
492
+ gap: 6px;
493
+ flex-wrap: wrap;
494
+ }
495
+
496
+ .bom-class-badge {
497
+ background: #f1f5f9;
498
+ color: #475569;
499
+ border: 1px solid #cbd5e1;
500
+ border-radius: 4px;
501
+ padding: 1px 5px;
502
+ font-size: 10px;
503
+ font-weight: 600;
504
+ }
505
+
506
+ .bom-defect-badge {
507
+ background: #fef2f2;
508
+ color: #dc2626;
509
+ border: 1px solid #fecaca;
510
+ border-radius: 4px;
511
+ padding: 1px 6px;
512
+ font-size: 10.5px;
513
+ font-weight: 700;
514
+ }
515
+
516
+ .null-badge {
517
+ color: #94a3b8;
518
+ font-style: italic;
519
+ font-size: 10.5px;
520
+ }
521
+
522
+ /* Under the Bonnet & Live Query Tables */
523
+ .store-sub-header {
524
+ font-size: 10px;
525
+ font-weight: 700;
526
+ text-transform: uppercase;
527
+ color: #475569;
528
+ background: #f8fafc;
529
+ padding: 3px 6px;
530
+ border-left: 3px solid #0284c7;
531
+ margin-bottom: 4px;
532
+ }
533
+
534
+ .storage-table {
535
+ width: 100%;
536
+ border-collapse: collapse;
537
+ font-size: 11px;
538
+ margin-bottom: 4px;
539
+ }
540
+
541
+ th,
542
+ td {
543
+ text-align: left;
544
+ padding: 4px 6px;
545
+ border-bottom: 1px solid #f1f5f9;
546
+ }
547
+
548
+ th {
549
+ color: #64748b;
550
+ font-weight: 600;
551
+ background: #fafafa;
552
+ }
553
+
554
+ .active-row {
555
+ background: #f0fdf4;
556
+ }
557
+
558
+ .closed-row {
559
+ color: #94a3b8;
560
+ text-decoration: line-through;
561
+ }
562
+
563
+ .empty-placeholder {
564
+ color: #94a3b8;
565
+ font-size: 11.5px;
566
+ padding: 6px 4px;
567
+ font-style: italic;
568
+ }
569
+
570
+ pre.code-block {
571
+ margin: 0;
572
+ font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
573
+ font-size: 11px;
574
+ color: #334155;
575
+ line-height: 1.38;
576
+ background: #f8fafc;
577
+ padding: 8px;
578
+ border-radius: 4px;
579
+ border: 1px solid #e2e8f0;
580
+ overflow-x: auto;
581
+ white-space: pre-wrap;
582
+ }
583
+
584
+ pre.query-code-block {
585
+ flex-shrink: 0;
586
+ margin-bottom: 8px;
587
+ }
588
+
589
+ .query-result-box {
590
+ border: 1px solid #e2e8f0;
591
+ border-radius: 6px;
592
+ padding: 6px 8px;
593
+ background: #ffffff;
594
+ display: flex;
595
+ flex-direction: column;
596
+ gap: 4px;
597
+ }
598
+
599
+ .query-result-header {
600
+ display: flex;
601
+ justify-content: space-between;
602
+ align-items: center;
603
+ font-size: 10.5px;
604
+ font-weight: 700;
605
+ color: #334155;
606
+ border-bottom: 1px solid #f1f5f9;
607
+ padding-bottom: 4px;
608
+ }
609
+
610
+ .defect-counter-badge {
611
+ padding: 2px 7px;
612
+ border-radius: 999px;
613
+ font-size: 10px;
614
+ font-weight: 700;
615
+ }
616
+
617
+ .defect-alert-zero {
618
+ background: #fee2e2;
619
+ color: #b91c1c;
620
+ border: 1px solid #fca5a5;
621
+ }
622
+
623
+ .defect-alert-found {
624
+ background: #dcfce7;
625
+ color: #15803d;
626
+ border: 1px solid #86efac;
627
+ }
628
+
629
  /* Main Menu / Landing View */
630
  .landing-view {
631
  display: flex;
 
697
  cursor: pointer;
698
  transition: all 0.2s ease-in-out;
699
  width: 100%;
700
+ white-space: normal;
701
  }
702
 
703
  .menu-btn .btn-main-text {
 
890
  cursor: pointer;
891
  }
892
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
893
  .player-time {
894
  font-size: 12px;
895
  font-variant-numeric: tabular-nums;
 
904
  box-shadow: 0 0 10px 3px rgba(255, 215, 0, 0.8) !important;
905
  background-color: rgba(255, 255, 200, 0.3) !important;
906
  transition: all 0.3s ease;
907
+ }