@oml/language 0.13.0 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/out/oml/index.d.ts +3 -1
  2. package/out/oml/index.js +19 -1
  3. package/out/oml/index.js.map +1 -1
  4. package/out/oml/oml-candidates.d.ts +3 -3
  5. package/out/oml/oml-candidates.js +1 -1
  6. package/out/oml/oml-candidates.js.map +1 -1
  7. package/out/oml/oml-diagram.d.ts +17 -0
  8. package/out/oml/oml-diagram.js +1549 -0
  9. package/out/oml/oml-diagram.js.map +1 -0
  10. package/out/oml/oml-document.d.ts +5 -0
  11. package/out/oml/oml-document.js +12 -1
  12. package/out/oml/oml-document.js.map +1 -1
  13. package/out/oml/oml-index.d.ts +2 -1
  14. package/out/oml/oml-index.js +90 -9
  15. package/out/oml/oml-index.js.map +1 -1
  16. package/out/oml/oml-scope.js +4 -3
  17. package/out/oml/oml-scope.js.map +1 -1
  18. package/out/oml/oml-search.d.ts +24 -0
  19. package/out/oml/oml-search.js +95 -0
  20. package/out/oml/oml-search.js.map +1 -0
  21. package/out/oml/{oml-edit.d.ts → oml-update.d.ts} +2 -0
  22. package/out/oml/{oml-edit.js → oml-update.js} +256 -56
  23. package/out/oml/oml-update.js.map +1 -0
  24. package/out/oml/oml-utils.d.ts +1 -1
  25. package/out/oml/oml-utils.js +3 -4
  26. package/out/oml/oml-utils.js.map +1 -1
  27. package/out/oml/oml-validator.d.ts +1 -0
  28. package/out/oml/oml-validator.js +17 -2
  29. package/out/oml/oml-validator.js.map +1 -1
  30. package/package.json +4 -2
  31. package/src/oml/index.ts +32 -1
  32. package/src/oml/oml-candidates.ts +4 -4
  33. package/src/oml/oml-diagram.ts +1708 -0
  34. package/src/oml/oml-document.ts +13 -1
  35. package/src/oml/oml-index.ts +87 -9
  36. package/src/oml/oml-scope.ts +4 -3
  37. package/src/oml/oml-search.ts +132 -0
  38. package/src/oml/{oml-edit.ts → oml-update.ts} +302 -55
  39. package/src/oml/oml-utils.ts +3 -4
  40. package/src/oml/oml-validator.ts +17 -2
  41. package/out/oml/oml-edit.js.map +0 -1
@@ -32,10 +32,11 @@ type ConnectionLike = {
32
32
  };
33
33
  };
34
34
 
35
- const OmlEditRequestType = new RequestType<OmlEditRequest, OmlEditResponse, void>('oml/edit');
35
+ const OmlEditRequestType = new RequestType<OmlEditRequest, OmlEditResponse, void>('oml/update');
36
36
  const RDF_TYPE_IRI = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
37
37
  const OML_HAS_SOURCE_IRI = 'http://opencaesar.io/oml#hasSource';
38
38
  const OML_HAS_TARGET_IRI = 'http://opencaesar.io/oml#hasTarget';
39
+ const XSD_STRING_IRI = 'http://www.w3.org/2001/XMLSchema#string';
39
40
 
40
41
  type OmlEditOperation =
41
42
  | { kind: 'createInstance'; ontologyIri: string; instanceName: string }
@@ -53,6 +54,7 @@ type OmlEditOperation =
53
54
 
54
55
  export type OmlEditRequest = {
55
56
  operations: OmlEditOperation[];
57
+ referencingUri?: string;
56
58
  };
57
59
 
58
60
  export type OmlEditError = {
@@ -65,36 +67,47 @@ export type OmlEditResponse = {
65
67
  errors?: OmlEditError[];
66
68
  };
67
69
 
68
- export function registerOmlEditRequests(connection: ConnectionLike, shared: any): void {
69
- connection.onRequest(OmlEditRequestType, async (params: OmlEditRequest): Promise<OmlEditResponse> => {
70
- const operations = Array.isArray(params?.operations) ? params.operations : [];
71
- const contexts = new Map<string, OntologyContext>();
72
- const changedOntologyIris = new Set<string>();
73
- for (let i = 0; i < operations.length; i += 1) {
74
- const operation = operations[i];
75
- try {
76
- const context = await resolveOntologyContext(shared, operation.ontologyIri, contexts);
77
- const changed = await executeOperation(shared, context, operation, contexts);
78
- for (const ontologyIri of changed) {
79
- changedOntologyIris.add(ontologyIri);
80
- }
81
- } catch (error) {
82
- await restoreContextsFromDocuments(shared, contexts);
83
- const message = error instanceof Error ? error.message : String(error);
84
- const details = error instanceof Error && error.stack
85
- ? `${message}\n${error.stack}`
86
- : message;
87
- connection.console?.error(`[oml] OmlEditRequest failed at operation ${i} (${operation.kind}): ${details}`);
88
- return {
89
- errors: [{
90
- operationIndex: i,
91
- message
92
- }]
93
- };
70
+ export async function applyOmlUpdate(
71
+ shared: any,
72
+ params: OmlEditRequest,
73
+ logError?: (message: string) => void,
74
+ ): Promise<OmlEditResponse> {
75
+ const operations = Array.isArray(params?.operations) ? params.operations : [];
76
+ const referencingUri = typeof params?.referencingUri === 'string'
77
+ ? params.referencingUri.trim()
78
+ : undefined;
79
+ const contexts = new Map<string, OntologyContext>();
80
+ const changedOntologyIris = new Set<string>();
81
+ for (let i = 0; i < operations.length; i += 1) {
82
+ const operation = operations[i];
83
+ try {
84
+ const context = await resolveOntologyContext(shared, operation.ontologyIri, contexts, referencingUri);
85
+ const changed = await executeOperation(shared, context, operation, contexts);
86
+ for (const ontologyIri of changed) {
87
+ changedOntologyIris.add(ontologyIri);
94
88
  }
89
+ } catch (error) {
90
+ await restoreContextsFromDocuments(shared, contexts);
91
+ const message = error instanceof Error ? error.message : String(error);
92
+ const details = error instanceof Error && error.stack
93
+ ? `${message}\n${error.stack}`
94
+ : message;
95
+ logError?.(`[oml] OmlEditRequest failed at operation ${i} (${operation.kind}): ${details}`);
96
+ return {
97
+ errors: [{
98
+ operationIndex: i,
99
+ message
100
+ }]
101
+ };
95
102
  }
96
- const edit = buildWorkspaceEdit(contexts, changedOntologyIris);
97
- return edit ? { edit } : {};
103
+ }
104
+ const edit = buildWorkspaceEdit(contexts, changedOntologyIris);
105
+ return edit ? { edit } : {};
106
+ }
107
+
108
+ export function registerOmlEditRequests(connection: ConnectionLike, shared: any): void {
109
+ connection.onRequest(OmlEditRequestType, async (params: OmlEditRequest): Promise<OmlEditResponse> => {
110
+ return await applyOmlUpdate(shared, params, (message) => connection.console?.error(message));
98
111
  });
99
112
  }
100
113
 
@@ -105,14 +118,19 @@ type OntologyContext = {
105
118
  document: any;
106
119
  };
107
120
 
108
- async function resolveOntologyContext(shared: any, ontologyIri: string, cache: Map<string, OntologyContext>): Promise<OntologyContext> {
121
+ async function resolveOntologyContext(
122
+ shared: any,
123
+ ontologyIri: string,
124
+ cache: Map<string, OntologyContext>,
125
+ referencingUri?: string
126
+ ): Promise<OntologyContext> {
109
127
  const normalizedOntologyIri = normalizeIri(ontologyIri);
110
128
  const cached = cache.get(normalizedOntologyIri);
111
129
  if (cached) {
112
130
  return cached;
113
131
  }
114
132
  const index = getOntologyModelIndex(shared);
115
- const modelUri = index.resolveModelUri(normalizedOntologyIri);
133
+ const modelUri = index.resolveModelUri(normalizedOntologyIri, referencingUri);
116
134
  if (!modelUri) {
117
135
  throw new Error(`Unable to resolve ontology '${ontologyIri}'.`);
118
136
  }
@@ -357,7 +375,9 @@ async function addAssertion(shared: any, ontology: any, subjectIri: string, pred
357
375
  }
358
376
 
359
377
  subject.ownedPropertyValues ??= [];
360
- let propertyAssertion = subject.ownedPropertyValues.find((assertion: any) => matchesPredicateRef(ontology, assertion?.property, normalizedPredicate));
378
+ const matchingAssertions = subject.ownedPropertyValues
379
+ .filter((assertion: any) => matchesPredicateRef(ontology, assertion?.property, normalizedPredicate));
380
+ let propertyAssertion = matchingAssertions[0];
361
381
  if (!propertyAssertion) {
362
382
  await ensureReferenceImport(shared, ontology, normalizedPredicate);
363
383
  propertyAssertion = {
@@ -368,6 +388,8 @@ async function addAssertion(shared: any, ontology: any, subjectIri: string, pred
368
388
  containedValues: []
369
389
  };
370
390
  pushAstArrayChild(subject, 'ownedPropertyValues', propertyAssertion);
391
+ } else if (matchingAssertions.length > 1) {
392
+ normalizePropertyAssertionGroup(subject, propertyAssertion, matchingAssertions.slice(1));
371
393
  }
372
394
  if (isIriLike(objectValue)) {
373
395
  await ensureReferenceImport(shared, ontology, normalizeIri(asRequiredIri(objectValue, 'assertion object')));
@@ -375,6 +397,7 @@ async function addAssertion(shared: any, ontology: any, subjectIri: string, pred
375
397
  await ensureTypedLiteralDatatypeImport(shared, ontology, objectValue);
376
398
  }
377
399
  appendAssertionValue(ontology, propertyAssertion, objectValue);
400
+ normalizeAllPropertyAssertions(subject, ontology);
378
401
  return true;
379
402
  }
380
403
 
@@ -507,19 +530,27 @@ async function addAnnotation(shared: any, ontology: any, subjectIri: string, pre
507
530
  const normalizedPredicate = normalizeIri(predicateIri);
508
531
  await ensureReferenceImport(shared, ontology, normalizedPredicate);
509
532
  subject.ownedAnnotations ??= [];
510
- const annotation: any = {
511
- $type: 'Annotation',
512
- property: asRef(ontology, normalizedPredicate),
513
- literalValues: [],
514
- referencedValues: []
515
- };
533
+ const matchingAnnotations = subject.ownedAnnotations
534
+ .filter((entry: any) => matchesPredicateRef(ontology, entry?.property, normalizedPredicate));
535
+ let annotation = matchingAnnotations[0];
536
+ if (!annotation) {
537
+ annotation = {
538
+ $type: 'Annotation',
539
+ property: asRef(ontology, normalizedPredicate),
540
+ literalValues: [],
541
+ referencedValues: []
542
+ };
543
+ pushAstArrayChild(subject, 'ownedAnnotations', annotation);
544
+ } else if (matchingAnnotations.length > 1) {
545
+ normalizeAnnotationGroup(subject, annotation, matchingAnnotations.slice(1));
546
+ }
516
547
  if (isIriLike(objectValue)) {
517
548
  await ensureReferenceImport(shared, ontology, normalizeIri(asRequiredIri(objectValue, 'annotation object')));
518
549
  } else {
519
550
  await ensureTypedLiteralDatatypeImport(shared, ontology, objectValue);
520
551
  }
521
552
  appendAnnotationValue(ontology, annotation, objectValue);
522
- pushAstArrayChild(subject, 'ownedAnnotations', annotation);
553
+ normalizeAllAnnotations(subject, ontology);
523
554
  return true;
524
555
  }
525
556
 
@@ -928,7 +959,7 @@ function removeAssertionValue(ontology: any, assertion: any, objectValue: unknow
928
959
  assertion.referencedValues = (assertion.referencedValues ?? []).filter((ref: any) => !matchesPredicateRef(ontology, ref, normalizedIri));
929
960
  return;
930
961
  }
931
- const expectedLiteral = asLiteral(ontology, objectValue);
962
+ const expectedLiteral = toLiteralRemovalMatchSpec(ontology, objectValue);
932
963
  assertion.literalValues = (assertion.literalValues ?? []).filter((literal: any) => !matchesLiteralForRemoval(ontology, literal, expectedLiteral));
933
964
  }
934
965
 
@@ -949,7 +980,7 @@ function removeAnnotationValue(ontology: any, annotation: any, objectValue: unkn
949
980
  annotation.referencedValues = (annotation.referencedValues ?? []).filter((ref: any) => !matchesPredicateRef(ontology, ref, normalizedIri));
950
981
  return;
951
982
  }
952
- const expectedLiteral = asLiteral(ontology, objectValue);
983
+ const expectedLiteral = toLiteralRemovalMatchSpec(ontology, objectValue);
953
984
  annotation.literalValues = (annotation.literalValues ?? []).filter((literal: any) => !matchesLiteralForRemoval(ontology, literal, expectedLiteral));
954
985
  }
955
986
 
@@ -968,15 +999,44 @@ function literalTypeIri(ontology: any, literal: any): string {
968
999
  return normalizeIri(expandRefTextToIri(ontology, refText) ?? refText);
969
1000
  }
970
1001
 
971
- function matchesLiteralForRemoval(ontology: any, existingLiteral: any, expectedLiteral: any): boolean {
972
- if (lexicalLiteralValue(existingLiteral) !== lexicalLiteralValue(expectedLiteral)) {
1002
+ type LiteralRemovalMatchSpec = {
1003
+ lexical: string;
1004
+ languageTag?: string;
1005
+ typeIri?: string;
1006
+ };
1007
+
1008
+ function toLiteralRemovalMatchSpec(ontology: any, objectValue: unknown): LiteralRemovalMatchSpec {
1009
+ if (isTypedQuotedLiteralTransport(objectValue)) {
1010
+ return {
1011
+ lexical: String(objectValue.value ?? ''),
1012
+ languageTag: literalLanguageTagFromTransport(objectValue),
1013
+ typeIri: literalDatatypeIriFromTransport(ontology, objectValue),
1014
+ };
1015
+ }
1016
+ const expectedLiteral = asLiteral(ontology, objectValue);
1017
+ return {
1018
+ lexical: lexicalLiteralValue(expectedLiteral),
1019
+ languageTag: literalLanguageTag(expectedLiteral),
1020
+ typeIri: literalTypeIri(ontology, expectedLiteral),
1021
+ };
1022
+ }
1023
+
1024
+ function matchesLiteralForRemoval(ontology: any, existingLiteral: any, expected: LiteralRemovalMatchSpec): boolean {
1025
+ if (lexicalLiteralValue(existingLiteral) !== expected.lexical) {
1026
+ return false;
1027
+ }
1028
+ const existingLanguage = literalLanguageTag(existingLiteral);
1029
+ if (existingLanguage && expected.languageTag && normalizeLanguageTag(existingLanguage) !== normalizeLanguageTag(expected.languageTag)) {
973
1030
  return false;
974
1031
  }
975
- const expectedType = literalTypeIri(ontology, expectedLiteral);
1032
+ const expectedType = expected.typeIri ?? '';
976
1033
  if (!expectedType) {
977
1034
  return true;
978
1035
  }
979
1036
  const existingType = literalTypeIri(ontology, existingLiteral);
1037
+ if (!existingType) {
1038
+ return true;
1039
+ }
980
1040
  return isSameIriTarget(existingType, expectedType);
981
1041
  }
982
1042
 
@@ -1324,13 +1384,19 @@ function pickImportPrefix(namespace: string, usedPrefixes: ReadonlySet<string>):
1324
1384
  function asLiteral(ontology: any, value: unknown): any {
1325
1385
  if (isTypedQuotedLiteralTransport(value)) {
1326
1386
  const literalValue = String(value.value ?? '');
1327
- const datatypeIri = typeof value.datatypeIri === 'string' ? value.datatypeIri.trim() : '';
1328
- const datatypeRefText = typeof value.datatypeRefText === 'string' ? value.datatypeRefText.trim() : '';
1329
- return datatypeIri
1330
- ? { $type: 'QuotedLiteral', value: literalValue, type: { $refText: toRefText(ontology, normalizeIri(datatypeIri)) } }
1331
- : datatypeRefText
1332
- ? { $type: 'QuotedLiteral', value: literalValue, type: { $refText: datatypeRefText } }
1333
- : { $type: 'QuotedLiteral', value: literalValue };
1387
+ const langTag = literalLanguageTagFromTransport(value);
1388
+ const datatypeIri = literalDatatypeIriFromTransport(ontology, value);
1389
+ const literal: Record<string, unknown> = {
1390
+ $type: 'QuotedLiteral',
1391
+ value: literalValue,
1392
+ };
1393
+ if (langTag) {
1394
+ literal.langTag = langTag;
1395
+ }
1396
+ if (datatypeIri && !isXsdStringDatatypeIri(ontology, datatypeIri)) {
1397
+ literal.type = { $refText: toRefText(ontology, datatypeIri) };
1398
+ }
1399
+ return literal;
1334
1400
  }
1335
1401
  if (typeof value === 'boolean') {
1336
1402
  return { $type: 'BooleanLiteral', value };
@@ -1348,14 +1414,24 @@ async function ensureTypedLiteralDatatypeImport(shared: any, ontology: any, valu
1348
1414
  if (!isTypedQuotedLiteralTransport(value)) {
1349
1415
  return;
1350
1416
  }
1351
- const datatypeIri = typeof value.datatypeIri === 'string' ? normalizeIri(value.datatypeIri) : '';
1352
- if (!datatypeIri) {
1417
+ const datatypeIri = literalDatatypeIriFromTransport(ontology, value);
1418
+ if (!datatypeIri || isXsdStringDatatypeIri(ontology, datatypeIri)) {
1353
1419
  return;
1354
1420
  }
1355
1421
  await ensureReferenceImport(shared, ontology, datatypeIri);
1356
1422
  }
1357
1423
 
1358
- function isTypedQuotedLiteralTransport(value: unknown): value is { $type?: unknown; value?: unknown; datatypeIri?: unknown; datatypeRefText?: unknown } {
1424
+ function isTypedQuotedLiteralTransport(value: unknown): value is {
1425
+ $type?: unknown;
1426
+ value?: unknown;
1427
+ langTag?: unknown;
1428
+ language?: unknown;
1429
+ datatypeIri?: unknown;
1430
+ datatypeRefText?: unknown;
1431
+ typeIri?: unknown;
1432
+ typeRefText?: unknown;
1433
+ type?: unknown;
1434
+ } {
1359
1435
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
1360
1436
  return false;
1361
1437
  }
@@ -1363,7 +1439,71 @@ function isTypedQuotedLiteralTransport(value: unknown): value is { $type?: unkno
1363
1439
  if (record.$type === 'QuotedLiteral') {
1364
1440
  return true;
1365
1441
  }
1366
- return (typeof record.datatypeIri === 'string' || typeof record.datatypeRefText === 'string') && 'value' in record;
1442
+ return (
1443
+ typeof record.datatypeIri === 'string'
1444
+ || typeof record.datatypeRefText === 'string'
1445
+ || typeof record.typeIri === 'string'
1446
+ || typeof record.typeRefText === 'string'
1447
+ || typeof record.type === 'string'
1448
+ ) && 'value' in record;
1449
+ }
1450
+
1451
+ function literalLanguageTag(literal: any): string {
1452
+ if (!literal || typeof literal !== 'object') {
1453
+ return '';
1454
+ }
1455
+ return typeof literal.langTag === 'string' ? literal.langTag.trim() : '';
1456
+ }
1457
+
1458
+ function literalLanguageTagFromTransport(value: {
1459
+ langTag?: unknown;
1460
+ language?: unknown;
1461
+ }): string {
1462
+ if (typeof value.langTag === 'string') {
1463
+ return value.langTag.trim();
1464
+ }
1465
+ if (typeof value.language === 'string') {
1466
+ return value.language.trim();
1467
+ }
1468
+ return '';
1469
+ }
1470
+
1471
+ function normalizeLanguageTag(value: string): string {
1472
+ return value.trim().toLowerCase();
1473
+ }
1474
+
1475
+ function literalDatatypeIriFromTransport(ontology: any, value: {
1476
+ datatypeIri?: unknown;
1477
+ datatypeRefText?: unknown;
1478
+ typeIri?: unknown;
1479
+ typeRefText?: unknown;
1480
+ type?: unknown;
1481
+ }): string {
1482
+ const directIri = typeof value.datatypeIri === 'string'
1483
+ ? value.datatypeIri.trim()
1484
+ : typeof value.typeIri === 'string'
1485
+ ? value.typeIri.trim()
1486
+ : '';
1487
+ if (directIri) {
1488
+ const expanded = expandRefTextToIri(ontology, directIri);
1489
+ return normalizeIri(expanded ?? directIri);
1490
+ }
1491
+ const refText = typeof value.datatypeRefText === 'string'
1492
+ ? value.datatypeRefText.trim()
1493
+ : typeof value.typeRefText === 'string'
1494
+ ? value.typeRefText.trim()
1495
+ : typeof value.type === 'string'
1496
+ ? value.type.trim()
1497
+ : '';
1498
+ if (!refText) {
1499
+ return '';
1500
+ }
1501
+ return normalizeIri(expandRefTextToIri(ontology, refText) ?? refText);
1502
+ }
1503
+
1504
+ function isXsdStringDatatypeIri(ontology: any, value: string): boolean {
1505
+ const normalized = normalizeIri(expandRefTextToIri(ontology, value) ?? value);
1506
+ return normalized === XSD_STRING_IRI;
1367
1507
  }
1368
1508
 
1369
1509
  function attachAstNode<T extends Record<string, any>>(node: T, parent: any, containerProperty: string): T {
@@ -1405,6 +1545,113 @@ function normalizeIri(value: string): string {
1405
1545
  return String(value ?? '').trim().replace(/^<|>$/g, '');
1406
1546
  }
1407
1547
 
1548
+ function normalizePropertyAssertionGroup(subject: any, target: any, duplicates: any[]): void {
1549
+ if (!Array.isArray(subject?.ownedPropertyValues) || !target || duplicates.length === 0) {
1550
+ return;
1551
+ }
1552
+ target.literalValues ??= [];
1553
+ target.referencedValues ??= [];
1554
+ target.containedValues ??= [];
1555
+ for (const duplicate of duplicates) {
1556
+ for (const literal of duplicate?.literalValues ?? []) {
1557
+ attachAstNode(literal, target, 'literalValues');
1558
+ target.literalValues.push(literal);
1559
+ }
1560
+ for (const ref of duplicate?.referencedValues ?? []) {
1561
+ target.referencedValues.push(ref);
1562
+ }
1563
+ for (const contained of duplicate?.containedValues ?? []) {
1564
+ attachAstNode(contained, target, 'containedValues');
1565
+ target.containedValues.push(contained);
1566
+ }
1567
+ }
1568
+ const duplicateSet = new Set(duplicates);
1569
+ subject.ownedPropertyValues = subject.ownedPropertyValues.filter((entry: any) => !duplicateSet.has(entry));
1570
+ }
1571
+
1572
+ function normalizeAnnotationGroup(subject: any, target: any, duplicates: any[]): void {
1573
+ if (!Array.isArray(subject?.ownedAnnotations) || !target || duplicates.length === 0) {
1574
+ return;
1575
+ }
1576
+ target.literalValues ??= [];
1577
+ target.referencedValues ??= [];
1578
+ for (const duplicate of duplicates) {
1579
+ for (const literal of duplicate?.literalValues ?? []) {
1580
+ attachAstNode(literal, target, 'literalValues');
1581
+ target.literalValues.push(literal);
1582
+ }
1583
+ for (const ref of duplicate?.referencedValues ?? []) {
1584
+ target.referencedValues.push(ref);
1585
+ }
1586
+ }
1587
+ const duplicateSet = new Set(duplicates);
1588
+ subject.ownedAnnotations = subject.ownedAnnotations.filter((entry: any) => !duplicateSet.has(entry));
1589
+ }
1590
+
1591
+ function normalizeAllPropertyAssertions(subject: any, ontology: any): void {
1592
+ if (!Array.isArray(subject?.ownedPropertyValues) || subject.ownedPropertyValues.length < 2) {
1593
+ return;
1594
+ }
1595
+ const groups = new Map<string, { target: any; duplicates: any[] }>();
1596
+ for (const assertion of subject.ownedPropertyValues) {
1597
+ const key = propertyRefMergeKey(ontology, assertion?.property);
1598
+ if (!key) {
1599
+ continue;
1600
+ }
1601
+ const group = groups.get(key);
1602
+ if (!group) {
1603
+ groups.set(key, { target: assertion, duplicates: [] });
1604
+ continue;
1605
+ }
1606
+ group.duplicates.push(assertion);
1607
+ }
1608
+ for (const { target, duplicates } of groups.values()) {
1609
+ if (duplicates.length > 0) {
1610
+ normalizePropertyAssertionGroup(subject, target, duplicates);
1611
+ }
1612
+ }
1613
+ }
1614
+
1615
+ function normalizeAllAnnotations(subject: any, ontology: any): void {
1616
+ if (!Array.isArray(subject?.ownedAnnotations) || subject.ownedAnnotations.length < 2) {
1617
+ return;
1618
+ }
1619
+ const groups = new Map<string, { target: any; duplicates: any[] }>();
1620
+ for (const annotation of subject.ownedAnnotations) {
1621
+ const key = propertyRefMergeKey(ontology, annotation?.property);
1622
+ if (!key) {
1623
+ continue;
1624
+ }
1625
+ const group = groups.get(key);
1626
+ if (!group) {
1627
+ groups.set(key, { target: annotation, duplicates: [] });
1628
+ continue;
1629
+ }
1630
+ group.duplicates.push(annotation);
1631
+ }
1632
+ for (const { target, duplicates } of groups.values()) {
1633
+ if (duplicates.length > 0) {
1634
+ normalizeAnnotationGroup(subject, target, duplicates);
1635
+ }
1636
+ }
1637
+ }
1638
+
1639
+ function propertyRefMergeKey(ontology: any, ref: any): string | undefined {
1640
+ const refText = getRefText(ref);
1641
+ if (refText) {
1642
+ const expanded = expandRefTextToIri(ontology, refText);
1643
+ return expanded
1644
+ ? `iri:${normalizeIri(expanded)}`
1645
+ : `ref:${normalizeIri(refText)}`;
1646
+ }
1647
+ const resolved = ref?.ref ?? ref?._ref;
1648
+ const resolvedIri = resolved ? getIriForNode(resolved) : undefined;
1649
+ if (resolvedIri) {
1650
+ return `iri:${normalizeIri(resolvedIri)}`;
1651
+ }
1652
+ return undefined;
1653
+ }
1654
+
1408
1655
  function isSourcePredicate(predicateIri: string): boolean {
1409
1656
  return normalizeIri(predicateIri) === OML_HAS_SOURCE_IRI;
1410
1657
  }
@@ -160,16 +160,15 @@ export const getWorkspaceSnapshot = (documents: LangiumDocuments): string =>
160
160
  .sort()
161
161
  .join('|');
162
162
 
163
- const GITHUB_COPILOT_TRANSIENT_DOCUMENT_SCHEMES = new Set([
163
+ const OML_LS_IGNORED_DOCUMENT_SCHEMES = new Set([
164
164
  'chat-editing-text-model',
165
165
  'chat-editing-snapshot-text-model',
166
166
  'vscode-chat-code-block',
167
- 'git',
168
167
  ]);
169
168
 
170
- export function isTransientEditorDocumentUri(uri: string): boolean {
169
+ export function isIgnoredByOmlLsDocumentUri(uri: string): boolean {
171
170
  try {
172
- return GITHUB_COPILOT_TRANSIENT_DOCUMENT_SCHEMES.has(URI.parse(uri).scheme);
171
+ return OML_LS_IGNORED_DOCUMENT_SCHEMES.has(URI.parse(uri).scheme);
173
172
  } catch {
174
173
  return false;
175
174
  }
@@ -1,6 +1,6 @@
1
1
  // Copyright (c) 2026 Modelware. All rights reserved.
2
2
 
3
- import { AstUtils } from 'langium';
3
+ import { AstUtils, URI } from 'langium';
4
4
  import type { ValidationAcceptor, ValidationChecks } from 'langium';
5
5
  import type { OmlServices } from './oml-module.js';
6
6
  import { getOntologyModelIndex } from './oml-index.js';
@@ -270,8 +270,15 @@ export class OmlValidator {
270
270
  const namespace = rawNamespace.replace(/^<|>$/g, '');
271
271
  const iri = namespace.replace(/[#/]?$/, '');
272
272
  const docUri = ontology?.$document?.uri?.toString?.() ?? '';
273
+ const currentScheme = this.getUriScheme(docUri);
273
274
  const duplicateModelUris = getOntologyModelIndex(this.services.shared).getDuplicateWorkspaceModelUris(iri);
274
- const conflictingModelUris = duplicateModelUris.filter((modelUri) => modelUri !== docUri);
275
+ const conflictingModelUris = duplicateModelUris.filter((modelUri) => {
276
+ if (modelUri === docUri) {
277
+ return false;
278
+ }
279
+ const otherScheme = this.getUriScheme(modelUri);
280
+ return !currentScheme || !otherScheme || currentScheme === otherScheme;
281
+ });
275
282
  if (conflictingModelUris.length > 0) {
276
283
  accept('error', `Ontology IRI '${iri}' is also declared by '${conflictingModelUris[0]}'`, {
277
284
  node: ontology,
@@ -309,6 +316,14 @@ export class OmlValidator {
309
316
  }
310
317
  }
311
318
 
319
+ private getUriScheme(value: string): string | undefined {
320
+ try {
321
+ return URI.parse(value).scheme;
322
+ } catch {
323
+ return undefined;
324
+ }
325
+ }
326
+
312
327
  checkQuotedLiteralNonStandardType(literal: QuotedLiteral, accept: ValidationAcceptor): void {
313
328
  const scalar = literal.type?.ref as any;
314
329
  if (!scalar) {